check / check (push) Failing after 2m14s
Hold the 100,000-row cap after a failed write's rows are put back, dropping and counting the oldest rows past it. Stop counts the unwritten origin changes. The prefix handler's per-write origin change limit is a field, so a test checks that one write leaves the rest. The server and README now say where origin changes come from. README, TODO and schema say 72 hours or 500 rows, whichever keeps more. The prefix page and TODO list the origin change columns and say what the peer count counts. The page takes its 5,000-row note from the constant. Model: deepseek-v4-pro
382 lines
11 KiB
HTML
382 lines
11 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 nonce="{{.CSPNonce}}">
|
|
* {
|
|
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;
|
|
}
|
|
.origin-item .peer-count {
|
|
color: #7f8c8d;
|
|
margin-left: 10px;
|
|
}
|
|
.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">
|
|
{{if .ASN}}
|
|
<a href="/as/{{.ASN}}" class="as-link">AS{{.ASN}}</a>
|
|
{{if .Handle}} ({{.Handle}}){{end}}
|
|
{{if .Description}} {{.Description}}{{end}}
|
|
{{else}}
|
|
No known origin
|
|
{{end}}
|
|
<span class="peer-count">{{.PeerCount}} peer{{if ne .PeerCount 1}}s{{end}}</span>
|
|
</div>
|
|
{{end}}
|
|
</div>
|
|
</div>
|
|
{{end}}
|
|
|
|
{{if .OriginChanges}}
|
|
<div class="origins-section">
|
|
<h3>Origin Changes</h3>
|
|
{{if .MoreOriginChanges}}
|
|
<p>There are more than {{.MaxOriginChanges}} changes. Showing the newest {{.MaxOriginChanges}}.</p>
|
|
{{end}}
|
|
<table class="route-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Time (UTC)</th>
|
|
<th>Change</th>
|
|
<th>Origin AS</th>
|
|
<th>Peers</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{{range .OriginChanges}}
|
|
<tr>
|
|
<td>{{.Time.Format "2006-01-02 15:04:05"}}</td>
|
|
<td>{{if .Announced}}Announced{{else}}Withdrawn{{end}}</td>
|
|
<td>
|
|
{{if .OriginASN}}
|
|
<a href="/as/{{.OriginASN}}" class="as-link">AS{{.OriginASN}}</a>
|
|
{{else}}
|
|
No known origin
|
|
{{end}}
|
|
</td>
|
|
<td>{{.PeerCount}}</td>
|
|
</tr>
|
|
{{end}}
|
|
</tbody>
|
|
</table>
|
|
</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>
|
|
{{if .OriginASN}}
|
|
<a href="/as/{{.OriginASN}}" class="as-link">AS{{.OriginASN}}</a>
|
|
{{else}}
|
|
No known origin
|
|
{{end}}
|
|
</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>{{if not .LastUpdated.IsZero}}{{.LastUpdated.Format "2006-01-02 15:04:05"}}{{end}}</td>
|
|
<td class="age">{{if not .LastUpdated.IsZero}}{{.LastUpdated | timeSince}}{{end}}</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> |