Implement AS and prefix detail pages

- Implement handleASDetail() and handlePrefixDetail() HTML handlers
- Create AS detail HTML template with prefix listings
- Create prefix detail HTML template with route information
- Add timeSince template function for human-readable durations
- Update templates.go to include new templates
- Server-side rendered pages as requested (no client-side API calls)
This commit is contained in:
2025-07-28 04:26:20 +02:00
parent 27ae80ea2e
commit aeeb5e7d7d
4 changed files with 707 additions and 7 deletions

View File

@@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"errors"
"net"
"net/http"
"runtime"
"strconv"
@@ -458,16 +459,160 @@ func (s *Server) handlePrefixDetailJSON() http.HandlerFunc {
// handleASDetail returns a handler that serves the AS detail HTML page
func (s *Server) handleASDetail() http.HandlerFunc {
return func(w http.ResponseWriter, _ *http.Request) {
// TODO: Implement AS detail HTML page
http.Error(w, "Not implemented", http.StatusNotImplemented)
return func(w http.ResponseWriter, r *http.Request) {
asnStr := chi.URLParam(r, "asn")
asn, err := strconv.Atoi(asnStr)
if err != nil {
http.Error(w, "Invalid ASN", http.StatusBadRequest)
return
}
asInfo, prefixes, err := s.db.GetASDetails(asn)
if err != nil {
if errors.Is(err, database.ErrNoRoute) {
http.Error(w, "AS not found", http.StatusNotFound)
} else {
s.logger.Error("Failed to get AS details", "error", err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
}
return
}
// Group prefixes by IP version
const ipVersionV4 = 4
var ipv4Prefixes, ipv6Prefixes []database.LiveRoute
for _, p := range prefixes {
if p.IPVersion == ipVersionV4 {
ipv4Prefixes = append(ipv4Prefixes, p)
} else {
ipv6Prefixes = append(ipv6Prefixes, p)
}
}
// Prepare template data
data := struct {
ASN *database.ASN
IPv4Prefixes []database.LiveRoute
IPv6Prefixes []database.LiveRoute
TotalCount int
IPv4Count int
IPv6Count int
}{
ASN: asInfo,
IPv4Prefixes: ipv4Prefixes,
IPv6Prefixes: ipv6Prefixes,
TotalCount: len(prefixes),
IPv4Count: len(ipv4Prefixes),
IPv6Count: len(ipv6Prefixes),
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
tmpl := templates.ASDetailTemplate()
if err := tmpl.Execute(w, data); err != nil {
s.logger.Error("Failed to render AS detail template", "error", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
}
}
// handlePrefixDetail returns a handler that serves the prefix detail HTML page
func (s *Server) handlePrefixDetail() http.HandlerFunc {
return func(w http.ResponseWriter, _ *http.Request) {
// TODO: Implement prefix detail HTML page
http.Error(w, "Not implemented", http.StatusNotImplemented)
return func(w http.ResponseWriter, r *http.Request) {
prefix := chi.URLParam(r, "prefix")
if prefix == "" {
http.Error(w, "Prefix parameter is required", http.StatusBadRequest)
return
}
routes, err := s.db.GetPrefixDetails(prefix)
if err != nil {
if errors.Is(err, database.ErrNoRoute) {
http.Error(w, "Prefix not found", http.StatusNotFound)
} else {
s.logger.Error("Failed to get prefix details", "error", err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
}
return
}
// Group by origin AS and collect unique AS info
type ASNInfo struct {
Number int
Handle string
Description string
PeerCount int
}
originMap := make(map[int]*ASNInfo)
for _, route := range routes {
if _, exists := originMap[route.OriginASN]; !exists {
// Get AS info from database
asInfo, _, _ := s.db.GetASDetails(route.OriginASN)
handle := ""
description := ""
if asInfo != nil {
handle = asInfo.Handle
description = asInfo.Description
}
originMap[route.OriginASN] = &ASNInfo{
Number: route.OriginASN,
Handle: handle,
Description: description,
PeerCount: 0,
}
}
originMap[route.OriginASN].PeerCount++
}
// Get the first route to extract some common info
var maskLength, ipVersion int
if len(routes) > 0 {
// Parse CIDR to get mask length and IP version
_, ipNet, err := net.ParseCIDR(prefix)
if err == nil {
ones, _ := ipNet.Mask.Size()
maskLength = ones
if ipNet.IP.To4() != nil {
ipVersion = 4
} else {
ipVersion = 6
}
}
}
// Convert origin map to sorted slice
var origins []*ASNInfo
for _, origin := range originMap {
origins = append(origins, origin)
}
// Prepare template data
data := struct {
Prefix string
MaskLength int
IPVersion int
Routes []database.LiveRoute
Origins []*ASNInfo
PeerCount int
OriginCount int
}{
Prefix: prefix,
MaskLength: maskLength,
IPVersion: ipVersion,
Routes: routes,
Origins: origins,
PeerCount: len(routes),
OriginCount: len(originMap),
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
tmpl := templates.PrefixDetailTemplate()
if err := tmpl.Execute(w, data); err != nil {
s.logger.Error("Failed to render prefix detail template", "error", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
}
}