···358358 scanMap["response_time"] = scan.ResponseTime
359359 }
360360361361- // NEW: Add version if available
362361 if scan.Version != "" {
363362 scanMap["version"] = scan.Version
363363+ }
364364+365365+ if scan.UsedIP != "" {
366366+ scanMap["used_ip"] = scan.UsedIP
364367 }
365368366369 // Use the top-level UserCount field first
+38-8
internal/pds/client.go
···44 "context"
55 "encoding/json"
66 "fmt"
77+ "net"
78 "net/http"
89 "time"
910)
···112113}
113114114115// CheckHealth performs a basic health check, ensuring the endpoint returns JSON with a "version"
115115-func (c *Client) CheckHealth(ctx context.Context, endpoint string) (bool, time.Duration, string, error) {
116116+// Returns: available, responseTime, version, usedIP, error
117117+func (c *Client) CheckHealth(ctx context.Context, endpoint string) (bool, time.Duration, string, string, error) {
116118 startTime := time.Now()
117119118120 url := fmt.Sprintf("%s/xrpc/_health", endpoint)
119121 req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
120122 if err != nil {
121121- return false, 0, "", err
123123+ return false, 0, "", "", err
124124+ }
125125+126126+ // Create a custom dialer to track which IP was actually used
127127+ var usedIP string
128128+ transport := &http.Transport{
129129+ DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
130130+ conn, err := (&net.Dialer{
131131+ Timeout: 30 * time.Second,
132132+ KeepAlive: 30 * time.Second,
133133+ }).DialContext(ctx, network, addr)
134134+135135+ if err == nil && conn != nil {
136136+ if remoteAddr := conn.RemoteAddr(); remoteAddr != nil {
137137+ // Extract IP from "ip:port" format
138138+ if tcpAddr, ok := remoteAddr.(*net.TCPAddr); ok {
139139+ usedIP = tcpAddr.IP.String()
140140+ }
141141+ }
142142+ }
143143+144144+ return conn, err
145145+ },
146146+ }
147147+148148+ // Create a client with our custom transport
149149+ client := &http.Client{
150150+ Timeout: c.httpClient.Timeout,
151151+ Transport: transport,
122152 }
123153124124- resp, err := c.httpClient.Do(req)
154154+ resp, err := client.Do(req)
125155 duration := time.Since(startTime)
126156127157 if err != nil {
128128- return false, duration, "", err
158158+ return false, duration, "", usedIP, err
129159 }
130160 defer resp.Body.Close()
131161132162 if resp.StatusCode != http.StatusOK {
133133- return false, duration, "", fmt.Errorf("health check returned status %d", resp.StatusCode)
163163+ return false, duration, "", usedIP, fmt.Errorf("health check returned status %d", resp.StatusCode)
134164 }
135165136166 // Decode the JSON response and check for "version"
···139169 }
140170141171 if err := json.NewDecoder(resp.Body).Decode(&healthResponse); err != nil {
142142- return false, duration, "", fmt.Errorf("failed to decode health JSON: %w", err)
172172+ return false, duration, "", usedIP, fmt.Errorf("failed to decode health JSON: %w", err)
143173 }
144174145175 if healthResponse.Version == "" {
146146- return false, duration, "", fmt.Errorf("health JSON response missing 'version' field")
176176+ return false, duration, "", usedIP, fmt.Errorf("health JSON response missing 'version' field")
147177 }
148178149179 // All checks passed
150150- return true, duration, healthResponse.Version, nil
180180+ return true, duration, healthResponse.Version, usedIP, nil
151181}
+9-6
internal/pds/scanner.go
···146146 go s.updateIPInfoIfNeeded(ctx, ips.IPv6)
147147 }
148148149149- // STEP 2: Health check (rest remains the same)
150150- available, responseTime, version, err := s.client.CheckHealth(ctx, ep.Endpoint)
149149+ // STEP 2: Health check (now returns which IP was used)
150150+ available, responseTime, version, usedIP, err := s.client.CheckHealth(ctx, ep.Endpoint)
151151 if err != nil || !available {
152152 errMsg := "health check failed"
153153 if err != nil {
···157157 Status: storage.EndpointStatusOffline,
158158 ResponseTime: responseTime,
159159 ErrorMessage: errMsg,
160160+ UsedIP: usedIP, // Save even if failed
160161 })
161162 return
162163 }
···189190 Description: desc,
190191 DIDs: dids,
191192 Version: version,
193193+ UsedIP: usedIP, // NEW: Save which IP was used
192194 })
193195194196 // Save repos in batches (only tracks changes)
···245247 Metadata: make(map[string]interface{}),
246248 }
247249248248- var userCount int64 // NEW: Declare user count
250250+ var userCount int64
249251250252 // Add PDS-specific metadata
251253 if result.Status == storage.EndpointStatusOnline {
252252- userCount = int64(len(result.DIDs)) // NEW: Get user count
253253- scanData.Metadata["user_count"] = userCount // Keep in JSON for completeness
254254+ userCount = int64(len(result.DIDs))
255255+ scanData.Metadata["user_count"] = userCount
254256 if result.Description != nil {
255257 scanData.Metadata["server_info"] = result.Description
256258 }
···267269 Status: result.Status,
268270 ResponseTime: result.ResponseTime.Seconds() * 1000, // Convert to ms
269271 UserCount: userCount,
270270- Version: result.Version, // NEW: Set the version field
272272+ Version: result.Version,
273273+ UsedIP: result.UsedIP, // NEW
271274 ScanData: scanData,
272275 ScannedAt: time.Now().UTC(),
273276 }
+2-1
internal/pds/types.go
···3737 ErrorMessage string
3838 Description *ServerDescription
3939 DIDs []string
4040- Version string // NEW: Add this field to pass the version
4040+ Version string
4141+ UsedIP string // NEW
4142}
+15-8
internal/storage/postgres.go
···120120 CREATE INDEX IF NOT EXISTS idx_ip_infos_country_code ON ip_infos(country_code);
121121 CREATE INDEX IF NOT EXISTS idx_ip_infos_asn ON ip_infos(asn);
122122123123- -- Endpoint scans (renamed from pds_scans)
123123+ -- Endpoint scans
124124 CREATE TABLE IF NOT EXISTS endpoint_scans (
125125 id BIGSERIAL PRIMARY KEY,
126126 endpoint_id BIGINT NOT NULL,
···128128 response_time DOUBLE PRECISION,
129129 user_count BIGINT,
130130 version TEXT,
131131+ used_ip TEXT,
131132 scan_data JSONB,
132133 scanned_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
133134 FOREIGN KEY (endpoint_id) REFERENCES endpoints(id) ON DELETE CASCADE
···136137 CREATE INDEX IF NOT EXISTS idx_endpoint_scans_endpoint_status_scanned ON endpoint_scans(endpoint_id, status, scanned_at DESC);
137138 CREATE INDEX IF NOT EXISTS idx_endpoint_scans_scanned_at ON endpoint_scans(scanned_at);
138139 CREATE INDEX IF NOT EXISTS idx_endpoint_scans_user_count ON endpoint_scans(user_count DESC NULLS LAST);
140140+ CREATE INDEX IF NOT EXISTS idx_endpoint_scans_used_ip ON endpoint_scans(used_ip);
141141+139142140143 CREATE TABLE IF NOT EXISTS plc_metrics (
141144 id BIGSERIAL PRIMARY KEY,
···490493 defer tx.Rollback()
491494492495 query := `
493493- INSERT INTO endpoint_scans (endpoint_id, status, response_time, user_count, version, scan_data, scanned_at)
494494- VALUES ($1, $2, $3, $4, $5, $6, $7)
496496+ INSERT INTO endpoint_scans (endpoint_id, status, response_time, user_count, version, used_ip, scan_data, scanned_at)
497497+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
495498 `
496496- _, err = tx.ExecContext(ctx, query, scan.EndpointID, scan.Status, scan.ResponseTime, scan.UserCount, scan.Version, scanDataJSON, scan.ScannedAt)
499499+ _, err = tx.ExecContext(ctx, query, scan.EndpointID, scan.Status, scan.ResponseTime, scan.UserCount, scan.Version, scan.UsedIP, scanDataJSON, scan.ScannedAt)
497500 if err != nil {
498501 return err
499502 }
···520523521524func (p *PostgresDB) GetEndpointScans(ctx context.Context, endpointID int64, limit int) ([]*EndpointScan, error) {
522525 query := `
523523- SELECT id, endpoint_id, status, response_time, user_count, version, scan_data, scanned_at
526526+ SELECT id, endpoint_id, status, response_time, user_count, version, used_ip, scan_data, scanned_at
524527 FROM endpoint_scans
525528 WHERE endpoint_id = $1
526529 ORDER BY scanned_at DESC
···538541 var scan EndpointScan
539542 var responseTime sql.NullFloat64
540543 var userCount sql.NullInt64
541541- var version sql.NullString // NEW
544544+ var version, usedIP sql.NullString
542545 var scanDataJSON []byte
543546544544- err := rows.Scan(&scan.ID, &scan.EndpointID, &scan.Status, &responseTime, &userCount, &version, &scanDataJSON, &scan.ScannedAt)
547547+ err := rows.Scan(&scan.ID, &scan.EndpointID, &scan.Status, &responseTime, &userCount, &version, &usedIP, &scanDataJSON, &scan.ScannedAt)
545548 if err != nil {
546549 return nil, err
547550 }
···554557 scan.UserCount = userCount.Int64
555558 }
556559557557- if version.Valid { // NEW
560560+ if version.Valid {
558561 scan.Version = version.String
562562+ }
563563+564564+ if usedIP.Valid {
565565+ scan.UsedIP = usedIP.String
559566 }
560567561568 if len(scanDataJSON) > 0 {
+2-1
internal/storage/types.go
···5454 Status int
5555 ResponseTime float64
5656 UserCount int64
5757- Version string // NEW: Add this field
5757+ Version string
5858+ UsedIP string // NEW: Track which IP was actually used
5859 ScanData *EndpointScanData
5960 ScannedAt time.Time
6061}