country and version stats

Changed files
+244
internal
+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
··· 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
··· 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
··· 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
··· 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 + }