+41
internal/api/handlers.go
+41
internal/api/handlers.go
···
1260
1260
return allOps
1261
1261
}
1262
1262
1263
+
func (s *Server) handleGetCountryLeaderboard(w http.ResponseWriter, r *http.Request) {
1264
+
resp := newResponse(w)
1265
+
1266
+
stats, err := s.db.GetCountryLeaderboard(r.Context())
1267
+
if err != nil {
1268
+
resp.error(err.Error(), http.StatusInternalServerError)
1269
+
return
1270
+
}
1271
+
1272
+
resp.json(stats)
1273
+
}
1274
+
1275
+
func (s *Server) handleGetVersionStats(w http.ResponseWriter, r *http.Request) {
1276
+
resp := newResponse(w)
1277
+
1278
+
stats, err := s.db.GetVersionStats(r.Context())
1279
+
if err != nil {
1280
+
resp.error(err.Error(), http.StatusInternalServerError)
1281
+
return
1282
+
}
1283
+
1284
+
// Add summary totals
1285
+
var totalPDS int64
1286
+
var totalUsers int64
1287
+
for _, stat := range stats {
1288
+
totalPDS += stat.PDSCount
1289
+
totalUsers += stat.TotalUsers
1290
+
}
1291
+
1292
+
result := map[string]interface{}{
1293
+
"versions": stats,
1294
+
"summary": map[string]interface{}{
1295
+
"total_pds_with_version": totalPDS,
1296
+
"total_users": totalUsers,
1297
+
"version_count": len(stats),
1298
+
},
1299
+
}
1300
+
1301
+
resp.json(result)
1302
+
}
1303
+
1263
1304
// ===== HEALTH HANDLER =====
1264
1305
1265
1306
func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
+2
internal/api/server.go
+2
internal/api/server.go
···
63
63
// NEW: PDS-specific endpoints (virtual, created via JOINs)
64
64
api.HandleFunc("/pds", s.handleGetPDSList).Methods("GET")
65
65
api.HandleFunc("/pds/stats", s.handleGetPDSStats).Methods("GET")
66
+
api.HandleFunc("/pds/countries", s.handleGetCountryLeaderboard).Methods("GET")
67
+
api.HandleFunc("/pds/versions", s.handleGetVersionStats).Methods("GET")
66
68
api.HandleFunc("/pds/{endpoint}", s.handleGetPDSDetail).Methods("GET")
67
69
68
70
// PLC Bundle routes
+3
internal/storage/db.go
+3
internal/storage/db.go
···
75
75
GetDIDRecord(ctx context.Context, did string) (*DIDRecord, error)
76
76
AddBundleDIDs(ctx context.Context, bundleNum int, dids []string) error
77
77
GetTotalDIDCount(ctx context.Context) (int64, error)
78
+
79
+
GetCountryLeaderboard(ctx context.Context) ([]*CountryStats, error)
80
+
GetVersionStats(ctx context.Context) ([]*VersionStats, error)
78
81
}
+177
internal/storage/postgres.go
+177
internal/storage/postgres.go
···
1531
1531
err := p.db.QueryRowContext(ctx, query).Scan(&count)
1532
1532
return count, err
1533
1533
}
1534
+
1535
+
func (p *PostgresDB) GetCountryLeaderboard(ctx context.Context) ([]*CountryStats, error) {
1536
+
query := `
1537
+
WITH pds_by_country AS (
1538
+
SELECT
1539
+
i.country,
1540
+
i.country_code,
1541
+
COUNT(DISTINCT e.id) as active_pds_count,
1542
+
SUM(latest.user_count) as total_users,
1543
+
AVG(latest.response_time) as avg_response_time
1544
+
FROM endpoints e
1545
+
JOIN ip_infos i ON e.ip = i.ip
1546
+
LEFT JOIN LATERAL (
1547
+
SELECT user_count, response_time
1548
+
FROM endpoint_scans
1549
+
WHERE endpoint_id = e.id
1550
+
ORDER BY scanned_at DESC
1551
+
LIMIT 1
1552
+
) 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 != ''
1557
+
GROUP BY i.country, i.country_code
1558
+
),
1559
+
totals AS (
1560
+
SELECT
1561
+
SUM(active_pds_count) as total_pds,
1562
+
SUM(total_users) as total_users_global
1563
+
FROM pds_by_country
1564
+
)
1565
+
SELECT
1566
+
pbc.country,
1567
+
pbc.country_code,
1568
+
pbc.active_pds_count,
1569
+
ROUND((pbc.active_pds_count * 100.0 / NULLIF(t.total_pds, 0))::numeric, 2) as pds_percentage,
1570
+
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,
1572
+
ROUND(COALESCE(pbc.avg_response_time, 0)::numeric, 2) as avg_response_time_ms
1573
+
FROM pds_by_country pbc
1574
+
CROSS JOIN totals t
1575
+
ORDER BY pbc.active_pds_count DESC;
1576
+
`
1577
+
1578
+
rows, err := p.db.QueryContext(ctx, query)
1579
+
if err != nil {
1580
+
return nil, err
1581
+
}
1582
+
defer rows.Close()
1583
+
1584
+
var stats []*CountryStats
1585
+
for rows.Next() {
1586
+
var s CountryStats
1587
+
var pdsPercentage, usersPercentage sql.NullFloat64
1588
+
1589
+
err := rows.Scan(
1590
+
&s.Country,
1591
+
&s.CountryCode,
1592
+
&s.ActivePDSCount,
1593
+
&pdsPercentage,
1594
+
&s.TotalUsers,
1595
+
&usersPercentage,
1596
+
&s.AvgResponseTimeMS,
1597
+
)
1598
+
if err != nil {
1599
+
return nil, err
1600
+
}
1601
+
1602
+
if pdsPercentage.Valid {
1603
+
s.PDSPercentage = pdsPercentage.Float64
1604
+
}
1605
+
if usersPercentage.Valid {
1606
+
s.UsersPercentage = usersPercentage.Float64
1607
+
}
1608
+
1609
+
stats = append(stats, &s)
1610
+
}
1611
+
1612
+
return stats, rows.Err()
1613
+
}
1614
+
1615
+
func (p *PostgresDB) GetVersionStats(ctx context.Context) ([]*VersionStats, error) {
1616
+
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
1623
+
FROM endpoints e
1624
+
JOIN endpoint_scans es ON e.id = es.endpoint_id
1625
+
WHERE e.endpoint_type = 'pds'
1626
+
AND e.status = 1
1627
+
AND es.version IS NOT NULL
1628
+
AND es.version != ''
1629
+
ORDER BY e.id, es.scanned_at DESC
1630
+
),
1631
+
version_groups AS (
1632
+
SELECT
1633
+
version,
1634
+
COUNT(*) as pds_count,
1635
+
SUM(user_count) as total_users,
1636
+
MIN(scanned_at) as first_seen,
1637
+
MAX(scanned_at) as last_seen
1638
+
FROM latest_scans
1639
+
GROUP BY version
1640
+
),
1641
+
totals AS (
1642
+
SELECT
1643
+
SUM(pds_count) as total_pds,
1644
+
SUM(total_users) as total_users_global
1645
+
FROM version_groups
1646
+
)
1647
+
SELECT
1648
+
vg.version,
1649
+
vg.pds_count,
1650
+
(vg.pds_count * 100.0 / NULLIF(t.total_pds, 0))::numeric as percentage,
1651
+
COALESCE(vg.total_users, 0) as total_users,
1652
+
(COALESCE(vg.total_users, 0) * 100.0 / NULLIF(t.total_users_global, 0))::numeric as users_percentage,
1653
+
vg.first_seen,
1654
+
vg.last_seen
1655
+
FROM version_groups vg
1656
+
CROSS JOIN totals t
1657
+
ORDER BY vg.pds_count DESC
1658
+
`
1659
+
1660
+
rows, err := p.db.QueryContext(ctx, query)
1661
+
if err != nil {
1662
+
return nil, err
1663
+
}
1664
+
defer rows.Close()
1665
+
1666
+
var stats []*VersionStats
1667
+
for rows.Next() {
1668
+
var s VersionStats
1669
+
var percentage, usersPercentage sql.NullFloat64
1670
+
1671
+
err := rows.Scan(
1672
+
&s.Version,
1673
+
&s.PDSCount,
1674
+
&percentage,
1675
+
&s.TotalUsers,
1676
+
&usersPercentage,
1677
+
&s.FirstSeen,
1678
+
&s.LastSeen,
1679
+
)
1680
+
if err != nil {
1681
+
return nil, err
1682
+
}
1683
+
1684
+
if percentage.Valid {
1685
+
s.Percentage = percentage.Float64
1686
+
s.PercentageText = formatPercentage(percentage.Float64)
1687
+
}
1688
+
if usersPercentage.Valid {
1689
+
s.UsersPercentage = usersPercentage.Float64
1690
+
}
1691
+
1692
+
stats = append(stats, &s)
1693
+
}
1694
+
1695
+
return stats, rows.Err()
1696
+
}
1697
+
1698
+
// Helper function (add if not already present)
1699
+
func formatPercentage(pct float64) string {
1700
+
if pct >= 10 {
1701
+
return fmt.Sprintf("%.2f%%", pct)
1702
+
} else if pct >= 1 {
1703
+
return fmt.Sprintf("%.3f%%", pct)
1704
+
} else if pct >= 0.01 {
1705
+
return fmt.Sprintf("%.4f%%", pct)
1706
+
} else if pct > 0 {
1707
+
return fmt.Sprintf("%.6f%%", pct)
1708
+
}
1709
+
return "0%"
1710
+
}
+21
internal/storage/types.go
+21
internal/storage/types.go
···
215
215
ScannedAt time.Time
216
216
}
217
217
}
218
+
219
+
type CountryStats struct {
220
+
Country string `json:"country"`
221
+
CountryCode string `json:"country_code"`
222
+
ActivePDSCount int64 `json:"active_pds_count"`
223
+
PDSPercentage float64 `json:"pds_percentage"`
224
+
TotalUsers int64 `json:"total_users"`
225
+
UsersPercentage float64 `json:"users_percentage"`
226
+
AvgResponseTimeMS float64 `json:"avg_response_time_ms"`
227
+
}
228
+
229
+
type VersionStats struct {
230
+
Version string `json:"version"`
231
+
PDSCount int64 `json:"pds_count"`
232
+
Percentage float64 `json:"percentage"`
233
+
PercentageText string `json:"percentage_text"`
234
+
TotalUsers int64 `json:"total_users"`
235
+
UsersPercentage float64 `json:"users_percentage"`
236
+
FirstSeen time.Time `json:"first_seen"`
237
+
LastSeen time.Time `json:"last_seen"`
238
+
}