check / check (push) Waiting to run
The prefix page looked up each origin AS's details only for its handle and description, and that lookup also reads every live route the AS originates. GetPrefixDetailsContext already selects the handle and description; it now returns them keyed by AS number, and the page uses them instead. The origin list was handed each description but never rendered it; it now shows it after the handle. Model: opus-5-5
335 lines
9.5 KiB
HTML
335 lines
9.5 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>{{.Prefix}} - RouteWatch</title>
|
|
<style>
|
|
* {
|
|
box-sizing: border-box;
|
|
}
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
margin: 0;
|
|
padding: 0;
|
|
background: #f5f5f5;
|
|
color: #333;
|
|
}
|
|
|
|
/* Navbar styles */
|
|
.navbar {
|
|
background: #2c3e50;
|
|
padding: 0 20px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
height: 56px;
|
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
}
|
|
.navbar-brand {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
font-size: 18px;
|
|
font-weight: 600;
|
|
}
|
|
.navbar-brand a {
|
|
color: white;
|
|
text-decoration: none;
|
|
}
|
|
.navbar-brand a:hover {
|
|
color: #ecf0f1;
|
|
}
|
|
.navbar-brand .by {
|
|
font-weight: normal;
|
|
color: #95a5a6;
|
|
font-size: 14px;
|
|
}
|
|
.navbar-brand .author {
|
|
color: #3498db;
|
|
font-weight: normal;
|
|
}
|
|
.navbar-brand .author:hover {
|
|
text-decoration: underline;
|
|
}
|
|
.navbar-links {
|
|
display: flex;
|
|
gap: 20px;
|
|
}
|
|
.navbar-links a {
|
|
color: #ecf0f1;
|
|
text-decoration: none;
|
|
font-size: 14px;
|
|
padding: 8px 12px;
|
|
border-radius: 4px;
|
|
transition: background-color 0.2s;
|
|
}
|
|
.navbar-links a:hover {
|
|
background: rgba(255,255,255,0.1);
|
|
}
|
|
.navbar-links a.active {
|
|
background: rgba(255,255,255,0.15);
|
|
}
|
|
|
|
/* Main content */
|
|
.main-content {
|
|
padding: 20px;
|
|
}
|
|
.container {
|
|
width: 90%;
|
|
max-width: 1600px;
|
|
margin: 0 auto;
|
|
background: white;
|
|
padding: 30px;
|
|
border-radius: 8px;
|
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
}
|
|
h1 {
|
|
margin: 0 0 10px 0;
|
|
color: #2c3e50;
|
|
font-family: monospace;
|
|
font-size: 28px;
|
|
}
|
|
.subtitle {
|
|
color: #7f8c8d;
|
|
margin-bottom: 30px;
|
|
}
|
|
.info-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
gap: 20px;
|
|
margin-bottom: 30px;
|
|
}
|
|
.info-card {
|
|
background: #f8f9fa;
|
|
padding: 20px;
|
|
border-radius: 6px;
|
|
border-left: 4px solid #3498db;
|
|
}
|
|
.info-label {
|
|
font-size: 14px;
|
|
color: #7f8c8d;
|
|
margin-bottom: 5px;
|
|
}
|
|
.info-value {
|
|
font-size: 24px;
|
|
font-weight: bold;
|
|
color: #2c3e50;
|
|
}
|
|
.routes-section {
|
|
margin-top: 30px;
|
|
}
|
|
.routes-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 20px;
|
|
}
|
|
.routes-header h2 {
|
|
margin: 0;
|
|
color: #2c3e50;
|
|
}
|
|
.route-count {
|
|
background: #e74c3c;
|
|
color: white;
|
|
padding: 5px 12px;
|
|
border-radius: 20px;
|
|
font-size: 14px;
|
|
font-weight: bold;
|
|
}
|
|
.route-table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
background: white;
|
|
border: 1px solid #e0e0e0;
|
|
border-radius: 6px;
|
|
overflow: hidden;
|
|
}
|
|
.route-table th {
|
|
background: #34495e;
|
|
color: white;
|
|
padding: 12px;
|
|
text-align: left;
|
|
font-weight: 600;
|
|
}
|
|
.route-table td {
|
|
padding: 12px;
|
|
border-bottom: 1px solid #e0e0e0;
|
|
white-space: nowrap;
|
|
}
|
|
.route-table tr:hover {
|
|
background: #f8f9fa;
|
|
}
|
|
.route-table tr:last-child td {
|
|
border-bottom: none;
|
|
}
|
|
.as-link {
|
|
color: #3498db;
|
|
text-decoration: none;
|
|
}
|
|
.as-link:hover {
|
|
text-decoration: underline;
|
|
}
|
|
.peer-ip {
|
|
font-family: monospace;
|
|
font-size: 14px;
|
|
color: #555;
|
|
}
|
|
.as-path {
|
|
font-family: monospace;
|
|
font-size: 13px;
|
|
color: #666;
|
|
max-width: 600px;
|
|
word-wrap: break-word;
|
|
white-space: normal !important;
|
|
line-height: 1.5;
|
|
}
|
|
.as-path .as-link {
|
|
font-weight: 600;
|
|
}
|
|
.age {
|
|
color: #7f8c8d;
|
|
font-size: 14px;
|
|
}
|
|
.nav-link {
|
|
display: inline-block;
|
|
margin-bottom: 20px;
|
|
color: #3498db;
|
|
text-decoration: none;
|
|
}
|
|
.nav-link:hover {
|
|
text-decoration: underline;
|
|
}
|
|
.origins-section {
|
|
margin-top: 30px;
|
|
background: #f8f9fa;
|
|
padding: 20px;
|
|
border-radius: 6px;
|
|
}
|
|
.origins-section h3 {
|
|
margin-top: 0;
|
|
color: #2c3e50;
|
|
}
|
|
.origin-list {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 10px;
|
|
}
|
|
.origin-item {
|
|
background: white;
|
|
padding: 10px 15px;
|
|
border-radius: 4px;
|
|
border: 1px solid #e0e0e0;
|
|
}
|
|
.empty-state {
|
|
text-align: center;
|
|
padding: 40px;
|
|
color: #7f8c8d;
|
|
}
|
|
@media (max-width: 768px) {
|
|
.container {
|
|
padding: 20px;
|
|
}
|
|
.info-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
.route-table {
|
|
font-size: 14px;
|
|
}
|
|
.as-path {
|
|
max-width: 100%;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<nav class="navbar">
|
|
<div class="navbar-brand">
|
|
<a href="/">routewatch</a>
|
|
<span class="by">by</span>
|
|
<a href="https://sneak.berlin" class="author">@sneak</a>
|
|
</div>
|
|
<div class="navbar-links">
|
|
<a href="/">Home</a>
|
|
<a href="/status">Status</a>
|
|
</div>
|
|
</nav>
|
|
|
|
<main class="main-content">
|
|
<div class="container">
|
|
<h1>{{.Prefix}}</h1>
|
|
<p class="subtitle">IPv{{.IPVersion}} Prefix{{if .MaskLength}} • /{{.MaskLength}}{{end}}</p>
|
|
|
|
<div class="info-grid">
|
|
<div class="info-card">
|
|
<div class="info-label">Seen from Peers</div>
|
|
<div class="info-value">{{.PeerCount}}</div>
|
|
</div>
|
|
<div class="info-card">
|
|
<div class="info-label">Origin ASNs</div>
|
|
<div class="info-value">{{.OriginCount}}</div>
|
|
</div>
|
|
<div class="info-card">
|
|
<div class="info-label">IP Version</div>
|
|
<div class="info-value">IPv{{.IPVersion}}</div>
|
|
</div>
|
|
</div>
|
|
|
|
{{if .Origins}}
|
|
<div class="origins-section">
|
|
<h3>Origin ASNs</h3>
|
|
<div class="origin-list">
|
|
{{range .Origins}}
|
|
<div class="origin-item">
|
|
<a href="/as/{{.ASN}}" class="as-link">AS{{.ASN}}</a>
|
|
{{if .Handle}} ({{.Handle}}){{end}}
|
|
{{if .Description}} {{.Description}}{{end}}
|
|
<span style="color: #7f8c8d; margin-left: 10px;">{{.PeerCount}} peer{{if ne .PeerCount 1}}s{{end}}</span>
|
|
</div>
|
|
{{end}}
|
|
</div>
|
|
</div>
|
|
{{end}}
|
|
|
|
{{if .Routes}}
|
|
<div class="routes-section">
|
|
<div class="routes-header">
|
|
<h2>Route Details</h2>
|
|
<span class="route-count">{{.PeerCount}} route{{if ne .PeerCount 1}}s{{end}}</span>
|
|
</div>
|
|
<table class="route-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Origin AS</th>
|
|
<th>Peer IP</th>
|
|
<th>AS Path</th>
|
|
<th>Next Hop</th>
|
|
<th>Last Updated</th>
|
|
<th>Age</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{{range .Routes}}
|
|
<tr>
|
|
<td>
|
|
<a href="/as/{{.OriginASN}}" class="as-link">AS{{.OriginASN}}</a>
|
|
</td>
|
|
<td class="peer-ip">{{.PeerIP}}</td>
|
|
<td class="as-path">{{range $i, $as := .ASPathWithHandle}}{{if $i}} → {{end}}<a href="/as/{{$as.ASN}}" class="as-link">{{if $as.Handle}}{{$as.Handle}}{{else}}AS{{$as.ASN}}{{end}}</a>{{end}}</td>
|
|
<td class="peer-ip">{{.NextHop}}</td>
|
|
<td>{{.LastUpdated.Format "2006-01-02 15:04:05"}}</td>
|
|
<td class="age">{{.LastUpdated | timeSince}}</td>
|
|
</tr>
|
|
{{end}}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{{else}}
|
|
<div class="empty-state">
|
|
<p>No routes found for this prefix</p>
|
|
</div>
|
|
{{end}}
|
|
</div>
|
|
</main>
|
|
</body>
|
|
</html> |