Add navbar and home page with search functionality
- Create new home page (/) with overview stats, ASN lookup, AS name search, and IP address lookup with JSON display - Add responsive navbar to all pages with app branding - Navbar shows "routewatch by @sneak" with link to author - Status page accessible via navbar link - Remove redirect from / to /status, serve home page instead
This commit is contained in:
parent
45810e3fc8
commit
4284e923a6
@ -87,10 +87,16 @@ func (s *Server) handleHealthCheck() http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleRoot returns a handler that redirects to /status.
|
// handleIndex returns a handler that serves the home page.
|
||||||
func (s *Server) handleRoot() http.HandlerFunc {
|
func (s *Server) handleIndex() http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, _ *http.Request) {
|
||||||
http.Redirect(w, r, "/status", http.StatusSeeOther)
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
|
||||||
|
tmpl := templates.IndexTemplate()
|
||||||
|
if err := tmpl.Execute(w, nil); err != nil {
|
||||||
|
s.logger.Error("Failed to render index template", "error", err)
|
||||||
|
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -21,7 +21,7 @@ func (s *Server) setupRoutes() {
|
|||||||
r.Use(JSONResponseMiddleware)
|
r.Use(JSONResponseMiddleware)
|
||||||
|
|
||||||
// Routes
|
// Routes
|
||||||
r.Get("/", s.handleRoot())
|
r.Get("/", s.handleIndex())
|
||||||
r.Get("/status", s.handleStatusHTML())
|
r.Get("/status", s.handleStatusHTML())
|
||||||
r.Get("/status.json", JSONValidationMiddleware(s.handleStatusJSON()).ServeHTTP)
|
r.Get("/status.json", JSONValidationMiddleware(s.handleStatusJSON()).ServeHTTP)
|
||||||
r.Get("/.well-known/healthcheck.json", JSONValidationMiddleware(s.handleHealthCheck()).ServeHTTP)
|
r.Get("/.well-known/healthcheck.json", JSONValidationMiddleware(s.handleHealthCheck()).ServeHTTP)
|
||||||
|
|||||||
@ -5,13 +5,74 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>AS{{.ASN.ASN}} - {{.ASN.Handle}} - RouteWatch</title>
|
<title>AS{{.ASN.ASN}} - {{.ASN.Handle}} - RouteWatch</title>
|
||||||
<style>
|
<style>
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
body {
|
body {
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 20px;
|
padding: 0;
|
||||||
background: #f5f5f5;
|
background: #f5f5f5;
|
||||||
color: #333;
|
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;
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.navbar-brand:hover {
|
||||||
|
color: #ecf0f1;
|
||||||
|
}
|
||||||
|
.navbar-brand .by {
|
||||||
|
font-weight: normal;
|
||||||
|
color: #95a5a6;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.navbar-brand .author {
|
||||||
|
color: #3498db;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.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 {
|
.container {
|
||||||
max-width: 1200px;
|
max-width: 1200px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
@ -133,8 +194,20 @@
|
|||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<nav class="navbar">
|
||||||
|
<a href="/" class="navbar-brand">
|
||||||
|
<span>{{appName}}</span>
|
||||||
|
<span class="by">by</span>
|
||||||
|
<a href="{{appAuthorURL}}" class="author">{{appAuthor}}</a>
|
||||||
|
</a>
|
||||||
|
<div class="navbar-links">
|
||||||
|
<a href="/">Home</a>
|
||||||
|
<a href="/status">Status</a>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<main class="main-content">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<a href="/status" class="nav-link">← Back to Status</a>
|
|
||||||
|
|
||||||
<h1>AS{{.ASN.ASN}}{{if .ASN.Handle}} - {{.ASN.Handle}}{{end}}</h1>
|
<h1>AS{{.ASN.ASN}}{{if .ASN.Handle}} - {{.ASN.Handle}}{{end}}</h1>
|
||||||
{{if .ASN.Description}}
|
{{if .ASN.Description}}
|
||||||
@ -266,5 +339,6 @@
|
|||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
</main>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
466
internal/templates/index.html
Normal file
466
internal/templates/index.html
Normal file
@ -0,0 +1,466 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>RouteWatch - BGP Route Monitor</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;
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.navbar-brand:hover {
|
||||||
|
color: #ecf0f1;
|
||||||
|
}
|
||||||
|
.navbar-brand .by {
|
||||||
|
font-weight: normal;
|
||||||
|
color: #95a5a6;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.navbar-brand .author {
|
||||||
|
color: #3498db;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.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 {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 30px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hero section */
|
||||||
|
.hero {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
.hero h1 {
|
||||||
|
font-size: 32px;
|
||||||
|
color: #2c3e50;
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
}
|
||||||
|
.hero p {
|
||||||
|
color: #7f8c8d;
|
||||||
|
font-size: 16px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stats overview */
|
||||||
|
.stats-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
.stat-card {
|
||||||
|
background: white;
|
||||||
|
padding: 24px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.stat-value {
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #2c3e50;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.stat-label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #7f8c8d;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
.stat-card.connected .stat-value {
|
||||||
|
color: #27ae60;
|
||||||
|
}
|
||||||
|
.stat-card.disconnected .stat-value {
|
||||||
|
color: #e74c3c;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Search section */
|
||||||
|
.search-section {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
|
||||||
|
gap: 24px;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
.search-card {
|
||||||
|
background: white;
|
||||||
|
padding: 24px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
.search-card h2 {
|
||||||
|
margin: 0 0 16px 0;
|
||||||
|
font-size: 18px;
|
||||||
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
.search-input-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.search-input-group input {
|
||||||
|
flex: 1;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
outline: none;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
}
|
||||||
|
.search-input-group input:focus {
|
||||||
|
border-color: #3498db;
|
||||||
|
}
|
||||||
|
.search-input-group button {
|
||||||
|
padding: 12px 24px;
|
||||||
|
background: #3498db;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
.search-input-group button:hover {
|
||||||
|
background: #2980b9;
|
||||||
|
}
|
||||||
|
.search-input-group button:disabled {
|
||||||
|
background: #bdc3c7;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
.search-hint {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #95a5a6;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* IP Lookup result */
|
||||||
|
.ip-result {
|
||||||
|
margin-top: 16px;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.ip-result.visible {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.ip-result-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.ip-result-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
.ip-result-header button {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #e74c3c;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.ip-result pre {
|
||||||
|
background: #2c3e50;
|
||||||
|
color: #ecf0f1;
|
||||||
|
padding: 16px;
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow-x: auto;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
margin: 0;
|
||||||
|
max-height: 400px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
.ip-result .error {
|
||||||
|
background: #fee;
|
||||||
|
color: #c00;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.ip-result .loading {
|
||||||
|
color: #7f8c8d;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Footer */
|
||||||
|
.footer {
|
||||||
|
margin-top: 40px;
|
||||||
|
padding: 20px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 -2px 10px rgba(0,0,0,0.05);
|
||||||
|
text-align: center;
|
||||||
|
color: #7f8c8d;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.footer a {
|
||||||
|
color: #3498db;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.footer a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
.footer .separator {
|
||||||
|
margin: 0 10px;
|
||||||
|
color: #ddd;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<nav class="navbar">
|
||||||
|
<a href="/" class="navbar-brand">
|
||||||
|
<span>{{appName}}</span>
|
||||||
|
<span class="by">by</span>
|
||||||
|
<a href="{{appAuthorURL}}" class="author">{{appAuthor}}</a>
|
||||||
|
</a>
|
||||||
|
<div class="navbar-links">
|
||||||
|
<a href="/" class="active">Home</a>
|
||||||
|
<a href="/status">Status</a>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<main class="main-content">
|
||||||
|
<div class="hero">
|
||||||
|
<h1>RouteWatch</h1>
|
||||||
|
<p>Real-time BGP route monitoring powered by RIPE RIS Live</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stats-grid">
|
||||||
|
<div class="stat-card" id="status-card">
|
||||||
|
<div class="stat-value" id="stat-status">-</div>
|
||||||
|
<div class="stat-label">Status</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-value" id="stat-routes">-</div>
|
||||||
|
<div class="stat-label">Live Routes</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-value" id="stat-asns">-</div>
|
||||||
|
<div class="stat-label">Autonomous Systems</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-value" id="stat-prefixes">-</div>
|
||||||
|
<div class="stat-label">Prefixes</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-value" id="stat-peers">-</div>
|
||||||
|
<div class="stat-label">BGP Peers</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-value" id="stat-updates">-</div>
|
||||||
|
<div class="stat-label">Updates/sec</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="search-section">
|
||||||
|
<div class="search-card">
|
||||||
|
<h2>AS Number Lookup</h2>
|
||||||
|
<form id="asn-form" class="search-input-group">
|
||||||
|
<input type="text" id="asn-input" placeholder="e.g., 15169 or AS15169" autocomplete="off">
|
||||||
|
<button type="submit">Lookup</button>
|
||||||
|
</form>
|
||||||
|
<p class="search-hint">Enter an AS number to view its announced prefixes and peers</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="search-card">
|
||||||
|
<h2>AS Name Search</h2>
|
||||||
|
<form id="asname-form" class="search-input-group">
|
||||||
|
<input type="text" id="asname-input" placeholder="e.g., Google, Cloudflare" autocomplete="off">
|
||||||
|
<button type="submit">Search</button>
|
||||||
|
</form>
|
||||||
|
<p class="search-hint">Search for autonomous systems by organization name</p>
|
||||||
|
<div id="asname-results"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="search-card">
|
||||||
|
<h2>IP Address Lookup</h2>
|
||||||
|
<form id="ip-form" class="search-input-group">
|
||||||
|
<input type="text" id="ip-input" placeholder="e.g., 8.8.8.8 or 2001:4860:4860::8888" autocomplete="off">
|
||||||
|
<button type="submit">Lookup</button>
|
||||||
|
</form>
|
||||||
|
<p class="search-hint">Get routing information for any IP address</p>
|
||||||
|
<div id="ip-result" class="ip-result">
|
||||||
|
<div class="ip-result-header">
|
||||||
|
<h3>Result</h3>
|
||||||
|
<button type="button" id="ip-result-close">Clear</button>
|
||||||
|
</div>
|
||||||
|
<pre id="ip-result-content"></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="footer">
|
||||||
|
<span><a href="{{appRepoURL}}">{{appName}}</a> by <a href="{{appAuthorURL}}">{{appAuthor}}</a></span>
|
||||||
|
<span class="separator">|</span>
|
||||||
|
<span>{{appLicense}}</span>
|
||||||
|
<span class="separator">|</span>
|
||||||
|
<span><a href="{{appGitCommitURL}}">{{appGitRevision}}</a></span>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function formatNumber(num) {
|
||||||
|
if (num >= 1000000) {
|
||||||
|
return (num / 1000000).toFixed(1) + 'M';
|
||||||
|
} else if (num >= 1000) {
|
||||||
|
return (num / 1000).toFixed(1) + 'K';
|
||||||
|
}
|
||||||
|
return num.toLocaleString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch and display stats
|
||||||
|
function updateStats() {
|
||||||
|
fetch('/api/v1/stats')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(response => {
|
||||||
|
if (response.status !== 'ok') return;
|
||||||
|
const data = response.data;
|
||||||
|
|
||||||
|
const statusCard = document.getElementById('status-card');
|
||||||
|
const statusEl = document.getElementById('stat-status');
|
||||||
|
statusEl.textContent = data.connected ? 'Connected' : 'Disconnected';
|
||||||
|
statusCard.className = 'stat-card ' + (data.connected ? 'connected' : 'disconnected');
|
||||||
|
|
||||||
|
document.getElementById('stat-routes').textContent = formatNumber(data.live_routes);
|
||||||
|
document.getElementById('stat-asns').textContent = formatNumber(data.asns);
|
||||||
|
document.getElementById('stat-prefixes').textContent = formatNumber(data.prefixes);
|
||||||
|
|
||||||
|
if (data.stream) {
|
||||||
|
document.getElementById('stat-peers').textContent = formatNumber(data.stream.bgp_peer_count);
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalUpdates = data.ipv4_updates_per_sec + data.ipv6_updates_per_sec;
|
||||||
|
document.getElementById('stat-updates').textContent = totalUpdates.toFixed(1);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
document.getElementById('stat-status').textContent = 'Error';
|
||||||
|
document.getElementById('status-card').className = 'stat-card disconnected';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ASN lookup
|
||||||
|
document.getElementById('asn-form').addEventListener('submit', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
let asn = document.getElementById('asn-input').value.trim();
|
||||||
|
// Remove 'AS' prefix if present
|
||||||
|
asn = asn.replace(/^AS/i, '');
|
||||||
|
if (asn && /^\d+$/.test(asn)) {
|
||||||
|
window.location.href = '/as/' + asn;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// AS name search
|
||||||
|
document.getElementById('asname-form').addEventListener('submit', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const query = document.getElementById('asname-input').value.trim();
|
||||||
|
if (!query) return;
|
||||||
|
|
||||||
|
const resultsDiv = document.getElementById('asname-results');
|
||||||
|
resultsDiv.innerHTML = '<p class="loading" style="color: #7f8c8d; margin-top: 12px;">Searching...</p>';
|
||||||
|
|
||||||
|
// Use a simple client-side search against the asinfo data
|
||||||
|
// For now, redirect to AS page if it looks like an ASN
|
||||||
|
if (/^\d+$/.test(query)) {
|
||||||
|
window.location.href = '/as/' + query;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show a message that server-side search is coming
|
||||||
|
resultsDiv.innerHTML = '<p style="color: #7f8c8d; margin-top: 12px; font-size: 13px;">AS name search coming soon. For now, try an AS number.</p>';
|
||||||
|
});
|
||||||
|
|
||||||
|
// IP lookup
|
||||||
|
document.getElementById('ip-form').addEventListener('submit', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const ip = document.getElementById('ip-input').value.trim();
|
||||||
|
if (!ip) return;
|
||||||
|
|
||||||
|
const resultDiv = document.getElementById('ip-result');
|
||||||
|
const contentEl = document.getElementById('ip-result-content');
|
||||||
|
|
||||||
|
resultDiv.classList.add('visible');
|
||||||
|
contentEl.className = '';
|
||||||
|
contentEl.textContent = 'Loading...';
|
||||||
|
contentEl.classList.add('loading');
|
||||||
|
|
||||||
|
fetch('/ip/' + encodeURIComponent(ip))
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(response => {
|
||||||
|
contentEl.classList.remove('loading');
|
||||||
|
if (response.status === 'error') {
|
||||||
|
contentEl.className = 'error';
|
||||||
|
contentEl.textContent = 'Error: ' + response.error.msg;
|
||||||
|
} else {
|
||||||
|
contentEl.className = '';
|
||||||
|
contentEl.textContent = JSON.stringify(response.data, null, 2);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
contentEl.classList.remove('loading');
|
||||||
|
contentEl.className = 'error';
|
||||||
|
contentEl.textContent = 'Error: ' + error.message;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close IP result
|
||||||
|
document.getElementById('ip-result-close').addEventListener('click', function() {
|
||||||
|
document.getElementById('ip-result').classList.remove('visible');
|
||||||
|
document.getElementById('ip-input').value = '';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initial load and refresh stats every 5 seconds
|
||||||
|
updateStats();
|
||||||
|
setInterval(updateStats, 5000);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -5,13 +5,74 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>{{.Prefix}} - RouteWatch</title>
|
<title>{{.Prefix}} - RouteWatch</title>
|
||||||
<style>
|
<style>
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
body {
|
body {
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 20px;
|
padding: 0;
|
||||||
background: #f5f5f5;
|
background: #f5f5f5;
|
||||||
color: #333;
|
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;
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.navbar-brand:hover {
|
||||||
|
color: #ecf0f1;
|
||||||
|
}
|
||||||
|
.navbar-brand .by {
|
||||||
|
font-weight: normal;
|
||||||
|
color: #95a5a6;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.navbar-brand .author {
|
||||||
|
color: #3498db;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.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 {
|
.container {
|
||||||
width: 90%;
|
width: 90%;
|
||||||
max-width: 1600px;
|
max-width: 1600px;
|
||||||
@ -180,9 +241,20 @@
|
|||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<nav class="navbar">
|
||||||
<a href="/status" class="nav-link">← Back to Status</a>
|
<a href="/" class="navbar-brand">
|
||||||
|
<span>{{appName}}</span>
|
||||||
|
<span class="by">by</span>
|
||||||
|
<a href="{{appAuthorURL}}" class="author">{{appAuthor}}</a>
|
||||||
|
</a>
|
||||||
|
<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>
|
<h1>{{.Prefix}}</h1>
|
||||||
<p class="subtitle">IPv{{.IPVersion}} Prefix{{if .MaskLength}} • /{{.MaskLength}}{{end}}</p>
|
<p class="subtitle">IPv{{.IPVersion}} Prefix{{if .MaskLength}} • /{{.MaskLength}}{{end}}</p>
|
||||||
|
|
||||||
@ -255,5 +327,6 @@
|
|||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
</main>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@ -5,12 +5,74 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Prefixes with /{{ .MaskLength }} - RouteWatch</title>
|
<title>Prefixes with /{{ .MaskLength }} - RouteWatch</title>
|
||||||
<style>
|
<style>
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
body {
|
body {
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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;
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.navbar-brand:hover {
|
||||||
|
color: #ecf0f1;
|
||||||
|
}
|
||||||
|
.navbar-brand .by {
|
||||||
|
font-weight: normal;
|
||||||
|
color: #95a5a6;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.navbar-brand .author {
|
||||||
|
color: #3498db;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.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 {
|
||||||
max-width: 1200px;
|
max-width: 1200px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
background: #f5f5f5;
|
|
||||||
}
|
}
|
||||||
h1 {
|
h1 {
|
||||||
color: #333;
|
color: #333;
|
||||||
@ -78,7 +140,19 @@
|
|||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<a href="/status" class="back-link">← Back to Status</a>
|
<nav class="navbar">
|
||||||
|
<a href="/" class="navbar-brand">
|
||||||
|
<span>{{appName}}</span>
|
||||||
|
<span class="by">by</span>
|
||||||
|
<a href="{{appAuthorURL}}" class="author">{{appAuthor}}</a>
|
||||||
|
</a>
|
||||||
|
<div class="navbar-links">
|
||||||
|
<a href="/">Home</a>
|
||||||
|
<a href="/status">Status</a>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<main class="main-content">
|
||||||
<h1>IPv{{ .IPVersion }} Prefixes with /{{ .MaskLength }}</h1>
|
<h1>IPv{{ .IPVersion }} Prefixes with /{{ .MaskLength }}</h1>
|
||||||
<p class="subtitle">Showing {{ .Count }} randomly selected prefixes</p>
|
<p class="subtitle">Showing {{ .Count }} randomly selected prefixes</p>
|
||||||
|
|
||||||
@ -104,5 +178,6 @@
|
|||||||
{{ end }}
|
{{ end }}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
</main>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@ -5,12 +5,74 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>RouteWatch Status</title>
|
<title>RouteWatch Status</title>
|
||||||
<style>
|
<style>
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
body {
|
body {
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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;
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.navbar-brand:hover {
|
||||||
|
color: #ecf0f1;
|
||||||
|
}
|
||||||
|
.navbar-brand .by {
|
||||||
|
font-weight: normal;
|
||||||
|
color: #95a5a6;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.navbar-brand .author {
|
||||||
|
color: #3498db;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.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 {
|
||||||
max-width: 1200px;
|
max-width: 1200px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
background: #f5f5f5;
|
|
||||||
}
|
}
|
||||||
h1 {
|
h1 {
|
||||||
color: #333;
|
color: #333;
|
||||||
@ -96,6 +158,19 @@
|
|||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<nav class="navbar">
|
||||||
|
<a href="/" class="navbar-brand">
|
||||||
|
<span>{{appName}}</span>
|
||||||
|
<span class="by">by</span>
|
||||||
|
<a href="{{appAuthorURL}}" class="author">{{appAuthor}}</a>
|
||||||
|
</a>
|
||||||
|
<div class="navbar-links">
|
||||||
|
<a href="/">Home</a>
|
||||||
|
<a href="/status" class="active">Status</a>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<main class="main-content">
|
||||||
<h1>RouteWatch Status</h1>
|
<h1>RouteWatch Status</h1>
|
||||||
<div id="error" class="error" style="display: none;"></div>
|
<div id="error" class="error" style="display: none;"></div>
|
||||||
<div class="status-grid">
|
<div class="status-grid">
|
||||||
@ -542,6 +617,7 @@
|
|||||||
updateStatus();
|
updateStatus();
|
||||||
setInterval(updateStatus, 2000);
|
setInterval(updateStatus, 2000);
|
||||||
</script>
|
</script>
|
||||||
|
</main>
|
||||||
|
|
||||||
<footer class="footer">
|
<footer class="footer">
|
||||||
<span><a href="{{appRepoURL}}">{{appName}}</a> by <a href="{{appAuthorURL}}">{{appAuthor}}</a></span>
|
<span><a href="{{appRepoURL}}">{{appName}}</a> by <a href="{{appAuthorURL}}">{{appAuthor}}</a></span>
|
||||||
|
|||||||
@ -12,6 +12,9 @@ import (
|
|||||||
"git.eeqj.de/sneak/routewatch/internal/version"
|
"git.eeqj.de/sneak/routewatch/internal/version"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
//go:embed index.html
|
||||||
|
var indexHTML string
|
||||||
|
|
||||||
//go:embed status.html
|
//go:embed status.html
|
||||||
var statusHTML string
|
var statusHTML string
|
||||||
|
|
||||||
@ -26,6 +29,8 @@ var prefixLengthHTML string
|
|||||||
|
|
||||||
// Templates contains all parsed templates
|
// Templates contains all parsed templates
|
||||||
type Templates struct {
|
type Templates struct {
|
||||||
|
// Index is the template for the home page
|
||||||
|
Index *template.Template
|
||||||
// Status is the template for the main status page
|
// Status is the template for the main status page
|
||||||
Status *template.Template
|
Status *template.Template
|
||||||
// ASDetail is the template for displaying AS (Autonomous System) details
|
// ASDetail is the template for displaying AS (Autonomous System) details
|
||||||
@ -116,6 +121,12 @@ func initTemplates() {
|
|||||||
"appGitCommitURL": func() string { return version.CommitURL() },
|
"appGitCommitURL": func() string { return version.CommitURL() },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Parse index template
|
||||||
|
defaultTemplates.Index, err = template.New("index").Funcs(funcs).Parse(indexHTML)
|
||||||
|
if err != nil {
|
||||||
|
panic("failed to parse index template: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
// Parse status template
|
// Parse status template
|
||||||
defaultTemplates.Status, err = template.New("status").Funcs(funcs).Parse(statusHTML)
|
defaultTemplates.Status, err = template.New("status").Funcs(funcs).Parse(statusHTML)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -148,6 +159,11 @@ func Get() *Templates {
|
|||||||
return defaultTemplates
|
return defaultTemplates
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IndexTemplate returns the parsed index template
|
||||||
|
func IndexTemplate() *template.Template {
|
||||||
|
return Get().Index
|
||||||
|
}
|
||||||
|
|
||||||
// StatusTemplate returns the parsed status template
|
// StatusTemplate returns the parsed status template
|
||||||
func StatusTemplate() *template.Template {
|
func StatusTemplate() *template.Template {
|
||||||
return Get().Status
|
return Get().Status
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user