package main
const dashboardTmpl = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>status dashboard</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { background: #111; color: #ccc; font-family: monospace; font-size: 14px; padding: 20px; }
h1 { color: #fff; margin-bottom: 20px; font-size: 22px; }
h2 { color: #aaa; margin: 30px 0 12px; font-size: 16px; text-transform: uppercase; letter-spacing: 1px; }
table { width: 100%; border-collapse: collapse; margin-bottom: 20px; }
th { background: #1e1e1e; color: #888; text-align: left; padding: 8px 10px; border-bottom: 1px solid #333; }
td { padding: 8px 10px; border-bottom: 1px solid #222; vertical-align: middle; }
tr:hover td { background: #161616; }
a { color: #5af; text-decoration: none; }
a:hover { text-decoration: underline; }
.badge { display: inline-block; padding: 2px 8px; border-radius: 3px; font-size: 12px; font-weight: bold; }
.badge-ok { background: #1a3a1a; color: #4f4; }
.badge-warn { background: #3a2a00; color: #fa0; }
.badge-err { background: #3a1010; color: #f44; }
.badge-disabled{ background: #222; color: #666; }
.badge-nodata { background: #1a1a2a; color: #66a; }
.badge-online { background: #1a3a1a; color: #4f4; }
.badge-offline { background: #222; color: #666; }
.uptime-bar { display: inline-block; width: 80px; height: 10px; background: #2a2a2a; border-radius: 3px; vertical-align: middle; margin-right: 6px; overflow: hidden; }
.uptime-fill { height: 100%; border-radius: 3px; }
.fill-ok { background: #4f4; }
.fill-warn { background: #fa0; }
.fill-err { background: #f44; }
.fill-none { background: #444; }
button, input[type=submit] { background: #222; color: #aaa; border: 1px solid #444; padding: 4px 10px; cursor: pointer; font-family: monospace; font-size: 13px; border-radius: 3px; }
button:hover, input[type=submit]:hover { background: #2a2a2a; color: #fff; border-color: #666; }
button.danger { border-color: #633; color: #f66; }
button.danger:hover { background: #2a1010; }
.form-row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin-top: 10px; }
input[type=text], input[type=url] { background: #1a1a1a; color: #ccc; border: 1px solid #444; padding: 5px 8px; font-family: monospace; font-size: 13px; border-radius: 3px; }
input[type=text]:focus, input[type=url]:focus { outline: none; border-color: #5af; }
.pct { font-size: 12px; color: #888; }
form { display: inline; }
</style>
</head>
<body>
<h1>status</h1>
<h2>Servers (24h uptime)</h2>
<table>
<tr>
<th>URL</th>
<th>Name</th>
<th>Anon Name</th>
<th>OK Checks</th>
<th>Uptime</th>
<th>Avg TTFB</th>
<th>Status</th>
<th>Actions</th>
</tr>
{{range .Servers}}
<tr>
<td><a href="/chart/{{.ID}}">{{.URL}}</a></td>
<td>{{.Name}}</td>
<td style="color:#888">{{if .AnonName}}{{.AnonName}}{{else}}<span style="color:#444">—</span>{{end}}</td>
<td>{{.Successes}}</td>
<td>
{{if gt .Total 0}}
<span class="uptime-bar"><span class="uptime-fill {{if ge .UptimePct 99.0}}fill-ok{{else if ge .UptimePct 95.0}}fill-warn{{else}}fill-err{{end}}" style="width:{{printf "%.0f" .UptimePct}}%"></span></span>
<span class="pct">{{printf "%.1f" .UptimePct}}%</span>
{{else}}
<span class="uptime-bar"><span class="uptime-fill fill-none" style="width:0%"></span></span>
<span class="pct">—</span>
{{end}}
</td>
<td style="color:{{if gt .AvgTTFBMS 0}}{{if le .AvgTTFBMS 200}}#5cb85c{{else if le .AvgTTFBMS 500}}#e6a817{{else}}#d9534f{{end}}{{else}}#444{{end}}">
{{if gt .AvgTTFBMS 0}}{{.AvgTTFBMS}} ms{{else}}—{{end}}
</td>
<td>
{{if eq .UptimeClass "ok"}}<span class="badge badge-ok">OK</span>
{{else if eq .UptimeClass "warn"}}<span class="badge badge-warn">DEGRADED</span>
{{else if eq .UptimeClass "err"}}<span class="badge badge-err">DOWN</span>
{{else if eq .UptimeClass "disabled"}}<span class="badge badge-disabled">DISABLED</span>
{{else}}<span class="badge badge-nodata">NO DATA</span>{{end}}
</td>
<td>
{{if .Active}}
<form method="POST" action="/servers/toggle">
<input type="hidden" name="id" value="{{.ID}}">
<input type="hidden" name="active" value="0">
<button type="submit">Pause</button>
</form>
{{else}}
<form method="POST" action="/servers/toggle">
<input type="hidden" name="id" value="{{.ID}}">
<input type="hidden" name="active" value="1">
<button type="submit">Enable</button>
</form>
{{end}}
<form method="POST" action="/servers/delete" onsubmit="return confirm('Delete server {{.URL}}?')">
<input type="hidden" name="id" value="{{.ID}}">
<button type="submit" class="danger">Delete</button>
</form>
</td>
</tr>
{{else}}
<tr><td colspan="8" style="color:#555;text-align:center;padding:20px">No servers configured</td></tr>
{{end}}
</table>
<form method="POST" action="/servers/add">
<div class="form-row">
<input type="url" name="url" placeholder="https://example.com" required style="width:300px">
<input type="text" name="name" placeholder="Name (optional)" style="width:180px">
<input type="text" name="anon_name" placeholder="Anon name (public)" style="width:180px">
</div>
<div class="form-row">
<input type="number" name="expected_code" value="200" min="100" max="599" style="width:90px" title="Expected HTTP code">
<input type="text" name="expected_body" placeholder="Expected body substring (optional)" style="width:260px">
<label style="color:#888"><input type="checkbox" name="insecure" value="1"> insecure TLS</label>
<input type="submit" value="Add Server">
</div>
</form>
<h2>Agents</h2>
<table>
<tr>
<th>Name</th>
<th>Anon Name</th>
<th>IP</th>
<th>Last Seen</th>
<th>Status</th>
<th>Actions</th>
</tr>
{{range .Agents}}
<tr>
<td>{{.Name}}</td>
<td style="color:#888">{{if .AnonName}}{{.AnonName}}{{else}}<span style="color:#444">—</span>{{end}}</td>
<td>{{if .LastIP}}{{.LastIP}}{{else}}<span style="color:#555">—</span>{{end}}</td>
<td>{{.LastSeenStr}}</td>
<td>
{{if .Online}}<span class="badge badge-online">ONLINE</span>
{{else}}<span class="badge badge-offline">OFFLINE</span>{{end}}
</td>
<td>
<form method="POST" action="/agents/delete" onsubmit="return confirm('Delete agent {{.Name}}?')">
<input type="hidden" name="id" value="{{.ID}}">
<button type="submit" class="danger">Delete</button>
</form>
</td>
</tr>
{{else}}
<tr><td colspan="6" style="color:#555;text-align:center;padding:20px">No agents registered</td></tr>
{{end}}
</table>
<form method="POST" action="/agents/add">
<div class="form-row">
<input type="text" name="name" placeholder="Agent name" required style="width:200px">
<input type="text" name="anon_name" placeholder="Anon name (public)" style="width:180px">
<input type="submit" value="Add Agent">
</div>
</form>
</body>
</html>`
const tokenTmpl = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Agent Created — status</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { background: #111; color: #ccc; font-family: monospace; font-size: 14px; padding: 40px; }
h1 { color: #fff; margin-bottom: 24px; }
.token-box { background: #1a1a1a; border: 1px solid #444; border-radius: 6px; padding: 24px; max-width: 640px; }
.token-label { color: #888; margin-bottom: 8px; }
.token-value { font-size: 15px; color: #5af; word-break: break-all; background: #0d0d0d; padding: 12px; border-radius: 4px; border: 1px solid #333; margin-bottom: 16px; }
.warning { background: #2a1a00; border: 1px solid #664400; color: #fa0; padding: 12px; border-radius: 4px; margin-bottom: 20px; line-height: 1.5; }
a { color: #5af; text-decoration: none; }
a:hover { text-decoration: underline; }
.agent-name { color: #fff; font-size: 16px; margin-bottom: 20px; }
</style>
</head>
<body>
<h1>Agent Created</h1>
<div class="token-box">
<div class="agent-name">Agent: {{index . "Name"}}</div>
<div class="warning">
⚠ Save this token now — it will NOT be shown again.<br>
Set it as the AGENT_TOKEN environment variable on the agent host.
</div>
<div class="token-label">Agent Token:</div>
<div class="token-value">{{index . "Token"}}</div>
<a href="/">← Back to dashboard</a>
</div>
</body>
</html>`
const publicTmpl = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Status</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { background: #111; color: #ccc; font-family: monospace; font-size: 14px; padding: 20px; }
h1 { color: #fff; margin-bottom: 20px; font-size: 22px; }
h2 { color: #aaa; margin: 30px 0 12px; font-size: 16px; text-transform: uppercase; letter-spacing: 1px; }
table { width: 100%; border-collapse: collapse; margin-bottom: 20px; }
th { background: #1e1e1e; color: #888; text-align: left; padding: 8px 10px; border-bottom: 1px solid #333; }
td { padding: 8px 10px; border-bottom: 1px solid #222; vertical-align: middle; }
tr:hover td { background: #161616; }
.badge { display: inline-block; padding: 2px 8px; border-radius: 3px; font-size: 12px; font-weight: bold; }
.badge-ok { background: #1a3a1a; color: #4f4; }
.badge-warn { background: #3a2a00; color: #fa0; }
.badge-err { background: #3a1010; color: #f44; }
.badge-nodata { background: #1a1a2a; color: #66a; }
.badge-online { background: #1a3a1a; color: #4f4; }
.badge-offline { background: #222; color: #666; }
.uptime-bar { display: inline-block; width: 80px; height: 10px; background: #2a2a2a; border-radius: 3px; vertical-align: middle; margin-right: 6px; overflow: hidden; }
.uptime-fill { height: 100%; border-radius: 3px; }
.fill-ok { background: #4f4; }
.fill-warn { background: #fa0; }
.fill-err { background: #f44; }
.fill-none { background: #444; }
.pct { font-size: 12px; color: #888; }
</style>
</head>
<body>
<h1>Status</h1>
<h2>Services (24h)</h2>
<table>
<tr>
<th>Name</th>
<th>OK Checks</th>
<th>Uptime</th>
<th>Avg TTFB</th>
<th>Status</th>
</tr>
{{range .Servers}}
<tr>
<td>{{.AnonName}}</td>
<td>{{.Successes}}</td>
<td>
{{if gt .Successes 0}}
<span class="uptime-bar"><span class="uptime-fill {{if ge .UptimePct 99.0}}fill-ok{{else if ge .UptimePct 95.0}}fill-warn{{else}}fill-err{{end}}" style="width:{{printf "%.0f" .UptimePct}}%"></span></span>
<span class="pct">{{printf "%.1f" .UptimePct}}%</span>
{{else}}
<span class="uptime-bar"><span class="uptime-fill fill-none" style="width:0%"></span></span>
<span class="pct">—</span>
{{end}}
</td>
<td style="color:{{if gt .AvgTTFBMS 0}}{{if le .AvgTTFBMS 200}}#5cb85c{{else if le .AvgTTFBMS 500}}#e6a817{{else}}#d9534f{{end}}{{else}}#444{{end}}">
{{if gt .AvgTTFBMS 0}}{{.AvgTTFBMS}} ms{{else}}—{{end}}
</td>
<td>
{{if eq .UptimeClass "ok"}}<span class="badge badge-ok">OK</span>
{{else if eq .UptimeClass "warn"}}<span class="badge badge-warn">DEGRADED</span>
{{else if eq .UptimeClass "err"}}<span class="badge badge-err">DOWN</span>
{{else}}<span class="badge badge-nodata">NO DATA</span>{{end}}
</td>
</tr>
{{else}}
<tr><td colspan="5" style="color:#555;text-align:center;padding:20px">No data available</td></tr>
{{end}}
</table>
<h2>Nodes</h2>
<table>
<tr>
<th>Name</th>
<th>Status</th>
</tr>
{{range .Agents}}
<tr>
<td>{{.AnonName}}</td>
<td>
{{if .Online}}<span class="badge badge-online">ONLINE</span>
{{else}}<span class="badge badge-offline">OFFLINE</span>{{end}}
</td>
</tr>
{{else}}
<tr><td colspan="2" style="color:#555;text-align:center;padding:20px">No nodes</td></tr>
{{end}}
</table>
</body>
</html>`
const chartTmpl = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{.Server.URL}} — status</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { background: #111; color: #ccc; font-family: monospace; font-size: 14px; padding: 20px; }
h1 { color: #fff; margin-bottom: 4px; font-size: 18px; word-break: break-all; }
.subtitle { color: #666; margin-bottom: 20px; font-size: 13px; }
a { color: #5af; text-decoration: none; }
a:hover { text-decoration: underline; }
.controls { margin-bottom: 20px; display: flex; gap: 8px; align-items: center; }
.controls span { color: #888; margin-right: 4px; }
button { background: #222; color: #aaa; border: 1px solid #444; padding: 4px 12px; cursor: pointer; font-family: monospace; font-size: 13px; border-radius: 3px; }
button:hover { background: #2a2a2a; color: #fff; border-color: #666; }
button.active { background: #1a2a3a; color: #5af; border-color: #5af; }
.chart-container { background: #151515; border: 1px solid #222; border-radius: 6px; padding: 16px; margin-bottom: 20px; }
.chart-title { color: #888; font-size: 12px; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 12px; }
canvas { max-height: 220px; }
.back { margin-top: 10px; }
#status { color: #666; font-size: 12px; margin-left: 12px; }
</style>
</head>
<body>
<div class="back"><a href="/">← Dashboard</a></div>
<br>
<h1>{{.Server.URL}}</h1>
<div class="subtitle">{{if .Server.Name}}{{.Server.Name}}{{else}}(no name){{end}}</div>
<div class="controls">
<span>Range:</span>
<button onclick="load(6)" id="btn6">6h</button>
<button onclick="load(24)" id="btn24" class="active">24h</button>
<button onclick="load(48)" id="btn48">48h</button>
<button onclick="load(168)" id="btn168">7d</button>
<span style="color:#555; margin: 0 4px 0 8px">|</span>
<span>Agent:</span>
<select id="agentSel" onchange="load(currentHours)" style="background:#222;color:#aaa;border:1px solid #444;padding:4px 8px;font-family:monospace;font-size:13px;border-radius:3px;cursor:pointer">
<option value="0">all</option>
{{range .Agents}}<option value="{{.ID}}">{{.Name}}{{if .LastIP}} ({{.LastIP}}){{end}}</option>{{end}}
</select>
<span id="status"></span>
</div>
<div class="chart-container">
<div class="chart-title">Availability</div>
<canvas id="availChart"></canvas>
</div>
<div class="chart-container">
<div class="chart-title">Response Time (ms)</div>
<canvas id="rtChart"></canvas>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns@3.0.0/dist/chartjs-adapter-date-fns.bundle.min.js"></script>
<script>
const serverID = {{.Server.ID}};
const agentNames = {0: 'all', {{range .Agents}}{{.ID}}: '{{.Name}}', {{end}}};
const darkColor = '#333';
const gridColor = '#1e1e1e';
const availCtx = document.getElementById('availChart').getContext('2d');
const rtCtx = document.getElementById('rtChart').getContext('2d');
const availChart = new Chart(availCtx, {
type: 'scatter',
data: { datasets: [
{ label: 'Success', data: [], backgroundColor: '#4f4', pointRadius: 4 },
{ label: 'Failure', data: [], backgroundColor: '#f44', pointRadius: 4 },
]},
options: {
animation: false,
plugins: {
legend: { labels: { color: '#888', font: { family: 'monospace' } } },
tooltip: {
callbacks: {
label: ctx => {
const p = ctx.raw;
const name = agentNames[p.aid] || ('agent ' + p.aid);
return name + ' ' + new Date(p.x).toLocaleTimeString();
},
title: () => '',
},
},
},
scales: {
x: {
type: 'time',
time: { tooltipFormat: 'yyyy-MM-dd HH:mm' },
ticks: { color: '#666', maxTicksLimit: 10 },
grid: { color: gridColor },
},
y: {
min: -0.1, max: 1.1,
ticks: {
color: '#666',
callback: v => v === 1 ? 'OK' : v === 0 ? 'FAIL' : '',
stepSize: 1,
},
grid: { color: gridColor },
},
},
},
});
const rtChart = new Chart(rtCtx, {
type: 'line',
data: { datasets: [
{ label: 'Total ms', data: [], borderColor: '#5af', backgroundColor: 'transparent', pointRadius: 2, tension: 0.2 },
{ label: 'TTFB ms', data: [], borderColor: '#fa0', backgroundColor: 'transparent', pointRadius: 2, tension: 0.2 },
]},
options: {
animation: false,
plugins: { legend: { labels: { color: '#888', font: { family: 'monospace' } } } },
scales: {
x: {
type: 'time',
time: { tooltipFormat: 'yyyy-MM-dd HH:mm' },
ticks: { color: '#666', maxTicksLimit: 10 },
grid: { color: gridColor },
},
y: {
ticks: { color: '#666' },
grid: { color: gridColor },
title: { display: true, text: 'ms', color: '#666' },
},
},
},
});
let currentHours = 24;
async function load(hours) {
currentHours = hours;
[6,24,48,168].forEach(h => {
const btn = document.getElementById('btn'+h);
btn.className = h === hours ? 'active' : '';
});
const status = document.getElementById('status');
status.textContent = 'Loading...';
try {
const agentID = document.getElementById('agentSel').value;
const resp = await fetch('/api/chart-data/' + serverID + '?hours=' + hours + '&agent=' + agentID);
if (!resp.ok) throw new Error('HTTP ' + resp.status);
const data = await resp.json();
status.textContent = (data ? data.length : 0) + ' points';
const successPts = [], failPts = [], totalPts = [], ttfbPts = [];
(data || []).forEach(p => {
const t = new Date(p.t);
if (p.s) {
successPts.push({ x: t, y: 1, aid: p.aid });
} else {
failPts.push({ x: t, y: 0, aid: p.aid });
}
if (p.ms > 0) totalPts.push({ x: t, y: p.ms });
if (p.ttfb > 0) ttfbPts.push({ x: t, y: p.ttfb });
});
availChart.data.datasets[0].data = successPts;
availChart.data.datasets[1].data = failPts;
availChart.update();
rtChart.data.datasets[0].data = totalPts;
rtChart.data.datasets[1].data = ttfbPts;
rtChart.update();
} catch(e) {
status.textContent = 'Error: ' + e.message;
}
}
load(24);
</script>
</body>
</html>`