<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Oculus - Unified Telemetry</title>
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="/static/css/tailwind.css" />
</head>
<body
class="font-inter bg-neutral-950 bg-[radial-gradient(ellipse_at_top_right,_var(--tw-gradient-stops))] from-indigo-950 via-neutral-950 to-black text-neutral-100 min-h-screen overflow-x-hidden selection:bg-indigo-500/30 selection:text-indigo-200"
>
<div class="fixed inset-0 z-[-1]">
<div
class="absolute top-0 left-0 w-[500px] h-[500px] bg-blue-500/10 rounded-full blur-[120px] animate-pulse-glow"
></div>
<div
class="absolute bottom-0 right-0 w-[500px] h-[500px] bg-purple-500/10 rounded-full blur-[120px] animate-pulse-glow"
style="animation-delay: 2s"
></div>
</div>
<div class="flex h-screen overflow-hidden">
<aside
class="w-72 glass-panel border-r-0 z-20 flex flex-col transition-all duration-300"
>
<div class="p-8 pb-8">
<div class="flex items-center gap-3 mb-8 group cursor-default">
<div
class="relative w-10 h-10 flex items-center justify-center bg-gradient-to-br from-blue-500 to-purple-600 rounded-xl shadow-lg shadow-blue-500/20 group-hover:shadow-blue-500/40 transition-all duration-300"
>
<span class="text-xl">👁️</span>
<div
class="absolute inset-0 bg-white/20 rounded-xl opacity-0 group-hover:opacity-100 transition-opacity"
></div>
</div>
<div>
<h1
class="text-2xl font-bold bg-gradient-to-r from-white to-white/60 bg-clip-text text-transparent"
>
Oculus
</h1>
<p
class="text-[10px] uppercase tracking-[0.2em] text-blue-400 font-semibold"
>
Telemetry
</p>
</div>
</div>
<nav class="space-y-2">
<div
class="text-xs font-semibold text-neutral-300 uppercase tracking-wider px-4 py-2 mb-2"
>
Modules
</div>
<div class="nav-item space-y-2">
<div
onclick="switchModule('metrics')"
id="nav-metrics"
class="group flex items-center gap-3 px-4 py-3 rounded-xl cursor-pointer transition-all duration-200 relative overflow-hidden"
>
<div
class="hidden active-bg absolute inset-0 bg-gradient-to-r from-blue-500/20 to-purple-500/20 border border-blue-500/20 rounded-xl"
></div>
<span
class="text-xl relative z-10 transition-transform group-hover:scale-110 duration-200"
>📊</span
>
<span class="font-medium relative z-10">Metrics</span>
<div
class="absolute inset-0 bg-white/5 opacity-0 group-hover:opacity-100 transition-opacity rounded-xl"
></div>
</div>
<div
onclick="switchModule('events')"
id="nav-events"
class="group flex items-center gap-3 px-4 py-3 rounded-xl cursor-pointer transition-all duration-200 relative overflow-hidden"
>
<div
class="hidden active-bg absolute inset-0 bg-gradient-to-r from-blue-500/20 to-purple-500/20 border border-blue-500/20 rounded-xl"
></div>
<span
class="text-xl relative z-10 transition-transform group-hover:scale-110 duration-200"
>🔔</span
>
<span class="font-medium relative z-10">Events</span>
<div
class="absolute inset-0 bg-white/5 opacity-0 group-hover:opacity-100 transition-opacity rounded-xl"
></div>
</div>
<div
onclick="switchModule('collectors')"
id="nav-collectors"
class="group flex items-center gap-3 px-4 py-3 rounded-xl cursor-pointer transition-all duration-200 relative overflow-hidden"
>
<div
class="hidden active-bg absolute inset-0 bg-gradient-to-r from-blue-500/20 to-purple-500/20 border border-blue-500/20 rounded-xl"
></div>
<span
class="text-xl relative z-10 transition-transform group-hover:scale-110 duration-200"
>⚙️</span
>
<span class="font-medium relative z-10">Collectors</span>
<div
class="absolute inset-0 bg-white/5 opacity-0 group-hover:opacity-100 transition-opacity rounded-xl"
></div>
</div>
</div>
</nav>
</div>
<div class="mt-auto p-8 border-t border-white/5">
<div
class="flex items-center gap-3 opacity-60 hover:opacity-100 transition-opacity cursor-pointer"
>
<div
class="w-8 h-8 rounded-full bg-gradient-to-tr from-neutral-700 to-neutral-600 border border-white/10"
></div>
<div>
<div class="text-sm font-medium text-neutral-100">Admin User</div>
<div class="text-xs text-neutral-300">Connected</div>
</div>
</div>
</div>
</aside>
<main
class="flex-1 overflow-y-auto px-12 py-10 scrollbar-custom relative"
>
<header class="flex justify-between items-end mb-10">
<div>
<h2 class="text-4xl font-bold text-white mb-2 tracking-tight">
Dashboard
</h2>
</div>
<div class="flex gap-4">
<div class="relative">
<select
id="global-time-range"
class="glass-input px-4 py-2 pr-10 rounded-lg text-sm font-medium text-neutral-200 hover:text-white cursor-pointer appearance-none bg-transparent"
onchange="updateTimeRange(this.value)"
>
<option value="1h" class="bg-neutral-900">Last 1 Hour</option>
<option value="6h" class="bg-neutral-900">Last 6 Hours</option>
<option value="12h" class="bg-neutral-900">
Last 12 Hours
</option>
<option value="24h" selected class="bg-neutral-900">
Last 24 Hours
</option>
<option value="7d" class="bg-neutral-900">Last 7 Days</option>
<option value="30d" class="bg-neutral-900">Last 30 Days</option>
</select>
<div
class="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 text-neutral-400 text-xs"
>
▼
</div>
</div>
</div>
</header>
<section
id="metrics"
class="glass-panel rounded-2xl p-8 mb-8 animate-fade-in"
>
<div
class="flex flex-col md:flex-row justify-between items-start md:items-center mb-6 pb-6 border-b border-white/5 gap-4"
>
<div>
<h3
class="text-2xl font-semibold text-white flex items-center gap-3"
>
📊
<span
class="bg-gradient-to-r from-blue-400 to-purple-400 bg-clip-text text-transparent"
>Live Metrics</span
>
</h3>
</div>
<div class="flex items-center gap-4">
<span
id="metrics-last-updated"
class="text-xs text-neutral-400"
></span>
<label class="flex items-center gap-2 cursor-pointer">
<span
class="text-xs text-neutral-300 font-medium uppercase tracking-wider"
>Auto</span
>
<div class="relative">
<input
type="checkbox"
id="metrics-auto-refresh"
class="sr-only peer"
checked
/>
<div
class="w-9 h-5 bg-neutral-700 rounded-full peer peer-checked:bg-blue-500/50 transition-colors"
></div>
<div
class="absolute left-0.5 top-0.5 w-4 h-4 bg-neutral-400 rounded-full peer-checked:translate-x-4 peer-checked:bg-blue-400 transition-all"
></div>
</div>
</label>
<button
type="button"
onclick="refreshMetrics()"
class="flex items-center gap-2 px-3 py-1.5 rounded-lg text-sm font-medium text-blue-400 hover:text-blue-300 hover:bg-blue-500/10 transition-all"
title="Refresh now"
>
<svg
class="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
/>
</svg>
Refresh
</button>
<div
id="metrics-indicator"
class="htmx-indicator flex items-center gap-2 text-sm text-blue-400"
>
<svg
class="w-4 h-4 animate-spin"
fill="none"
viewBox="0 0 24 24"
>
<circle
class="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
stroke-width="4"
></circle>
<path
class="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
Syncing...
</div>
</div>
</div>
<div
id="metrics-stats"
class="flex flex-wrap items-center gap-x-6 gap-y-4 mb-6 py-4 px-6 rounded-xl bg-black/20 border border-white/5"
>
<div class="flex items-center gap-3">
<span class="stat-label">Total</span>
<span class="text-xl font-bold text-white" id="stat-total"
>-</span
>
</div>
<div class="w-px h-8 bg-white/20 mx-2"></div>
<div class="flex items-center gap-3">
<span class="stat-label">Success Rate</span>
<span class="text-xl font-bold text-white" id="stat-success-rate"
>-</span
>
<div class="w-16 stat-bar">
<div
id="stat-success-bar"
class="stat-bar-fill bg-green-500"
style="width: 0%"
></div>
</div>
</div>
<div class="w-px h-8 bg-white/20 mx-2"></div>
<div class="flex items-center gap-3">
<span class="stat-label">Success</span>
<span class="text-xl font-bold text-green-400" id="stat-success"
>-</span
>
</div>
<div class="w-px h-8 bg-white/20 mx-2"></div>
<div class="flex items-center gap-3">
<span class="stat-label">Failure</span>
<span class="text-xl font-bold text-orange-400" id="stat-failure"
>-</span
>
</div>
</div>
<div
id="metrics-alert"
class="hidden mb-4 rounded-xl border border-amber-400/40 bg-amber-500/10 px-4 py-3 text-sm text-amber-100"
role="alert"
aria-live="polite"
>
<span class="font-semibold">Metrics not available.</span>
<span data-alert-message class="ml-2"
>Waiting for a successful response…</span
>
</div>
<form
id="metrics-form"
hx-get="/api/metrics"
hx-target="#metrics-data"
hx-trigger="load, submit, change from:select, keyup delay:500ms from:input, every 30s"
hx-indicator="#metrics-indicator"
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-4 mb-8"
>
<input type="hidden" name="range" value="24h" />
<div class="flex flex-col gap-2">
<label
class="text-xs text-neutral-300 font-semibold uppercase tracking-wider pl-1"
>Category</label
>
<select
name="category"
class="glass-input px-4 py-2.5 rounded-xl text-neutral-100 text-sm appearance-none cursor-pointer hover:bg-white/5"
>
<option value="" class="bg-neutral-900">All Categories</option>
<option value="network.tcp" class="bg-neutral-900">
Network TCP
</option>
<option value="network.ping" class="bg-neutral-900">
Network Ping
</option>
<option value="network.http" class="bg-neutral-900">
Network HTTP
</option>
<option value="crypto" class="bg-neutral-900">Crypto</option>
<option value="polymarket" class="bg-neutral-900">
Polymarket
</option>
<option value="stock" class="bg-neutral-900">Stock</option>
<option value="custom" class="bg-neutral-900">Custom</option>
</select>
</div>
<div class="flex flex-col gap-2">
<label
class="text-xs text-neutral-300 font-semibold uppercase tracking-wider pl-1"
>Name</label
>
<input
type="text"
name="name"
placeholder="Search metrics..."
class="glass-input px-4 py-2.5 rounded-xl text-neutral-100 text-sm placeholder-neutral-500"
/>
</div>
<div class="flex flex-col gap-2">
<label
class="text-xs text-neutral-300 font-semibold uppercase tracking-wider pl-1"
>Target</label
>
<input
type="text"
name="target"
placeholder="e.g. 127.0.0.1"
class="glass-input px-4 py-2.5 rounded-xl text-neutral-100 text-sm placeholder-neutral-500"
/>
</div>
<div class="flex flex-col gap-2">
<label
class="text-xs text-neutral-300 font-semibold uppercase tracking-wider pl-1"
>Limit</label
>
<select
name="limit"
class="glass-input px-4 py-2.5 rounded-xl text-neutral-100 text-sm appearance-none cursor-pointer hover:bg-white/5"
>
<option value="10" class="bg-neutral-900">10 items</option>
<option value="25" class="bg-neutral-900">25 items</option>
<option value="50" selected class="bg-neutral-900">
50 items
</option>
<option value="100" class="bg-neutral-900">100 items</option>
</select>
</div>
<div class="flex flex-col gap-2">
<label
class="text-xs text-neutral-300 font-semibold uppercase tracking-wider pl-1"
>Sort</label
>
<select
name="order"
class="glass-input px-4 py-2.5 rounded-xl text-neutral-100 text-sm appearance-none cursor-pointer hover:bg-white/5"
>
<option value="desc" class="bg-neutral-900">
Newest First
</option>
<option value="asc" class="bg-neutral-900">Oldest First</option>
</select>
</div>
<div class="hidden">
<button type="submit">Search</button>
</div>
</form>
<div
id="metrics-data"
data-alert-id="metrics-alert"
class="min-h-[300px] rounded-xl overflow-hidden bg-black/50 border border-white/10"
>
<div
class="flex flex-col items-center justify-center h-[300px] text-neutral-300 gap-4"
>
<div
class="w-8 h-8 border-2 border-blue-500 border-t-transparent rounded-full animate-spin"
></div>
<span class="text-sm font-medium animate-pulse"
>Loading metric data...</span
>
</div>
</div>
</section>
<section
id="events"
class="hidden glass-panel rounded-2xl p-8 mb-8 animate-fade-in"
>
<div
class="flex flex-col md:flex-row justify-between items-start md:items-center mb-6 pb-6 border-b border-white/5 gap-4"
>
<div>
<h3
class="text-2xl font-semibold text-white flex items-center gap-3"
>
🔔
<span
class="bg-gradient-to-r from-orange-400 to-red-400 bg-clip-text text-transparent"
>System Events</span
>
</h3>
</div>
<div class="flex items-center gap-4">
<span
id="events-last-updated"
class="text-xs text-neutral-400"
></span>
<label class="flex items-center gap-2 cursor-pointer">
<span
class="text-xs text-neutral-300 font-medium uppercase tracking-wider"
>Auto</span
>
<div class="relative">
<input
type="checkbox"
id="events-auto-refresh"
class="sr-only peer"
checked
/>
<div
class="w-9 h-5 bg-neutral-700 rounded-full peer peer-checked:bg-orange-500/50 transition-colors"
></div>
<div
class="absolute left-0.5 top-0.5 w-4 h-4 bg-neutral-400 rounded-full peer-checked:translate-x-4 peer-checked:bg-orange-400 transition-all"
></div>
</div>
</label>
<button
type="button"
onclick="refreshEvents()"
class="flex items-center gap-2 px-3 py-1.5 rounded-lg text-sm font-medium text-orange-400 hover:text-orange-300 hover:bg-orange-500/10 transition-all"
title="Refresh now"
>
<svg
class="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
/>
</svg>
Refresh
</button>
<div
id="events-indicator"
class="htmx-indicator flex items-center gap-2 text-sm text-orange-400"
>
<svg
class="w-4 h-4 animate-spin"
fill="none"
viewBox="0 0 24 24"
>
<circle
class="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
stroke-width="4"
></circle>
<path
class="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
Syncing...
</div>
</div>
</div>
<div
id="events-alert"
class="hidden mb-4 rounded-xl border border-amber-400/40 bg-amber-500/10 px-4 py-3 text-sm text-amber-100"
role="alert"
aria-live="polite"
>
<span class="font-semibold">Events not available.</span>
<span data-alert-message class="ml-2"
>Waiting for a successful response…</span
>
</div>
<form
id="events-form"
hx-get="/api/events"
hx-target="#events-data"
hx-trigger="load, submit, change from:select, keyup delay:500ms from:input, every 30s"
hx-indicator="#events-indicator"
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-4 mb-8"
>
<input type="hidden" name="range" value="24h" />
<div class="flex flex-col gap-2">
<label
class="text-xs text-neutral-300 font-semibold uppercase tracking-wider pl-1"
>Source</label
>
<select
name="source"
class="glass-input px-4 py-2.5 rounded-xl text-neutral-100 text-sm appearance-none cursor-pointer hover:bg-white/5"
>
<option value="" class="bg-neutral-900">All Sources</option>
<option value="collector.network.tcp" class="bg-neutral-900">
Collector Network TCP
</option>
<option value="collector.network.ping" class="bg-neutral-900">
Collector Network Ping
</option>
<option value="collector.network.http" class="bg-neutral-900">
Collector Network HTTP
</option>
<option value="rule" class="bg-neutral-900">Rule Engine</option>
<option value="system" class="bg-neutral-900">System</option>
</select>
</div>
<div class="flex flex-col gap-2">
<label
class="text-xs text-neutral-300 font-semibold uppercase tracking-wider pl-1"
>Kind</label
>
<select
name="kind"
class="glass-input px-4 py-2.5 rounded-xl text-neutral-100 text-sm appearance-none cursor-pointer hover:bg-white/5"
>
<option value="" class="bg-neutral-900">All Kinds</option>
<option value="alert" class="bg-neutral-900">Alert</option>
<option value="error" class="bg-neutral-900">Error</option>
<option value="system" class="bg-neutral-900">System</option>
<option value="audit" class="bg-neutral-900">Audit</option>
</select>
</div>
<div class="flex flex-col gap-2">
<label
class="text-xs text-neutral-300 font-semibold uppercase tracking-wider pl-1"
>Severity</label
>
<select
name="severity"
class="glass-input px-4 py-2.5 rounded-xl text-neutral-100 text-sm appearance-none cursor-pointer hover:bg-white/5"
>
<option value="" class="bg-neutral-900">All Severities</option>
<option value="debug" class="bg-neutral-900">Debug</option>
<option value="info" class="bg-neutral-900">Info</option>
<option value="warn" class="bg-neutral-900">Warning</option>
<option value="error" class="bg-neutral-900">Error</option>
<option value="critical" class="bg-neutral-900">
Critical
</option>
</select>
</div>
<div class="flex flex-col gap-2">
<label
class="text-xs text-neutral-300 font-semibold uppercase tracking-wider pl-1"
>Limit</label
>
<select
name="limit"
class="glass-input px-4 py-2.5 rounded-xl text-neutral-100 text-sm appearance-none cursor-pointer hover:bg-white/5"
>
<option value="10" class="bg-neutral-900">10 items</option>
<option value="25" class="bg-neutral-900">25 items</option>
<option value="50" selected class="bg-neutral-900">
50 items
</option>
<option value="100" class="bg-neutral-900">100 items</option>
</select>
</div>
<div class="flex flex-col gap-2">
<label
class="text-xs text-neutral-300 font-semibold uppercase tracking-wider pl-1"
>Sort</label
>
<select
name="order"
class="glass-input px-4 py-2.5 rounded-xl text-neutral-100 text-sm appearance-none cursor-pointer hover:bg-white/5"
>
<option value="desc" class="bg-neutral-900">
Newest First
</option>
<option value="asc" class="bg-neutral-900">Oldest First</option>
</select>
</div>
<div class="hidden">
<button type="submit">Search</button>
</div>
</form>
<div
id="events-data"
data-alert-id="events-alert"
class="min-h-[300px] rounded-xl overflow-hidden bg-black/50 border border-white/10"
>
<div
class="flex flex-col items-center justify-center h-[300px] text-neutral-300 gap-4"
>
<div
class="w-8 h-8 border-2 border-orange-500 border-t-transparent rounded-full animate-spin"
></div>
<span class="text-sm font-medium animate-pulse"
>Loading system events...</span
>
</div>
</div>
</section>
<section
id="collectors"
class="hidden glass-panel rounded-2xl p-8 mb-8 animate-fade-in"
>
<div
class="flex flex-col md:flex-row justify-between items-start md:items-center mb-6 pb-6 border-b border-white/5 gap-4"
>
<div>
<h3
class="text-2xl font-semibold text-white flex items-center gap-3"
>
⚙️
<span
class="bg-gradient-to-r from-green-400 to-teal-400 bg-clip-text text-transparent"
>Collectors</span
>
</h3>
</div>
<div class="flex items-center gap-4">
<span
id="collectors-last-updated"
class="text-xs text-neutral-400"
></span>
<button
type="button"
onclick="refreshCollectors()"
class="flex items-center gap-2 px-3 py-1.5 rounded-lg text-sm font-medium text-green-400 hover:text-green-300 hover:bg-green-500/10 transition-all"
title="Refresh now"
>
<svg
class="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
/>
</svg>
Refresh
</button>
<div
id="collectors-indicator"
class="htmx-indicator flex items-center gap-2 text-sm text-green-400"
>
<svg
class="w-4 h-4 animate-spin"
fill="none"
viewBox="0 0 24 24"
>
<circle
class="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
stroke-width="4"
></circle>
<path
class="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
Loading...
</div>
</div>
</div>
<div
id="collectors-alert"
class="hidden mb-4 rounded-xl border border-amber-400/40 bg-amber-500/10 px-4 py-3 text-sm text-amber-100"
role="alert"
aria-live="polite"
>
<span class="font-semibold">Collectors not available.</span>
<span data-alert-message class="ml-2"
>Waiting for a successful response…</span
>
</div>
<div
id="collectors-data"
data-alert-id="collectors-alert"
hx-get="/api/collectors/html"
hx-trigger="load"
hx-indicator="#collectors-indicator"
class="min-h-[300px] rounded-xl overflow-hidden bg-black/50 border border-white/10"
>
<div
class="flex flex-col items-center justify-center h-[300px] text-neutral-300 gap-4"
>
<div
class="w-8 h-8 border-2 border-green-500 border-t-transparent rounded-full animate-spin"
></div>
<span class="text-sm font-medium animate-pulse"
>Loading collectors...</span
>
</div>
</div>
</section>
</main>
</div>
<script>
function switchModule(moduleId) {
document
.querySelectorAll("main > section")
.forEach((section) => section.classList.add("hidden"));
document.getElementById(moduleId)?.classList.remove("hidden");
document.querySelectorAll(".nav-item > div").forEach((item) => {
const isActive = item.getAttribute("onclick")?.includes(moduleId);
const activeBg = item.querySelector(".active-bg");
const textSpans = item.querySelectorAll("span");
if (isActive) {
activeBg?.classList.remove("hidden");
textSpans.forEach((s) => {
s.classList.add("text-white");
s.classList.remove("text-neutral-400");
});
item.classList.add("shadow-lg", "shadow-blue-500/10");
} else {
activeBg?.classList.add("hidden");
textSpans.forEach((s) => {
s.classList.remove("text-white");
s.classList.add("text-neutral-400");
});
item.classList.remove("shadow-lg", "shadow-blue-500/10");
}
});
}
document.addEventListener("DOMContentLoaded", () =>
switchModule("metrics")
);
</script>
<script>
(function () {
const hideAlert = (id) => {
const el = document.getElementById(id);
if (!el) return;
el.classList.add("hidden");
};
const showAlert = (id, message) => {
const el = document.getElementById(id);
if (!el) return;
const messageEl = el.querySelector("[data-alert-message]");
if (messageEl) {
messageEl.textContent = message;
}
el.classList.remove("hidden");
};
const resolveTarget = (event) =>
event.detail?.target || event.detail?.elt || event.target;
document.body.addEventListener("htmx:send", (event) => {
const alertId = resolveTarget(event)?.dataset?.alertId;
if (alertId) {
hideAlert(alertId);
}
});
document.body.addEventListener("htmx:afterSwap", (event) => {
const alertId = resolveTarget(event)?.dataset?.alertId;
if (alertId) {
hideAlert(alertId);
}
});
document.body.addEventListener("htmx:responseError", (event) => {
const alertId = resolveTarget(event)?.dataset?.alertId;
if (!alertId) return;
const status = event.detail.xhr?.status;
const statusText = event.detail.xhr?.statusText || "Request failed";
const snippet =
event.detail.xhr?.responseText?.trim().slice(0, 160) ||
"No response body";
showAlert(alertId, `${status || "Error"}: ${statusText}. ${snippet}`);
});
})();
function updateTimeRange(value) {
document.querySelectorAll('input[name="range"]').forEach((input) => {
input.value = value;
htmx.trigger(input.form, "submit");
});
loadMetricStats();
}
</script>
<script>
let metricsAutoRefresh = true;
let eventsAutoRefresh = true;
function formatTime(date) {
return date.toLocaleTimeString("en-US", { hour12: false });
}
function updateLastRefresh(module) {
const el = document.getElementById(`${module}-last-updated`);
if (el) {
el.textContent = `Updated ${formatTime(new Date())}`;
}
}
function refreshMetrics() {
htmx.trigger(document.getElementById("metrics-form"), "submit");
loadMetricStats();
}
function refreshEvents() {
htmx.trigger(document.getElementById("events-form"), "submit");
}
function refreshCollectors() {
htmx.trigger(document.getElementById("collectors-data"), "load");
updateLastRefresh("collectors");
}
document.addEventListener("DOMContentLoaded", () => {
const metricsToggle = document.getElementById("metrics-auto-refresh");
const eventsToggle = document.getElementById("events-auto-refresh");
const metricsForm = document.getElementById("metrics-form");
const eventsForm = document.getElementById("events-form");
const metricsTrigger = metricsForm.getAttribute("hx-trigger");
const eventsTrigger = eventsForm.getAttribute("hx-trigger");
metricsToggle?.addEventListener("change", (e) => {
metricsAutoRefresh = e.target.checked;
if (metricsAutoRefresh) {
metricsForm.setAttribute("hx-trigger", metricsTrigger);
} else {
metricsForm.setAttribute(
"hx-trigger",
metricsTrigger.replace(/, every 30s/g, "")
);
}
htmx.process(metricsForm);
});
eventsToggle?.addEventListener("change", (e) => {
eventsAutoRefresh = e.target.checked;
if (eventsAutoRefresh) {
eventsForm.setAttribute("hx-trigger", eventsTrigger);
} else {
eventsForm.setAttribute(
"hx-trigger",
eventsTrigger.replace(/, every 30s/g, "")
);
}
htmx.process(eventsForm);
});
document.body.addEventListener("htmx:afterSwap", (event) => {
const target = event.detail.target;
if (target.id === "metrics-data") {
updateLastRefresh("metrics");
} else if (target.id === "events-data") {
updateLastRefresh("events");
}
});
loadMetricStats();
setInterval(() => {
if (metricsAutoRefresh) {
loadMetricStats();
}
}, 30000);
});
async function loadMetricStats() {
const rangeInput = document.querySelector(
'#metrics-form input[name="range"]'
);
const range = rangeInput ? rangeInput.value : "24h";
try {
const response = await fetch(`/api/metrics/stats?range=${range}`);
if (!response.ok) throw new Error("Failed to load stats");
const stats = await response.json();
document.getElementById("stat-total").textContent =
stats.total.toLocaleString();
document.getElementById("stat-success").textContent =
stats.success_count.toLocaleString();
document.getElementById("stat-failure").textContent =
stats.failure_count.toLocaleString();
const rate =
stats.total > 0 ? (stats.success_count / stats.total) * 100 : 0;
document.getElementById(
"stat-success-rate"
).textContent = `${rate.toFixed(1)}%`;
document.getElementById("stat-success-bar").style.width = `${rate}%`;
} catch (error) {
console.error("Failed to load metric stats:", error);
}
}
</script>
<style>
@keyframes fade-in {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.animate-fade-in {
animation: fade-in 0.5s cubic-bezier(0.16, 1, 0.3, 1);
}
</style>
</body>
</html>