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

admin-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.

admin-status.tsxBlame854 lines · 2 contributors
cf03ff5Test User1/**
2 * BLOCK S4 — Site-admin synthetic-monitor dashboard.
3 *
4 * GET /admin/status — red/green dashboard (SSE-live)
5 * POST /admin/status/run — run the suite synchronously
6 *
7 * Both gated behind `isSiteAdmin`. The public `/status` page (see
8 * `src/routes/status.tsx`) stays open to everyone; this is the detailed
9 * 14-row health table for the owner.
c9188afClaude10 *
11 * Visual recipe (2026 polish — mirrors admin-integrations / admin-ops /
12 * admin-deploys-page):
13 * - Gradient hairline strip across the top of the hero (purple→cyan, 2px)
14 * - Soft radial orb in the corner of the hero
15 * - Eyebrow with pill icon + actor name
16 * - Display headline with gradient-text on the verb ("Live now." /
17 * "Falling.")
18 * - Live-update banner with pulsing green dot + SSE event counter
19 * - Real-time activity feed: rows with tabular-nums timestamps, mono
20 * action verb with subtle accent, target check name
21 *
22 * Scoped CSS — every class prefixed `.status-` so this surface can't
23 * bleed into other admin pages.
cf03ff5Test User24 */
c9188afClaude25/* eslint-disable @typescript-eslint/no-explicit-any */
cf03ff5Test User26
27import { Hono } from "hono";
28import { Layout } from "../views/layout";
29import { softAuth } from "../middleware/auth";
30import type { AuthEnv } from "../middleware/auth";
31import { isSiteAdmin } from "../lib/admin";
32import {
33 latestStatusByCheck,
34 recentRedChecks,
35 runSyntheticChecks,
36 persistChecks,
37 SSE_TOPIC,
38 SYNTHETIC_CHECKS,
39 type SyntheticCheckResult,
40} from "../lib/synthetic-monitor";
41
42const adminStatus = new Hono<AuthEnv>();
43adminStatus.use("*", softAuth);
44
45async function gate(c: any): Promise<{ user: any } | Response> {
46 const user = c.get("user");
47 if (!user) return c.redirect("/login?next=/admin/status");
48 if (!(await isSiteAdmin(user.id))) {
49 return c.html(
50 <Layout title="Forbidden" user={user}>
c9188afClaude51 <div class="status-403">
cf03ff5Test User52 <h2>403 — Not a site admin</h2>
53 <p>You don't have permission to view this page.</p>
54 </div>
c9188afClaude55 <style dangerouslySetInnerHTML={{ __html: STATUS_CSS }} />
cf03ff5Test User56 </Layout>,
57 403
58 );
59 }
60 return { user };
61}
62
63function fmtAgo(checkedAt: Date | undefined): string {
64 if (!checkedAt) return "never";
65 const diffMs = Date.now() - checkedAt.getTime();
66 if (diffMs < 0) return "just now";
67 const s = Math.floor(diffMs / 1000);
68 if (s < 5) return "just now";
69 if (s < 60) return `${s}s ago`;
70 const m = Math.floor(s / 60);
71 if (m < 60) return `${m}m ago`;
72 const h = Math.floor(m / 60);
73 if (h < 24) return `${h}h ago`;
74 const d = Math.floor(h / 24);
75 return `${d}d ago`;
76}
77
c9188afClaude78function dotClass(status: SyntheticCheckResult["status"] | undefined): string {
79 if (status === "green") return "is-green";
80 if (status === "red") return "is-red";
81 if (status === "yellow") return "is-yellow";
82 return "is-idle";
83}
84
85function IconArrowLeft() {
86 return (
87 <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
88 <line x1="19" y1="12" x2="5" y2="12" />
89 <polyline points="12 19 5 12 12 5" />
90 </svg>
91 );
92}
93function IconPulse() {
94 return (
95 <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
96 <polyline points="22 12 18 12 15 21 9 3 6 12 2 12" />
97 </svg>
98 );
99}
100function IconPlay() {
101 return (
102 <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
103 <polygon points="5 3 19 12 5 21 5 3" />
104 </svg>
105 );
106}
107
cf03ff5Test User108adminStatus.get("/admin/status", async (c) => {
109 const g = await gate(c);
110 if (g instanceof Response) return g;
111 const { user } = g;
112
113 const latest = await latestStatusByCheck();
114 const recent = await recentRedChecks(24, 25);
115
116 const allGreen = SYNTHETIC_CHECKS.every(
117 (spec) => latest[spec.name]?.status === "green"
118 );
c9188afClaude119 const redCount = SYNTHETIC_CHECKS.filter(
120 (spec) => latest[spec.name]?.status === "red"
121 ).length;
cf03ff5Test User122
123 // Find the most-recent checkedAt across all rows so we can render the
124 // "last run Xs ago" badge.
125 let lastRunAt: Date | null = null;
126 for (const spec of SYNTHETIC_CHECKS) {
127 const row = latest[spec.name];
128 if (!row) continue;
129 if (!lastRunAt || row.checkedAt > lastRunAt) lastRunAt = row.checkedAt;
130 }
131
c9188afClaude132 // Headline state — green/falling/idle for the gradient-text variant.
133 const headlineState: "green" | "falling" | "idle" = !lastRunAt
134 ? "idle"
135 : allGreen
136 ? "green"
137 : "falling";
138 const headline =
139 headlineState === "idle"
140 ? "Idle."
141 : headlineState === "green"
142 ? "Live now."
143 : "Falling.";
144
cf03ff5Test User145 return c.html(
146 <Layout title="Synthetic monitor — admin" user={user}>
c9188afClaude147 <div class="status-wrap">
148 {/* ─── Hero ─── */}
149 <section class="status-hero">
150 <div class="status-hero-orb" aria-hidden="true" />
151 <div class="status-hero-inner">
152 <div class="status-hero-top">
153 <div class="status-hero-text">
154 <div class="status-eyebrow">
155 <span class="status-eyebrow-pill" aria-hidden="true">
156 <IconPulse />
157 </span>
158 Synthetic monitor · Site admin · <span class="status-who">{user.username}</span>
159 </div>
160 <h1 class="status-title">
161 <span
162 class={
163 "status-title-grad " +
164 (headlineState === "falling"
165 ? "is-falling"
166 : headlineState === "idle"
167 ? "is-idle"
168 : "is-green")
169 }
cf03ff5Test User170 >
c9188afClaude171 {headline}
172 </span>
173 </h1>
174 <p class="status-sub">
175 {allGreen
176 ? "All synthetic checks are green. Runs every autopilot tick."
177 : `${redCount} check${redCount === 1 ? "" : "s"} red — see the table below for the failing rows.`}{" "}
178 Last run{" "}
179 <span data-last-run-at class="status-tabular">
180 {fmtAgo(lastRunAt ?? undefined)}
181 </span>
182 .
183 </p>
184 </div>
185 <a href="/admin" class="status-hero-back">
186 <IconArrowLeft />
187 Back to admin
188 </a>
189 </div>
190 </div>
191 </section>
cf03ff5Test User192
c9188afClaude193 {/* ─── Live SSE banner ─── */}
194 <div
195 class="status-live"
196 role="status"
197 aria-live="polite"
198 data-sse-banner
cf03ff5Test User199 >
c9188afClaude200 <span class="status-live-dot" aria-hidden="true" />
201 <span class="status-live-label" data-sse-state>
202 SSE connecting…
203 </span>
204 <span class="status-live-sep" aria-hidden="true">·</span>
205 <span class="status-live-rate">
206 <span class="status-tabular" data-sse-rate>0</span> events / min
207 </span>
208 <span class="status-live-sep" aria-hidden="true">·</span>
209 <span class="status-live-topic">
210 topic <code>{SSE_TOPIC}</code>
211 </span>
212 </div>
213
214 {/* ─── Synthetic-check table ─── */}
215 <section class="status-section">
216 <header class="status-section-head">
217 <div class="status-section-head-text">
218 <h3 class="status-section-title">Synthetic checks</h3>
219 <p class="status-section-sub">
220 {SYNTHETIC_CHECKS.length} probes — every row patches in place
221 as the SSE stream fires.
222 </p>
223 </div>
224 <form action="/admin/status/run" method="post" class="status-runform">
225 <button type="submit" class="status-btn status-btn-primary">
226 <IconPlay />
227 Run all checks now
228 </button>
229 </form>
230 </header>
231 <div class="status-section-body status-section-body--flush">
232 <table class="status-table" id="synthetic-table">
233 <thead>
234 <tr>
235 <th class="status-col-dot"></th>
236 <th class="status-col-name">Check</th>
237 <th class="status-col-num">Status</th>
238 <th class="status-col-num">Duration</th>
239 <th class="status-col-when">Last run</th>
240 </tr>
241 </thead>
242 <tbody>
243 {SYNTHETIC_CHECKS.map((spec) => {
244 const row = latest[spec.name];
245 return (
246 <tr data-check-name={spec.name}>
247 <td class="status-col-dot" data-cell="dot">
248 <span
249 class={"status-dot " + dotClass(row?.status)}
250 aria-label={row?.status ?? "idle"}
251 />
252 </td>
253 <td class="status-col-name">
254 <code class="status-action">{spec.name}</code>
255 {row?.error ? (
256 <div class="status-error" data-cell="error">
257 {row.error}
258 </div>
259 ) : null}
260 </td>
261 <td class="status-col-num status-tabular" data-cell="status-code">
262 {row?.statusCode ?? "—"}
263 </td>
264 <td class="status-col-num status-tabular" data-cell="duration">
265 {row ? `${row.durationMs}ms` : "—"}
266 </td>
267 <td class="status-col-when status-tabular" data-cell="ago">
268 {fmtAgo(row?.checkedAt)}
269 </td>
270 </tr>
271 );
272 })}
273 </tbody>
274 </table>
275 </div>
276 </section>
277
278 {/* ─── Recent activity feed (red checks in last 24h) ─── */}
279 <section class="status-section">
280 <header class="status-section-head">
281 <div class="status-section-head-text">
282 <h3 class="status-section-title">Recent activity</h3>
283 <p class="status-section-sub">
284 Red checks in the last 24 hours — most recent first.
285 </p>
286 </div>
287 </header>
288 <div class="status-section-body status-section-body--flush">
289 {recent.length === 0 ? (
290 <div class="status-empty">
291 No red checks in the last 24 hours.
cf03ff5Test User292 </div>
c9188afClaude293 ) : (
294 <ol class="status-feed" aria-label="Recent red checks">
295 {recent.map((r) => (
296 <li class="status-feed-row">
297 <span
298 class="status-feed-when status-tabular"
299 title={r.checkedAt.toISOString()}
300 >
301 {fmtAgo(r.checkedAt)}
302 </span>
303 <span class="status-feed-action">
304 <span class="status-dot is-red" aria-hidden="true" />
305 <code>red</code>
306 </span>
307 <a
308 class="status-feed-target"
309 href={`#check-${r.name}`}
310 onclick={`var t=document.querySelector('tr[data-check-name="${r.name}"]');if(t){t.scrollIntoView({behavior:'smooth',block:'center'});t.classList.add('status-row-flash');setTimeout(function(){t.classList.remove('status-row-flash');},1600);}`}
311 >
312 {r.name}
313 </a>
314 <span class="status-feed-msg">
315 {r.error || "(no error message)"}
316 </span>
317 </li>
318 ))}
319 </ol>
320 )}
cf03ff5Test User321 </div>
c9188afClaude322 </section>
cf03ff5Test User323
c9188afClaude324 {/* Live update via SSE. Each event is a SyntheticCheckResult; we
325 patch the matching <tr data-check-name=...> in place. The
326 banner counts events received in the last 60s. */}
cf03ff5Test User327 <script
328 dangerouslySetInnerHTML={{
329 __html: `
330(function() {
c9188afClaude331 var bannerLabel = document.querySelector('[data-sse-state]');
332 var bannerRate = document.querySelector('[data-sse-rate]');
333 var bannerEl = document.querySelector('[data-sse-banner]');
334 var eventTimes = [];
335 function setBanner(state, cls) {
336 if (bannerLabel) bannerLabel.textContent = state;
337 if (bannerEl) {
338 bannerEl.classList.remove('is-on','is-off','is-connecting');
339 bannerEl.classList.add(cls);
340 }
341 }
342 function tickRate() {
343 var cutoff = Date.now() - 60000;
344 while (eventTimes.length && eventTimes[0] < cutoff) eventTimes.shift();
345 if (bannerRate) bannerRate.textContent = String(eventTimes.length);
346 }
347 setBanner('SSE connecting…', 'is-connecting');
348 setInterval(tickRate, 1000);
cf03ff5Test User349 try {
350 var src = new EventSource('/live-events/${SSE_TOPIC}');
c9188afClaude351 src.onopen = function(){ setBanner('SSE connected', 'is-on'); };
352 src.onerror = function(){ setBanner('SSE reconnecting…', 'is-connecting'); };
cf03ff5Test User353 src.addEventListener('check', function(ev) {
c9188afClaude354 eventTimes.push(Date.now()); tickRate();
cf03ff5Test User355 var data;
356 try { data = JSON.parse(ev.data); } catch (e) { return; }
357 var row = document.querySelector('tr[data-check-name="' + data.name + '"]');
358 if (!row) return;
359 var dot = row.querySelector('[data-cell="dot"]');
360 var statusCode = row.querySelector('[data-cell="status-code"]');
361 var duration = row.querySelector('[data-cell="duration"]');
362 var ago = row.querySelector('[data-cell="ago"]');
c9188afClaude363 if (dot) {
364 var span = dot.querySelector('.status-dot');
365 var cls = data.status === 'green' ? 'is-green'
366 : data.status === 'red' ? 'is-red'
367 : data.status === 'yellow'? 'is-yellow' : 'is-idle';
368 if (span) { span.className = 'status-dot ' + cls; }
369 }
370 if (statusCode) statusCode.textContent = data.statusCode != null ? String(data.statusCode) : '—';
cf03ff5Test User371 if (duration) duration.textContent = data.durationMs + 'ms';
372 if (ago) ago.textContent = 'just now';
373 var lastRun = document.querySelector('[data-last-run-at]');
374 if (lastRun) lastRun.textContent = 'just now';
c9188afClaude375 row.classList.add('status-row-flash');
376 setTimeout(function(){ row.classList.remove('status-row-flash'); }, 1100);
cf03ff5Test User377 });
c9188afClaude378 } catch (e) {
379 setBanner('SSE unavailable', 'is-off');
380 }
cf03ff5Test User381})();
382`,
383 }}
384 />
385 </div>
c9188afClaude386 <style dangerouslySetInnerHTML={{ __html: STATUS_CSS }} />
cf03ff5Test User387 </Layout>
388 );
389});
390
391adminStatus.post("/admin/status/run", async (c) => {
392 const g = await gate(c);
393 if (g instanceof Response) return g;
394
395 // Fire-and-forget the run so we don't block the redirect on a slow
396 // network. Persist + SSE happens inside the helper.
397 void (async () => {
398 try {
399 const results = await runSyntheticChecks();
400 await persistChecks(results);
401 } catch (err) {
402 console.error("[admin-status] manual run failed:", err);
403 }
404 })();
405
406 return c.redirect("/admin/status");
407});
408
c9188afClaude409// ---------------------------------------------------------------------------
410// Scoped CSS — every class prefixed `.status-` so the synthetic-monitor
411// surface can't bleed into other admin pages. Mirrors the gradient-hairline
412// hero + section-card motif from admin-ops / admin-deploys-page.
413// ---------------------------------------------------------------------------
414
415const STATUS_CSS = `
416 .status-wrap {
417 max-width: 1100px;
418 margin: 0 auto;
419 padding: var(--space-6) var(--space-4) var(--space-12);
420 }
421 .status-tabular { font-variant-numeric: tabular-nums; }
422
423 /* ─── Hero ─── */
424 .status-hero {
425 position: relative;
426 margin-bottom: var(--space-5);
427 padding: clamp(28px, 4vw, 44px) clamp(24px, 4vw, 44px);
428 background: var(--bg-elevated);
429 border: 1px solid var(--border);
430 border-radius: 18px;
431 overflow: hidden;
432 box-shadow: 0 1px 0 rgba(255,255,255,0.04), 0 18px 44px -16px rgba(0,0,0,0.42);
433 }
434 .status-hero::before {
435 content: '';
436 position: absolute;
437 top: 0; left: 0; right: 0;
438 height: 2px;
439 background: linear-gradient(90deg, transparent 0%, #8c6dff 30%, #36c5d6 70%, transparent 100%);
440 opacity: 0.75;
441 pointer-events: none;
442 }
443 .status-hero-orb {
444 position: absolute;
445 inset: -30% -10% auto auto;
446 width: 460px; height: 460px;
447 background: radial-gradient(circle, rgba(52,211,153,0.20), rgba(54,197,214,0.10) 45%, transparent 70%);
448 filter: blur(80px);
449 opacity: 0.7;
450 pointer-events: none;
451 z-index: 0;
452 }
453 .status-hero-inner { position: relative; z-index: 1; }
454 .status-hero-top {
455 display: flex;
456 align-items: flex-start;
457 justify-content: space-between;
458 gap: var(--space-4);
459 flex-wrap: wrap;
460 }
461 .status-hero-text { flex: 1; min-width: 280px; }
462 .status-eyebrow {
463 display: inline-flex;
464 align-items: center;
465 gap: 8px;
466 font-size: 12px;
467 color: var(--text-muted);
468 margin-bottom: 14px;
469 letter-spacing: 0.02em;
470 }
471 .status-eyebrow-pill {
472 display: inline-flex;
473 align-items: center;
474 justify-content: center;
475 width: 18px; height: 18px;
476 border-radius: 6px;
477 background: rgba(52,211,153,0.14);
478 color: #6ee7b7;
479 box-shadow: inset 0 0 0 1px rgba(52,211,153,0.35);
480 }
481 .status-who { color: var(--accent); font-weight: 600; }
482 .status-title {
483 font-family: var(--font-display);
484 font-size: clamp(28px, 4vw, 40px);
485 font-weight: 800;
486 letter-spacing: -0.028em;
487 line-height: 1.05;
488 margin: 0 0 var(--space-2);
489 color: var(--text-strong);
490 }
491 .status-title-grad {
492 background-image: linear-gradient(135deg, #6ee7b7 0%, #34d399 50%, #36c5d6 100%);
493 -webkit-background-clip: text;
494 background-clip: text;
495 -webkit-text-fill-color: transparent;
496 color: transparent;
497 }
498 .status-title-grad.is-falling {
499 background-image: linear-gradient(135deg, #fca5a5 0%, #f87171 50%, #ef4444 100%);
500 }
501 .status-title-grad.is-idle {
502 background-image: linear-gradient(135deg, #a48bff 0%, #8c6dff 50%, #36c5d6 100%);
503 }
504 .status-sub {
505 font-size: 14.5px;
506 color: var(--text-muted);
507 margin: 0;
508 line-height: 1.55;
509 max-width: 640px;
510 }
511 .status-hero-back {
512 display: inline-flex;
513 align-items: center;
514 gap: 6px;
515 padding: 7px 12px;
516 font-size: 12.5px;
517 color: var(--text-muted);
518 background: rgba(255,255,255,0.02);
519 border: 1px solid var(--border);
520 border-radius: 8px;
521 text-decoration: none;
522 font-weight: 500;
523 transition: border-color 120ms ease, color 120ms ease, background 120ms ease;
524 flex-shrink: 0;
525 }
526 .status-hero-back:hover {
527 border-color: var(--border-strong);
528 color: var(--text-strong);
529 background: rgba(255,255,255,0.04);
530 text-decoration: none;
531 }
532
533 /* ─── Live SSE banner ─── */
534 .status-live {
535 display: flex;
536 align-items: center;
537 gap: 10px;
538 padding: 10px 14px;
539 margin-bottom: var(--space-4);
540 background: rgba(52,211,153,0.05);
541 border: 1px solid rgba(52,211,153,0.22);
542 border-radius: 10px;
543 font-size: 12.5px;
544 color: var(--text);
545 flex-wrap: wrap;
546 }
547 .status-live.is-connecting {
548 background: rgba(251,191,36,0.05);
549 border-color: rgba(251,191,36,0.22);
550 color: #fde68a;
551 }
552 .status-live.is-off {
553 background: rgba(248,113,113,0.06);
554 border-color: rgba(248,113,113,0.25);
555 color: #fecaca;
556 }
557 .status-live-dot {
558 width: 8px; height: 8px;
559 border-radius: 9999px;
560 background: #34d399;
561 box-shadow: 0 0 0 3px rgba(52,211,153,0.20);
562 animation: status-pulse 1.4s ease-in-out infinite;
563 flex-shrink: 0;
564 }
565 .status-live.is-connecting .status-live-dot {
566 background: #fbbf24;
567 box-shadow: 0 0 0 3px rgba(251,191,36,0.22);
568 }
569 .status-live.is-off .status-live-dot {
570 background: #f87171;
571 box-shadow: 0 0 0 3px rgba(248,113,113,0.22);
572 animation: none;
573 }
574 @keyframes status-pulse {
575 0%, 100% { opacity: 1; transform: scale(1); }
576 50% { opacity: 0.55; transform: scale(0.85); }
577 }
578 @media (prefers-reduced-motion: reduce) {
579 .status-live-dot { animation: none; }
580 }
581 .status-live-label {
582 font-family: var(--font-mono);
583 font-size: 12px;
584 color: inherit;
585 font-weight: 600;
586 }
587 .status-live-rate { color: inherit; }
588 .status-live-topic {
589 color: var(--text-muted);
590 margin-left: auto;
591 font-size: 11.5px;
592 }
593 .status-live-topic code {
594 font-family: var(--font-mono);
595 font-size: 11.5px;
596 background: rgba(255,255,255,0.04);
597 border: 1px solid var(--border);
598 padding: 1px 6px;
599 border-radius: 4px;
600 color: var(--text);
601 }
602 .status-live-sep { color: var(--text-muted); opacity: 0.45; }
603
604 /* ─── Section cards ─── */
605 .status-section {
606 margin-bottom: var(--space-5);
607 background: var(--bg-elevated);
608 border: 1px solid var(--border);
609 border-radius: 14px;
610 overflow: hidden;
611 }
612 .status-section-head {
613 padding: var(--space-4) var(--space-5);
614 border-bottom: 1px solid var(--border);
615 display: flex;
616 align-items: flex-start;
617 justify-content: space-between;
618 gap: var(--space-3);
619 flex-wrap: wrap;
620 }
621 .status-section-head-text { flex: 1; min-width: 240px; }
622 .status-section-title {
623 margin: 0;
624 font-family: var(--font-display);
625 font-size: 17px;
626 font-weight: 700;
627 letter-spacing: -0.018em;
628 color: var(--text-strong);
629 }
630 .status-section-sub {
631 margin: 4px 0 0;
632 font-size: 12.5px;
633 color: var(--text-muted);
634 }
635 .status-section-body { padding: var(--space-4) var(--space-5); }
636 .status-section-body--flush { padding: 0; }
637
638 .status-runform { margin: 0; }
639 .status-btn {
640 display: inline-flex;
641 align-items: center;
642 gap: 6px;
643 padding: 9px 16px;
644 border-radius: 10px;
645 font-size: 13px;
646 font-weight: 600;
647 text-decoration: none;
648 border: 1px solid transparent;
649 cursor: pointer;
650 font: inherit;
651 transition: transform 120ms ease, box-shadow 120ms ease, background 120ms ease, border-color 120ms ease;
652 line-height: 1;
653 }
654 .status-btn-primary {
655 background: linear-gradient(135deg, #8c6dff 0%, #36c5d6 100%);
656 color: #ffffff;
657 box-shadow: 0 6px 18px -6px rgba(140,109,255,0.50), inset 0 1px 0 rgba(255,255,255,0.16);
658 }
659 .status-btn-primary:hover {
660 transform: translateY(-1px);
661 box-shadow: 0 10px 24px -8px rgba(140,109,255,0.60), inset 0 1px 0 rgba(255,255,255,0.20);
662 }
663
664 /* ─── Synthetic table ─── */
665 .status-table {
666 width: 100%;
667 border-collapse: collapse;
668 font-size: 13px;
669 }
670 .status-table thead th {
671 padding: 10px 14px;
672 text-align: left;
673 font-family: var(--font-mono);
674 font-size: 10.5px;
675 text-transform: uppercase;
676 letter-spacing: 0.14em;
677 color: var(--text-muted);
678 font-weight: 700;
679 background: rgba(255,255,255,0.012);
680 border-bottom: 1px solid var(--border);
681 }
682 .status-table tbody td {
683 padding: 10px 14px;
684 border-bottom: 1px solid var(--border);
685 vertical-align: middle;
686 }
687 .status-table tbody tr:last-child td { border-bottom: 0; }
688 .status-table tbody tr {
689 transition: background 200ms ease;
690 }
691 .status-table tbody tr:hover {
692 background: rgba(255,255,255,0.022);
693 }
694 .status-table .status-col-dot { width: 30px; }
695 .status-table .status-col-num { width: 90px; }
696 .status-table .status-col-when { width: 110px; color: var(--text-muted); }
697 .status-row-flash {
698 background: rgba(140,109,255,0.10) !important;
699 animation: status-flash 1.1s ease-out;
700 }
701 @keyframes status-flash {
702 0% { background: rgba(140,109,255,0.30); }
703 100% { background: rgba(140,109,255,0.00); }
704 }
705
706 .status-action {
707 font-family: var(--font-mono);
708 font-size: 12.5px;
709 color: var(--text-strong);
710 background: linear-gradient(135deg, rgba(140,109,255,0.10), rgba(54,197,214,0.06));
711 border: 1px solid rgba(140,109,255,0.20);
712 padding: 2px 8px;
713 border-radius: 5px;
714 }
715 .status-error {
716 margin-top: 4px;
717 font-family: var(--font-mono);
718 font-size: 11.5px;
719 color: #fca5a5;
720 line-height: 1.4;
721 }
722
723 .status-dot {
724 display: inline-block;
725 width: 10px; height: 10px;
726 border-radius: 9999px;
727 background: var(--text-muted);
728 box-shadow: 0 0 0 3px rgba(255,255,255,0.04);
729 vertical-align: middle;
730 }
731 .status-dot.is-green {
732 background: #34d399;
733 box-shadow: 0 0 0 3px rgba(52,211,153,0.18), 0 0 8px rgba(52,211,153,0.40);
734 }
735 .status-dot.is-red {
736 background: #f87171;
737 box-shadow: 0 0 0 3px rgba(248,113,113,0.20), 0 0 10px rgba(248,113,113,0.45);
738 animation: status-pulse 1.6s ease-in-out infinite;
739 }
740 .status-dot.is-yellow {
741 background: #fbbf24;
742 box-shadow: 0 0 0 3px rgba(251,191,36,0.22);
743 }
744 .status-dot.is-idle { background: var(--text-muted); }
745 @media (prefers-reduced-motion: reduce) {
746 .status-dot.is-red { animation: none; }
747 }
748
749 /* ─── Feed ─── */
750 .status-empty {
751 padding: 22px 16px;
752 text-align: center;
753 color: var(--text-muted);
754 font-size: 13px;
755 }
756 .status-feed {
757 list-style: none;
758 margin: 0;
759 padding: 0;
760 }
761 .status-feed-row {
762 display: grid;
763 grid-template-columns: 96px 84px minmax(140px, 220px) 1fr;
764 gap: 14px;
765 align-items: center;
766 padding: 10px 14px;
767 border-bottom: 1px solid var(--border);
768 font-size: 13px;
769 }
770 .status-feed-row:last-child { border-bottom: 0; }
771 .status-feed-row:hover { background: rgba(255,255,255,0.022); }
772 .status-feed-when {
773 font-family: var(--font-mono);
774 font-size: 11.5px;
775 color: var(--text-muted);
776 }
777 .status-feed-action {
778 display: inline-flex;
779 align-items: center;
780 gap: 6px;
781 font-size: 12px;
782 color: #fca5a5;
783 }
784 .status-feed-action code {
785 font-family: var(--font-mono);
786 font-size: 11.5px;
787 background: rgba(248,113,113,0.08);
788 border: 1px solid rgba(248,113,113,0.28);
789 padding: 1px 6px;
790 border-radius: 4px;
791 color: #fca5a5;
792 text-transform: uppercase;
793 letter-spacing: 0.06em;
794 font-weight: 700;
795 }
796 .status-feed-target {
797 font-family: var(--font-mono);
798 font-size: 12.5px;
799 color: var(--text-strong);
800 background: rgba(255,255,255,0.04);
801 border: 1px solid var(--border);
802 padding: 2px 8px;
803 border-radius: 5px;
804 text-decoration: none;
805 overflow: hidden;
806 text-overflow: ellipsis;
807 white-space: nowrap;
808 }
809 .status-feed-target:hover {
810 background: rgba(140,109,255,0.10);
811 border-color: rgba(140,109,255,0.40);
812 color: var(--text-strong);
813 text-decoration: none;
814 }
815 .status-feed-msg {
816 color: var(--text-muted);
817 font-size: 12.5px;
818 overflow: hidden;
819 text-overflow: ellipsis;
820 white-space: nowrap;
821 }
822
823 /* ─── 403 fallback ─── */
824 .status-403 {
825 max-width: 540px;
826 margin: var(--space-12) auto;
827 padding: var(--space-6);
828 text-align: center;
829 background: var(--bg-elevated);
830 border: 1px solid var(--border);
831 border-radius: 16px;
832 }
833 .status-403 h2 {
834 font-family: var(--font-display);
835 font-size: 22px;
836 margin: 0 0 8px;
837 color: var(--text-strong);
838 }
839 .status-403 p { color: var(--text-muted); margin: 0; font-size: 14px; }
840
841 @media (max-width: 720px) {
842 .status-wrap { padding: var(--space-4) var(--space-3) var(--space-8); }
843 .status-feed-row {
844 grid-template-columns: 96px 84px 1fr;
845 grid-template-areas:
846 'when action target'
847 'when action msg';
848 }
849 .status-feed-msg { grid-column: 3 / 4; }
850 .status-live-topic { margin-left: 0; }
851 }
852`;
853
cf03ff5Test User854export default adminStatus;