feat: dashboard, visual query builder, expanded query language, cert expiry support
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
<!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 check for each monitor
|
||||
const monitorsWithChecks = await Promise.all(
|
||||
monitors.map(async (m) => {
|
||||
try {
|
||||
const checks = await api(`/checks/${m.id}?limit=20`);
|
||||
return { ...m, checks };
|
||||
} catch {
|
||||
return { ...m, checks: [] };
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
const upCount = monitorsWithChecks.filter(m => m.checks[0]?.up === true).length;
|
||||
const downCount = monitorsWithChecks.filter(m => m.checks[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 = monitorsWithChecks.map(m => {
|
||||
const lastCheck = m.checks[0];
|
||||
const latencies = m.checks.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(lastCheck?.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">${lastCheck ? timeAgo(lastCheck.checked_at) : 'no checks'}</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>
|
||||
Reference in New Issue
Block a user