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:
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user