pingql/apps/web/src/dashboard/home.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>