Compare commits
6 Commits
optimize-s
...
5fb3fc0381
| Author | SHA1 | Date | |
|---|---|---|---|
| 5fb3fc0381 | |||
| 9a63553f8d | |||
| ba13c76c53 | |||
| 1dcde74a90 | |||
| 81267431f7 | |||
| dc3ceb8d94 |
@@ -884,11 +884,11 @@ func (d *Database) DeleteLiveRoute(prefix string, originASN int, peerIP string)
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetPrefixDistribution returns the distribution of prefixes by mask length
|
// GetPrefixDistribution returns the distribution of unique prefixes by mask length
|
||||||
func (d *Database) GetPrefixDistribution() (ipv4 []PrefixDistribution, ipv6 []PrefixDistribution, err error) {
|
func (d *Database) GetPrefixDistribution() (ipv4 []PrefixDistribution, ipv6 []PrefixDistribution, err error) {
|
||||||
// IPv4 distribution
|
// IPv4 distribution - count unique prefixes, not routes
|
||||||
query := `
|
query := `
|
||||||
SELECT mask_length, COUNT(*) as count
|
SELECT mask_length, COUNT(DISTINCT prefix) as count
|
||||||
FROM live_routes
|
FROM live_routes
|
||||||
WHERE ip_version = 4
|
WHERE ip_version = 4
|
||||||
GROUP BY mask_length
|
GROUP BY mask_length
|
||||||
@@ -908,9 +908,9 @@ func (d *Database) GetPrefixDistribution() (ipv4 []PrefixDistribution, ipv6 []Pr
|
|||||||
ipv4 = append(ipv4, dist)
|
ipv4 = append(ipv4, dist)
|
||||||
}
|
}
|
||||||
|
|
||||||
// IPv6 distribution
|
// IPv6 distribution - count unique prefixes, not routes
|
||||||
query = `
|
query = `
|
||||||
SELECT mask_length, COUNT(*) as count
|
SELECT mask_length, COUNT(DISTINCT prefix) as count
|
||||||
FROM live_routes
|
FROM live_routes
|
||||||
WHERE ip_version = 6
|
WHERE ip_version = 6
|
||||||
GROUP BY mask_length
|
GROUP BY mask_length
|
||||||
@@ -1227,3 +1227,58 @@ func (d *Database) GetPrefixDetails(prefix string) ([]LiveRoute, error) {
|
|||||||
|
|
||||||
return routes, nil
|
return routes, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetRandomPrefixesByLength returns a random sample of prefixes with the specified mask length
|
||||||
|
func (d *Database) GetRandomPrefixesByLength(maskLength, ipVersion, limit int) ([]LiveRoute, error) {
|
||||||
|
// Select unique prefixes with their most recent route information
|
||||||
|
query := `
|
||||||
|
WITH unique_prefixes AS (
|
||||||
|
SELECT prefix, MAX(last_updated) as max_updated
|
||||||
|
FROM live_routes
|
||||||
|
WHERE mask_length = ? AND ip_version = ?
|
||||||
|
GROUP BY prefix
|
||||||
|
ORDER BY RANDOM()
|
||||||
|
LIMIT ?
|
||||||
|
)
|
||||||
|
SELECT lr.prefix, lr.mask_length, lr.ip_version, lr.origin_asn, lr.as_path,
|
||||||
|
lr.peer_ip, lr.last_updated
|
||||||
|
FROM live_routes lr
|
||||||
|
INNER JOIN unique_prefixes up ON lr.prefix = up.prefix AND lr.last_updated = up.max_updated
|
||||||
|
WHERE lr.mask_length = ? AND lr.ip_version = ?
|
||||||
|
`
|
||||||
|
|
||||||
|
rows, err := d.db.Query(query, maskLength, ipVersion, limit, maskLength, ipVersion)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to query random prefixes: %w", err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
_ = rows.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
var routes []LiveRoute
|
||||||
|
for rows.Next() {
|
||||||
|
var route LiveRoute
|
||||||
|
var pathJSON string
|
||||||
|
err := rows.Scan(
|
||||||
|
&route.Prefix,
|
||||||
|
&route.MaskLength,
|
||||||
|
&route.IPVersion,
|
||||||
|
&route.OriginASN,
|
||||||
|
&pathJSON,
|
||||||
|
&route.PeerIP,
|
||||||
|
&route.LastUpdated,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode AS path
|
||||||
|
if err := json.Unmarshal([]byte(pathJSON), &route.ASPath); err != nil {
|
||||||
|
route.ASPath = []int{}
|
||||||
|
}
|
||||||
|
|
||||||
|
routes = append(routes, route)
|
||||||
|
}
|
||||||
|
|
||||||
|
return routes, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ type Store interface {
|
|||||||
// AS and prefix detail operations
|
// AS and prefix detail operations
|
||||||
GetASDetails(asn int) (*ASN, []LiveRoute, error)
|
GetASDetails(asn int) (*ASN, []LiveRoute, error)
|
||||||
GetPrefixDetails(prefix string) ([]LiveRoute, error)
|
GetPrefixDetails(prefix string) ([]LiveRoute, error)
|
||||||
|
GetRandomPrefixesByLength(maskLength, ipVersion, limit int) ([]LiveRoute, error)
|
||||||
|
|
||||||
// Lifecycle
|
// Lifecycle
|
||||||
Close() error
|
Close() error
|
||||||
|
|||||||
@@ -92,3 +92,5 @@ CREATE INDEX IF NOT EXISTS idx_live_routes_ip_version_mask ON live_routes(ip_ver
|
|||||||
CREATE INDEX IF NOT EXISTS idx_live_routes_last_updated ON live_routes(last_updated);
|
CREATE INDEX IF NOT EXISTS idx_live_routes_last_updated ON live_routes(last_updated);
|
||||||
-- Indexes for IPv4 range queries
|
-- Indexes for IPv4 range queries
|
||||||
CREATE INDEX IF NOT EXISTS idx_live_routes_ipv4_range ON live_routes(v4_ip_start, v4_ip_end) WHERE ip_version = 4;
|
CREATE INDEX IF NOT EXISTS idx_live_routes_ipv4_range ON live_routes(v4_ip_start, v4_ip_end) WHERE ip_version = 4;
|
||||||
|
-- Index to optimize COUNT(DISTINCT prefix) queries
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_live_routes_ip_mask_prefix ON live_routes(ip_version, mask_length, prefix);
|
||||||
@@ -221,6 +221,11 @@ func (m *mockStore) GetPrefixDetails(prefix string) ([]database.LiveRoute, error
|
|||||||
return []database.LiveRoute{}, nil
|
return []database.LiveRoute{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *mockStore) GetRandomPrefixesByLength(maskLength, ipVersion, limit int) ([]database.LiveRoute, error) {
|
||||||
|
// Return empty routes for now
|
||||||
|
return []database.LiveRoute{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
// UpsertLiveRouteBatch mock implementation
|
// UpsertLiveRouteBatch mock implementation
|
||||||
func (m *mockStore) UpsertLiveRouteBatch(routes []*database.LiveRoute) error {
|
func (m *mockStore) UpsertLiveRouteBatch(routes []*database.LiveRoute) error {
|
||||||
m.mu.Lock()
|
m.mu.Lock()
|
||||||
|
|||||||
@@ -11,12 +11,14 @@ import (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
// asHandlerQueueSize is the queue capacity for ASN operations
|
// asHandlerQueueSize is the queue capacity for ASN operations
|
||||||
|
// DO NOT set this higher than 100000 without explicit instructions
|
||||||
asHandlerQueueSize = 100000
|
asHandlerQueueSize = 100000
|
||||||
|
|
||||||
// asnBatchSize is the number of ASN operations to batch together
|
// asnBatchSize is the number of ASN operations to batch together
|
||||||
asnBatchSize = 10000
|
asnBatchSize = 30000
|
||||||
|
|
||||||
// asnBatchTimeout is the maximum time to wait before flushing a batch
|
// asnBatchTimeout is the maximum time to wait before flushing a batch
|
||||||
|
// DO NOT reduce this timeout - larger batches are more efficient
|
||||||
asnBatchTimeout = 2 * time.Second
|
asnBatchTimeout = 2 * time.Second
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -15,12 +15,14 @@ import (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
// prefixHandlerQueueSize is the queue capacity for prefix tracking operations
|
// prefixHandlerQueueSize is the queue capacity for prefix tracking operations
|
||||||
|
// DO NOT set this higher than 100000 without explicit instructions
|
||||||
prefixHandlerQueueSize = 100000
|
prefixHandlerQueueSize = 100000
|
||||||
|
|
||||||
// prefixBatchSize is the number of prefix updates to batch together
|
// prefixBatchSize is the number of prefix updates to batch together
|
||||||
prefixBatchSize = 5000
|
prefixBatchSize = 20000
|
||||||
|
|
||||||
// prefixBatchTimeout is the maximum time to wait before flushing a batch
|
// prefixBatchTimeout is the maximum time to wait before flushing a batch
|
||||||
|
// DO NOT reduce this timeout - larger batches are more efficient
|
||||||
prefixBatchTimeout = 1 * time.Second
|
prefixBatchTimeout = 1 * time.Second
|
||||||
|
|
||||||
// IP version constants
|
// IP version constants
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"html/template"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
@@ -15,6 +16,7 @@ import (
|
|||||||
|
|
||||||
"git.eeqj.de/sneak/routewatch/internal/database"
|
"git.eeqj.de/sneak/routewatch/internal/database"
|
||||||
"git.eeqj.de/sneak/routewatch/internal/templates"
|
"git.eeqj.de/sneak/routewatch/internal/templates"
|
||||||
|
asinfo "git.eeqj.de/sneak/routewatch/pkg/asinfo"
|
||||||
"github.com/dustin/go-humanize"
|
"github.com/dustin/go-humanize"
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
)
|
)
|
||||||
@@ -645,12 +647,41 @@ func (s *Server) handlePrefixDetail() http.HandlerFunc {
|
|||||||
origins = append(origins, origin)
|
origins = append(origins, origin)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create enhanced routes with AS path handles
|
||||||
|
type ASPathEntry struct {
|
||||||
|
Number int
|
||||||
|
Handle string
|
||||||
|
}
|
||||||
|
type EnhancedRoute struct {
|
||||||
|
database.LiveRoute
|
||||||
|
ASPathWithHandle []ASPathEntry
|
||||||
|
}
|
||||||
|
|
||||||
|
enhancedRoutes := make([]EnhancedRoute, len(routes))
|
||||||
|
for i, route := range routes {
|
||||||
|
enhancedRoute := EnhancedRoute{
|
||||||
|
LiveRoute: route,
|
||||||
|
ASPathWithHandle: make([]ASPathEntry, len(route.ASPath)),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look up handle for each AS in the path
|
||||||
|
for j, asn := range route.ASPath {
|
||||||
|
handle := asinfo.GetHandle(asn)
|
||||||
|
enhancedRoute.ASPathWithHandle[j] = ASPathEntry{
|
||||||
|
Number: asn,
|
||||||
|
Handle: handle,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enhancedRoutes[i] = enhancedRoute
|
||||||
|
}
|
||||||
|
|
||||||
// Prepare template data
|
// Prepare template data
|
||||||
data := struct {
|
data := struct {
|
||||||
Prefix string
|
Prefix string
|
||||||
MaskLength int
|
MaskLength int
|
||||||
IPVersion int
|
IPVersion int
|
||||||
Routes []database.LiveRoute
|
Routes []EnhancedRoute
|
||||||
Origins []*ASNInfo
|
Origins []*ASNInfo
|
||||||
PeerCount int
|
PeerCount int
|
||||||
OriginCount int
|
OriginCount int
|
||||||
@@ -658,7 +689,7 @@ func (s *Server) handlePrefixDetail() http.HandlerFunc {
|
|||||||
Prefix: prefix,
|
Prefix: prefix,
|
||||||
MaskLength: maskLength,
|
MaskLength: maskLength,
|
||||||
IPVersion: ipVersion,
|
IPVersion: ipVersion,
|
||||||
Routes: routes,
|
Routes: enhancedRoutes,
|
||||||
Origins: origins,
|
Origins: origins,
|
||||||
PeerCount: len(routes),
|
PeerCount: len(routes),
|
||||||
OriginCount: len(originMap),
|
OriginCount: len(originMap),
|
||||||
@@ -702,3 +733,115 @@ func (s *Server) handleIPRedirect() http.HandlerFunc {
|
|||||||
http.Redirect(w, r, "/prefix/"+url.QueryEscape(asInfo.Prefix), http.StatusSeeOther)
|
http.Redirect(w, r, "/prefix/"+url.QueryEscape(asInfo.Prefix), http.StatusSeeOther)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handlePrefixLength shows a random sample of prefixes with the specified mask length
|
||||||
|
func (s *Server) handlePrefixLength() http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
lengthStr := chi.URLParam(r, "length")
|
||||||
|
if lengthStr == "" {
|
||||||
|
http.Error(w, "Length parameter is required", http.StatusBadRequest)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
maskLength, err := strconv.Atoi(lengthStr)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Invalid mask length", http.StatusBadRequest)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine IP version based on mask length
|
||||||
|
const (
|
||||||
|
maxIPv4MaskLength = 32
|
||||||
|
maxIPv6MaskLength = 128
|
||||||
|
)
|
||||||
|
var ipVersion int
|
||||||
|
if maskLength <= maxIPv4MaskLength {
|
||||||
|
ipVersion = 4
|
||||||
|
} else if maskLength <= maxIPv6MaskLength {
|
||||||
|
ipVersion = 6
|
||||||
|
} else {
|
||||||
|
http.Error(w, "Invalid mask length", http.StatusBadRequest)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get random sample of prefixes
|
||||||
|
const maxPrefixes = 500
|
||||||
|
prefixes, err := s.db.GetRandomPrefixesByLength(maskLength, ipVersion, maxPrefixes)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("Failed to get prefixes by length", "error", err)
|
||||||
|
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort prefixes for display
|
||||||
|
sort.Slice(prefixes, func(i, j int) bool {
|
||||||
|
// First compare by IP version
|
||||||
|
if prefixes[i].IPVersion != prefixes[j].IPVersion {
|
||||||
|
return prefixes[i].IPVersion < prefixes[j].IPVersion
|
||||||
|
}
|
||||||
|
// Then by prefix
|
||||||
|
return prefixes[i].Prefix < prefixes[j].Prefix
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create enhanced prefixes with AS descriptions
|
||||||
|
type EnhancedPrefix struct {
|
||||||
|
database.LiveRoute
|
||||||
|
OriginASDescription string
|
||||||
|
Age string
|
||||||
|
}
|
||||||
|
|
||||||
|
enhancedPrefixes := make([]EnhancedPrefix, len(prefixes))
|
||||||
|
for i, prefix := range prefixes {
|
||||||
|
enhancedPrefixes[i] = EnhancedPrefix{
|
||||||
|
LiveRoute: prefix,
|
||||||
|
Age: formatAge(prefix.LastUpdated),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get AS description
|
||||||
|
if asInfo, ok := asinfo.Get(prefix.OriginASN); ok {
|
||||||
|
enhancedPrefixes[i].OriginASDescription = asInfo.Description
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render template
|
||||||
|
data := map[string]interface{}{
|
||||||
|
"MaskLength": maskLength,
|
||||||
|
"IPVersion": ipVersion,
|
||||||
|
"Prefixes": enhancedPrefixes,
|
||||||
|
"Count": len(prefixes),
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpl := template.Must(template.ParseFiles("internal/templates/prefix_length.html"))
|
||||||
|
if err := tmpl.Execute(w, data); err != nil {
|
||||||
|
s.logger.Error("Failed to render prefix length template", "error", err)
|
||||||
|
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// formatAge returns a human-readable age string
|
||||||
|
func formatAge(timestamp time.Time) string {
|
||||||
|
age := time.Since(timestamp)
|
||||||
|
|
||||||
|
const hoursPerDay = 24
|
||||||
|
|
||||||
|
if age < time.Minute {
|
||||||
|
return "< 1m"
|
||||||
|
} else if age < time.Hour {
|
||||||
|
minutes := int(age.Minutes())
|
||||||
|
|
||||||
|
return strconv.Itoa(minutes) + "m"
|
||||||
|
} else if age < hoursPerDay*time.Hour {
|
||||||
|
hours := int(age.Hours())
|
||||||
|
|
||||||
|
return strconv.Itoa(hours) + "h"
|
||||||
|
}
|
||||||
|
|
||||||
|
days := int(age.Hours() / hoursPerDay)
|
||||||
|
|
||||||
|
return strconv.Itoa(days) + "d"
|
||||||
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ func (s *Server) setupRoutes() {
|
|||||||
// AS and prefix detail pages
|
// AS and prefix detail pages
|
||||||
r.Get("/as/{asn}", s.handleASDetail())
|
r.Get("/as/{asn}", s.handleASDetail())
|
||||||
r.Get("/prefix/{prefix}", s.handlePrefixDetail())
|
r.Get("/prefix/{prefix}", s.handlePrefixDetail())
|
||||||
|
r.Get("/prefixlength/{length}", s.handlePrefixLength())
|
||||||
r.Get("/ip/{ip}", s.handleIPRedirect())
|
r.Get("/ip/{ip}", s.handleIPRedirect())
|
||||||
|
|
||||||
// API routes
|
// API routes
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -95,6 +96,9 @@ func (s *Streamer) RegisterHandler(handler MessageHandler) {
|
|||||||
info := &handlerInfo{
|
info := &handlerInfo{
|
||||||
handler: handler,
|
handler: handler,
|
||||||
queue: make(chan *ristypes.RISMessage, handler.QueueCapacity()),
|
queue: make(chan *ristypes.RISMessage, handler.QueueCapacity()),
|
||||||
|
metrics: handlerMetrics{
|
||||||
|
minTime: time.Duration(math.MaxInt64), // Initialize to max so first value sets the floor
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
s.handlers = append(s.handlers, info)
|
s.handlers = append(s.handlers, info)
|
||||||
@@ -170,7 +174,7 @@ func (s *Streamer) runHandlerWorker(info *handlerInfo) {
|
|||||||
info.metrics.totalTime += elapsed
|
info.metrics.totalTime += elapsed
|
||||||
|
|
||||||
// Update min time
|
// Update min time
|
||||||
if info.metrics.minTime == 0 || elapsed < info.metrics.minTime {
|
if elapsed < info.metrics.minTime {
|
||||||
info.metrics.minTime = elapsed
|
info.metrics.minTime = elapsed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,8 @@
|
|||||||
color: #333;
|
color: #333;
|
||||||
}
|
}
|
||||||
.container {
|
.container {
|
||||||
max-width: 1200px;
|
width: 90%;
|
||||||
|
max-width: 1600px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
background: white;
|
background: white;
|
||||||
padding: 30px;
|
padding: 30px;
|
||||||
@@ -91,6 +92,7 @@
|
|||||||
.route-table td {
|
.route-table td {
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
border-bottom: 1px solid #e0e0e0;
|
border-bottom: 1px solid #e0e0e0;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
.route-table tr:hover {
|
.route-table tr:hover {
|
||||||
background: #f8f9fa;
|
background: #f8f9fa;
|
||||||
@@ -114,9 +116,13 @@
|
|||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: #666;
|
color: #666;
|
||||||
max-width: 300px;
|
max-width: 600px;
|
||||||
overflow-x: auto;
|
word-wrap: break-word;
|
||||||
white-space: nowrap;
|
white-space: normal !important;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
.as-path .as-link {
|
||||||
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
.age {
|
.age {
|
||||||
color: #7f8c8d;
|
color: #7f8c8d;
|
||||||
@@ -168,7 +174,7 @@
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
.as-path {
|
.as-path {
|
||||||
max-width: 150px;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -234,7 +240,7 @@
|
|||||||
<a href="/as/{{.OriginASN}}" class="as-link">AS{{.OriginASN}}</a>
|
<a href="/as/{{.OriginASN}}" class="as-link">AS{{.OriginASN}}</a>
|
||||||
</td>
|
</td>
|
||||||
<td class="peer-ip">{{.PeerIP}}</td>
|
<td class="peer-ip">{{.PeerIP}}</td>
|
||||||
<td class="as-path">{{range $i, $as := .ASPath}}{{if $i}} → {{end}}{{$as}}{{end}}</td>
|
<td class="as-path">{{range $i, $as := .ASPathWithHandle}}{{if $i}} → {{end}}<a href="/as/{{$as.Number}}" class="as-link">{{if $as.Handle}}{{$as.Handle}}{{else}}AS{{$as.Number}}{{end}}</a>{{end}}</td>
|
||||||
<td class="peer-ip">{{.NextHop}}</td>
|
<td class="peer-ip">{{.NextHop}}</td>
|
||||||
<td>{{.LastUpdated.Format "2006-01-02 15:04:05"}}</td>
|
<td>{{.LastUpdated.Format "2006-01-02 15:04:05"}}</td>
|
||||||
<td class="age">{{.LastUpdated | timeSince}}</td>
|
<td class="age">{{.LastUpdated | timeSince}}</td>
|
||||||
|
|||||||
108
internal/templates/prefix_length.html
Normal file
108
internal/templates/prefix_length.html
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Prefixes with /{{ .MaskLength }} - RouteWatch</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.subtitle {
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
.info-card {
|
||||||
|
background: white;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
background: white;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
th {
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 12px;
|
||||||
|
text-align: left;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
border-bottom: 2px solid #dee2e6;
|
||||||
|
}
|
||||||
|
td {
|
||||||
|
padding: 12px;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
}
|
||||||
|
tr:last-child td {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
tr:hover {
|
||||||
|
background: #f8f9fa;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
color: #0066cc;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
.prefix-link {
|
||||||
|
font-family: 'SF Mono', Monaco, 'Cascadia Mono', 'Roboto Mono', Consolas, 'Courier New', monospace;
|
||||||
|
}
|
||||||
|
.as-link {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.age {
|
||||||
|
color: #666;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
.back-link {
|
||||||
|
display: inline-block;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
color: #0066cc;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<a href="/status" class="back-link">← Back to Status</a>
|
||||||
|
<h1>IPv{{ .IPVersion }} Prefixes with /{{ .MaskLength }}</h1>
|
||||||
|
<p class="subtitle">Showing {{ .Count }} randomly selected prefixes</p>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Prefix</th>
|
||||||
|
<th>Age</th>
|
||||||
|
<th>Origin AS</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{{ range .Prefixes }}
|
||||||
|
<tr>
|
||||||
|
<td><a href="/prefix/{{ .Prefix }}" class="prefix-link">{{ .Prefix }}</a></td>
|
||||||
|
<td class="age">{{ .Age }}</td>
|
||||||
|
<td>
|
||||||
|
<a href="/as/{{ .OriginASN }}" class="as-link">
|
||||||
|
AS{{ .OriginASN }}{{ if .OriginASDescription }} ({{ .OriginASDescription }}){{ end }}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{{ end }}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -231,7 +231,7 @@
|
|||||||
metric.className = 'metric';
|
metric.className = 'metric';
|
||||||
metric.innerHTML = `
|
metric.innerHTML = `
|
||||||
<span class="metric-label">/${item.mask_length}</span>
|
<span class="metric-label">/${item.mask_length}</span>
|
||||||
<span class="metric-value">${formatNumber(item.count)}</span>
|
<a href="/prefixlength/${item.mask_length}" class="metric-value" style="text-decoration: none; color: inherit;">${formatNumber(item.count)}</a>
|
||||||
`;
|
`;
|
||||||
container.appendChild(metric);
|
container.appendChild(metric);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user