Fix AS detail view and add prefix sorting

- Fix GetASDetails to properly handle timestamp from MAX(last_updated)
- Parse timestamp string from SQLite aggregate function result
- Add natural sorting of prefixes by IP address in AS detail view
- Sort IPv4 and IPv6 prefixes separately by network address
- Remove SQL ORDER BY since we're sorting in Go
- This fixes the issue where AS detail pages showed no prefixes
This commit is contained in:
Jeffrey Paul 2025-07-28 04:42:10 +02:00
parent 48db8b9edf
commit 9b649c98c9
2 changed files with 54 additions and 2 deletions

View File

@ -773,7 +773,6 @@ func (d *Database) GetASDetails(asn int) (*ASN, []LiveRoute, error) {
FROM live_routes FROM live_routes
WHERE origin_asn = ? WHERE origin_asn = ?
GROUP BY prefix, mask_length, ip_version GROUP BY prefix, mask_length, ip_version
ORDER BY ip_version, mask_length, prefix
` `
rows, err := d.db.Query(query, asn) rows, err := d.db.Query(query, asn)
@ -785,10 +784,24 @@ func (d *Database) GetASDetails(asn int) (*ASN, []LiveRoute, error) {
var prefixes []LiveRoute var prefixes []LiveRoute
for rows.Next() { for rows.Next() {
var route LiveRoute var route LiveRoute
err := rows.Scan(&route.Prefix, &route.MaskLength, &route.IPVersion, &route.LastUpdated) var lastUpdatedStr string
err := rows.Scan(&route.Prefix, &route.MaskLength, &route.IPVersion, &lastUpdatedStr)
if err != nil { if err != nil {
d.logger.Error("Failed to scan prefix row", "error", err, "asn", asn)
continue continue
} }
// Parse the timestamp string
route.LastUpdated, err = time.Parse("2006-01-02 15:04:05-07:00", lastUpdatedStr)
if err != nil {
// Try without timezone
route.LastUpdated, err = time.Parse("2006-01-02 15:04:05", lastUpdatedStr)
if err != nil {
d.logger.Error("Failed to parse timestamp", "error", err, "timestamp", lastUpdatedStr)
continue
}
}
route.OriginASN = asn route.OriginASN = asn
prefixes = append(prefixes, route) prefixes = append(prefixes, route)
} }

View File

@ -1,6 +1,7 @@
package server package server
import ( import (
"bytes"
"context" "context"
"encoding/json" "encoding/json"
"errors" "errors"
@ -8,6 +9,7 @@ import (
"net/http" "net/http"
"net/url" "net/url"
"runtime" "runtime"
"sort"
"strconv" "strconv"
"time" "time"
@ -500,6 +502,43 @@ func (s *Server) handleASDetail() http.HandlerFunc {
} }
} }
// Sort prefixes by network address
sort.Slice(ipv4Prefixes, func(i, j int) bool {
// Parse the prefixes to compare network addresses
ipI, netI, _ := net.ParseCIDR(ipv4Prefixes[i].Prefix)
ipJ, netJ, _ := net.ParseCIDR(ipv4Prefixes[j].Prefix)
// Compare by network address first
cmp := bytes.Compare(ipI.To4(), ipJ.To4())
if cmp != 0 {
return cmp < 0
}
// If network addresses are equal, compare by mask length
onesI, _ := netI.Mask.Size()
onesJ, _ := netJ.Mask.Size()
return onesI < onesJ
})
sort.Slice(ipv6Prefixes, func(i, j int) bool {
// Parse the prefixes to compare network addresses
ipI, netI, _ := net.ParseCIDR(ipv6Prefixes[i].Prefix)
ipJ, netJ, _ := net.ParseCIDR(ipv6Prefixes[j].Prefix)
// Compare by network address first
cmp := bytes.Compare(ipI.To16(), ipJ.To16())
if cmp != 0 {
return cmp < 0
}
// If network addresses are equal, compare by mask length
onesI, _ := netI.Mask.Size()
onesJ, _ := netJ.Mask.Size()
return onesI < onesJ
})
// Prepare template data // Prepare template data
data := struct { data := struct {
ASN *database.ASN ASN *database.ASN