check / check (push) Canceled after 0s
The AS Name Search card on the home page said the search was coming
soon. It now asks the new GET /api/v1/search/as?q=<text>, which
searches the AS list built into asinfo, and lists the matches as links
to their AS pages. An empty q answers 400. asinfo.Search now ignores
case and returns its matches in AS number order. asinfo puts each
handle and description in lower case once, when the list loads.
A match can be an AS that the database has no record of. Its AS page
and /api/v1/as/{asn} then take its handle and description from asinfo
and still list its prefixes. They answer 404 only for an AS with no
record, no asinfo entry and no prefix. Empty prefix lists in that JSON
answer are [] rather than null.
Model: opus-5-5
348 lines
10 KiB
HTML
348 lines
10 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>AS{{.ASN.ASN}} - {{.ASN.Handle}} - 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 {
|
|
max-width: 1200px;
|
|
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;
|
|
}
|
|
.subtitle {
|
|
color: #7f8c8d;
|
|
margin-bottom: 30px;
|
|
}
|
|
.info-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(250px, 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;
|
|
}
|
|
.prefix-section {
|
|
margin-top: 30px;
|
|
}
|
|
.prefix-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 20px;
|
|
}
|
|
.prefix-header h2 {
|
|
margin: 0;
|
|
color: #2c3e50;
|
|
}
|
|
.prefix-count {
|
|
background: #e74c3c;
|
|
color: white;
|
|
padding: 5px 12px;
|
|
border-radius: 20px;
|
|
font-size: 14px;
|
|
font-weight: bold;
|
|
}
|
|
.prefix-table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
background: white;
|
|
border: 1px solid #e0e0e0;
|
|
border-radius: 6px;
|
|
overflow: hidden;
|
|
}
|
|
.prefix-table th {
|
|
background: #34495e;
|
|
color: white;
|
|
padding: 12px;
|
|
text-align: left;
|
|
font-weight: 600;
|
|
}
|
|
.prefix-table td {
|
|
padding: 12px;
|
|
border-bottom: 1px solid #e0e0e0;
|
|
}
|
|
.prefix-table tr:hover {
|
|
background: #f8f9fa;
|
|
}
|
|
.prefix-table tr:last-child td {
|
|
border-bottom: none;
|
|
}
|
|
.prefix-link {
|
|
color: #3498db;
|
|
text-decoration: none;
|
|
font-family: monospace;
|
|
}
|
|
.prefix-link:hover {
|
|
text-decoration: underline;
|
|
}
|
|
.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;
|
|
}
|
|
.empty-state {
|
|
text-align: center;
|
|
padding: 40px;
|
|
color: #7f8c8d;
|
|
}
|
|
@media (max-width: 768px) {
|
|
.container {
|
|
padding: 20px;
|
|
}
|
|
.info-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
}
|
|
</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>AS{{.ASN.ASN}}{{if .ASN.Handle}} - {{.ASN.Handle}}{{end}}</h1>
|
|
{{if .ASN.Description}}
|
|
<p class="subtitle">{{.ASN.Description}}</p>
|
|
{{end}}
|
|
|
|
<div class="info-grid">
|
|
<div class="info-card">
|
|
<div class="info-label">Total Prefixes</div>
|
|
<div class="info-value">{{.TotalCount}}</div>
|
|
</div>
|
|
<div class="info-card">
|
|
<div class="info-label">IPv4 Prefixes</div>
|
|
<div class="info-value">{{.IPv4Count}}</div>
|
|
</div>
|
|
<div class="info-card">
|
|
<div class="info-label">IPv6 Prefixes</div>
|
|
<div class="info-value">{{.IPv6Count}}</div>
|
|
</div>
|
|
<div class="info-card">
|
|
<div class="info-label">Peer ASNs</div>
|
|
<div class="info-value">{{.PeerCount}}</div>
|
|
</div>
|
|
{{if not .ASN.FirstSeen.IsZero}}
|
|
<div class="info-card">
|
|
<div class="info-label">First Seen</div>
|
|
<div class="info-value">{{.ASN.FirstSeen.Format "2006-01-02"}}</div>
|
|
</div>
|
|
{{end}}
|
|
</div>
|
|
|
|
{{if .IPv4Prefixes}}
|
|
<div class="prefix-section">
|
|
<div class="prefix-header">
|
|
<h2>IPv4 Prefixes</h2>
|
|
<span class="prefix-count">{{.IPv4Count}}</span>
|
|
</div>
|
|
<table class="prefix-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Prefix</th>
|
|
<th>Mask Length</th>
|
|
<th>Last Updated</th>
|
|
<th>Age</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{{range .IPv4Prefixes}}
|
|
<tr>
|
|
<td><a href="{{.Prefix | prefixURL}}" class="prefix-link">{{.Prefix}}</a></td>
|
|
<td>/{{.MaskLength}}</td>
|
|
<td>{{.LastUpdated.Format "2006-01-02 15:04:05"}}</td>
|
|
<td class="age">{{.LastUpdated | timeSince}}</td>
|
|
</tr>
|
|
{{end}}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{{end}}
|
|
|
|
{{if .IPv6Prefixes}}
|
|
<div class="prefix-section">
|
|
<div class="prefix-header">
|
|
<h2>IPv6 Prefixes</h2>
|
|
<span class="prefix-count">{{.IPv6Count}}</span>
|
|
</div>
|
|
<table class="prefix-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Prefix</th>
|
|
<th>Mask Length</th>
|
|
<th>Last Updated</th>
|
|
<th>Age</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{{range .IPv6Prefixes}}
|
|
<tr>
|
|
<td><a href="{{.Prefix | prefixURL}}" class="prefix-link">{{.Prefix}}</a></td>
|
|
<td>/{{.MaskLength}}</td>
|
|
<td>{{.LastUpdated.Format "2006-01-02 15:04:05"}}</td>
|
|
<td class="age">{{.LastUpdated | timeSince}}</td>
|
|
</tr>
|
|
{{end}}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{{end}}
|
|
|
|
{{if eq .TotalCount 0}}
|
|
<div class="empty-state">
|
|
<p>No prefixes announced by this AS</p>
|
|
</div>
|
|
{{end}}
|
|
|
|
{{if .Peers}}
|
|
<div class="prefix-section">
|
|
<div class="prefix-header">
|
|
<h2>Peer ASNs</h2>
|
|
<span class="prefix-count">{{.PeerCount}}</span>
|
|
</div>
|
|
<table class="prefix-table">
|
|
<thead>
|
|
<tr>
|
|
<th>ASN</th>
|
|
<th>Handle</th>
|
|
<th>Description</th>
|
|
<th>First Seen</th>
|
|
<th>Last Seen</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{{range .Peers}}
|
|
<tr>
|
|
<td><a href="/as/{{.ASN}}" class="prefix-link">AS{{.ASN}}</a></td>
|
|
<td>{{if .Handle}}{{.Handle}}{{else}}-{{end}}</td>
|
|
<td>{{if .Description}}{{.Description}}{{else}}-{{end}}</td>
|
|
<td>{{.FirstSeen.Format "2006-01-02"}}</td>
|
|
<td>{{.LastSeen.Format "2006-01-02"}}</td>
|
|
</tr>
|
|
{{end}}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{{else}}
|
|
<div class="prefix-section">
|
|
<h2>Peer ASNs</h2>
|
|
<div class="empty-state">
|
|
<p>No peering relationships found for this AS</p>
|
|
</div>
|
|
</div>
|
|
{{end}}
|
|
</div>
|
|
</main>
|
|
</body>
|
|
</html> |