+43
-5
internal/api/handlers.go
+43
-5
internal/api/handlers.go
···
235
235
"status": statusToString(pds.Status),
236
236
}
237
237
238
+
// Add server_did if available
239
+
if pds.ServerDID != "" {
240
+
response["server_did"] = pds.ServerDID
241
+
}
242
+
238
243
// Add last_checked if available
239
244
if !pds.LastChecked.IsZero() {
240
245
response["last_checked"] = pds.LastChecked
···
244
249
if pds.LatestScan != nil {
245
250
response["user_count"] = pds.LatestScan.UserCount
246
251
response["response_time"] = pds.LatestScan.ResponseTime
247
-
if pds.LatestScan.Version != "" { // NEW: Add this block
252
+
if pds.LatestScan.Version != "" {
248
253
response["version"] = pds.LatestScan.Version
249
254
}
250
255
if !pds.LatestScan.ScannedAt.IsZero() {
···
271
276
if pds.IPInfo.ASN > 0 {
272
277
response["asn"] = pds.IPInfo.ASN
273
278
}
274
-
if pds.IPInfo.IsDatacenter {
275
-
response["is_datacenter"] = pds.IPInfo.IsDatacenter
276
-
}
277
279
}
278
280
279
281
return response
280
282
}
281
283
282
284
func formatPDSDetail(pds *storage.PDSDetail) map[string]interface{} {
283
-
// Start with list item formatting
285
+
// Start with list item formatting (includes server_did)
284
286
response := formatPDSListItem(&pds.PDSListItem)
287
+
288
+
// Add is_primary flag
289
+
response["is_primary"] = pds.IsPrimary
290
+
291
+
// Add aliases if available
292
+
if len(pds.Aliases) > 0 {
293
+
response["aliases"] = pds.Aliases
294
+
response["alias_count"] = len(pds.Aliases)
295
+
}
285
296
286
297
// Add server_info and version from latest scan (PDSDetail's LatestScan takes precedence)
287
298
if pds.LatestScan != nil {
···
1355
1366
}
1356
1367
1357
1368
resp.json(result)
1369
+
}
1370
+
1371
+
func (s *Server) handleGetDuplicateEndpoints(w http.ResponseWriter, r *http.Request) {
1372
+
resp := newResponse(w)
1373
+
1374
+
duplicates, err := s.db.GetDuplicateEndpoints(r.Context())
1375
+
if err != nil {
1376
+
resp.error(err.Error(), http.StatusInternalServerError)
1377
+
return
1378
+
}
1379
+
1380
+
// Format response
1381
+
result := make([]map[string]interface{}, 0)
1382
+
for serverDID, endpoints := range duplicates {
1383
+
result = append(result, map[string]interface{}{
1384
+
"server_did": serverDID,
1385
+
"primary": endpoints[0], // First discovered
1386
+
"aliases": endpoints[1:], // Other domains
1387
+
"alias_count": len(endpoints) - 1,
1388
+
"total_domains": len(endpoints),
1389
+
})
1390
+
}
1391
+
1392
+
resp.json(map[string]interface{}{
1393
+
"duplicates": result,
1394
+
"total_duplicate_servers": len(duplicates),
1395
+
})
1358
1396
}
1359
1397
1360
1398
// ===== UTILITY FUNCTIONS =====
+1
internal/api/server.go
+1
internal/api/server.go
···
65
65
api.HandleFunc("/pds/stats", s.handleGetPDSStats).Methods("GET")
66
66
api.HandleFunc("/pds/countries", s.handleGetCountryLeaderboard).Methods("GET")
67
67
api.HandleFunc("/pds/versions", s.handleGetVersionStats).Methods("GET")
68
+
api.HandleFunc("/pds/duplicates", s.handleGetDuplicateEndpoints).Methods("GET")
68
69
api.HandleFunc("/pds/{endpoint}", s.handleGetPDSDetail).Methods("GET")
69
70
70
71
// PLC Bundle routes
+5
-13
internal/pds/scanner.go
+5
-13
internal/pds/scanner.go
···
123
123
}
124
124
}
125
125
126
-
func (s *Scanner) worker(ctx context.Context, jobs <-chan *storage.Endpoint) {
127
-
for server := range jobs {
128
-
select {
129
-
case <-ctx.Done():
130
-
return
131
-
default:
132
-
s.scanAndSaveEndpoint(ctx, server)
133
-
}
134
-
}
135
-
}
136
-
137
126
func (s *Scanner) scanAndSaveEndpoint(ctx context.Context, ep *storage.Endpoint) {
138
127
// STEP 1: Resolve IP (before any network call)
139
128
ip, err := ipinfo.ExtractIPFromEndpoint(ep.Endpoint)
···
150
139
s.db.UpdateEndpointIP(ctx, ep.ID, ip, time.Now().UTC())
151
140
152
141
// STEP 2: Health check
153
-
available, responseTime, version, err := s.client.CheckHealth(ctx, ep.Endpoint) // CHANGED: receive version
142
+
available, responseTime, version, err := s.client.CheckHealth(ctx, ep.Endpoint)
154
143
if err != nil || !available {
155
144
errMsg := "health check failed"
156
145
if err != nil {
···
168
157
desc, err := s.client.DescribeServer(ctx, ep.Endpoint)
169
158
if err != nil {
170
159
log.Verbose("Warning: failed to describe server %s: %v", stripansi.Strip(ep.Endpoint), err)
160
+
} else if desc != nil && desc.DID != "" {
161
+
// NEW: Update server DID
162
+
s.db.UpdateEndpointServerDID(ctx, ep.ID, desc.DID)
171
163
}
172
164
173
165
dids, err := s.client.ListRepos(ctx, ep.Endpoint)
···
182
174
ResponseTime: responseTime,
183
175
Description: desc,
184
176
DIDs: dids,
185
-
Version: version, // CHANGED: Pass version
177
+
Version: version,
186
178
})
187
179
188
180
// STEP 5: Fetch IP info if needed (async, with backoff)
+2
internal/storage/db.go
+2
internal/storage/db.go
···
31
31
SaveEndpointScan(ctx context.Context, scan *EndpointScan) error
32
32
SetScanRetention(retention int)
33
33
UpdateEndpointStatus(ctx context.Context, endpointID int64, update *EndpointUpdate) error
34
+
UpdateEndpointServerDID(ctx context.Context, endpointID int64, serverDID string) error
35
+
GetDuplicateEndpoints(ctx context.Context) (map[string][]string, error)
34
36
35
37
// PDS virtual endpoints (created via JOINs)
36
38
GetPDSList(ctx context.Context, filter *EndpointFilter) ([]*PDSListItem, error)
+226
-104
internal/storage/postgres.go
+226
-104
internal/storage/postgres.go
···
12
12
"github.com/jackc/pgx/v5"
13
13
"github.com/jackc/pgx/v5/pgxpool"
14
14
_ "github.com/jackc/pgx/v5/stdlib"
15
+
"github.com/lib/pq"
15
16
)
16
17
17
18
type PostgresDB struct {
···
77
78
id BIGSERIAL PRIMARY KEY,
78
79
endpoint_type TEXT NOT NULL DEFAULT 'pds',
79
80
endpoint TEXT NOT NULL,
81
+
server_did TEXT,
80
82
discovered_at TIMESTAMP NOT NULL,
81
83
last_checked TIMESTAMP,
82
84
status INTEGER DEFAULT 0,
···
90
92
CREATE INDEX IF NOT EXISTS idx_endpoints_status ON endpoints(status);
91
93
CREATE INDEX IF NOT EXISTS idx_endpoints_type ON endpoints(endpoint_type);
92
94
CREATE INDEX IF NOT EXISTS idx_endpoints_ip ON endpoints(ip);
95
+
CREATE INDEX IF NOT EXISTS idx_endpoints_server_did ON endpoints(server_did);
93
96
94
97
-- IP infos table (IP as PRIMARY KEY)
95
98
CREATE TABLE IF NOT EXISTS ip_infos (
···
276
279
277
280
func (p *PostgresDB) GetEndpoints(ctx context.Context, filter *EndpointFilter) ([]*Endpoint, error) {
278
281
query := `
279
-
SELECT id, endpoint_type, endpoint, discovered_at, last_checked, status,
280
-
ip, ip_resolved_at, updated_at
281
-
FROM endpoints
282
-
WHERE 1=1
283
-
`
282
+
SELECT DISTINCT ON (COALESCE(server_did, id::text))
283
+
id, endpoint_type, endpoint, server_did, discovered_at, last_checked, status,
284
+
ip, ip_resolved_at, updated_at
285
+
FROM endpoints
286
+
WHERE 1=1
287
+
`
284
288
args := []interface{}{}
285
289
argIdx := 1
286
290
···
303
307
argIdx++
304
308
}
305
309
306
-
// FIXED: Filter for stale endpoints only
310
+
// Filter for stale endpoints only
307
311
if filter.OnlyStale && filter.RecheckInterval > 0 {
308
-
// Calculate cutoff time in UTC (Go side, not PostgreSQL side)
309
312
cutoffTime := time.Now().UTC().Add(-filter.RecheckInterval)
310
313
query += fmt.Sprintf(" AND (last_checked IS NULL OR last_checked < $%d)", argIdx)
311
314
args = append(args, cutoffTime)
···
313
316
}
314
317
}
315
318
316
-
query += " ORDER BY id DESC"
319
+
// NEW: Order by server_did and discovered_at to get primary endpoints
320
+
query += " ORDER BY COALESCE(server_did, id::text), discovered_at ASC"
317
321
318
322
if filter != nil && filter.Limit > 0 {
319
323
query += fmt.Sprintf(" LIMIT $%d OFFSET $%d", argIdx, argIdx+1)
···
330
334
for rows.Next() {
331
335
var ep Endpoint
332
336
var lastChecked, ipResolvedAt sql.NullTime
333
-
var ip sql.NullString
337
+
var ip, serverDID sql.NullString
334
338
335
339
err := rows.Scan(
336
-
&ep.ID, &ep.EndpointType, &ep.Endpoint, &ep.DiscoveredAt, &lastChecked,
340
+
&ep.ID, &ep.EndpointType, &ep.Endpoint, &serverDID, &ep.DiscoveredAt, &lastChecked,
337
341
&ep.Status, &ip, &ipResolvedAt, &ep.UpdatedAt,
338
342
)
339
343
if err != nil {
340
344
return nil, err
341
345
}
342
346
347
+
if serverDID.Valid {
348
+
ep.ServerDID = serverDID.String
349
+
}
343
350
if lastChecked.Valid {
344
351
ep.LastChecked = lastChecked.Time
345
352
}
···
376
383
return err
377
384
}
378
385
386
+
func (p *PostgresDB) UpdateEndpointServerDID(ctx context.Context, endpointID int64, serverDID string) error {
387
+
query := `
388
+
UPDATE endpoints
389
+
SET server_did = $1, updated_at = $2
390
+
WHERE id = $3
391
+
`
392
+
_, err := p.db.ExecContext(ctx, query, serverDID, time.Now().UTC(), endpointID)
393
+
return err
394
+
}
395
+
396
+
func (p *PostgresDB) GetDuplicateEndpoints(ctx context.Context) (map[string][]string, error) {
397
+
query := `
398
+
SELECT server_did, array_agg(endpoint ORDER BY discovered_at ASC) as endpoints
399
+
FROM endpoints
400
+
WHERE server_did IS NOT NULL
401
+
AND server_did != ''
402
+
AND endpoint_type = 'pds'
403
+
GROUP BY server_did
404
+
HAVING COUNT(*) > 1
405
+
ORDER BY COUNT(*) DESC
406
+
`
407
+
408
+
rows, err := p.db.QueryContext(ctx, query)
409
+
if err != nil {
410
+
return nil, err
411
+
}
412
+
defer rows.Close()
413
+
414
+
duplicates := make(map[string][]string)
415
+
for rows.Next() {
416
+
var serverDID string
417
+
var endpoints []string
418
+
419
+
err := rows.Scan(&serverDID, pq.Array(&endpoints))
420
+
if err != nil {
421
+
return nil, err
422
+
}
423
+
424
+
duplicates[serverDID] = endpoints
425
+
}
426
+
427
+
return duplicates, rows.Err()
428
+
}
429
+
379
430
// ===== SCAN OPERATIONS =====
380
431
381
432
func (p *PostgresDB) SetScanRetention(retention int) {
···
480
531
481
532
func (p *PostgresDB) GetPDSList(ctx context.Context, filter *EndpointFilter) ([]*PDSListItem, error) {
482
533
query := `
483
-
SELECT
484
-
e.id, e.endpoint, e.discovered_at, e.last_checked, e.status, e.ip,
485
-
latest.user_count, latest.response_time, latest.version, latest.scanned_at,
486
-
i.city, i.country, i.country_code, i.asn, i.asn_org,
487
-
i.is_datacenter, i.is_vpn, i.latitude, i.longitude
488
-
FROM endpoints e
489
-
LEFT JOIN LATERAL (
490
-
SELECT
491
-
user_count,
492
-
response_time,
493
-
version,
494
-
scanned_at
495
-
FROM endpoint_scans
496
-
WHERE endpoint_id = e.id AND status = 1
497
-
ORDER BY scanned_at DESC
498
-
LIMIT 1
499
-
) latest ON true
500
-
LEFT JOIN ip_infos i ON e.ip = i.ip
501
-
WHERE e.endpoint_type = 'pds'
502
-
`
534
+
WITH unique_servers AS (
535
+
SELECT DISTINCT ON (COALESCE(server_did, id::text))
536
+
id,
537
+
endpoint,
538
+
server_did,
539
+
discovered_at,
540
+
last_checked,
541
+
status,
542
+
ip
543
+
FROM endpoints
544
+
WHERE endpoint_type = 'pds'
545
+
ORDER BY COALESCE(server_did, id::text), discovered_at ASC
546
+
)
547
+
SELECT
548
+
e.id, e.endpoint, e.server_did, e.discovered_at, e.last_checked, e.status, e.ip,
549
+
latest.user_count, latest.response_time, latest.version, latest.scanned_at,
550
+
i.city, i.country, i.country_code, i.asn, i.asn_org,
551
+
i.is_datacenter, i.is_vpn, i.latitude, i.longitude
552
+
FROM unique_servers e
553
+
LEFT JOIN LATERAL (
554
+
SELECT
555
+
user_count,
556
+
response_time,
557
+
version,
558
+
scanned_at
559
+
FROM endpoint_scans
560
+
WHERE endpoint_id = e.id AND status = 1
561
+
ORDER BY scanned_at DESC
562
+
LIMIT 1
563
+
) latest ON true
564
+
LEFT JOIN ip_infos i ON e.ip = i.ip
565
+
WHERE 1=1
566
+
`
503
567
504
568
args := []interface{}{}
505
569
argIdx := 1
···
541
605
var items []*PDSListItem
542
606
for rows.Next() {
543
607
item := &PDSListItem{}
544
-
var ip, city, country, countryCode, asnOrg sql.NullString
608
+
var ip, serverDID, city, country, countryCode, asnOrg sql.NullString
545
609
var asn sql.NullInt32
546
610
var isDatacenter, isVPN sql.NullBool
547
611
var lat, lon sql.NullFloat64
548
612
var userCount sql.NullInt32
549
613
var responseTime sql.NullFloat64
550
-
var version sql.NullString // ADD THIS LINE
614
+
var version sql.NullString
551
615
var scannedAt sql.NullTime
552
616
553
617
err := rows.Scan(
554
-
&item.ID, &item.Endpoint, &item.DiscoveredAt, &item.LastChecked, &item.Status, &ip,
555
-
&userCount, &responseTime, &version, &scannedAt, // ADD &version HERE
618
+
&item.ID, &item.Endpoint, &serverDID, &item.DiscoveredAt, &item.LastChecked, &item.Status, &ip,
619
+
&userCount, &responseTime, &version, &scannedAt,
556
620
&city, &country, &countryCode, &asn, &asnOrg,
557
621
&isDatacenter, &isVPN, &lat, &lon,
558
622
)
···
562
626
563
627
if ip.Valid {
564
628
item.IP = ip.String
629
+
}
630
+
if serverDID.Valid {
631
+
item.ServerDID = serverDID.String
565
632
}
566
633
567
634
// Add latest scan data if available
···
603
670
604
671
func (p *PostgresDB) GetPDSDetail(ctx context.Context, endpoint string) (*PDSDetail, error) {
605
672
query := `
606
-
SELECT
607
-
e.id, e.endpoint, e.discovered_at, e.last_checked, e.status, e.ip,
608
-
latest.user_count,
609
-
latest.response_time,
610
-
latest.version, -- ADD THIS LINE
611
-
latest.scan_data->'metadata'->'server_info' as server_info,
612
-
latest.scanned_at,
613
-
i.city, i.country, i.country_code, i.asn, i.asn_org,
614
-
i.is_datacenter, i.is_vpn, i.latitude, i.longitude,
615
-
i.raw_data
616
-
FROM endpoints e
617
-
LEFT JOIN LATERAL (
618
-
SELECT scan_data, response_time, version, scanned_at, user_count -- ADD version HERE
619
-
FROM endpoint_scans
620
-
WHERE endpoint_id = e.id
621
-
ORDER BY scanned_at DESC
622
-
LIMIT 1
623
-
) latest ON true
624
-
LEFT JOIN ip_infos i ON e.ip = i.ip
625
-
WHERE e.endpoint = $1 AND e.endpoint_type = 'pds'
626
-
`
673
+
SELECT
674
+
e.id, e.endpoint, e.server_did, e.discovered_at, e.last_checked, e.status, e.ip,
675
+
latest.user_count,
676
+
latest.response_time,
677
+
latest.version,
678
+
latest.scan_data->'metadata'->'server_info' as server_info,
679
+
latest.scanned_at,
680
+
i.city, i.country, i.country_code, i.asn, i.asn_org,
681
+
i.is_datacenter, i.is_vpn, i.latitude, i.longitude,
682
+
i.raw_data
683
+
FROM endpoints e
684
+
LEFT JOIN LATERAL (
685
+
SELECT scan_data, response_time, version, scanned_at, user_count
686
+
FROM endpoint_scans
687
+
WHERE endpoint_id = e.id
688
+
ORDER BY scanned_at DESC
689
+
LIMIT 1
690
+
) latest ON true
691
+
LEFT JOIN ip_infos i ON e.ip = i.ip
692
+
WHERE e.endpoint = $1 AND e.endpoint_type = 'pds'
693
+
`
627
694
628
695
detail := &PDSDetail{}
629
-
var ip, city, country, countryCode, asnOrg sql.NullString
696
+
var ip, city, country, countryCode, asnOrg, serverDID sql.NullString
630
697
var asn sql.NullInt32
631
698
var isDatacenter, isVPN sql.NullBool
632
699
var lat, lon sql.NullFloat64
633
700
var userCount sql.NullInt32
634
701
var responseTime sql.NullFloat64
635
-
var version sql.NullString // ADD THIS LINE
702
+
var version sql.NullString
636
703
var serverInfoJSON []byte
637
704
var scannedAt sql.NullTime
638
705
var rawDataJSON []byte
639
706
640
707
err := p.db.QueryRowContext(ctx, query, endpoint).Scan(
641
-
&detail.ID, &detail.Endpoint, &detail.DiscoveredAt, &detail.LastChecked, &detail.Status, &ip,
642
-
&userCount, &responseTime, &version, &serverInfoJSON, &scannedAt, // ADD &version HERE
708
+
&detail.ID, &detail.Endpoint, &serverDID, &detail.DiscoveredAt, &detail.LastChecked, &detail.Status, &ip,
709
+
&userCount, &responseTime, &version, &serverInfoJSON, &scannedAt,
643
710
&city, &country, &countryCode, &asn, &asnOrg,
644
711
&isDatacenter, &isVPN, &lat, &lon,
645
712
&rawDataJSON,
···
652
719
detail.IP = ip.String
653
720
}
654
721
722
+
// NEW: Get aliases if this endpoint has a server_did
723
+
if serverDID.Valid && serverDID.String != "" {
724
+
aliasQuery := `
725
+
SELECT endpoint, discovered_at
726
+
FROM endpoints
727
+
WHERE server_did = $1
728
+
AND endpoint_type = 'pds'
729
+
AND endpoint != $2
730
+
ORDER BY discovered_at ASC
731
+
`
732
+
733
+
rows, err := p.db.QueryContext(ctx, aliasQuery, serverDID.String, endpoint)
734
+
if err == nil {
735
+
defer rows.Close()
736
+
737
+
var aliases []string
738
+
var primaryDiscoveredAt time.Time
739
+
740
+
for rows.Next() {
741
+
var alias string
742
+
var discoveredAt time.Time
743
+
if err := rows.Scan(&alias, &discoveredAt); err == nil {
744
+
aliases = append(aliases, alias)
745
+
if primaryDiscoveredAt.IsZero() || discoveredAt.Before(detail.DiscoveredAt) {
746
+
primaryDiscoveredAt = discoveredAt
747
+
}
748
+
}
749
+
}
750
+
751
+
detail.Aliases = aliases
752
+
detail.IsPrimary = detail.DiscoveredAt.Equal(primaryDiscoveredAt) ||
753
+
detail.DiscoveredAt.Before(primaryDiscoveredAt)
754
+
}
755
+
} else {
756
+
// No server_did means it's a unique server
757
+
detail.IsPrimary = true
758
+
}
759
+
655
760
// Parse latest scan data
656
761
if userCount.Valid {
657
762
var serverInfo interface{}
···
662
767
detail.LatestScan = &struct {
663
768
UserCount int
664
769
ResponseTime float64
665
-
Version string // ADD THIS LINE
770
+
Version string
666
771
ServerInfo interface{}
667
772
ScannedAt time.Time
668
773
}{
669
774
UserCount: int(userCount.Int32),
670
775
ResponseTime: responseTime.Float64,
671
-
Version: version.String, // ADD THIS LINE
776
+
Version: version.String,
672
777
ServerInfo: serverInfo,
673
778
ScannedAt: scannedAt.Time,
674
779
}
···
687
792
IsVPN: isVPN.Bool,
688
793
Latitude: float32(lat.Float64),
689
794
Longitude: float32(lon.Float64),
690
-
// RawData is unmarshaled below
691
795
}
692
796
693
-
// NEW: Unmarshal the raw_data JSON
694
797
if len(rawDataJSON) > 0 {
695
-
if err := json.Unmarshal(rawDataJSON, &detail.IPInfo.RawData); err != nil {
696
-
// Log the error but don't fail the request
697
-
fmt.Printf("Warning: failed to unmarshal raw_data for IP %s: %v\n", ip.String, err)
698
-
}
798
+
json.Unmarshal(rawDataJSON, &detail.IPInfo.RawData)
699
799
}
700
800
}
701
801
···
703
803
}
704
804
705
805
func (p *PostgresDB) GetPDSStats(ctx context.Context) (*PDSStats, error) {
706
-
// PDS stats - aggregate from latest scans
707
806
query := `
708
-
WITH latest_scans AS (
709
-
SELECT DISTINCT ON (endpoint_id)
710
-
endpoint_id,
711
-
user_count,
807
+
WITH unique_servers AS (
808
+
SELECT DISTINCT ON (COALESCE(server_did, id::text))
809
+
id,
810
+
COALESCE(server_did, id::text) as server_identity,
712
811
status
713
-
FROM endpoint_scans
714
-
WHERE endpoint_id IN (SELECT id FROM endpoints WHERE endpoint_type = 'pds')
715
-
ORDER BY endpoint_id, scanned_at DESC
716
-
)
717
-
SELECT
718
-
COUNT(*) as total,
719
-
SUM(CASE WHEN status = 1 THEN 1 ELSE 0 END) as online,
720
-
SUM(CASE WHEN status = 2 THEN 1 ELSE 0 END) as offline,
721
-
SUM(user_count) as total_users
722
-
FROM latest_scans
723
-
`
812
+
FROM endpoints
813
+
WHERE endpoint_type = 'pds'
814
+
ORDER BY COALESCE(server_did, id::text), discovered_at ASC
815
+
),
816
+
latest_scans AS (
817
+
SELECT DISTINCT ON (us.id)
818
+
us.id,
819
+
es.user_count,
820
+
us.status
821
+
FROM unique_servers us
822
+
LEFT JOIN endpoint_scans es ON us.id = es.endpoint_id
823
+
ORDER BY us.id, es.scanned_at DESC
824
+
)
825
+
SELECT
826
+
COUNT(*) as total,
827
+
SUM(CASE WHEN status = 1 THEN 1 ELSE 0 END) as online,
828
+
SUM(CASE WHEN status = 2 THEN 1 ELSE 0 END) as offline,
829
+
SUM(COALESCE(user_count, 0)) as total_users
830
+
FROM latest_scans
831
+
`
724
832
725
833
stats := &PDSStats{}
726
834
err := p.db.QueryRowContext(ctx, query).Scan(
···
1534
1642
1535
1643
func (p *PostgresDB) GetCountryLeaderboard(ctx context.Context) ([]*CountryStats, error) {
1536
1644
query := `
1537
-
WITH pds_by_country AS (
1645
+
WITH unique_servers AS (
1646
+
SELECT DISTINCT ON (COALESCE(e.server_did, e.id::text))
1647
+
e.id,
1648
+
e.ip,
1649
+
e.status
1650
+
FROM endpoints e
1651
+
WHERE e.endpoint_type = 'pds'
1652
+
ORDER BY COALESCE(e.server_did, e.id::text), e.discovered_at ASC
1653
+
),
1654
+
pds_by_country AS (
1538
1655
SELECT
1539
1656
i.country,
1540
1657
i.country_code,
1541
-
COUNT(DISTINCT e.id) as active_pds_count,
1658
+
COUNT(DISTINCT us.id) as active_pds_count,
1542
1659
SUM(latest.user_count) as total_users,
1543
1660
AVG(latest.response_time) as avg_response_time
1544
-
FROM endpoints e
1545
-
JOIN ip_infos i ON e.ip = i.ip
1661
+
FROM unique_servers us
1662
+
JOIN ip_infos i ON us.ip = i.ip
1546
1663
LEFT JOIN LATERAL (
1547
1664
SELECT user_count, response_time
1548
1665
FROM endpoint_scans
1549
-
WHERE endpoint_id = e.id
1666
+
WHERE endpoint_id = us.id
1550
1667
ORDER BY scanned_at DESC
1551
1668
LIMIT 1
1552
1669
) latest ON true
1553
-
WHERE e.endpoint_type = 'pds'
1554
-
AND e.status = 1
1555
-
AND i.country IS NOT NULL
1556
-
AND i.country != ''
1670
+
WHERE us.status = 1
1671
+
AND i.country IS NOT NULL
1672
+
AND i.country != ''
1557
1673
GROUP BY i.country, i.country_code
1558
1674
),
1559
1675
totals AS (
···
1566
1682
pbc.country,
1567
1683
pbc.country_code,
1568
1684
pbc.active_pds_count,
1569
-
ROUND((pbc.active_pds_count * 100.0 / NULLIF(t.total_pds, 0))::numeric, 2) as pds_percentage,
1685
+
ROUND((pbc.active_pds_count * 100.0 / NULLIF(t.total_pds, 0))::numeric, 4) as pds_percentage,
1570
1686
COALESCE(pbc.total_users, 0) as total_users,
1571
-
ROUND((COALESCE(pbc.total_users, 0) * 100.0 / NULLIF(t.total_users_global, 0))::numeric, 2) as users_percentage,
1687
+
ROUND((COALESCE(pbc.total_users, 0) * 100.0 / NULLIF(t.total_users_global, 0))::numeric, 4) as users_percentage,
1572
1688
ROUND(COALESCE(pbc.avg_response_time, 0)::numeric, 2) as avg_response_time_ms
1573
1689
FROM pds_by_country pbc
1574
1690
CROSS JOIN totals t
1575
-
ORDER BY pbc.active_pds_count DESC;
1691
+
ORDER BY pbc.active_pds_count DESC
1576
1692
`
1577
1693
1578
1694
rows, err := p.db.QueryContext(ctx, query)
···
1614
1730
1615
1731
func (p *PostgresDB) GetVersionStats(ctx context.Context) ([]*VersionStats, error) {
1616
1732
query := `
1617
-
WITH latest_scans AS (
1618
-
SELECT DISTINCT ON (e.id)
1619
-
e.id,
1620
-
es.version,
1621
-
es.user_count,
1622
-
es.scanned_at
1733
+
WITH unique_servers AS (
1734
+
SELECT DISTINCT ON (COALESCE(e.server_did, e.id::text))
1735
+
e.id
1623
1736
FROM endpoints e
1624
-
JOIN endpoint_scans es ON e.id = es.endpoint_id
1625
1737
WHERE e.endpoint_type = 'pds'
1626
1738
AND e.status = 1
1627
-
AND es.version IS NOT NULL
1739
+
ORDER BY COALESCE(e.server_did, e.id::text), e.discovered_at ASC
1740
+
),
1741
+
latest_scans AS (
1742
+
SELECT DISTINCT ON (us.id)
1743
+
us.id,
1744
+
es.version,
1745
+
es.user_count,
1746
+
es.scanned_at
1747
+
FROM unique_servers us
1748
+
JOIN endpoint_scans es ON us.id = es.endpoint_id
1749
+
WHERE es.version IS NOT NULL
1628
1750
AND es.version != ''
1629
-
ORDER BY e.id, es.scanned_at DESC
1751
+
ORDER BY us.id, es.scanned_at DESC
1630
1752
),
1631
1753
version_groups AS (
1632
1754
SELECT
+8
-2
internal/storage/types.go
+8
-2
internal/storage/types.go
···
20
20
ID int64
21
21
EndpointType string
22
22
Endpoint string
23
+
ServerDID string
23
24
DiscoveredAt time.Time
24
25
LastChecked time.Time
25
26
Status int
···
185
186
// From endpoints table
186
187
ID int64
187
188
Endpoint string
189
+
ServerDID string // NEW: Add this
188
190
DiscoveredAt time.Time
189
191
LastChecked time.Time
190
192
Status int
···
194
196
LatestScan *struct {
195
197
UserCount int
196
198
ResponseTime float64
197
-
Version string // NEW: Add this
199
+
Version string
198
200
ScannedAt time.Time
199
201
}
200
202
···
210
212
LatestScan *struct {
211
213
UserCount int
212
214
ResponseTime float64
213
-
Version string // ADD THIS LINE
215
+
Version string
214
216
ServerInfo interface{} // Full server description
215
217
ScannedAt time.Time
216
218
}
219
+
220
+
// NEW: Aliases (other domains pointing to same server)
221
+
Aliases []string `json:"aliases,omitempty"`
222
+
IsPrimary bool `json:"is_primary"`
217
223
}
218
224
219
225
type CountryStats struct {