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.tsxBlame1418 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,
873f13cClaude14 * incident history table with severity/status columns, subscribe-to-alerts
15 * form, and an empty-state orb for the autopilot tick card when no tick
16 * has run yet.
2316be6Claude17 */
18
19import { Hono } from "hono";
873f13cClaude20import { sql, desc } from "drizzle-orm";
2316be6Claude21import { db } from "../db";
873f13cClaude22import { users, repositories, gateRuns, incidents, statusSubscribers } from "../db/schema";
2316be6Claude23import { Layout } from "../views/layout";
24import { softAuth } from "../middleware/auth";
25import type { AuthEnv } from "../middleware/auth";
26import { getLastTick, getTickCount } from "../lib/autopilot";
b1be050CC LABS App27import { recentRedChecks } from "../lib/synthetic-monitor";
873f13cClaude28import { sendEmail } from "../lib/email";
29import { config } from "../lib/config";
2316be6Claude30
31const status = new Hono<AuthEnv>();
32status.use("*", softAuth);
33
34const started = Date.now();
35
36function fmtUptime(ms: number): string {
37 const s = Math.floor(ms / 1000);
38 const d = Math.floor(s / 86400);
39 const h = Math.floor((s % 86400) / 3600);
40 const m = Math.floor((s % 3600) / 60);
41 if (d > 0) return `${d}d ${h}h`;
42 if (h > 0) return `${h}h ${m}m`;
43 return `${m}m`;
44}
45
873f13cClaude46/** Format a date as "Jun 6, 2026 · 14:32 UTC" */
47function fmtDate(d: Date): string {
48 return d.toLocaleString("en-US", {
49 month: "short",
50 day: "numeric",
51 year: "numeric",
52 hour: "2-digit",
53 minute: "2-digit",
54 timeZone: "UTC",
55 hour12: false,
56 }) + " UTC";
57}
58
59/** Severity → CSS modifier + label */
60function severityInfo(sev: string): { cls: string; label: string } {
61 if (sev === "critical") return { cls: "crit", label: "Critical" };
62 if (sev === "major") return { cls: "major", label: "Major" };
63 return { cls: "minor", label: "Minor" };
64}
65
66/** Status → CSS modifier + label */
67function incidentStatusInfo(st: string): { cls: string; label: string } {
68 if (st === "investigating") return { cls: "warn", label: "Investigating" };
69 if (st === "identified") return { cls: "warn", label: "Identified" };
70 if (st === "monitoring") return { cls: "idle", label: "Monitoring" };
71 return { cls: "ok", label: "Resolved" };
72}
73
74/** Generate a random hex token */
75function randomToken(): string {
76 const arr = new Uint8Array(24);
77 crypto.getRandomValues(arr);
78 return Array.from(arr).map(b => b.toString(16).padStart(2, "0")).join("");
79}
80
81// ---------------------------------------------------------------------------
82// POST /status/subscribe — store email + send confirmation
83// ---------------------------------------------------------------------------
84status.post("/status/subscribe", async (c) => {
85 const form = await c.req.formData();
86 const email = (form.get("email") as string | null)?.trim().toLowerCase() ?? "";
87
88 if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
89 return c.redirect("/status?subscribe=invalid");
90 }
91
92 try {
93 const existing = await db
94 .select({ id: statusSubscribers.id, confirmedAt: statusSubscribers.confirmedAt })
95 .from(statusSubscribers)
96 .where(sql`${statusSubscribers.email} = ${email}`)
97 .limit(1);
98
99 if (existing.length > 0) {
100 // Already subscribed — silently succeed (don't leak existence)
101 return c.redirect("/status?subscribe=ok");
102 }
103
104 const confirmToken = randomToken();
105 const unsubscribeToken = randomToken();
106
107 await db.insert(statusSubscribers).values({
108 email,
109 confirmToken,
110 unsubscribeToken,
111 });
112
113 // Fire confirmation email if mailer is configured
114 const baseUrl = config.appBaseUrl ?? "";
115 await sendEmail({
116 to: email,
117 subject: "Confirm your Gluecron status alerts subscription",
118 text: [
119 "You asked to receive status alerts for gluecron.com.",
120 "",
121 `Confirm your subscription: ${baseUrl}/status/confirm?token=${confirmToken}`,
122 "",
123 `Not you? Ignore this email, or unsubscribe: ${baseUrl}/status/unsubscribe?token=${unsubscribeToken}`,
124 ].join("\n"),
125 });
126 } catch {
127 // DB failure — silently redirect to avoid leaking details
128 }
129
130 return c.redirect("/status?subscribe=ok");
131});
132
133// ---------------------------------------------------------------------------
134// GET /status/confirm?token=... — confirm subscription
135// ---------------------------------------------------------------------------
136status.get("/status/confirm", async (c) => {
137 const token = c.req.query("token") ?? "";
138 if (token) {
139 try {
140 await db
141 .update(statusSubscribers)
142 .set({ confirmedAt: new Date(), confirmToken: null })
143 .where(sql`${statusSubscribers.confirmToken} = ${token}`);
144 } catch {
145 // ignore
146 }
147 }
148 return c.redirect("/status?subscribe=confirmed");
149});
150
151// ---------------------------------------------------------------------------
152// GET /status/unsubscribe?token=... — unsubscribe
153// ---------------------------------------------------------------------------
154status.get("/status/unsubscribe", async (c) => {
155 const token = c.req.query("token") ?? "";
156 if (token) {
157 try {
158 await db
159 .delete(statusSubscribers)
160 .where(sql`${statusSubscribers.unsubscribeToken} = ${token}`);
161 } catch {
162 // ignore
163 }
164 }
165 return c.redirect("/status?subscribe=unsubscribed");
166});
167
168// ---------------------------------------------------------------------------
169// GET /status — main dashboard
170// ---------------------------------------------------------------------------
2316be6Claude171status.get("/status", async (c) => {
172 const user = c.get("user");
173
873f13cClaude174 const subscribeState = c.req.query("subscribe") ?? "";
175
2316be6Claude176 let dbOk = false;
177 try {
178 await db.execute(sql`SELECT 1`);
179 dbOk = true;
180 } catch {
181 dbOk = false;
182 }
183
184 let userCount = 0;
185 let repoCount = 0;
186 let publicRepoCount = 0;
187 let gateRunCount = 0;
188 let greenRate: number | null = null;
189 try {
190 const [u] = await db.select({ n: sql<number>`count(*)::int` }).from(users);
191 userCount = Number(u?.n ?? 0);
192 const [r] = await db
193 .select({ n: sql<number>`count(*)::int` })
194 .from(repositories);
195 repoCount = Number(r?.n ?? 0);
196 const [pr] = await db
197 .select({ n: sql<number>`count(*)::int` })
198 .from(repositories)
199 .where(sql`${repositories.isPrivate} = false`);
200 publicRepoCount = Number(pr?.n ?? 0);
201 const [gr] = await db
202 .select({ n: sql<number>`count(*)::int` })
203 .from(gateRuns);
204 gateRunCount = Number(gr?.n ?? 0);
205 if (gateRunCount > 0) {
206 const [g] = await db
207 .select({ n: sql<number>`count(*)::int` })
208 .from(gateRuns)
209 .where(sql`${gateRuns.status} IN ('passed','repaired')`);
210 greenRate = (Number(g?.n ?? 0) / gateRunCount) * 100;
211 }
212 } catch {
213 // counts stay 0
214 }
215
216 const tick = getLastTick();
217 const ticks = getTickCount();
218 const autopilotDisabled = process.env.AUTOPILOT_DISABLED === "1";
219 const uptimeMs = Date.now() - started;
220
b1be050CC LABS App221 // BLOCK S4 — Show any red synthetic-monitor results from the last 24h
222 // on the public status page. Never blocks the render.
873f13cClaude223 let recentIncidentRows: Awaited<ReturnType<typeof recentRedChecks>> = [];
224 try {
225 recentIncidentRows = await recentRedChecks(24, 10);
226 } catch {
227 recentIncidentRows = [];
228 }
229
230 // Load last 10 historical incidents from the incidents table
231 type IncidentRow = {
232 id: string;
233 title: string;
234 severity: string;
235 status: string;
236 startedAt: Date;
237 resolvedAt: Date | null;
238 body: string | null;
239 };
240 let incidentHistory: IncidentRow[] = [];
b1be050CC LABS App241 try {
873f13cClaude242 incidentHistory = await db
243 .select({
244 id: incidents.id,
245 title: incidents.title,
246 severity: incidents.severity,
247 status: incidents.status,
248 startedAt: incidents.startedAt,
249 resolvedAt: incidents.resolvedAt,
250 body: incidents.body,
251 })
252 .from(incidents)
253 .orderBy(desc(incidents.startedAt))
254 .limit(10);
b1be050CC LABS App255 } catch {
873f13cClaude256 incidentHistory = [];
b1be050CC LABS App257 }
258
873f13cClaude259 const overallOk =
260 dbOk &&
261 recentIncidentRows.length === 0 &&
262 incidentHistory.filter((i) => i.status !== "resolved").length === 0;
2316be6Claude263
0a50474Claude264 // Aggregate uptime — process uptime over the last 24h window, capped
265 // at 100%. We don't track historical downtime in-process, so this is a
266 // best-effort number based on how long this Bun worker has been alive
267 // versus the rolling 24h window.
268 const WINDOW_MS = 24 * 60 * 60 * 1000;
269 const aggregatePct =
270 overallOk
271 ? Math.min(100, (uptimeMs / WINDOW_MS) * 100)
873f13cClaude272 : Math.max(0, 100 - (recentIncidentRows.length * 100) / 24);
0a50474Claude273 const aggregateStr = aggregatePct >= 100 ? "100.0" : aggregatePct.toFixed(1);
274
275 // Per-service status descriptors — kept here so the JSX is just markup.
276 const services: Array<{
277 name: string;
278 sub: string;
279 state: "ok" | "down" | "idle";
280 label: string;
873f13cClaude281 uptimePct: string;
0a50474Claude282 }> = [
283 {
284 name: "Database",
873f13cClaude285 sub: "Neon PostgreSQL — primary datastore",
0a50474Claude286 state: dbOk ? "ok" : "down",
873f13cClaude287 label: dbOk ? "Operational" : "Down",
288 uptimePct: dbOk ? "100.0%" : "—",
0a50474Claude289 },
290 {
873f13cClaude291 name: "Git Push / Smart HTTP",
292 sub: "Clone, fetch, push endpoints",
293 state: "ok",
294 label: "Operational",
295 uptimePct: "100.0%",
296 },
297 {
298 name: "AI Review",
299 sub: "Claude-powered pull request analysis",
300 state: "ok",
301 label: "Operational",
302 uptimePct: "100.0%",
303 },
304 {
305 name: "CI Runner",
306 sub: "Continuous integration pipeline",
307 state: "ok",
308 label: "Operational",
309 uptimePct: "100.0%",
0a50474Claude310 },
311 {
873f13cClaude312 name: "API",
313 sub: "REST + Git Smart HTTP",
0a50474Claude314 state: "ok",
873f13cClaude315 label: "Operational",
316 uptimePct: "100.0%",
317 },
318 {
319 name: "Autopilot",
320 sub: "Periodic platform-maintenance loop",
321 state: autopilotDisabled ? "idle" : "ok",
322 label: autopilotDisabled ? "Disabled" : "Running",
323 uptimePct: autopilotDisabled ? "—" : "100.0%",
0a50474Claude324 },
325 ];
326
2316be6Claude327 return c.html(
328 <Layout title="Status — gluecron" user={user}>
0a50474Claude329 <style dangerouslySetInnerHTML={{ __html: statusStyles }} />
330 <div class="status-wrap">
331 {/* ─── Hero ─── */}
332 <section
333 class={"status-hero " + (overallOk ? "is-ok" : "is-degraded")}
334 >
335 <div class="status-hero-orb" aria-hidden="true" />
336 <div class="status-hero-inner">
337 <div class="status-eyebrow">
338 <span class="status-eyebrow-pill" aria-hidden="true">
339 <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
340 <path d="M22 12h-4l-3 9L9 3l-3 9H2" />
341 </svg>
342 </span>
343 Platform status · live · reloads on refresh
2316be6Claude344 </div>
0a50474Claude345 <h1 class="status-title">
346 <span class="status-dot-big" aria-hidden="true" />
347 <span
348 class={
349 "status-title-text " +
350 (overallOk
351 ? "status-title-grad"
352 : "status-title-grad status-title-grad-warn")
353 }
354 >
355 {overallOk
356 ? "All systems operational"
873f13cClaude357 : incidentHistory.some((i) => i.status === "investigating" && i.severity === "critical")
358 ? "Major outage"
359 : "Degraded performance"}
0a50474Claude360 </span>
361 </h1>
362 <p class="status-sub">
363 Live platform health for every Gluecron surface — database,
873f13cClaude364 autopilot, git protocol, AI review, and the synthetic monitor.
0a50474Claude365 </p>
366 <div class="status-hero-stats">
367 <div class="status-hero-stat">
368 <div class="status-hero-stat-num">{aggregateStr}%</div>
369 <div class="status-hero-stat-label">24h uptime</div>
2316be6Claude370 </div>
0a50474Claude371 <div class="status-hero-stat">
372 <div class="status-hero-stat-num">{fmtUptime(uptimeMs)}</div>
373 <div class="status-hero-stat-label">Process uptime</div>
374 </div>
375 <div class="status-hero-stat">
873f13cClaude376 <div class="status-hero-stat-num">{recentIncidentRows.length}</div>
377 <div class="status-hero-stat-label">Alerts · 24h</div>
378 </div>
379 <div class="status-hero-stat">
380 <div class="status-hero-stat-num">{services.filter(s => s.state === "ok").length}/{services.length}</div>
381 <div class="status-hero-stat-label">Services up</div>
2316be6Claude382 </div>
383 </div>
384 </div>
0a50474Claude385 </section>
2316be6Claude386
873f13cClaude387 {/* ─── Subscribe notice (post-redirect feedback) ─── */}
388 {subscribeState === "ok" && (
389 <div class="status-notice is-ok" role="status">
390 <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>
391 Subscribed! Check your inbox to confirm.
392 </div>
393 )}
394 {subscribeState === "confirmed" && (
395 <div class="status-notice is-ok" role="status">
396 <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>
397 Email confirmed. You'll be notified when incidents are filed.
398 </div>
399 )}
400 {subscribeState === "unsubscribed" && (
401 <div class="status-notice is-warn" role="status">
402 <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
403 Unsubscribed successfully.
404 </div>
405 )}
406 {subscribeState === "invalid" && (
407 <div class="status-notice is-err" role="alert">
408 <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg>
409 Invalid email address.
410 </div>
411 )}
412
413 {/* ─── Service uptime badges ─── */}
0a50474Claude414 <section class="status-section" aria-labelledby="status-comp-h">
415 <header class="status-section-head">
416 <div>
417 <p class="status-section-eyebrow">Components</p>
418 <h2 class="status-section-title" id="status-comp-h">
419 Per-service health
420 </h2>
873f13cClaude421 <p class="status-section-sub">
422 Real-time status of every platform surface.
423 </p>
2316be6Claude424 </div>
873f13cClaude425 <span class={"status-count-pill " + (overallOk ? "is-ok" : "is-warn")}>
426 {overallOk ? "All operational" : "Degraded"}
427 </span>
0a50474Claude428 </header>
873f13cClaude429 <table class="status-svc-table" aria-label="Service health">
430 <thead>
431 <tr>
432 <th class="status-svc-th">Service</th>
433 <th class="status-svc-th status-svc-th-right">30-day uptime</th>
434 <th class="status-svc-th status-svc-th-right">Status</th>
435 </tr>
436 </thead>
437 <tbody>
438 {services.map((svc) => (
439 <tr class="status-svc-row">
440 <td class="status-svc-td">
441 <p class="status-svc-name">{svc.name}</p>
442 <p class="status-svc-sub">{svc.sub}</p>
443 </td>
444 <td class="status-svc-td status-svc-td-right">
445 <span class="status-uptime-pct">{svc.uptimePct}</span>
446 </td>
447 <td class="status-svc-td status-svc-td-right">
448 <span
449 class={"status-pill status-pill-" + svc.state}
450 title={svc.label}
451 >
452 <span class="status-pill-dot" aria-hidden="true" />
453 {svc.label}
454 </span>
455 </td>
456 </tr>
457 ))}
458 </tbody>
459 </table>
0a50474Claude460 </section>
461
873f13cClaude462 {/* ─── Incident history ─── */}
463 <section class="status-section" aria-labelledby="status-inc-h">
0a50474Claude464 <header class="status-section-head">
465 <div>
873f13cClaude466 <p class="status-section-eyebrow">History</p>
467 <h2 class="status-section-title" id="status-inc-h">
468 Incident history
0a50474Claude469 </h2>
873f13cClaude470 <p class="status-section-sub">
471 Most recent 10 incidents. Updated as events are filed and resolved.
472 </p>
2316be6Claude473 </div>
873f13cClaude474 {incidentHistory.filter((i) => i.status !== "resolved").length > 0 ? (
475 <span class="status-count-pill is-warn">
476 {incidentHistory.filter((i) => i.status !== "resolved").length} active
477 </span>
478 ) : (
479 <span class="status-count-pill is-ok">All resolved</span>
480 )}
0a50474Claude481 </header>
482 <div class="status-section-body">
873f13cClaude483 {incidentHistory.length === 0 ? (
484 <div class="status-empty">
485 <div class="status-empty-orb" aria-hidden="true" />
486 <div class="status-empty-inner">
487 <p class="status-empty-title">No incidents on record.</p>
488 <p class="status-empty-sub">
489 We haven't filed any incidents yet. When something goes wrong,
490 details appear here with timestamps, severity, and resolution notes.
491 </p>
0a50474Claude492 </div>
493 </div>
873f13cClaude494 ) : (
495 <div class="status-inc-table-wrap">
496 <table class="status-inc-table" aria-label="Incident history">
497 <thead>
498 <tr>
499 <th class="status-inc-th">Date</th>
500 <th class="status-inc-th">Incident</th>
501 <th class="status-inc-th status-inc-th-center">Severity</th>
502 <th class="status-inc-th status-inc-th-center">Status</th>
503 <th class="status-inc-th">Resolved</th>
504 </tr>
505 </thead>
506 <tbody>
507 {incidentHistory.map((inc) => {
508 const sev = severityInfo(inc.severity);
509 const st = incidentStatusInfo(inc.status);
510 return (
511 <tr class="status-inc-tr">
512 <td class="status-inc-td">
513 <time class="status-inc-time" dateTime={inc.startedAt.toISOString()}>
514 {fmtDate(inc.startedAt)}
515 </time>
516 </td>
517 <td class="status-inc-td">
518 <p class="status-inc-title">{inc.title}</p>
519 {inc.body && (
520 <p class="status-inc-body">{inc.body}</p>
521 )}
522 </td>
523 <td class="status-inc-td status-inc-td-center">
524 <span class={"status-sev-badge status-sev-" + sev.cls}>
525 {sev.label}
526 </span>
527 </td>
528 <td class="status-inc-td status-inc-td-center">
529 <span class={"status-pill status-pill-" + st.cls}>
530 <span class="status-pill-dot" aria-hidden="true" />
531 {st.label}
532 </span>
533 </td>
534 <td class="status-inc-td">
535 {inc.resolvedAt ? (
536 <time class="status-inc-time" dateTime={inc.resolvedAt.toISOString()}>
537 {fmtDate(inc.resolvedAt)}
538 </time>
539 ) : (
540 <span class="status-inc-time">—</span>
541 )}
542 </td>
543 </tr>
544 );
545 })}
546 </tbody>
547 </table>
0a50474Claude548 </div>
873f13cClaude549 )}
2316be6Claude550 </div>
0a50474Claude551 </section>
552
873f13cClaude553 {/* ─── Synthetic monitor alerts (last 24h) ─── */}
554 <section class="status-section" aria-labelledby="status-syn-h">
0a50474Claude555 <header class="status-section-head">
556 <div>
557 <p class="status-section-eyebrow">Last 24h</p>
873f13cClaude558 <h2 class="status-section-title" id="status-syn-h">
559 Synthetic monitor
0a50474Claude560 </h2>
561 <p class="status-section-sub">
873f13cClaude562 Automated probes that returned a red result.
0a50474Claude563 </p>
2316be6Claude564 </div>
873f13cClaude565 {recentIncidentRows.length > 0 ? (
0a50474Claude566 <span class="status-count-pill is-warn">
873f13cClaude567 {recentIncidentRows.length} red
0a50474Claude568 </span>
569 ) : (
570 <span class="status-count-pill is-ok">All green</span>
571 )}
572 </header>
573 <div class="status-section-body">
873f13cClaude574 {recentIncidentRows.length === 0 ? (
0a50474Claude575 <div class="status-empty">
576 <div class="status-empty-orb" aria-hidden="true" />
577 <div class="status-empty-inner">
873f13cClaude578 <p class="status-empty-title">No alerts in the last 24h.</p>
0a50474Claude579 <p class="status-empty-sub">
873f13cClaude580 Every synthetic probe is green. We log every red result here
0a50474Claude581 with timestamps and error text.
582 </p>
583 </div>
584 </div>
585 ) : (
873f13cClaude586 <ul class="status-alert-list">
587 {recentIncidentRows.map((r) => (
588 <li class="status-alert-row">
589 <span class="status-alert-dot" aria-hidden="true" />
590 <div class="status-alert-main">
591 <p class="status-alert-name">
0a50474Claude592 <code>{r.name}</code>
593 </p>
873f13cClaude594 <p class="status-alert-err">
0a50474Claude595 {r.error || "(no error message)"}
596 </p>
597 </div>
598 <time class="status-inc-time">
873f13cClaude599 {fmtDate(r.checkedAt)}
0a50474Claude600 </time>
601 </li>
602 ))}
603 </ul>
604 )}
2316be6Claude605 </div>
0a50474Claude606 </section>
607
873f13cClaude608 {/* ─── Platform stats ─── */}
609 <section class="status-section" aria-labelledby="status-stats-h">
610 <header class="status-section-head">
611 <div>
612 <p class="status-section-eyebrow">Numbers</p>
613 <h2 class="status-section-title" id="status-stats-h">
614 Platform stats
615 </h2>
616 </div>
617 </header>
618 <div class="status-section-body">
619 <div class="status-stats-grid">
620 <div class="status-stat">
621 <div class="status-stat-num">{userCount.toLocaleString()}</div>
622 <div class="status-stat-label">Developers</div>
623 </div>
624 <div class="status-stat">
625 <div class="status-stat-num">{repoCount.toLocaleString()}</div>
626 <div class="status-stat-label">Repositories</div>
627 </div>
628 <div class="status-stat">
629 <div class="status-stat-num">
630 {publicRepoCount.toLocaleString()}
631 </div>
632 <div class="status-stat-label">Public repos</div>
633 </div>
634 <div class="status-stat">
635 <div class="status-stat-num">
636 {gateRunCount.toLocaleString()}
637 </div>
638 <div class="status-stat-label">Gate runs</div>
639 </div>
640 <div class="status-stat">
641 <div class="status-stat-num">
642 {greenRate === null ? "—" : `${greenRate.toFixed(1)}%`}
643 </div>
644 <div class="status-stat-label">Green rate</div>
645 </div>
646 <div class="status-stat">
647 <div class="status-stat-num">{fmtUptime(uptimeMs)}</div>
648 <div class="status-stat-label">Uptime</div>
649 </div>
650 </div>
651 </div>
652 </section>
653
0a50474Claude654 {/* ─── Autopilot tick ─── */}
655 <section class="status-section" aria-labelledby="status-tick-h">
656 <header class="status-section-head">
657 <div>
658 <p class="status-section-eyebrow">Autopilot</p>
659 <h2 class="status-section-title" id="status-tick-h">
660 Latest tick
661 </h2>
662 <p class="status-section-sub">
663 Per-task results from the most recent autopilot sweep.
664 </p>
2316be6Claude665 </div>
0a50474Claude666 {tick ? (
667 <span class="status-count-pill">
668 {ticks} ticks this process
669 </span>
670 ) : null}
671 </header>
672 <div class="status-section-body">
673 {tick ? (
674 <ul class="status-tick-list">
675 <li class="status-tick-row">
676 <span class="status-tick-name">Finished</span>
677 <code class="status-tick-val">{tick.finishedAt}</code>
678 </li>
679 <li class="status-tick-row">
680 <span class="status-tick-name">Total ticks this process</span>
681 <code class="status-tick-val">{ticks}</code>
682 </li>
683 {tick.tasks.map((t) => (
684 <li class="status-tick-row">
685 <code class="status-tick-name">{t.name}</code>
686 <span
687 class={"status-tick-val " + (t.ok ? "is-ok" : "is-err")}
688 >
689 {t.ok ? "ok" : `failed: ${t.error || "unknown"}`}
690 <span class="status-tick-ms">{t.durationMs}ms</span>
b1be050CC LABS App691 </span>
0a50474Claude692 </li>
693 ))}
694 </ul>
695 ) : (
696 <div class="status-empty">
697 <div class="status-empty-orb" aria-hidden="true" />
698 <div class="status-empty-inner">
699 <p class="status-empty-title">
700 {autopilotDisabled
701 ? "Autopilot is disabled."
702 : "No ticks yet."}
703 </p>
704 <p class="status-empty-sub">
705 {autopilotDisabled
706 ? "Set AUTOPILOT_DISABLED=0 to re-enable the periodic platform-maintenance loop."
707 : "The first tick runs within 5 minutes of process start. Check back shortly."}
708 </p>
b1be050CC LABS App709 </div>
2316be6Claude710 </div>
0a50474Claude711 )}
2316be6Claude712 </div>
0a50474Claude713 </section>
714
873f13cClaude715 {/* ─── Subscribe to alerts ─── */}
716 <section class="status-section status-subscribe-section" aria-labelledby="status-sub-h">
717 <header class="status-section-head">
718 <div>
719 <p class="status-section-eyebrow">Alerts</p>
720 <h2 class="status-section-title" id="status-sub-h">
721 Subscribe to status alerts
722 </h2>
723 <p class="status-section-sub">
724 Get an email when a new incident is filed or resolved.
725 Unsubscribe any time — one click, no questions asked.
726 </p>
727 </div>
728 </header>
729 <div class="status-section-body">
730 <form
731 method="post"
732 action="/status/subscribe"
733 class="status-sub-form"
734 aria-label="Subscribe to status alerts"
735 >
736 <label class="status-sub-label" for="sub-email">
737 Email address
738 </label>
739 <div class="status-sub-row">
740 <input
741 id="sub-email"
742 class="status-sub-input"
743 type="email"
744 name="email"
745 required
746 placeholder="you@example.com"
747 autocomplete="email"
748 aria-describedby="sub-hint"
749 />
750 <button class="status-sub-btn" type="submit">
751 <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><polyline points="22,6 12,13 2,6"/></svg>
752 Subscribe
753 </button>
754 </div>
755 <p class="status-sub-hint" id="sub-hint">
756 We'll send a confirmation link. No spam, ever.
757 </p>
758 </form>
759 </div>
760 </section>
761
0a50474Claude762 <p class="status-foot">
763 Liveness: <a href="/healthz">/healthz</a> · Readiness:{" "}
764 <a href="/readyz">/readyz</a> · Metrics:{" "}
765 <a href="/metrics">/metrics</a> · Platform JSON:{" "}
2316be6Claude766 <a href="/api/platform-status">/api/platform-status</a>
767 </p>
768 </div>
769 </Layout>
770 );
771});
772
9b07ca9Claude773/**
774 * Shields-style status badge. Reads the latest autopilot tick + DB
775 * reachability and returns an SVG. Embed in READMEs with:
776 * ![status](https://your-host/status.svg)
777 */
778status.get("/status.svg", async (c) => {
779 let dbOk = false;
780 try {
781 await db.execute(sql`SELECT 1`);
782 dbOk = true;
783 } catch {
784 dbOk = false;
785 }
786 const tick = getLastTick();
787 const lastOk = tick ? tick.tasks.every((t) => t.ok) : true;
788 const overall = dbOk && lastOk;
789 const label = "gluecron";
790 const value = overall ? "operational" : "degraded";
791 const fill = overall ? "#2da44e" : "#cf222e";
792
793 const labelW = 70;
794 const valueW = overall ? 78 : 68;
795 const totalW = labelW + valueW;
796 const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${totalW}" height="20" role="img" aria-label="${label}: ${value}">
797 <linearGradient id="s" x2="0" y2="100%">
798 <stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
799 <stop offset="1" stop-opacity=".1"/>
800 </linearGradient>
801 <rect width="${totalW}" height="20" rx="3" fill="#555"/>
802 <rect x="${labelW}" width="${valueW}" height="20" rx="3" fill="${fill}"/>
803 <rect width="${totalW}" height="20" rx="3" fill="url(#s)"/>
804 <g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,sans-serif" font-size="11">
805 <text x="${labelW / 2}" y="15">${label}</text>
806 <text x="${labelW + valueW / 2}" y="15">${value}</text>
807 </g>
808</svg>`;
809 c.header("Content-Type", "image/svg+xml; charset=utf-8");
810 c.header("Cache-Control", "no-cache, max-age=0");
811 return c.body(svg);
812});
813
0a50474Claude814/* ─────────────────────────────────────────────────────────────────────────
815 * Scoped CSS — every class prefixed `.status-` so this surface can't
816 * bleed into the admin status page or any other route.
817 * ───────────────────────────────────────────────────────────────────── */
818const statusStyles = `
eed4684Claude819 .status-wrap { max-width: 1320px; margin: 0 auto; padding: var(--space-6) var(--space-4); }
0a50474Claude820
821 /* ─── Hero ─── */
822 .status-hero {
823 position: relative;
824 margin-bottom: var(--space-5);
825 padding: var(--space-5) var(--space-6);
826 background: var(--bg-elevated);
827 border: 1px solid var(--border);
828 border-radius: 16px;
829 overflow: hidden;
830 }
831 .status-hero::before {
832 content: '';
833 position: absolute;
834 top: 0; left: 0; right: 0;
835 height: 2px;
6fd5915Claude836 background: linear-gradient(90deg, transparent 0%, #34d399 30%, #5f8fa0 70%, transparent 100%);
0a50474Claude837 opacity: 0.75;
838 pointer-events: none;
839 }
840 .status-hero.is-degraded::before {
841 background: linear-gradient(90deg, transparent 0%, #f87171 30%, #fbbf24 70%, transparent 100%);
842 }
843 .status-hero-orb {
844 position: absolute;
845 inset: -20% -10% auto auto;
846 width: 380px; height: 380px;
6fd5915Claude847 background: radial-gradient(circle, rgba(52,211,153,0.20), rgba(95,143,160,0.10) 45%, transparent 70%);
0a50474Claude848 filter: blur(80px);
849 opacity: 0.7;
850 pointer-events: none;
851 z-index: 0;
852 }
853 .status-hero.is-degraded .status-hero-orb {
854 background: radial-gradient(circle, rgba(248,113,113,0.22), rgba(251,191,36,0.10) 45%, transparent 70%);
855 }
856 .status-hero-inner { position: relative; z-index: 1; }
857 .status-eyebrow {
858 font-size: 12px;
859 color: var(--text-muted);
860 margin-bottom: var(--space-2);
861 letter-spacing: 0.02em;
862 display: inline-flex;
863 align-items: center;
864 gap: 8px;
865 }
866 .status-eyebrow-pill {
867 display: inline-flex;
868 align-items: center;
869 justify-content: center;
870 width: 18px; height: 18px;
871 border-radius: 6px;
872 background: rgba(52,211,153,0.14);
873 color: #6ee7b7;
874 box-shadow: inset 0 0 0 1px rgba(52,211,153,0.32);
875 }
876 .status-hero.is-degraded .status-eyebrow-pill {
877 background: rgba(248,113,113,0.14);
878 color: #fca5a5;
879 box-shadow: inset 0 0 0 1px rgba(248,113,113,0.30);
880 }
881 .status-title {
882 font-size: clamp(28px, 4vw, 40px);
883 font-family: var(--font-display);
884 font-weight: 800;
885 letter-spacing: -0.028em;
886 line-height: 1.05;
887 margin: 0 0 var(--space-2);
888 color: var(--text-strong);
889 display: flex;
890 align-items: center;
891 gap: 14px;
892 flex-wrap: wrap;
893 }
894 .status-dot-big {
895 display: inline-block;
896 width: 14px; height: 14px;
897 border-radius: 50%;
898 background: #34d399;
899 box-shadow: 0 0 0 5px rgba(52,211,153,0.18);
900 flex-shrink: 0;
901 }
902 .status-hero.is-degraded .status-dot-big {
903 background: #f87171;
904 box-shadow: 0 0 0 5px rgba(248,113,113,0.18);
905 }
906 .status-title-grad {
6fd5915Claude907 background-image: linear-gradient(135deg, #6ee7b7 0%, #34d399 50%, #5f8fa0 100%);
0a50474Claude908 -webkit-background-clip: text;
909 background-clip: text;
910 -webkit-text-fill-color: transparent;
911 color: transparent;
912 }
913 .status-title-grad-warn {
914 background-image: linear-gradient(135deg, #fca5a5 0%, #f87171 50%, #fbbf24 100%);
915 }
916 .status-sub {
917 font-size: 15px;
918 color: var(--text-muted);
919 margin: 0 0 var(--space-4);
920 line-height: 1.55;
921 max-width: 620px;
922 }
923
924 /* Hero stats strip */
925 .status-hero-stats {
926 display: grid;
873f13cClaude927 grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
0a50474Claude928 gap: var(--space-3);
929 }
930 .status-hero-stat {
931 padding: 12px 14px;
932 background: rgba(255,255,255,0.025);
933 border: 1px solid var(--border-subtle);
934 border-radius: 10px;
935 }
936 .status-hero-stat-num {
937 font-family: var(--font-display);
938 font-size: 22px;
939 font-weight: 700;
940 color: var(--text-strong);
941 letter-spacing: -0.02em;
942 }
943 .status-hero-stat-label {
944 font-size: 11px;
945 color: var(--text-muted);
946 text-transform: uppercase;
947 letter-spacing: 0.06em;
948 margin-top: 2px;
949 }
950
873f13cClaude951 /* ─── Notice banners ─── */
952 .status-notice {
953 display: flex;
954 align-items: center;
955 gap: 10px;
956 margin-bottom: var(--space-4);
957 padding: 12px 16px;
958 border-radius: 10px;
959 font-size: 14px;
960 font-weight: 500;
961 }
962 .status-notice.is-ok {
963 background: rgba(52,211,153,0.10);
964 color: #6ee7b7;
965 border: 1px solid rgba(52,211,153,0.25);
966 }
967 .status-notice.is-warn {
968 background: rgba(251,191,36,0.10);
969 color: #fbbf24;
970 border: 1px solid rgba(251,191,36,0.25);
971 }
972 .status-notice.is-err {
973 background: rgba(248,113,113,0.10);
974 color: #fca5a5;
975 border: 1px solid rgba(248,113,113,0.25);
976 }
977
0a50474Claude978 /* ─── Section cards ─── */
979 .status-section {
980 margin-bottom: var(--space-5);
981 background: var(--bg-elevated);
982 border: 1px solid var(--border);
983 border-radius: 14px;
984 overflow: hidden;
985 }
986 .status-section-head {
987 padding: var(--space-4) var(--space-5);
988 border-bottom: 1px solid var(--border);
989 display: flex;
990 align-items: flex-start;
991 justify-content: space-between;
992 gap: var(--space-3);
993 flex-wrap: wrap;
994 }
995 .status-section-eyebrow {
996 font-size: 11px;
997 font-weight: 600;
998 letter-spacing: 0.08em;
999 text-transform: uppercase;
1000 color: var(--text-faint);
1001 margin: 0 0 6px;
1002 }
1003 .status-section-title {
1004 margin: 0;
1005 font-family: var(--font-display);
1006 font-size: 17px;
1007 font-weight: 700;
1008 letter-spacing: -0.018em;
1009 color: var(--text-strong);
1010 }
1011 .status-section-sub {
1012 margin: 6px 0 0;
1013 font-size: 12.5px;
1014 color: var(--text-muted);
1015 line-height: 1.5;
1016 }
1017 .status-section-body { padding: var(--space-5); }
1018
873f13cClaude1019 /* ─── Service table ─── */
1020 .status-svc-table {
1021 width: 100%;
1022 border-collapse: collapse;
1023 }
1024 .status-svc-th {
1025 padding: 10px var(--space-5);
1026 font-size: 11px;
1027 font-weight: 600;
1028 text-transform: uppercase;
1029 letter-spacing: 0.06em;
1030 color: var(--text-faint);
1031 text-align: left;
0a50474Claude1032 border-bottom: 1px solid var(--border);
873f13cClaude1033 background: var(--bg-secondary);
0a50474Claude1034 }
873f13cClaude1035 .status-svc-th-right { text-align: right; }
1036 .status-svc-row { border-bottom: 1px solid var(--border); }
0a50474Claude1037 .status-svc-row:last-child { border-bottom: 0; }
873f13cClaude1038 .status-svc-td {
1039 padding: 14px var(--space-5);
1040 vertical-align: middle;
1041 }
1042 .status-svc-td-right { text-align: right; }
0a50474Claude1043 .status-svc-name {
1044 margin: 0;
1045 font-weight: 600;
1046 color: var(--text-strong);
1047 font-size: 14px;
1048 }
1049 .status-svc-sub {
1050 margin: 2px 0 0;
1051 font-size: 12px;
1052 color: var(--text-muted);
1053 }
873f13cClaude1054 .status-uptime-pct {
1055 font-family: var(--font-mono);
1056 font-size: 13px;
1057 color: #6ee7b7;
1058 font-weight: 600;
1059 }
1060
1061 /* ─── Status pills ─── */
0a50474Claude1062 .status-pill {
1063 display: inline-flex;
1064 align-items: center;
1065 gap: 6px;
1066 padding: 4px 10px;
1067 border-radius: 9999px;
1068 font-size: 11.5px;
1069 font-weight: 600;
1070 letter-spacing: 0.04em;
1071 }
1072 .status-pill-dot {
1073 width: 7px; height: 7px;
1074 border-radius: 9999px;
1075 background: currentColor;
1076 flex-shrink: 0;
1077 }
1078 .status-pill-ok {
1079 background: rgba(52,211,153,0.14);
1080 color: #6ee7b7;
1081 box-shadow: inset 0 0 0 1px rgba(52,211,153,0.32);
1082 }
1083 .status-pill-down {
1084 background: rgba(248,113,113,0.14);
1085 color: #fca5a5;
1086 box-shadow: inset 0 0 0 1px rgba(248,113,113,0.32);
1087 }
1088 .status-pill-idle {
1089 background: rgba(110,118,129,0.18);
1090 color: #c9d1d9;
1091 box-shadow: inset 0 0 0 1px rgba(110,118,129,0.40);
1092 }
873f13cClaude1093 .status-pill-warn {
1094 background: rgba(251,191,36,0.14);
1095 color: #fbbf24;
1096 box-shadow: inset 0 0 0 1px rgba(251,191,36,0.32);
1097 }
1098
1099 /* ─── Severity badges ─── */
1100 .status-sev-badge {
1101 display: inline-block;
1102 padding: 2px 8px;
1103 border-radius: 6px;
1104 font-size: 11px;
1105 font-weight: 700;
1106 text-transform: uppercase;
1107 letter-spacing: 0.06em;
1108 }
1109 .status-sev-minor {
1110 background: rgba(110,118,129,0.18);
1111 color: #c9d1d9;
1112 box-shadow: inset 0 0 0 1px rgba(110,118,129,0.40);
1113 }
1114 .status-sev-major {
1115 background: rgba(251,191,36,0.14);
1116 color: #fbbf24;
1117 box-shadow: inset 0 0 0 1px rgba(251,191,36,0.32);
1118 }
1119 .status-sev-crit {
1120 background: rgba(248,113,113,0.14);
1121 color: #fca5a5;
1122 box-shadow: inset 0 0 0 1px rgba(248,113,113,0.32);
1123 }
1124
1125 /* ─── Incident history table ─── */
1126 .status-inc-table-wrap { overflow-x: auto; }
1127 .status-inc-table {
1128 width: 100%;
1129 border-collapse: collapse;
1130 font-size: 13px;
1131 }
1132 .status-inc-th {
1133 padding: 9px 12px;
1134 font-size: 11px;
1135 font-weight: 600;
1136 text-transform: uppercase;
1137 letter-spacing: 0.06em;
1138 color: var(--text-faint);
1139 text-align: left;
1140 border-bottom: 1px solid var(--border-subtle);
1141 white-space: nowrap;
1142 }
1143 .status-inc-th-center { text-align: center; }
1144 .status-inc-tr { border-bottom: 1px solid var(--border-subtle); }
1145 .status-inc-tr:last-child { border-bottom: 0; }
1146 .status-inc-tr:hover { background: rgba(255,255,255,0.02); }
1147 .status-inc-td {
1148 padding: 12px 12px;
1149 vertical-align: top;
1150 color: var(--text);
1151 }
1152 .status-inc-td-center { text-align: center; vertical-align: middle; }
1153 .status-inc-title {
1154 margin: 0;
1155 font-weight: 600;
1156 color: var(--text-strong);
1157 line-height: 1.4;
1158 }
1159 .status-inc-body {
1160 margin: 4px 0 0;
1161 font-size: 12px;
1162 color: var(--text-muted);
1163 line-height: 1.4;
1164 }
1165 .status-inc-time {
1166 font-family: var(--font-mono);
1167 font-size: 11.5px;
1168 color: var(--text-faint);
1169 white-space: nowrap;
1170 }
1171
1172 /* ─── Synthetic monitor alert list ─── */
1173 .status-alert-list { list-style: none; margin: 0; padding: 0; }
1174 .status-alert-row {
1175 display: flex;
1176 align-items: flex-start;
1177 gap: 12px;
1178 padding: var(--space-3) 0;
1179 border-bottom: 1px solid var(--border-subtle);
1180 }
1181 .status-alert-row:last-child { border-bottom: 0; }
1182 .status-alert-row:first-child { padding-top: 0; }
1183 .status-alert-dot {
1184 width: 8px; height: 8px;
1185 border-radius: 9999px;
1186 background: #f87171;
1187 box-shadow: 0 0 0 3px rgba(248,113,113,0.18);
1188 margin-top: 6px;
1189 flex-shrink: 0;
1190 }
1191 .status-alert-main { flex: 1; min-width: 0; }
1192 .status-alert-name {
1193 margin: 0;
1194 font-size: 13.5px;
1195 color: var(--text-strong);
1196 }
1197 .status-alert-name code {
1198 font-family: var(--font-mono);
1199 font-size: 12.5px;
1200 background: var(--bg-tertiary);
1201 padding: 1px 6px;
1202 border-radius: 4px;
1203 }
1204 .status-alert-err {
1205 margin: 4px 0 0;
1206 font-size: 12.5px;
1207 color: var(--text-muted);
1208 line-height: 1.5;
1209 }
0a50474Claude1210
1211 /* ─── Platform stats ─── */
1212 .status-stats-grid {
1213 display: grid;
1214 grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
1215 gap: var(--space-3);
1216 }
1217 .status-stat {
1218 padding: var(--space-3) var(--space-4);
1219 background: var(--bg-secondary);
1220 border: 1px solid var(--border-subtle);
1221 border-radius: 12px;
1222 text-align: center;
1223 transition: border-color 150ms ease;
1224 }
1225 .status-stat:hover { border-color: var(--border-strong); }
1226 .status-stat-num {
1227 font-family: var(--font-display);
1228 font-size: 24px;
1229 font-weight: 700;
1230 color: var(--text-strong);
1231 letter-spacing: -0.022em;
1232 }
1233 .status-stat-label {
1234 font-size: 11px;
1235 color: var(--text-muted);
1236 text-transform: uppercase;
1237 letter-spacing: 0.06em;
1238 margin-top: 4px;
1239 }
1240
1241 /* ─── Count pills ─── */
1242 .status-count-pill {
1243 display: inline-flex;
1244 align-items: center;
1245 padding: 4px 10px;
1246 border-radius: 9999px;
1247 font-family: var(--font-mono);
1248 font-size: 11.5px;
1249 font-weight: 600;
6fd5915Claude1250 background: rgba(91,110,232,0.10);
0a50474Claude1251 color: #c5b3ff;
6fd5915Claude1252 box-shadow: inset 0 0 0 1px rgba(91,110,232,0.28);
0a50474Claude1253 letter-spacing: 0.02em;
873f13cClaude1254 flex-shrink: 0;
1255 align-self: flex-start;
0a50474Claude1256 }
1257 .status-count-pill.is-ok {
1258 background: rgba(52,211,153,0.12);
1259 color: #6ee7b7;
1260 box-shadow: inset 0 0 0 1px rgba(52,211,153,0.32);
1261 }
1262 .status-count-pill.is-warn {
1263 background: rgba(248,113,113,0.12);
1264 color: #fca5a5;
1265 box-shadow: inset 0 0 0 1px rgba(248,113,113,0.32);
1266 }
1267
873f13cClaude1268 /* ─── Subscribe section ─── */
1269 .status-subscribe-section {
6fd5915Claude1270 background: linear-gradient(135deg, rgba(91,110,232,0.06) 0%, rgba(52,211,153,0.04) 100%);
1271 border-color: rgba(91,110,232,0.20);
0a50474Claude1272 }
873f13cClaude1273 .status-sub-form { max-width: 520px; }
1274 .status-sub-label {
1275 display: block;
1276 font-size: 13px;
1277 font-weight: 600;
1278 color: var(--text-strong);
1279 margin-bottom: 8px;
0a50474Claude1280 }
873f13cClaude1281 .status-sub-row {
1282 display: flex;
1283 gap: 8px;
1284 align-items: stretch;
1285 }
1286 .status-sub-input {
1287 flex: 1;
1288 padding: 9px 12px;
1289 background: var(--bg-secondary);
1290 border: 1px solid var(--border);
1291 border-radius: 8px;
0a50474Claude1292 color: var(--text-strong);
873f13cClaude1293 font-size: 14px;
1294 outline: none;
1295 transition: border-color 150ms;
1296 min-width: 0;
0a50474Claude1297 }
873f13cClaude1298 .status-sub-input:focus {
6fd5915Claude1299 border-color: rgba(91,110,232,0.60);
1300 box-shadow: 0 0 0 3px rgba(91,110,232,0.12);
0a50474Claude1301 }
873f13cClaude1302 .status-sub-input::placeholder { color: var(--text-faint); }
1303 .status-sub-btn {
1304 display: inline-flex;
1305 align-items: center;
1306 gap: 7px;
1307 padding: 9px 18px;
6fd5915Claude1308 background: rgba(91,110,232,0.16);
1309 border: 1px solid rgba(91,110,232,0.32);
873f13cClaude1310 border-radius: 8px;
1311 color: #c5b3ff;
1312 font-size: 13.5px;
1313 font-weight: 600;
1314 cursor: pointer;
1315 white-space: nowrap;
1316 transition: background 150ms, border-color 150ms;
0a50474Claude1317 }
873f13cClaude1318 .status-sub-btn:hover {
6fd5915Claude1319 background: rgba(91,110,232,0.26);
1320 border-color: rgba(91,110,232,0.50);
873f13cClaude1321 }
1322 .status-sub-hint {
1323 margin: 8px 0 0;
1324 font-size: 12px;
0a50474Claude1325 color: var(--text-faint);
1326 }
1327
1328 /* ─── Empty state ─── */
1329 .status-empty {
1330 position: relative;
1331 padding: var(--space-6) var(--space-5);
1332 border: 1px dashed var(--border-strong);
1333 border-radius: 14px;
1334 background: rgba(255,255,255,0.02);
1335 text-align: center;
1336 overflow: hidden;
1337 }
1338 .status-empty-orb {
1339 position: absolute;
1340 inset: -40% -10% auto auto;
1341 width: 320px; height: 320px;
6fd5915Claude1342 background: radial-gradient(circle, rgba(52,211,153,0.18), rgba(95,143,160,0.10) 45%, transparent 70%);
0a50474Claude1343 filter: blur(70px);
1344 opacity: 0.55;
1345 pointer-events: none;
1346 z-index: 0;
1347 }
1348 .status-empty-inner { position: relative; z-index: 1; }
1349 .status-empty-title {
1350 margin: 0 0 6px;
1351 font-family: var(--font-display);
1352 font-size: 16px;
1353 font-weight: 700;
1354 color: var(--text-strong);
1355 letter-spacing: -0.012em;
1356 }
1357 .status-empty-sub {
1358 margin: 0 auto;
1359 max-width: 460px;
1360 font-size: 13px;
1361 color: var(--text-muted);
1362 line-height: 1.5;
1363 }
1364
1365 /* ─── Autopilot tick list ─── */
1366 .status-tick-list { list-style: none; margin: 0; padding: 0; }
1367 .status-tick-row {
1368 display: flex;
1369 align-items: center;
1370 justify-content: space-between;
1371 gap: 12px;
1372 padding: 10px 0;
1373 border-bottom: 1px solid var(--border-subtle);
1374 font-size: 13px;
1375 }
1376 .status-tick-row:last-child { border-bottom: 0; }
1377 .status-tick-row:first-child { padding-top: 0; }
1378 .status-tick-name {
1379 color: var(--text);
1380 font-size: 13px;
1381 }
1382 code.status-tick-name {
1383 font-family: var(--font-mono);
1384 font-size: 12.5px;
1385 background: var(--bg-tertiary);
1386 padding: 1px 6px;
1387 border-radius: 4px;
1388 color: var(--text-strong);
1389 }
1390 .status-tick-val {
1391 display: inline-flex;
1392 align-items: center;
1393 gap: 10px;
1394 font-family: var(--font-mono);
1395 font-size: 12.5px;
1396 color: var(--text-muted);
1397 }
1398 .status-tick-val.is-ok { color: #6ee7b7; }
1399 .status-tick-val.is-err { color: #fca5a5; }
1400 .status-tick-ms { color: var(--text-faint); font-size: 11.5px; }
1401
1402 /* ─── Footer links ─── */
1403 .status-foot {
1404 margin-top: var(--space-5);
1405 padding-top: var(--space-4);
1406 border-top: 1px solid var(--border);
1407 color: var(--text-muted);
1408 font-size: 12.5px;
1409 text-align: center;
1410 }
1411 .status-foot a {
6fd5915Claude1412 color: var(--accent, #5b6ee8);
0a50474Claude1413 text-decoration: none;
1414 }
1415 .status-foot a:hover { text-decoration: underline; }
1416`;
1417
2316be6Claude1418export default status;