Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history

status.tsx

Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.

status.tsxBlame862 lines · 2 contributors
2316be6Claude1/**
2 * Public /status — human-readable platform health dashboard.
3 *
4 * Unlike /healthz (LB liveness JSON) and /readyz (DB readiness JSON),
5 * /status renders a full HTML page anyone can load. Shows DB reachability,
6 * autopilot state, totals (users/repos/gate runs), and the most recent
7 * autopilot tick's task breakdown.
8 *
9 * Accessible without auth. Uses softAuth so the nav bar renders correctly
10 * for logged-in visitors.
0a50474Claude11 *
12 * 2026 polish: scoped `.status-` CSS, hero with eyebrow + gradient
13 * headline + aggregate uptime percentage, per-service health pills,
14 * recent incidents list with severity dots, and an empty-state orb
15 * for the autopilot tick card when no tick has run yet.
2316be6Claude16 */
17
18import { Hono } from "hono";
19import { sql } from "drizzle-orm";
20import { db } from "../db";
21import { users, repositories, gateRuns } from "../db/schema";
22import { Layout } from "../views/layout";
23import { softAuth } from "../middleware/auth";
24import type { AuthEnv } from "../middleware/auth";
25import { getLastTick, getTickCount } from "../lib/autopilot";
b1be050CC LABS App26import { recentRedChecks } from "../lib/synthetic-monitor";
2316be6Claude27
28const status = new Hono<AuthEnv>();
29status.use("*", softAuth);
30
31const started = Date.now();
32
33function fmtUptime(ms: number): string {
34 const s = Math.floor(ms / 1000);
35 const d = Math.floor(s / 86400);
36 const h = Math.floor((s % 86400) / 3600);
37 const m = Math.floor((s % 3600) / 60);
38 if (d > 0) return `${d}d ${h}h`;
39 if (h > 0) return `${h}h ${m}m`;
40 return `${m}m`;
41}
42
43status.get("/status", async (c) => {
44 const user = c.get("user");
45
46 let dbOk = false;
47 try {
48 await db.execute(sql`SELECT 1`);
49 dbOk = true;
50 } catch {
51 dbOk = false;
52 }
53
54 let userCount = 0;
55 let repoCount = 0;
56 let publicRepoCount = 0;
57 let gateRunCount = 0;
58 let greenRate: number | null = null;
59 try {
60 const [u] = await db.select({ n: sql<number>`count(*)::int` }).from(users);
61 userCount = Number(u?.n ?? 0);
62 const [r] = await db
63 .select({ n: sql<number>`count(*)::int` })
64 .from(repositories);
65 repoCount = Number(r?.n ?? 0);
66 const [pr] = await db
67 .select({ n: sql<number>`count(*)::int` })
68 .from(repositories)
69 .where(sql`${repositories.isPrivate} = false`);
70 publicRepoCount = Number(pr?.n ?? 0);
71 const [gr] = await db
72 .select({ n: sql<number>`count(*)::int` })
73 .from(gateRuns);
74 gateRunCount = Number(gr?.n ?? 0);
75 if (gateRunCount > 0) {
76 const [g] = await db
77 .select({ n: sql<number>`count(*)::int` })
78 .from(gateRuns)
79 .where(sql`${gateRuns.status} IN ('passed','repaired')`);
80 greenRate = (Number(g?.n ?? 0) / gateRunCount) * 100;
81 }
82 } catch {
83 // counts stay 0
84 }
85
86 const tick = getLastTick();
87 const ticks = getTickCount();
88 const autopilotDisabled = process.env.AUTOPILOT_DISABLED === "1";
89 const uptimeMs = Date.now() - started;
90
b1be050CC LABS App91 // BLOCK S4 — Show any red synthetic-monitor results from the last 24h
92 // on the public status page. Never blocks the render.
93 let recentIncidents: Awaited<ReturnType<typeof recentRedChecks>> = [];
94 try {
95 recentIncidents = await recentRedChecks(24, 10);
96 } catch {
97 recentIncidents = [];
98 }
99
100 const overallOk = dbOk && recentIncidents.length === 0;
2316be6Claude101
0a50474Claude102 // Aggregate uptime — process uptime over the last 24h window, capped
103 // at 100%. We don't track historical downtime in-process, so this is a
104 // best-effort number based on how long this Bun worker has been alive
105 // versus the rolling 24h window.
106 const WINDOW_MS = 24 * 60 * 60 * 1000;
107 const aggregatePct =
108 overallOk
109 ? Math.min(100, (uptimeMs / WINDOW_MS) * 100)
110 : Math.max(0, 100 - (recentIncidents.length * 100) / 24);
111 const aggregateStr = aggregatePct >= 100 ? "100.0" : aggregatePct.toFixed(1);
112
113 // Per-service status descriptors — kept here so the JSX is just markup.
114 const services: Array<{
115 name: string;
116 sub: string;
117 state: "ok" | "down" | "idle";
118 label: string;
119 }> = [
120 {
121 name: "Database",
122 sub: "Neon PostgreSQL",
123 state: dbOk ? "ok" : "down",
124 label: dbOk ? "operational" : "down",
125 },
126 {
127 name: "Autopilot",
128 sub: "Periodic platform-maintenance loop",
129 state: autopilotDisabled ? "idle" : "ok",
130 label: autopilotDisabled ? "disabled" : "running",
131 },
132 {
133 name: "Git Smart HTTP",
134 sub: "Clone, fetch, push",
135 state: "ok",
136 label: "operational",
137 },
138 ];
139
2316be6Claude140 return c.html(
141 <Layout title="Status — gluecron" user={user}>
0a50474Claude142 <style dangerouslySetInnerHTML={{ __html: statusStyles }} />
143 <div class="status-wrap">
144 {/* ─── Hero ─── */}
145 <section
146 class={"status-hero " + (overallOk ? "is-ok" : "is-degraded")}
147 >
148 <div class="status-hero-orb" aria-hidden="true" />
149 <div class="status-hero-inner">
150 <div class="status-eyebrow">
151 <span class="status-eyebrow-pill" aria-hidden="true">
152 <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
153 <path d="M22 12h-4l-3 9L9 3l-3 9H2" />
154 </svg>
155 </span>
156 Platform status · live · reloads on refresh
2316be6Claude157 </div>
0a50474Claude158 <h1 class="status-title">
159 <span class="status-dot-big" aria-hidden="true" />
160 <span
161 class={
162 "status-title-text " +
163 (overallOk
164 ? "status-title-grad"
165 : "status-title-grad status-title-grad-warn")
166 }
167 >
168 {overallOk
169 ? "All systems operational"
170 : "Service degraded"}
171 </span>
172 </h1>
173 <p class="status-sub">
174 Live platform health for every Gluecron surface — database,
175 autopilot, git protocol, and the synthetic monitor.
176 </p>
177 <div class="status-hero-stats">
178 <div class="status-hero-stat">
179 <div class="status-hero-stat-num">{aggregateStr}%</div>
180 <div class="status-hero-stat-label">24h uptime</div>
2316be6Claude181 </div>
0a50474Claude182 <div class="status-hero-stat">
183 <div class="status-hero-stat-num">{fmtUptime(uptimeMs)}</div>
184 <div class="status-hero-stat-label">Process uptime</div>
185 </div>
186 <div class="status-hero-stat">
187 <div class="status-hero-stat-num">{recentIncidents.length}</div>
188 <div class="status-hero-stat-label">Incidents · 24h</div>
2316be6Claude189 </div>
190 </div>
191 </div>
0a50474Claude192 </section>
2316be6Claude193
0a50474Claude194 {/* ─── Components / health pills ─── */}
195 <section class="status-section" aria-labelledby="status-comp-h">
196 <header class="status-section-head">
197 <div>
198 <p class="status-section-eyebrow">Components</p>
199 <h2 class="status-section-title" id="status-comp-h">
200 Per-service health
201 </h2>
2316be6Claude202 </div>
0a50474Claude203 </header>
204 <ul class="status-svc-list">
205 {services.map((svc) => (
206 <li class="status-svc-row">
207 <div class="status-svc-main">
208 <p class="status-svc-name">{svc.name}</p>
209 <p class="status-svc-sub">{svc.sub}</p>
210 </div>
211 <span
212 class={"status-pill status-pill-" + svc.state}
213 title={svc.label}
214 >
215 <span class="status-pill-dot" aria-hidden="true" />
216 {svc.label}
217 </span>
218 </li>
219 ))}
220 </ul>
221 </section>
222
223 {/* ─── Platform stats ─── */}
224 <section class="status-section" aria-labelledby="status-stats-h">
225 <header class="status-section-head">
226 <div>
227 <p class="status-section-eyebrow">Numbers</p>
228 <h2 class="status-section-title" id="status-stats-h">
229 Platform stats
230 </h2>
2316be6Claude231 </div>
0a50474Claude232 </header>
233 <div class="status-section-body">
234 <div class="status-stats-grid">
235 <div class="status-stat">
236 <div class="status-stat-num">{userCount.toLocaleString()}</div>
237 <div class="status-stat-label">Developers</div>
238 </div>
239 <div class="status-stat">
240 <div class="status-stat-num">{repoCount.toLocaleString()}</div>
241 <div class="status-stat-label">Repositories</div>
242 </div>
243 <div class="status-stat">
244 <div class="status-stat-num">
245 {publicRepoCount.toLocaleString()}
246 </div>
247 <div class="status-stat-label">Public repos</div>
248 </div>
249 <div class="status-stat">
250 <div class="status-stat-num">
251 {gateRunCount.toLocaleString()}
252 </div>
253 <div class="status-stat-label">Gate runs</div>
254 </div>
255 <div class="status-stat">
256 <div class="status-stat-num">
257 {greenRate === null ? "—" : `${greenRate.toFixed(1)}%`}
258 </div>
259 <div class="status-stat-label">Green rate</div>
260 </div>
261 <div class="status-stat">
262 <div class="status-stat-num">{fmtUptime(uptimeMs)}</div>
263 <div class="status-stat-label">Uptime</div>
264 </div>
2316be6Claude265 </div>
266 </div>
0a50474Claude267 </section>
268
269 {/* ─── Recent incidents ─── */}
270 <section class="status-section" aria-labelledby="status-inc-h">
271 <header class="status-section-head">
272 <div>
273 <p class="status-section-eyebrow">Last 24h</p>
274 <h2 class="status-section-title" id="status-inc-h">
275 Recent incidents
276 </h2>
277 <p class="status-section-sub">
278 Synthetic-monitor probes that returned a red result.
279 </p>
2316be6Claude280 </div>
0a50474Claude281 {recentIncidents.length > 0 ? (
282 <span class="status-count-pill is-warn">
283 {recentIncidents.length} red
284 </span>
285 ) : (
286 <span class="status-count-pill is-ok">All green</span>
287 )}
288 </header>
289 <div class="status-section-body">
290 {recentIncidents.length === 0 ? (
291 <div class="status-empty">
292 <div class="status-empty-orb" aria-hidden="true" />
293 <div class="status-empty-inner">
294 <p class="status-empty-title">No incidents in the last 24h.</p>
295 <p class="status-empty-sub">
296 Every synthetic probe is green. We log every red here
297 with timestamps and error text.
298 </p>
299 </div>
300 </div>
301 ) : (
302 <ul class="status-inc-list">
303 {recentIncidents.map((r) => (
304 <li class="status-inc-row">
305 <span class="status-inc-dot" aria-hidden="true" />
306 <div class="status-inc-main">
307 <p class="status-inc-name">
308 <code>{r.name}</code>
309 </p>
310 <p class="status-inc-err">
311 {r.error || "(no error message)"}
312 </p>
313 </div>
314 <time class="status-inc-time">
315 {r.checkedAt.toISOString()}
316 </time>
317 </li>
318 ))}
319 </ul>
320 )}
2316be6Claude321 </div>
0a50474Claude322 </section>
323
324 {/* ─── Autopilot tick ─── */}
325 <section class="status-section" aria-labelledby="status-tick-h">
326 <header class="status-section-head">
327 <div>
328 <p class="status-section-eyebrow">Autopilot</p>
329 <h2 class="status-section-title" id="status-tick-h">
330 Latest tick
331 </h2>
332 <p class="status-section-sub">
333 Per-task results from the most recent autopilot sweep.
334 </p>
2316be6Claude335 </div>
0a50474Claude336 {tick ? (
337 <span class="status-count-pill">
338 {ticks} ticks this process
339 </span>
340 ) : null}
341 </header>
342 <div class="status-section-body">
343 {tick ? (
344 <ul class="status-tick-list">
345 <li class="status-tick-row">
346 <span class="status-tick-name">Finished</span>
347 <code class="status-tick-val">{tick.finishedAt}</code>
348 </li>
349 <li class="status-tick-row">
350 <span class="status-tick-name">Total ticks this process</span>
351 <code class="status-tick-val">{ticks}</code>
352 </li>
353 {tick.tasks.map((t) => (
354 <li class="status-tick-row">
355 <code class="status-tick-name">{t.name}</code>
356 <span
357 class={"status-tick-val " + (t.ok ? "is-ok" : "is-err")}
358 >
359 {t.ok ? "ok" : `failed: ${t.error || "unknown"}`}
360 <span class="status-tick-ms">{t.durationMs}ms</span>
b1be050CC LABS App361 </span>
0a50474Claude362 </li>
363 ))}
364 </ul>
365 ) : (
366 <div class="status-empty">
367 <div class="status-empty-orb" aria-hidden="true" />
368 <div class="status-empty-inner">
369 <p class="status-empty-title">
370 {autopilotDisabled
371 ? "Autopilot is disabled."
372 : "No ticks yet."}
373 </p>
374 <p class="status-empty-sub">
375 {autopilotDisabled
376 ? "Set AUTOPILOT_DISABLED=0 to re-enable the periodic platform-maintenance loop."
377 : "The first tick runs within 5 minutes of process start. Check back shortly."}
378 </p>
b1be050CC LABS App379 </div>
2316be6Claude380 </div>
0a50474Claude381 )}
2316be6Claude382 </div>
0a50474Claude383 </section>
384
385 <p class="status-foot">
386 Liveness: <a href="/healthz">/healthz</a> · Readiness:{" "}
387 <a href="/readyz">/readyz</a> · Metrics:{" "}
388 <a href="/metrics">/metrics</a> · Platform JSON:{" "}
2316be6Claude389 <a href="/api/platform-status">/api/platform-status</a>
390 </p>
391 </div>
392 </Layout>
393 );
394});
395
9b07ca9Claude396/**
397 * Shields-style status badge. Reads the latest autopilot tick + DB
398 * reachability and returns an SVG. Embed in READMEs with:
399 * ![status](https://your-host/status.svg)
400 */
401status.get("/status.svg", async (c) => {
402 let dbOk = false;
403 try {
404 await db.execute(sql`SELECT 1`);
405 dbOk = true;
406 } catch {
407 dbOk = false;
408 }
409 const tick = getLastTick();
410 const lastOk = tick ? tick.tasks.every((t) => t.ok) : true;
411 const overall = dbOk && lastOk;
412 const label = "gluecron";
413 const value = overall ? "operational" : "degraded";
414 const fill = overall ? "#2da44e" : "#cf222e";
415
416 const labelW = 70;
417 const valueW = overall ? 78 : 68;
418 const totalW = labelW + valueW;
419 const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${totalW}" height="20" role="img" aria-label="${label}: ${value}">
420 <linearGradient id="s" x2="0" y2="100%">
421 <stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
422 <stop offset="1" stop-opacity=".1"/>
423 </linearGradient>
424 <rect width="${totalW}" height="20" rx="3" fill="#555"/>
425 <rect x="${labelW}" width="${valueW}" height="20" rx="3" fill="${fill}"/>
426 <rect width="${totalW}" height="20" rx="3" fill="url(#s)"/>
427 <g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,sans-serif" font-size="11">
428 <text x="${labelW / 2}" y="15">${label}</text>
429 <text x="${labelW + valueW / 2}" y="15">${value}</text>
430 </g>
431</svg>`;
432 c.header("Content-Type", "image/svg+xml; charset=utf-8");
433 c.header("Cache-Control", "no-cache, max-age=0");
434 return c.body(svg);
435});
436
0a50474Claude437/* ─────────────────────────────────────────────────────────────────────────
438 * Scoped CSS — every class prefixed `.status-` so this surface can't
439 * bleed into the admin status page or any other route.
440 * ───────────────────────────────────────────────────────────────────── */
441const statusStyles = `
442 .status-wrap { max-width: 960px; margin: 0 auto; padding: var(--space-6) var(--space-4); }
443
444 /* ─── Hero ─── */
445 .status-hero {
446 position: relative;
447 margin-bottom: var(--space-5);
448 padding: var(--space-5) var(--space-6);
449 background: var(--bg-elevated);
450 border: 1px solid var(--border);
451 border-radius: 16px;
452 overflow: hidden;
453 }
454 .status-hero::before {
455 content: '';
456 position: absolute;
457 top: 0; left: 0; right: 0;
458 height: 2px;
459 background: linear-gradient(90deg, transparent 0%, #34d399 30%, #36c5d6 70%, transparent 100%);
460 opacity: 0.75;
461 pointer-events: none;
462 }
463 .status-hero.is-degraded::before {
464 background: linear-gradient(90deg, transparent 0%, #f87171 30%, #fbbf24 70%, transparent 100%);
465 }
466 .status-hero-orb {
467 position: absolute;
468 inset: -20% -10% auto auto;
469 width: 380px; height: 380px;
470 background: radial-gradient(circle, rgba(52,211,153,0.20), rgba(54,197,214,0.10) 45%, transparent 70%);
471 filter: blur(80px);
472 opacity: 0.7;
473 pointer-events: none;
474 z-index: 0;
475 }
476 .status-hero.is-degraded .status-hero-orb {
477 background: radial-gradient(circle, rgba(248,113,113,0.22), rgba(251,191,36,0.10) 45%, transparent 70%);
478 }
479 .status-hero-inner { position: relative; z-index: 1; }
480 .status-eyebrow {
481 font-size: 12px;
482 color: var(--text-muted);
483 margin-bottom: var(--space-2);
484 letter-spacing: 0.02em;
485 display: inline-flex;
486 align-items: center;
487 gap: 8px;
488 }
489 .status-eyebrow-pill {
490 display: inline-flex;
491 align-items: center;
492 justify-content: center;
493 width: 18px; height: 18px;
494 border-radius: 6px;
495 background: rgba(52,211,153,0.14);
496 color: #6ee7b7;
497 box-shadow: inset 0 0 0 1px rgba(52,211,153,0.32);
498 }
499 .status-hero.is-degraded .status-eyebrow-pill {
500 background: rgba(248,113,113,0.14);
501 color: #fca5a5;
502 box-shadow: inset 0 0 0 1px rgba(248,113,113,0.30);
503 }
504 .status-title {
505 font-size: clamp(28px, 4vw, 40px);
506 font-family: var(--font-display);
507 font-weight: 800;
508 letter-spacing: -0.028em;
509 line-height: 1.05;
510 margin: 0 0 var(--space-2);
511 color: var(--text-strong);
512 display: flex;
513 align-items: center;
514 gap: 14px;
515 flex-wrap: wrap;
516 }
517 .status-dot-big {
518 display: inline-block;
519 width: 14px; height: 14px;
520 border-radius: 50%;
521 background: #34d399;
522 box-shadow: 0 0 0 5px rgba(52,211,153,0.18);
523 flex-shrink: 0;
524 }
525 .status-hero.is-degraded .status-dot-big {
526 background: #f87171;
527 box-shadow: 0 0 0 5px rgba(248,113,113,0.18);
528 }
529 .status-title-grad {
530 background-image: linear-gradient(135deg, #6ee7b7 0%, #34d399 50%, #36c5d6 100%);
531 -webkit-background-clip: text;
532 background-clip: text;
533 -webkit-text-fill-color: transparent;
534 color: transparent;
535 }
536 .status-title-grad-warn {
537 background-image: linear-gradient(135deg, #fca5a5 0%, #f87171 50%, #fbbf24 100%);
538 }
539 .status-sub {
540 font-size: 15px;
541 color: var(--text-muted);
542 margin: 0 0 var(--space-4);
543 line-height: 1.55;
544 max-width: 620px;
545 }
546
547 /* Hero stats strip */
548 .status-hero-stats {
549 display: grid;
550 grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
551 gap: var(--space-3);
552 }
553 .status-hero-stat {
554 padding: 12px 14px;
555 background: rgba(255,255,255,0.025);
556 border: 1px solid var(--border-subtle);
557 border-radius: 10px;
558 }
559 .status-hero-stat-num {
560 font-family: var(--font-display);
561 font-size: 22px;
562 font-weight: 700;
563 color: var(--text-strong);
564 letter-spacing: -0.02em;
565 }
566 .status-hero-stat-label {
567 font-size: 11px;
568 color: var(--text-muted);
569 text-transform: uppercase;
570 letter-spacing: 0.06em;
571 margin-top: 2px;
572 }
573
574 /* ─── Section cards ─── */
575 .status-section {
576 margin-bottom: var(--space-5);
577 background: var(--bg-elevated);
578 border: 1px solid var(--border);
579 border-radius: 14px;
580 overflow: hidden;
581 }
582 .status-section-head {
583 padding: var(--space-4) var(--space-5);
584 border-bottom: 1px solid var(--border);
585 display: flex;
586 align-items: flex-start;
587 justify-content: space-between;
588 gap: var(--space-3);
589 flex-wrap: wrap;
590 }
591 .status-section-eyebrow {
592 font-size: 11px;
593 font-weight: 600;
594 letter-spacing: 0.08em;
595 text-transform: uppercase;
596 color: var(--text-faint);
597 margin: 0 0 6px;
598 }
599 .status-section-title {
600 margin: 0;
601 font-family: var(--font-display);
602 font-size: 17px;
603 font-weight: 700;
604 letter-spacing: -0.018em;
605 color: var(--text-strong);
606 }
607 .status-section-sub {
608 margin: 6px 0 0;
609 font-size: 12.5px;
610 color: var(--text-muted);
611 line-height: 1.5;
612 }
613 .status-section-body { padding: var(--space-5); }
614
615 /* ─── Service pills ─── */
616 .status-svc-list { list-style: none; margin: 0; padding: 0; }
617 .status-svc-row {
618 display: flex;
619 align-items: center;
620 justify-content: space-between;
621 gap: var(--space-3);
622 padding: var(--space-3) var(--space-5);
623 border-bottom: 1px solid var(--border);
624 }
625 .status-svc-row:last-child { border-bottom: 0; }
626 .status-svc-main { flex: 1; }
627 .status-svc-name {
628 margin: 0;
629 font-weight: 600;
630 color: var(--text-strong);
631 font-size: 14px;
632 }
633 .status-svc-sub {
634 margin: 2px 0 0;
635 font-size: 12px;
636 color: var(--text-muted);
637 }
638 .status-pill {
639 display: inline-flex;
640 align-items: center;
641 gap: 6px;
642 padding: 4px 10px;
643 border-radius: 9999px;
644 font-size: 11.5px;
645 font-weight: 600;
646 letter-spacing: 0.04em;
647 text-transform: lowercase;
648 }
649 .status-pill-dot {
650 width: 7px; height: 7px;
651 border-radius: 9999px;
652 background: currentColor;
653 flex-shrink: 0;
654 }
655 .status-pill-ok {
656 background: rgba(52,211,153,0.14);
657 color: #6ee7b7;
658 box-shadow: inset 0 0 0 1px rgba(52,211,153,0.32);
659 }
660 .status-pill-down {
661 background: rgba(248,113,113,0.14);
662 color: #fca5a5;
663 box-shadow: inset 0 0 0 1px rgba(248,113,113,0.32);
664 }
665 .status-pill-idle {
666 background: rgba(110,118,129,0.18);
667 color: #c9d1d9;
668 box-shadow: inset 0 0 0 1px rgba(110,118,129,0.40);
669 }
670
671 /* ─── Platform stats ─── */
672 .status-stats-grid {
673 display: grid;
674 grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
675 gap: var(--space-3);
676 }
677 .status-stat {
678 padding: var(--space-3) var(--space-4);
679 background: var(--bg-secondary);
680 border: 1px solid var(--border-subtle);
681 border-radius: 12px;
682 text-align: center;
683 transition: border-color 150ms ease;
684 }
685 .status-stat:hover { border-color: var(--border-strong); }
686 .status-stat-num {
687 font-family: var(--font-display);
688 font-size: 24px;
689 font-weight: 700;
690 color: var(--text-strong);
691 letter-spacing: -0.022em;
692 }
693 .status-stat-label {
694 font-size: 11px;
695 color: var(--text-muted);
696 text-transform: uppercase;
697 letter-spacing: 0.06em;
698 margin-top: 4px;
699 }
700
701 /* ─── Count pills ─── */
702 .status-count-pill {
703 display: inline-flex;
704 align-items: center;
705 padding: 4px 10px;
706 border-radius: 9999px;
707 font-family: var(--font-mono);
708 font-size: 11.5px;
709 font-weight: 600;
710 background: rgba(140,109,255,0.10);
711 color: #c5b3ff;
712 box-shadow: inset 0 0 0 1px rgba(140,109,255,0.28);
713 letter-spacing: 0.02em;
714 }
715 .status-count-pill.is-ok {
716 background: rgba(52,211,153,0.12);
717 color: #6ee7b7;
718 box-shadow: inset 0 0 0 1px rgba(52,211,153,0.32);
719 }
720 .status-count-pill.is-warn {
721 background: rgba(248,113,113,0.12);
722 color: #fca5a5;
723 box-shadow: inset 0 0 0 1px rgba(248,113,113,0.32);
724 }
725
726 /* ─── Incidents list ─── */
727 .status-inc-list { list-style: none; margin: 0; padding: 0; }
728 .status-inc-row {
729 display: flex;
730 align-items: flex-start;
731 gap: 12px;
732 padding: var(--space-3) 0;
733 border-bottom: 1px solid var(--border-subtle);
734 }
735 .status-inc-row:last-child { border-bottom: 0; }
736 .status-inc-row:first-child { padding-top: 0; }
737 .status-inc-dot {
738 width: 8px; height: 8px;
739 border-radius: 9999px;
740 background: #f87171;
741 box-shadow: 0 0 0 3px rgba(248,113,113,0.18);
742 margin-top: 6px;
743 flex-shrink: 0;
744 }
745 .status-inc-main { flex: 1; min-width: 0; }
746 .status-inc-name {
747 margin: 0;
748 font-size: 13.5px;
749 color: var(--text-strong);
750 }
751 .status-inc-name code {
752 font-family: var(--font-mono);
753 font-size: 12.5px;
754 background: var(--bg-tertiary);
755 padding: 1px 6px;
756 border-radius: 4px;
757 }
758 .status-inc-err {
759 margin: 4px 0 0;
760 font-size: 12.5px;
761 color: var(--text-muted);
762 line-height: 1.5;
763 }
764 .status-inc-time {
765 font-family: var(--font-mono);
766 font-size: 11.5px;
767 color: var(--text-faint);
768 white-space: nowrap;
769 flex-shrink: 0;
770 }
771
772 /* ─── Empty state ─── */
773 .status-empty {
774 position: relative;
775 padding: var(--space-6) var(--space-5);
776 border: 1px dashed var(--border-strong);
777 border-radius: 14px;
778 background: rgba(255,255,255,0.02);
779 text-align: center;
780 overflow: hidden;
781 }
782 .status-empty-orb {
783 position: absolute;
784 inset: -40% -10% auto auto;
785 width: 320px; height: 320px;
786 background: radial-gradient(circle, rgba(52,211,153,0.18), rgba(54,197,214,0.10) 45%, transparent 70%);
787 filter: blur(70px);
788 opacity: 0.55;
789 pointer-events: none;
790 z-index: 0;
791 }
792 .status-empty-inner { position: relative; z-index: 1; }
793 .status-empty-title {
794 margin: 0 0 6px;
795 font-family: var(--font-display);
796 font-size: 16px;
797 font-weight: 700;
798 color: var(--text-strong);
799 letter-spacing: -0.012em;
800 }
801 .status-empty-sub {
802 margin: 0 auto;
803 max-width: 460px;
804 font-size: 13px;
805 color: var(--text-muted);
806 line-height: 1.5;
807 }
808
809 /* ─── Autopilot tick list ─── */
810 .status-tick-list { list-style: none; margin: 0; padding: 0; }
811 .status-tick-row {
812 display: flex;
813 align-items: center;
814 justify-content: space-between;
815 gap: 12px;
816 padding: 10px 0;
817 border-bottom: 1px solid var(--border-subtle);
818 font-size: 13px;
819 }
820 .status-tick-row:last-child { border-bottom: 0; }
821 .status-tick-row:first-child { padding-top: 0; }
822 .status-tick-name {
823 color: var(--text);
824 font-size: 13px;
825 }
826 code.status-tick-name {
827 font-family: var(--font-mono);
828 font-size: 12.5px;
829 background: var(--bg-tertiary);
830 padding: 1px 6px;
831 border-radius: 4px;
832 color: var(--text-strong);
833 }
834 .status-tick-val {
835 display: inline-flex;
836 align-items: center;
837 gap: 10px;
838 font-family: var(--font-mono);
839 font-size: 12.5px;
840 color: var(--text-muted);
841 }
842 .status-tick-val.is-ok { color: #6ee7b7; }
843 .status-tick-val.is-err { color: #fca5a5; }
844 .status-tick-ms { color: var(--text-faint); font-size: 11.5px; }
845
846 /* ─── Footer links ─── */
847 .status-foot {
848 margin-top: var(--space-5);
849 padding-top: var(--space-4);
850 border-top: 1px solid var(--border);
851 color: var(--text-muted);
852 font-size: 12.5px;
853 text-align: center;
854 }
855 .status-foot a {
856 color: var(--accent, #8c6dff);
857 text-decoration: none;
858 }
859 .status-foot a:hover { text-decoration: underline; }
860`;
861
2316be6Claude862export default status;