update

Changed files
+70 -25
internal
+4 -1
internal/api/handlers.go
··· 358 358 scanMap["response_time"] = scan.ResponseTime 359 359 } 360 360 361 - // NEW: Add version if available 362 361 if scan.Version != "" { 363 362 scanMap["version"] = scan.Version 363 + } 364 + 365 + if scan.UsedIP != "" { 366 + scanMap["used_ip"] = scan.UsedIP 364 367 } 365 368 366 369 // Use the top-level UserCount field first
+38 -8
internal/pds/client.go
··· 4 4 "context" 5 5 "encoding/json" 6 6 "fmt" 7 + "net" 7 8 "net/http" 8 9 "time" 9 10 ) ··· 112 113 } 113 114 114 115 // CheckHealth performs a basic health check, ensuring the endpoint returns JSON with a "version" 115 - func (c *Client) CheckHealth(ctx context.Context, endpoint string) (bool, time.Duration, string, error) { 116 + // Returns: available, responseTime, version, usedIP, error 117 + func (c *Client) CheckHealth(ctx context.Context, endpoint string) (bool, time.Duration, string, string, error) { 116 118 startTime := time.Now() 117 119 118 120 url := fmt.Sprintf("%s/xrpc/_health", endpoint) 119 121 req, err := http.NewRequestWithContext(ctx, "GET", url, nil) 120 122 if err != nil { 121 - return false, 0, "", err 123 + return false, 0, "", "", err 124 + } 125 + 126 + // Create a custom dialer to track which IP was actually used 127 + var usedIP string 128 + transport := &http.Transport{ 129 + DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { 130 + conn, err := (&net.Dialer{ 131 + Timeout: 30 * time.Second, 132 + KeepAlive: 30 * time.Second, 133 + }).DialContext(ctx, network, addr) 134 + 135 + if err == nil && conn != nil { 136 + if remoteAddr := conn.RemoteAddr(); remoteAddr != nil { 137 + // Extract IP from "ip:port" format 138 + if tcpAddr, ok := remoteAddr.(*net.TCPAddr); ok { 139 + usedIP = tcpAddr.IP.String() 140 + } 141 + } 142 + } 143 + 144 + return conn, err 145 + }, 146 + } 147 + 148 + // Create a client with our custom transport 149 + client := &http.Client{ 150 + Timeout: c.httpClient.Timeout, 151 + Transport: transport, 122 152 } 123 153 124 - resp, err := c.httpClient.Do(req) 154 + resp, err := client.Do(req) 125 155 duration := time.Since(startTime) 126 156 127 157 if err != nil { 128 - return false, duration, "", err 158 + return false, duration, "", usedIP, err 129 159 } 130 160 defer resp.Body.Close() 131 161 132 162 if resp.StatusCode != http.StatusOK { 133 - return false, duration, "", fmt.Errorf("health check returned status %d", resp.StatusCode) 163 + return false, duration, "", usedIP, fmt.Errorf("health check returned status %d", resp.StatusCode) 134 164 } 135 165 136 166 // Decode the JSON response and check for "version" ··· 139 169 } 140 170 141 171 if err := json.NewDecoder(resp.Body).Decode(&healthResponse); err != nil { 142 - return false, duration, "", fmt.Errorf("failed to decode health JSON: %w", err) 172 + return false, duration, "", usedIP, fmt.Errorf("failed to decode health JSON: %w", err) 143 173 } 144 174 145 175 if healthResponse.Version == "" { 146 - return false, duration, "", fmt.Errorf("health JSON response missing 'version' field") 176 + return false, duration, "", usedIP, fmt.Errorf("health JSON response missing 'version' field") 147 177 } 148 178 149 179 // All checks passed 150 - return true, duration, healthResponse.Version, nil 180 + return true, duration, healthResponse.Version, usedIP, nil 151 181 }
+9 -6
internal/pds/scanner.go
··· 146 146 go s.updateIPInfoIfNeeded(ctx, ips.IPv6) 147 147 } 148 148 149 - // STEP 2: Health check (rest remains the same) 150 - available, responseTime, version, err := s.client.CheckHealth(ctx, ep.Endpoint) 149 + // STEP 2: Health check (now returns which IP was used) 150 + available, responseTime, version, usedIP, err := s.client.CheckHealth(ctx, ep.Endpoint) 151 151 if err != nil || !available { 152 152 errMsg := "health check failed" 153 153 if err != nil { ··· 157 157 Status: storage.EndpointStatusOffline, 158 158 ResponseTime: responseTime, 159 159 ErrorMessage: errMsg, 160 + UsedIP: usedIP, // Save even if failed 160 161 }) 161 162 return 162 163 } ··· 189 190 Description: desc, 190 191 DIDs: dids, 191 192 Version: version, 193 + UsedIP: usedIP, // NEW: Save which IP was used 192 194 }) 193 195 194 196 // Save repos in batches (only tracks changes) ··· 245 247 Metadata: make(map[string]interface{}), 246 248 } 247 249 248 - var userCount int64 // NEW: Declare user count 250 + var userCount int64 249 251 250 252 // Add PDS-specific metadata 251 253 if result.Status == storage.EndpointStatusOnline { 252 - userCount = int64(len(result.DIDs)) // NEW: Get user count 253 - scanData.Metadata["user_count"] = userCount // Keep in JSON for completeness 254 + userCount = int64(len(result.DIDs)) 255 + scanData.Metadata["user_count"] = userCount 254 256 if result.Description != nil { 255 257 scanData.Metadata["server_info"] = result.Description 256 258 } ··· 267 269 Status: result.Status, 268 270 ResponseTime: result.ResponseTime.Seconds() * 1000, // Convert to ms 269 271 UserCount: userCount, 270 - Version: result.Version, // NEW: Set the version field 272 + Version: result.Version, 273 + UsedIP: result.UsedIP, // NEW 271 274 ScanData: scanData, 272 275 ScannedAt: time.Now().UTC(), 273 276 }
+2 -1
internal/pds/types.go
··· 37 37 ErrorMessage string 38 38 Description *ServerDescription 39 39 DIDs []string 40 - Version string // NEW: Add this field to pass the version 40 + Version string 41 + UsedIP string // NEW 41 42 }
+15 -8
internal/storage/postgres.go
··· 120 120 CREATE INDEX IF NOT EXISTS idx_ip_infos_country_code ON ip_infos(country_code); 121 121 CREATE INDEX IF NOT EXISTS idx_ip_infos_asn ON ip_infos(asn); 122 122 123 - -- Endpoint scans (renamed from pds_scans) 123 + -- Endpoint scans 124 124 CREATE TABLE IF NOT EXISTS endpoint_scans ( 125 125 id BIGSERIAL PRIMARY KEY, 126 126 endpoint_id BIGINT NOT NULL, ··· 128 128 response_time DOUBLE PRECISION, 129 129 user_count BIGINT, 130 130 version TEXT, 131 + used_ip TEXT, 131 132 scan_data JSONB, 132 133 scanned_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 133 134 FOREIGN KEY (endpoint_id) REFERENCES endpoints(id) ON DELETE CASCADE ··· 136 137 CREATE INDEX IF NOT EXISTS idx_endpoint_scans_endpoint_status_scanned ON endpoint_scans(endpoint_id, status, scanned_at DESC); 137 138 CREATE INDEX IF NOT EXISTS idx_endpoint_scans_scanned_at ON endpoint_scans(scanned_at); 138 139 CREATE INDEX IF NOT EXISTS idx_endpoint_scans_user_count ON endpoint_scans(user_count DESC NULLS LAST); 140 + CREATE INDEX IF NOT EXISTS idx_endpoint_scans_used_ip ON endpoint_scans(used_ip); 141 + 139 142 140 143 CREATE TABLE IF NOT EXISTS plc_metrics ( 141 144 id BIGSERIAL PRIMARY KEY, ··· 490 493 defer tx.Rollback() 491 494 492 495 query := ` 493 - INSERT INTO endpoint_scans (endpoint_id, status, response_time, user_count, version, scan_data, scanned_at) 494 - VALUES ($1, $2, $3, $4, $5, $6, $7) 496 + INSERT INTO endpoint_scans (endpoint_id, status, response_time, user_count, version, used_ip, scan_data, scanned_at) 497 + VALUES ($1, $2, $3, $4, $5, $6, $7, $8) 495 498 ` 496 - _, err = tx.ExecContext(ctx, query, scan.EndpointID, scan.Status, scan.ResponseTime, scan.UserCount, scan.Version, scanDataJSON, scan.ScannedAt) 499 + _, err = tx.ExecContext(ctx, query, scan.EndpointID, scan.Status, scan.ResponseTime, scan.UserCount, scan.Version, scan.UsedIP, scanDataJSON, scan.ScannedAt) 497 500 if err != nil { 498 501 return err 499 502 } ··· 520 523 521 524 func (p *PostgresDB) GetEndpointScans(ctx context.Context, endpointID int64, limit int) ([]*EndpointScan, error) { 522 525 query := ` 523 - SELECT id, endpoint_id, status, response_time, user_count, version, scan_data, scanned_at 526 + SELECT id, endpoint_id, status, response_time, user_count, version, used_ip, scan_data, scanned_at 524 527 FROM endpoint_scans 525 528 WHERE endpoint_id = $1 526 529 ORDER BY scanned_at DESC ··· 538 541 var scan EndpointScan 539 542 var responseTime sql.NullFloat64 540 543 var userCount sql.NullInt64 541 - var version sql.NullString // NEW 544 + var version, usedIP sql.NullString 542 545 var scanDataJSON []byte 543 546 544 - err := rows.Scan(&scan.ID, &scan.EndpointID, &scan.Status, &responseTime, &userCount, &version, &scanDataJSON, &scan.ScannedAt) 547 + err := rows.Scan(&scan.ID, &scan.EndpointID, &scan.Status, &responseTime, &userCount, &version, &usedIP, &scanDataJSON, &scan.ScannedAt) 545 548 if err != nil { 546 549 return nil, err 547 550 } ··· 554 557 scan.UserCount = userCount.Int64 555 558 } 556 559 557 - if version.Valid { // NEW 560 + if version.Valid { 558 561 scan.Version = version.String 562 + } 563 + 564 + if usedIP.Valid { 565 + scan.UsedIP = usedIP.String 559 566 } 560 567 561 568 if len(scanDataJSON) > 0 {
+2 -1
internal/storage/types.go
··· 54 54 Status int 55 55 ResponseTime float64 56 56 UserCount int64 57 - Version string // NEW: Add this field 57 + Version string 58 + UsedIP string // NEW: Track which IP was actually used 58 59 ScanData *EndpointScanData 59 60 ScannedAt time.Time 60 61 }