tangled
alpha
login
or
join now
atscan.net
/
atscand
1
fork
atom
wip
1
fork
atom
overview
issues
pulls
pipelines
pds duplicates
tree.fail
4 months ago
f608ec8b
a1069e99
+285
-124
6 changed files
expand all
collapse all
unified
split
internal
api
handlers.go
server.go
pds
scanner.go
storage
db.go
postgres.go
types.go
+43
-5
internal/api/handlers.go
···
235
235
"status": statusToString(pds.Status),
236
236
}
237
237
238
238
+
// Add server_did if available
239
239
+
if pds.ServerDID != "" {
240
240
+
response["server_did"] = pds.ServerDID
241
241
+
}
242
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
247
-
if pds.LatestScan.Version != "" { // NEW: Add this block
252
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
274
-
if pds.IPInfo.IsDatacenter {
275
275
-
response["is_datacenter"] = pds.IPInfo.IsDatacenter
276
276
-
}
277
279
}
278
280
279
281
return response
280
282
}
281
283
282
284
func formatPDSDetail(pds *storage.PDSDetail) map[string]interface{} {
283
283
-
// Start with list item formatting
285
285
+
// Start with list item formatting (includes server_did)
284
286
response := formatPDSListItem(&pds.PDSListItem)
287
287
+
288
288
+
// Add is_primary flag
289
289
+
response["is_primary"] = pds.IsPrimary
290
290
+
291
291
+
// Add aliases if available
292
292
+
if len(pds.Aliases) > 0 {
293
293
+
response["aliases"] = pds.Aliases
294
294
+
response["alias_count"] = len(pds.Aliases)
295
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
1369
+
}
1370
1370
+
1371
1371
+
func (s *Server) handleGetDuplicateEndpoints(w http.ResponseWriter, r *http.Request) {
1372
1372
+
resp := newResponse(w)
1373
1373
+
1374
1374
+
duplicates, err := s.db.GetDuplicateEndpoints(r.Context())
1375
1375
+
if err != nil {
1376
1376
+
resp.error(err.Error(), http.StatusInternalServerError)
1377
1377
+
return
1378
1378
+
}
1379
1379
+
1380
1380
+
// Format response
1381
1381
+
result := make([]map[string]interface{}, 0)
1382
1382
+
for serverDID, endpoints := range duplicates {
1383
1383
+
result = append(result, map[string]interface{}{
1384
1384
+
"server_did": serverDID,
1385
1385
+
"primary": endpoints[0], // First discovered
1386
1386
+
"aliases": endpoints[1:], // Other domains
1387
1387
+
"alias_count": len(endpoints) - 1,
1388
1388
+
"total_domains": len(endpoints),
1389
1389
+
})
1390
1390
+
}
1391
1391
+
1392
1392
+
resp.json(map[string]interface{}{
1393
1393
+
"duplicates": result,
1394
1394
+
"total_duplicate_servers": len(duplicates),
1395
1395
+
})
1358
1396
}
1359
1397
1360
1398
// ===== UTILITY FUNCTIONS =====
+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
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
···
123
123
}
124
124
}
125
125
126
126
-
func (s *Scanner) worker(ctx context.Context, jobs <-chan *storage.Endpoint) {
127
127
-
for server := range jobs {
128
128
-
select {
129
129
-
case <-ctx.Done():
130
130
-
return
131
131
-
default:
132
132
-
s.scanAndSaveEndpoint(ctx, server)
133
133
-
}
134
134
-
}
135
135
-
}
136
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
153
-
available, responseTime, version, err := s.client.CheckHealth(ctx, ep.Endpoint) // CHANGED: receive version
142
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
160
+
} else if desc != nil && desc.DID != "" {
161
161
+
// NEW: Update server DID
162
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
185
-
Version: version, // CHANGED: Pass version
177
177
+
Version: version,
186
178
})
187
179
188
180
// STEP 5: Fetch IP info if needed (async, with backoff)
+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
34
+
UpdateEndpointServerDID(ctx context.Context, endpointID int64, serverDID string) error
35
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
···
12
12
"github.com/jackc/pgx/v5"
13
13
"github.com/jackc/pgx/v5/pgxpool"
14
14
_ "github.com/jackc/pgx/v5/stdlib"
15
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
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
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
279
-
SELECT id, endpoint_type, endpoint, discovered_at, last_checked, status,
280
280
-
ip, ip_resolved_at, updated_at
281
281
-
FROM endpoints
282
282
-
WHERE 1=1
283
283
-
`
282
282
+
SELECT DISTINCT ON (COALESCE(server_did, id::text))
283
283
+
id, endpoint_type, endpoint, server_did, discovered_at, last_checked, status,
284
284
+
ip, ip_resolved_at, updated_at
285
285
+
FROM endpoints
286
286
+
WHERE 1=1
287
287
+
`
284
288
args := []interface{}{}
285
289
argIdx := 1
286
290
···
303
307
argIdx++
304
308
}
305
309
306
306
-
// FIXED: Filter for stale endpoints only
310
310
+
// Filter for stale endpoints only
307
311
if filter.OnlyStale && filter.RecheckInterval > 0 {
308
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
316
-
query += " ORDER BY id DESC"
319
319
+
// NEW: Order by server_did and discovered_at to get primary endpoints
320
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
333
-
var ip sql.NullString
337
337
+
var ip, serverDID sql.NullString
334
338
335
339
err := rows.Scan(
336
336
-
&ep.ID, &ep.EndpointType, &ep.Endpoint, &ep.DiscoveredAt, &lastChecked,
340
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
347
+
if serverDID.Valid {
348
348
+
ep.ServerDID = serverDID.String
349
349
+
}
343
350
if lastChecked.Valid {
344
351
ep.LastChecked = lastChecked.Time
345
352
}
···
376
383
return err
377
384
}
378
385
386
386
+
func (p *PostgresDB) UpdateEndpointServerDID(ctx context.Context, endpointID int64, serverDID string) error {
387
387
+
query := `
388
388
+
UPDATE endpoints
389
389
+
SET server_did = $1, updated_at = $2
390
390
+
WHERE id = $3
391
391
+
`
392
392
+
_, err := p.db.ExecContext(ctx, query, serverDID, time.Now().UTC(), endpointID)
393
393
+
return err
394
394
+
}
395
395
+
396
396
+
func (p *PostgresDB) GetDuplicateEndpoints(ctx context.Context) (map[string][]string, error) {
397
397
+
query := `
398
398
+
SELECT server_did, array_agg(endpoint ORDER BY discovered_at ASC) as endpoints
399
399
+
FROM endpoints
400
400
+
WHERE server_did IS NOT NULL
401
401
+
AND server_did != ''
402
402
+
AND endpoint_type = 'pds'
403
403
+
GROUP BY server_did
404
404
+
HAVING COUNT(*) > 1
405
405
+
ORDER BY COUNT(*) DESC
406
406
+
`
407
407
+
408
408
+
rows, err := p.db.QueryContext(ctx, query)
409
409
+
if err != nil {
410
410
+
return nil, err
411
411
+
}
412
412
+
defer rows.Close()
413
413
+
414
414
+
duplicates := make(map[string][]string)
415
415
+
for rows.Next() {
416
416
+
var serverDID string
417
417
+
var endpoints []string
418
418
+
419
419
+
err := rows.Scan(&serverDID, pq.Array(&endpoints))
420
420
+
if err != nil {
421
421
+
return nil, err
422
422
+
}
423
423
+
424
424
+
duplicates[serverDID] = endpoints
425
425
+
}
426
426
+
427
427
+
return duplicates, rows.Err()
428
428
+
}
429
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
483
-
SELECT
484
484
-
e.id, e.endpoint, e.discovered_at, e.last_checked, e.status, e.ip,
485
485
-
latest.user_count, latest.response_time, latest.version, latest.scanned_at,
486
486
-
i.city, i.country, i.country_code, i.asn, i.asn_org,
487
487
-
i.is_datacenter, i.is_vpn, i.latitude, i.longitude
488
488
-
FROM endpoints e
489
489
-
LEFT JOIN LATERAL (
490
490
-
SELECT
491
491
-
user_count,
492
492
-
response_time,
493
493
-
version,
494
494
-
scanned_at
495
495
-
FROM endpoint_scans
496
496
-
WHERE endpoint_id = e.id AND status = 1
497
497
-
ORDER BY scanned_at DESC
498
498
-
LIMIT 1
499
499
-
) latest ON true
500
500
-
LEFT JOIN ip_infos i ON e.ip = i.ip
501
501
-
WHERE e.endpoint_type = 'pds'
502
502
-
`
534
534
+
WITH unique_servers AS (
535
535
+
SELECT DISTINCT ON (COALESCE(server_did, id::text))
536
536
+
id,
537
537
+
endpoint,
538
538
+
server_did,
539
539
+
discovered_at,
540
540
+
last_checked,
541
541
+
status,
542
542
+
ip
543
543
+
FROM endpoints
544
544
+
WHERE endpoint_type = 'pds'
545
545
+
ORDER BY COALESCE(server_did, id::text), discovered_at ASC
546
546
+
)
547
547
+
SELECT
548
548
+
e.id, e.endpoint, e.server_did, e.discovered_at, e.last_checked, e.status, e.ip,
549
549
+
latest.user_count, latest.response_time, latest.version, latest.scanned_at,
550
550
+
i.city, i.country, i.country_code, i.asn, i.asn_org,
551
551
+
i.is_datacenter, i.is_vpn, i.latitude, i.longitude
552
552
+
FROM unique_servers e
553
553
+
LEFT JOIN LATERAL (
554
554
+
SELECT
555
555
+
user_count,
556
556
+
response_time,
557
557
+
version,
558
558
+
scanned_at
559
559
+
FROM endpoint_scans
560
560
+
WHERE endpoint_id = e.id AND status = 1
561
561
+
ORDER BY scanned_at DESC
562
562
+
LIMIT 1
563
563
+
) latest ON true
564
564
+
LEFT JOIN ip_infos i ON e.ip = i.ip
565
565
+
WHERE 1=1
566
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
544
-
var ip, city, country, countryCode, asnOrg sql.NullString
608
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
550
-
var version sql.NullString // ADD THIS LINE
614
614
+
var version sql.NullString
551
615
var scannedAt sql.NullTime
552
616
553
617
err := rows.Scan(
554
554
-
&item.ID, &item.Endpoint, &item.DiscoveredAt, &item.LastChecked, &item.Status, &ip,
555
555
-
&userCount, &responseTime, &version, &scannedAt, // ADD &version HERE
618
618
+
&item.ID, &item.Endpoint, &serverDID, &item.DiscoveredAt, &item.LastChecked, &item.Status, &ip,
619
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
629
+
}
630
630
+
if serverDID.Valid {
631
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
606
-
SELECT
607
607
-
e.id, e.endpoint, e.discovered_at, e.last_checked, e.status, e.ip,
608
608
-
latest.user_count,
609
609
-
latest.response_time,
610
610
-
latest.version, -- ADD THIS LINE
611
611
-
latest.scan_data->'metadata'->'server_info' as server_info,
612
612
-
latest.scanned_at,
613
613
-
i.city, i.country, i.country_code, i.asn, i.asn_org,
614
614
-
i.is_datacenter, i.is_vpn, i.latitude, i.longitude,
615
615
-
i.raw_data
616
616
-
FROM endpoints e
617
617
-
LEFT JOIN LATERAL (
618
618
-
SELECT scan_data, response_time, version, scanned_at, user_count -- ADD version HERE
619
619
-
FROM endpoint_scans
620
620
-
WHERE endpoint_id = e.id
621
621
-
ORDER BY scanned_at DESC
622
622
-
LIMIT 1
623
623
-
) latest ON true
624
624
-
LEFT JOIN ip_infos i ON e.ip = i.ip
625
625
-
WHERE e.endpoint = $1 AND e.endpoint_type = 'pds'
626
626
-
`
673
673
+
SELECT
674
674
+
e.id, e.endpoint, e.server_did, e.discovered_at, e.last_checked, e.status, e.ip,
675
675
+
latest.user_count,
676
676
+
latest.response_time,
677
677
+
latest.version,
678
678
+
latest.scan_data->'metadata'->'server_info' as server_info,
679
679
+
latest.scanned_at,
680
680
+
i.city, i.country, i.country_code, i.asn, i.asn_org,
681
681
+
i.is_datacenter, i.is_vpn, i.latitude, i.longitude,
682
682
+
i.raw_data
683
683
+
FROM endpoints e
684
684
+
LEFT JOIN LATERAL (
685
685
+
SELECT scan_data, response_time, version, scanned_at, user_count
686
686
+
FROM endpoint_scans
687
687
+
WHERE endpoint_id = e.id
688
688
+
ORDER BY scanned_at DESC
689
689
+
LIMIT 1
690
690
+
) latest ON true
691
691
+
LEFT JOIN ip_infos i ON e.ip = i.ip
692
692
+
WHERE e.endpoint = $1 AND e.endpoint_type = 'pds'
693
693
+
`
627
694
628
695
detail := &PDSDetail{}
629
629
-
var ip, city, country, countryCode, asnOrg sql.NullString
696
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
635
-
var version sql.NullString // ADD THIS LINE
702
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
641
-
&detail.ID, &detail.Endpoint, &detail.DiscoveredAt, &detail.LastChecked, &detail.Status, &ip,
642
642
-
&userCount, &responseTime, &version, &serverInfoJSON, &scannedAt, // ADD &version HERE
708
708
+
&detail.ID, &detail.Endpoint, &serverDID, &detail.DiscoveredAt, &detail.LastChecked, &detail.Status, &ip,
709
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
722
+
// NEW: Get aliases if this endpoint has a server_did
723
723
+
if serverDID.Valid && serverDID.String != "" {
724
724
+
aliasQuery := `
725
725
+
SELECT endpoint, discovered_at
726
726
+
FROM endpoints
727
727
+
WHERE server_did = $1
728
728
+
AND endpoint_type = 'pds'
729
729
+
AND endpoint != $2
730
730
+
ORDER BY discovered_at ASC
731
731
+
`
732
732
+
733
733
+
rows, err := p.db.QueryContext(ctx, aliasQuery, serverDID.String, endpoint)
734
734
+
if err == nil {
735
735
+
defer rows.Close()
736
736
+
737
737
+
var aliases []string
738
738
+
var primaryDiscoveredAt time.Time
739
739
+
740
740
+
for rows.Next() {
741
741
+
var alias string
742
742
+
var discoveredAt time.Time
743
743
+
if err := rows.Scan(&alias, &discoveredAt); err == nil {
744
744
+
aliases = append(aliases, alias)
745
745
+
if primaryDiscoveredAt.IsZero() || discoveredAt.Before(detail.DiscoveredAt) {
746
746
+
primaryDiscoveredAt = discoveredAt
747
747
+
}
748
748
+
}
749
749
+
}
750
750
+
751
751
+
detail.Aliases = aliases
752
752
+
detail.IsPrimary = detail.DiscoveredAt.Equal(primaryDiscoveredAt) ||
753
753
+
detail.DiscoveredAt.Before(primaryDiscoveredAt)
754
754
+
}
755
755
+
} else {
756
756
+
// No server_did means it's a unique server
757
757
+
detail.IsPrimary = true
758
758
+
}
759
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
665
-
Version string // ADD THIS LINE
770
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
671
-
Version: version.String, // ADD THIS LINE
776
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
690
-
// RawData is unmarshaled below
691
795
}
692
796
693
693
-
// NEW: Unmarshal the raw_data JSON
694
797
if len(rawDataJSON) > 0 {
695
695
-
if err := json.Unmarshal(rawDataJSON, &detail.IPInfo.RawData); err != nil {
696
696
-
// Log the error but don't fail the request
697
697
-
fmt.Printf("Warning: failed to unmarshal raw_data for IP %s: %v\n", ip.String, err)
698
698
-
}
798
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
706
-
// PDS stats - aggregate from latest scans
707
806
query := `
708
708
-
WITH latest_scans AS (
709
709
-
SELECT DISTINCT ON (endpoint_id)
710
710
-
endpoint_id,
711
711
-
user_count,
807
807
+
WITH unique_servers AS (
808
808
+
SELECT DISTINCT ON (COALESCE(server_did, id::text))
809
809
+
id,
810
810
+
COALESCE(server_did, id::text) as server_identity,
712
811
status
713
713
-
FROM endpoint_scans
714
714
-
WHERE endpoint_id IN (SELECT id FROM endpoints WHERE endpoint_type = 'pds')
715
715
-
ORDER BY endpoint_id, scanned_at DESC
716
716
-
)
717
717
-
SELECT
718
718
-
COUNT(*) as total,
719
719
-
SUM(CASE WHEN status = 1 THEN 1 ELSE 0 END) as online,
720
720
-
SUM(CASE WHEN status = 2 THEN 1 ELSE 0 END) as offline,
721
721
-
SUM(user_count) as total_users
722
722
-
FROM latest_scans
723
723
-
`
812
812
+
FROM endpoints
813
813
+
WHERE endpoint_type = 'pds'
814
814
+
ORDER BY COALESCE(server_did, id::text), discovered_at ASC
815
815
+
),
816
816
+
latest_scans AS (
817
817
+
SELECT DISTINCT ON (us.id)
818
818
+
us.id,
819
819
+
es.user_count,
820
820
+
us.status
821
821
+
FROM unique_servers us
822
822
+
LEFT JOIN endpoint_scans es ON us.id = es.endpoint_id
823
823
+
ORDER BY us.id, es.scanned_at DESC
824
824
+
)
825
825
+
SELECT
826
826
+
COUNT(*) as total,
827
827
+
SUM(CASE WHEN status = 1 THEN 1 ELSE 0 END) as online,
828
828
+
SUM(CASE WHEN status = 2 THEN 1 ELSE 0 END) as offline,
829
829
+
SUM(COALESCE(user_count, 0)) as total_users
830
830
+
FROM latest_scans
831
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
1537
-
WITH pds_by_country AS (
1645
1645
+
WITH unique_servers AS (
1646
1646
+
SELECT DISTINCT ON (COALESCE(e.server_did, e.id::text))
1647
1647
+
e.id,
1648
1648
+
e.ip,
1649
1649
+
e.status
1650
1650
+
FROM endpoints e
1651
1651
+
WHERE e.endpoint_type = 'pds'
1652
1652
+
ORDER BY COALESCE(e.server_did, e.id::text), e.discovered_at ASC
1653
1653
+
),
1654
1654
+
pds_by_country AS (
1538
1655
SELECT
1539
1656
i.country,
1540
1657
i.country_code,
1541
1541
-
COUNT(DISTINCT e.id) as active_pds_count,
1658
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
1544
-
FROM endpoints e
1545
1545
-
JOIN ip_infos i ON e.ip = i.ip
1661
1661
+
FROM unique_servers us
1662
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
1549
-
WHERE endpoint_id = e.id
1666
1666
+
WHERE endpoint_id = us.id
1550
1667
ORDER BY scanned_at DESC
1551
1668
LIMIT 1
1552
1669
) latest ON true
1553
1553
-
WHERE e.endpoint_type = 'pds'
1554
1554
-
AND e.status = 1
1555
1555
-
AND i.country IS NOT NULL
1556
1556
-
AND i.country != ''
1670
1670
+
WHERE us.status = 1
1671
1671
+
AND i.country IS NOT NULL
1672
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
1569
-
ROUND((pbc.active_pds_count * 100.0 / NULLIF(t.total_pds, 0))::numeric, 2) as pds_percentage,
1685
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
1571
-
ROUND((COALESCE(pbc.total_users, 0) * 100.0 / NULLIF(t.total_users_global, 0))::numeric, 2) as users_percentage,
1687
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
1575
-
ORDER BY pbc.active_pds_count DESC;
1691
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
1617
-
WITH latest_scans AS (
1618
1618
-
SELECT DISTINCT ON (e.id)
1619
1619
-
e.id,
1620
1620
-
es.version,
1621
1621
-
es.user_count,
1622
1622
-
es.scanned_at
1733
1733
+
WITH unique_servers AS (
1734
1734
+
SELECT DISTINCT ON (COALESCE(e.server_did, e.id::text))
1735
1735
+
e.id
1623
1736
FROM endpoints e
1624
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
1627
-
AND es.version IS NOT NULL
1739
1739
+
ORDER BY COALESCE(e.server_did, e.id::text), e.discovered_at ASC
1740
1740
+
),
1741
1741
+
latest_scans AS (
1742
1742
+
SELECT DISTINCT ON (us.id)
1743
1743
+
us.id,
1744
1744
+
es.version,
1745
1745
+
es.user_count,
1746
1746
+
es.scanned_at
1747
1747
+
FROM unique_servers us
1748
1748
+
JOIN endpoint_scans es ON us.id = es.endpoint_id
1749
1749
+
WHERE es.version IS NOT NULL
1628
1750
AND es.version != ''
1629
1629
-
ORDER BY e.id, es.scanned_at DESC
1751
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
···
20
20
ID int64
21
21
EndpointType string
22
22
Endpoint string
23
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
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
197
-
Version string // NEW: Add this
199
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
213
-
Version string // ADD THIS LINE
215
215
+
Version string
214
216
ServerInfo interface{} // Full server description
215
217
ScannedAt time.Time
216
218
}
219
219
+
220
220
+
// NEW: Aliases (other domains pointing to same server)
221
221
+
Aliases []string `json:"aliases,omitempty"`
222
222
+
IsPrimary bool `json:"is_primary"`
217
223
}
218
224
219
225
type CountryStats struct {