The old admin was five separate html pages, so every navigation was a full reload and the operational picture was scattered across all of them. Worth saying: the api was never the problem, every endpoint answers in under 200ms. It felt slow because of the architecture, not the backend. This is a single page console. React and htm from a cdn, no bundler and no babel-in-the-browser, because a runtime transpiler on every load is exactly the slowness we are trying to get rid of. Hash routing, polling that keeps the last good payload on screen instead of flashing a spinner, and stale responses are dropped so a slow request cannot overwrite a newer one. /admin/api/ops/overview answers the whole dashboard in one call rather than making the browser fan out and stitch. It carries the things that actually matter and were not visible anywhere before: when live evidence matures, why a cohort does or does not clear the trade gate, which stage of the pipeline has gone quiet, and what is sitting in dead letters. Controls, all of which change production and all of which ask twice: - requeue dead letters, which only ever moves dead_letter back to pending - execution mode and a kill switch, now read from autonomy_settings on every poll instead of only from AUTONOMY_EXECUTION_MODE, so halting no longer needs a redeploy first - run the reaction analysis and read its output The d3 graph is framed rather than ported. It works, and rewriting it would risk something valuable for nothing the operator can see. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WnNxwxfXSbeNtjvtz5gayb
198 lines
7.2 KiB
CSS
198 lines
7.2 KiB
CSS
/* Ops console. Dark by default because this is a thing you stare at during an
|
|
incident, but the tokens are defined so a light theme is a one line change. */
|
|
:root {
|
|
--bg: #0b0d10;
|
|
--panel: #12161b;
|
|
--panel-2: #171c22;
|
|
--line: #232a33;
|
|
--ink: #e6edf3;
|
|
--ink-dim: #9aa7b4;
|
|
--ink-faint: #6b7885;
|
|
--accent: #4c8dff;
|
|
--ok: #3fb950;
|
|
--warn: #d29922;
|
|
--bad: #f85149;
|
|
--radius: 10px;
|
|
--mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
|
|
--sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Inter, Roboto, sans-serif;
|
|
}
|
|
|
|
* { box-sizing: border-box; }
|
|
|
|
body {
|
|
margin: 0;
|
|
background: var(--bg);
|
|
color: var(--ink);
|
|
font: 14px/1.5 var(--sans);
|
|
-webkit-font-smoothing: antialiased;
|
|
}
|
|
|
|
.boot { display: grid; place-content: center; gap: 8px; height: 100vh; text-align: center; }
|
|
.boot-mark { font: 600 20px var(--mono); letter-spacing: .3em; }
|
|
.boot-note { color: var(--ink-faint); font-size: 13px; }
|
|
|
|
/* ---------- shell ---------- */
|
|
.shell { display: grid; grid-template-columns: 208px 1fr; min-height: 100vh; }
|
|
|
|
.side {
|
|
border-right: 1px solid var(--line);
|
|
background: var(--panel);
|
|
padding: 18px 12px;
|
|
position: sticky; top: 0; height: 100vh;
|
|
display: flex; flex-direction: column; gap: 4px;
|
|
}
|
|
.brand { font: 600 14px var(--mono); letter-spacing: .28em; padding: 6px 10px 16px; }
|
|
.nav-item {
|
|
display: flex; align-items: center; justify-content: space-between; gap: 8px;
|
|
padding: 8px 10px; border-radius: 8px; cursor: pointer;
|
|
color: var(--ink-dim); text-decoration: none; font-size: 13.5px;
|
|
border: 1px solid transparent;
|
|
}
|
|
.nav-item:hover { background: var(--panel-2); color: var(--ink); }
|
|
.nav-item.active { background: var(--panel-2); color: var(--ink); border-color: var(--line); }
|
|
.nav-spacer { flex: 1; }
|
|
|
|
.main { padding: 20px 24px 60px; min-width: 0; }
|
|
|
|
.topbar {
|
|
display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
|
|
margin-bottom: 18px;
|
|
}
|
|
.topbar h1 { font-size: 17px; margin: 0; font-weight: 600; }
|
|
.topbar .grow { flex: 1; }
|
|
|
|
/* ---------- primitives ---------- */
|
|
.grid { display: grid; gap: 14px; }
|
|
.cols-2 { grid-template-columns: repeat(auto-fit, minmax(340px, 1fr)); }
|
|
.cols-3 { grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); }
|
|
|
|
.card {
|
|
background: var(--panel);
|
|
border: 1px solid var(--line);
|
|
border-radius: var(--radius);
|
|
padding: 14px 16px;
|
|
min-width: 0;
|
|
}
|
|
.card h2 {
|
|
font-size: 11px; text-transform: uppercase; letter-spacing: .12em;
|
|
color: var(--ink-faint); margin: 0 0 12px; font-weight: 600;
|
|
}
|
|
|
|
.stat { font: 600 24px/1.15 var(--mono); }
|
|
.stat-sub { color: var(--ink-faint); font-size: 12px; margin-top: 2px; }
|
|
|
|
.dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; flex: none; }
|
|
.dot.ok { background: var(--ok); }
|
|
.dot.warn { background: var(--warn); }
|
|
.dot.bad { background: var(--bad); }
|
|
.dot.idle { background: var(--ink-faint); }
|
|
.dot.live { box-shadow: 0 0 0 0 rgba(63,185,80,.6); animation: pulse 2.4s infinite; }
|
|
@keyframes pulse {
|
|
70% { box-shadow: 0 0 0 7px rgba(63,185,80,0); }
|
|
100% { box-shadow: 0 0 0 0 rgba(63,185,80,0); }
|
|
}
|
|
|
|
.row { display: flex; align-items: center; gap: 10px; }
|
|
.row + .row { margin-top: 8px; }
|
|
.spread { justify-content: space-between; }
|
|
.muted { color: var(--ink-faint); }
|
|
.mono { font-family: var(--mono); }
|
|
.small { font-size: 12px; }
|
|
.nowrap { white-space: nowrap; }
|
|
|
|
table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
th {
|
|
text-align: left; font-weight: 500; color: var(--ink-faint);
|
|
font-size: 11px; text-transform: uppercase; letter-spacing: .08em;
|
|
padding: 0 10px 8px 0; border-bottom: 1px solid var(--line);
|
|
}
|
|
td { padding: 8px 10px 8px 0; border-bottom: 1px solid var(--line); vertical-align: top; }
|
|
tr:last-child td { border-bottom: 0; }
|
|
.num { text-align: right; font-family: var(--mono); }
|
|
.table-scroll { overflow-x: auto; }
|
|
|
|
.pill {
|
|
display: inline-flex; align-items: center; gap: 6px;
|
|
padding: 2px 8px; border-radius: 999px; font-size: 11.5px;
|
|
border: 1px solid var(--line); background: var(--panel-2); color: var(--ink-dim);
|
|
font-family: var(--mono);
|
|
}
|
|
.pill.ok { color: var(--ok); border-color: rgba(63,185,80,.35); }
|
|
.pill.warn { color: var(--warn); border-color: rgba(210,153,34,.35); }
|
|
.pill.bad { color: var(--bad); border-color: rgba(248,81,73,.35); }
|
|
|
|
button {
|
|
font: inherit; color: var(--ink); background: var(--panel-2);
|
|
border: 1px solid var(--line); border-radius: 8px;
|
|
padding: 7px 12px; cursor: pointer;
|
|
}
|
|
button:hover:not(:disabled) { border-color: var(--accent); }
|
|
button:disabled { opacity: .45; cursor: default; }
|
|
button.primary { background: var(--accent); border-color: var(--accent); color: #06101f; font-weight: 600; }
|
|
button.danger { color: var(--bad); border-color: rgba(248,81,73,.4); }
|
|
button.danger:hover:not(:disabled) { background: rgba(248,81,73,.12); border-color: var(--bad); }
|
|
button.sm { padding: 4px 9px; font-size: 12px; }
|
|
|
|
input, select, textarea {
|
|
font: inherit; color: var(--ink); background: var(--bg);
|
|
border: 1px solid var(--line); border-radius: 8px; padding: 7px 10px;
|
|
}
|
|
textarea { font-family: var(--mono); font-size: 12.5px; width: 100%; resize: vertical; }
|
|
input:focus, select:focus, textarea:focus { outline: none; border-color: var(--accent); }
|
|
|
|
pre {
|
|
margin: 0; padding: 12px; background: var(--bg);
|
|
border: 1px solid var(--line); border-radius: 8px;
|
|
overflow: auto; max-height: 460px;
|
|
font: 12px/1.5 var(--mono); color: var(--ink-dim);
|
|
white-space: pre;
|
|
}
|
|
|
|
/* ---------- bars ---------- */
|
|
.bar { height: 6px; border-radius: 3px; background: var(--panel-2); overflow: hidden; }
|
|
.bar > span { display: block; height: 100%; background: var(--accent); }
|
|
.bar.ok > span { background: var(--ok); }
|
|
.bar.warn > span { background: var(--warn); }
|
|
.bar.bad > span { background: var(--bad); }
|
|
|
|
.gate { display: grid; gap: 9px; }
|
|
.gate-row { display: grid; grid-template-columns: 1fr auto; gap: 4px 10px; align-items: center; }
|
|
.gate-label { font-size: 12.5px; color: var(--ink-dim); }
|
|
|
|
.banner {
|
|
display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
|
|
border: 1px solid rgba(248,81,73,.4); background: rgba(248,81,73,.08);
|
|
border-radius: var(--radius); padding: 12px 14px; margin-bottom: 14px;
|
|
}
|
|
.banner.warn { border-color: rgba(210,153,34,.4); background: rgba(210,153,34,.08); }
|
|
|
|
.toast {
|
|
position: fixed; right: 18px; bottom: 18px; z-index: 50;
|
|
background: var(--panel); border: 1px solid var(--line);
|
|
border-radius: 10px; padding: 11px 14px; max-width: 380px;
|
|
box-shadow: 0 10px 30px rgba(0,0,0,.45);
|
|
}
|
|
.toast.bad { border-color: rgba(248,81,73,.5); }
|
|
.toast.ok { border-color: rgba(63,185,80,.45); }
|
|
|
|
.empty { color: var(--ink-faint); font-size: 13px; padding: 8px 0; }
|
|
|
|
.skel {
|
|
background: linear-gradient(90deg, var(--panel-2) 25%, #1d232b 37%, var(--panel-2) 63%);
|
|
background-size: 400% 100%;
|
|
animation: shimmer 1.3s ease infinite;
|
|
border-radius: 6px; height: 14px;
|
|
}
|
|
@keyframes shimmer { 0% { background-position: 100% 0; } 100% { background-position: -100% 0; } }
|
|
|
|
@media (max-width: 820px) {
|
|
.shell { grid-template-columns: 1fr; }
|
|
.side {
|
|
position: static; height: auto; flex-direction: row; overflow-x: auto;
|
|
border-right: 0; border-bottom: 1px solid var(--line);
|
|
}
|
|
.brand { padding: 6px 10px; }
|
|
.nav-spacer { display: none; }
|
|
.main { padding: 16px; }
|
|
}
|