Single-page status dashboard for Molecules AI services. Pure static HTML+CSS+JS — zero build step, zero dependencies. Reads probe results directly from public Gitea raw URLs at runtime. Files: - site/index.html: structure + embedded CSS (light/dark via prefers -color-scheme; ~110 lines styling) - site/app.js: fetches .upptimerc.yml + per-site history JSONL, renders rows + summary + 24h-history sparkline, auto-refreshes every 5 min (matches probe cadence) - site/vercel.json: static-site config + security headers Why no framework - Page must load fast and never lie. React/Vue would be cargo-cult at this scale (3 visible elements, 1 data source). - Plain DOM + fetch removes the supply-chain surface a JS framework drags in. Zero npm deps, zero lockfile, zero CI build. Slugify rule mirrors the probe binary's slugify() in cmd/probe/main.go — both must agree on the file naming for history/<slug>.jsonl to round-trip cleanly. Out of scope (separate PRs / follow-ups) - Vercel project configuration + deploy (next commit) - Custom domain status.moleculesai.app - Historical data migration from old upptime JSON format - Alerting / RSS / status-as-API endpoints
138 lines
4.5 KiB
HTML
138 lines
4.5 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<meta name="description" content="Live status for Molecules AI services. Probes refresh every 5 minutes.">
|
|
<title>Molecules AI · Status</title>
|
|
<style>
|
|
:root {
|
|
color-scheme: light dark;
|
|
--bg: #0a0a0a;
|
|
--card: #141414;
|
|
--line: #2a2a2a;
|
|
--ink: #e5e5e5;
|
|
--ink-soft: #999;
|
|
--green: #34d399;
|
|
--amber: #fbbf24;
|
|
--red: #f87171;
|
|
--blue: #60a5fa;
|
|
}
|
|
* { box-sizing: border-box; }
|
|
body {
|
|
margin: 0;
|
|
background: var(--bg);
|
|
color: var(--ink);
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
|
|
-webkit-font-smoothing: antialiased;
|
|
line-height: 1.5;
|
|
}
|
|
.wrap { max-width: 980px; margin: 0 auto; padding: 32px 24px; }
|
|
header {
|
|
display: flex; align-items: baseline; justify-content: space-between;
|
|
margin-bottom: 32px; padding-bottom: 20px; border-bottom: 1px solid var(--line);
|
|
}
|
|
h1 { font-size: 22px; margin: 0; font-weight: 600; }
|
|
.meta { font-size: 13px; color: var(--ink-soft); }
|
|
.meta a { color: var(--blue); text-decoration: none; }
|
|
.meta a:hover { text-decoration: underline; }
|
|
|
|
.summary {
|
|
background: var(--card); border: 1px solid var(--line);
|
|
border-radius: 12px; padding: 24px; margin-bottom: 24px;
|
|
display: flex; align-items: center; gap: 20px;
|
|
}
|
|
.summary-dot {
|
|
width: 18px; height: 18px; border-radius: 50%;
|
|
flex-shrink: 0;
|
|
}
|
|
.summary-text strong { font-size: 18px; display: block; margin-bottom: 2px; }
|
|
.summary-text small { color: var(--ink-soft); font-size: 13px; }
|
|
|
|
.grid {
|
|
display: grid; gap: 12px;
|
|
}
|
|
.row {
|
|
background: var(--card); border: 1px solid var(--line);
|
|
border-radius: 10px; padding: 16px 20px;
|
|
display: grid;
|
|
grid-template-columns: 28px 1fr auto auto;
|
|
align-items: center; gap: 16px;
|
|
}
|
|
.dot {
|
|
width: 12px; height: 12px; border-radius: 50%;
|
|
box-shadow: 0 0 8px currentColor;
|
|
}
|
|
.dot.up { background: var(--green); color: var(--green); }
|
|
.dot.down { background: var(--red); color: var(--red); }
|
|
.dot.unknown { background: var(--ink-soft); color: var(--ink-soft); box-shadow: none; }
|
|
.row-name { font-weight: 500; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
.row-name a { color: var(--ink); text-decoration: none; }
|
|
.row-name a:hover { color: var(--blue); }
|
|
.row-name .url { display: block; font-size: 11px; color: var(--ink-soft); font-family: ui-monospace, 'SF Mono', monospace; }
|
|
.row-latency {
|
|
font-size: 13px; color: var(--ink-soft); font-variant-numeric: tabular-nums;
|
|
text-align: right; min-width: 70px;
|
|
}
|
|
.row-spark {
|
|
display: flex; gap: 2px; align-items: flex-end; height: 20px;
|
|
}
|
|
.row-spark span {
|
|
width: 3px; background: var(--green); display: block;
|
|
border-radius: 1px; opacity: 0.85;
|
|
}
|
|
.row-spark span.fail { background: var(--red); }
|
|
|
|
footer {
|
|
margin-top: 40px; padding-top: 20px; border-top: 1px solid var(--line);
|
|
font-size: 12px; color: var(--ink-soft); text-align: center;
|
|
}
|
|
footer a { color: var(--ink-soft); text-decoration: underline; }
|
|
|
|
.empty {
|
|
text-align: center; padding: 48px 24px; color: var(--ink-soft);
|
|
}
|
|
.skel {
|
|
height: 60px; background: var(--card); border: 1px solid var(--line);
|
|
border-radius: 10px; margin-bottom: 12px;
|
|
animation: pulse 1.6s ease-in-out infinite;
|
|
}
|
|
@keyframes pulse { 0%, 100% { opacity: 0.5; } 50% { opacity: 0.9; } }
|
|
|
|
@media (prefers-color-scheme: light) {
|
|
:root {
|
|
--bg: #fafafa; --card: #fff; --line: #e5e5e5; --ink: #1a1a1a; --ink-soft: #666;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="wrap">
|
|
<header>
|
|
<h1>Molecules AI · Status</h1>
|
|
<div class="meta" id="updated">checking…</div>
|
|
</header>
|
|
|
|
<div class="summary" id="summary">
|
|
<div class="summary-dot" style="background:var(--ink-soft)"></div>
|
|
<div class="summary-text">
|
|
<strong>Loading current status…</strong>
|
|
<small>Fetching latest probe results.</small>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid" id="grid">
|
|
<div class="skel"></div><div class="skel"></div><div class="skel"></div>
|
|
</div>
|
|
|
|
<footer>
|
|
Probes run every 5 minutes via Gitea Actions cron.
|
|
Source: <a href="https://git.moleculesai.app/molecule-ai/molecule-ai-status">molecule-ai/molecule-ai-status</a> ·
|
|
Probe binary: <a href="https://git.moleculesai.app/molecule-ai/molecule-ai-uptime-probe">molecule-ai-uptime-probe</a>
|
|
</footer>
|
|
</div>
|
|
|
|
<script src="./app.js"></script>
|
|
</body>
|
|
</html>
|