110 lines
4.8 KiB
HTML
110 lines
4.8 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>PingQL — Dashboard</title>
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
<style>
|
|
body { font-family: 'JetBrains Mono', 'SF Mono', 'Fira Code', ui-monospace, monospace; background: #0a0a0a; }
|
|
</style>
|
|
</head>
|
|
<body class="bg-[#0a0a0a] text-gray-100 min-h-screen">
|
|
<script src="/dashboard/app.js"></script>
|
|
|
|
<!-- Nav -->
|
|
<nav class="border-b border-gray-800 px-6 py-4 flex items-center justify-between">
|
|
<a href="/dashboard/home" class="text-xl font-bold tracking-tight">Ping<span class="text-blue-400">QL</span></a>
|
|
<div class="flex items-center gap-4">
|
|
<a href="/dashboard/monitors/new" class="bg-blue-600 hover:bg-blue-500 text-white text-sm font-medium px-4 py-2 rounded-lg transition-colors">+ New Monitor</a>
|
|
<button onclick="logout()" class="text-gray-500 hover:text-gray-300 text-sm transition-colors">Logout</button>
|
|
</div>
|
|
</nav>
|
|
|
|
<!-- Content -->
|
|
<main class="max-w-5xl mx-auto px-6 py-8">
|
|
<div class="flex items-center justify-between mb-6">
|
|
<h2 class="text-lg font-semibold text-gray-200">Monitors</h2>
|
|
<div id="summary" class="text-sm text-gray-500"></div>
|
|
</div>
|
|
|
|
<div id="monitors-list" class="space-y-3">
|
|
<div class="text-center py-16 text-gray-600">Loading...</div>
|
|
</div>
|
|
|
|
<div id="empty-state" class="hidden text-center py-16">
|
|
<p class="text-gray-500 mb-4">No monitors yet</p>
|
|
<a href="/dashboard/monitors/new" class="bg-blue-600 hover:bg-blue-500 text-white text-sm font-medium px-6 py-3 rounded-lg transition-colors inline-block">Create your first monitor</a>
|
|
</div>
|
|
</main>
|
|
|
|
<script>
|
|
if (!requireAuth()) throw 'auth';
|
|
|
|
async function load() {
|
|
try {
|
|
const monitors = await api('/monitors/');
|
|
const list = document.getElementById('monitors-list');
|
|
const emptyState = document.getElementById('empty-state');
|
|
const summary = document.getElementById('summary');
|
|
|
|
if (monitors.length === 0) {
|
|
list.classList.add('hidden');
|
|
emptyState.classList.remove('hidden');
|
|
return;
|
|
}
|
|
|
|
// Fetch last ping for each monitor
|
|
const monitorsWithPings = await Promise.all(
|
|
monitors.map(async (m) => {
|
|
try {
|
|
const pings = await api(`/monitors/${m.id}/pings?limit=20`);
|
|
return { ...m, pings };
|
|
} catch {
|
|
return { ...m, pings: [] };
|
|
}
|
|
})
|
|
);
|
|
|
|
const upCount = monitorsWithPings.filter(m => m.pings[0]?.up === true).length;
|
|
const downCount = monitorsWithPings.filter(m => m.pings[0]?.up === false).length;
|
|
summary.innerHTML = `<span class="text-green-400">${upCount} up</span> · <span class="${downCount > 0 ? 'text-red-400' : 'text-gray-500'}">${downCount} down</span> · ${monitors.length} total`;
|
|
|
|
list.innerHTML = monitorsWithPings.map(m => {
|
|
const lastPing = m.pings[0];
|
|
const latencies = m.pings.filter(c => c.latency_ms != null).map(c => c.latency_ms).reverse();
|
|
const avgLatency = latencies.length ? Math.round(latencies.reduce((a, b) => a + b, 0) / latencies.length) : null;
|
|
|
|
return `
|
|
<a href="/dashboard/monitors/${m.id}" class="block bg-gray-900 hover:bg-gray-800/80 border border-gray-800 rounded-xl p-4 transition-colors group">
|
|
<div class="flex items-center justify-between">
|
|
<div class="flex items-center gap-3 min-w-0">
|
|
${statusBadge(lastPing?.up)}
|
|
<div class="min-w-0">
|
|
<div class="font-medium text-gray-100 group-hover:text-white truncate">${escapeHtml(m.name)}</div>
|
|
<div class="text-xs text-gray-500 truncate">${escapeHtml(m.url)}</div>
|
|
</div>
|
|
</div>
|
|
<div class="flex items-center gap-6 shrink-0 ml-4">
|
|
<div class="hidden sm:block">${sparkline(latencies)}</div>
|
|
<div class="text-right">
|
|
<div class="text-sm text-gray-300">${avgLatency != null ? avgLatency + 'ms' : '—'}</div>
|
|
<div class="text-xs text-gray-500">${lastPing ? timeAgo(lastPing.pinged_at) : 'no pings'}</div>
|
|
</div>
|
|
<div class="text-xs px-2 py-1 rounded ${m.enabled ? 'bg-gray-800 text-gray-400' : 'bg-yellow-900/30 text-yellow-500'}">${m.enabled ? m.interval_s + 's' : 'paused'}</div>
|
|
</div>
|
|
</div>
|
|
</a>
|
|
`;
|
|
}).join('');
|
|
} catch (e) {
|
|
document.getElementById('monitors-list').innerHTML = `<div class="text-center py-8 text-red-400">${escapeHtml(e.message)}</div>`;
|
|
}
|
|
}
|
|
|
|
load();
|
|
setInterval(load, 30000);
|
|
</script>
|
|
</body>
|
|
</html>
|