Files
llmgw/internal/web/templates/base.css
T
Ilker ManapandClaude Opus 5 859f08da9b
CI / test (push) Successful in 1m19s
Approve accounts from the browser, and draw the traffic
Two things the dashboard was missing: it could not act, and it was a column of
numbers.

Account administration moves to /admin/users — approving into a project,
suspending, reinstating. That is the work an operator does constantly and it
should not need a terminal. Three things stay on the CLI, and the line is not
arbitrary: promoting someone to admin, resetting a password, and issuing a key
on another person's behalf each turn a stolen admin session into more than it
was — permanent access, or somebody else's account. Approving and suspending
do not. Suspending signs the account out everywhere, or suspension is advisory
until the session happens to expire, and an admin cannot suspend themselves,
which is not a thing an interface should let you do by misclicking.

The charts are inline SVG generated in Go. A charting library would mean a
JavaScript toolchain in CI and a megabyte of bundle for what is a list of
rectangles, and the single-binary goal rules that out. Hourly over a day: fine
enough to show a working day, coarse enough that one slow minute is not an
incident. Empty hours are filled in, because a chart with hours missing where
nothing happened reads as a chart with hours missing from the data.

The SVG is marked as trusted HTML, so everything that reaches it is either a
number this package computed or escaped explicitly, and a bar's colour can only
be one of four names — a colour that could be anything is a way to write CSS
into every page the chart appears on. There are tests for both.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 12:01:42 +02:00

89 lines
4.5 KiB
CSS

/* Deliberately one small stylesheet with no build step. The single-binary goal
rules out a JS toolchain, and nothing here needs one. */
:root {
color-scheme: light dark;
--bg: #fbfbfa;
--fg: #1c1c1a;
--muted: #6b6b66;
--line: #e3e3df;
--card: #ffffff;
--accent: #2f5d50;
--ok: #2f7a4f;
--warn: #9a6b1f;
--bad: #a33a32;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #17181a; --fg: #e8e8e4; --muted: #9a9a93; --line: #2c2e31;
--card: #1e2022; --accent: #7fb3a2; --ok: #6fbd8c; --warn: #d6a44e; --bad: #e07b72;
}
}
* { box-sizing: border-box; }
body {
margin: 0; background: var(--bg); color: var(--fg);
font: 15px/1.55 ui-sans-serif, -apple-system, "Segoe UI", Roboto, sans-serif;
}
.wrap { max-width: 1100px; margin: 0 auto; padding: 2.5rem 1.25rem 4rem; }
header { border-bottom: 1px solid var(--line); margin-bottom: 2rem; padding-bottom: 1.25rem; }
h1 { font-size: 1.5rem; margin: 0 0 .25rem; letter-spacing: -.01em; }
h2 { font-size: 1.05rem; margin: 2.25rem 0 .75rem; letter-spacing: -.005em; }
.sub { color: var(--muted); margin: 0; }
nav { margin-top: .75rem; display: flex; gap: 1rem; flex-wrap: wrap; }
nav a { color: var(--accent); text-decoration: none; font-size: .9rem; }
nav a:hover { text-decoration: underline; }
table { width: 100%; border-collapse: collapse; font-size: .875rem; background: var(--card);
border: 1px solid var(--line); border-radius: 6px; overflow: hidden; }
th, td { text-align: left; padding: .5rem .7rem; border-bottom: 1px solid var(--line); }
th { font-weight: 600; color: var(--muted); font-size: .78rem; text-transform: uppercase;
letter-spacing: .04em; }
tr:last-child td { border-bottom: none; }
td.num { text-align: right; font-variant-numeric: tabular-nums; }
code, pre { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .85em; }
pre { background: var(--card); border: 1px solid var(--line); border-radius: 6px;
padding: .9rem 1rem; overflow-x: auto; }
.state { font-weight: 600; font-size: .8rem; }
.state-ready, .state-active { color: var(--ok); }
.state-pending, .state-starting, .state-draining, .state-cordoned { color: var(--warn); }
.state-failed, .state-lost, .state-unreachable, .state-revoked { color: var(--bad); }
.empty { color: var(--muted); font-style: italic; padding: .9rem 0; }
.note { color: var(--muted); font-size: .85rem; margin: .4rem 0 0; }
footer { margin-top: 3rem; padding-top: 1rem; border-top: 1px solid var(--line);
color: var(--muted); font-size: .8rem; display: flex; justify-content: space-between; gap: 1rem; }
button {
font: inherit; font-weight: 600; cursor: pointer;
background: var(--accent); color: var(--bg); border: none;
border-radius: 6px; padding: .55rem 1.1rem;
}
button:hover { filter: brightness(1.1); }
ul { padding-left: 1.2rem; }
li { margin: .25rem 0; }
a { color: var(--accent); }
.wrap.narrow { max-width: 30rem; }
label { display: block; margin: .9rem 0; font-size: .9rem; font-weight: 600; }
input[type=text], input[type=email], input[type=password] {
display: block; width: 100%; margin-top: .3rem; font: inherit; font-weight: 400;
padding: .5rem .6rem; border: 1px solid var(--line); border-radius: 6px;
background: var(--card); color: var(--fg);
}
input:focus { outline: 2px solid var(--accent); outline-offset: 1px; }
.error { color: var(--bad); border: 1px solid var(--bad); border-radius: 6px;
padding: .6rem .8rem; background: var(--card); }
form.inline { display: inline; }
button.link { background: none; color: var(--accent); padding: 0;
text-decoration: underline; font-weight: 400; font-size: .9rem; }
button.link.danger { color: var(--bad); }
.ok { color: var(--ok); border: 1px solid var(--ok); border-radius: 6px;
padding: .6rem .8rem; background: var(--card); }
select { font: inherit; padding: .35rem .4rem; border: 1px solid var(--line);
border-radius: 6px; background: var(--card); color: var(--fg); }
.chart-wrap { position: relative; margin: .5rem 0 1.5rem; }
.chart { width: 100%; height: 120px; display: block;
background: var(--card); border: 1px solid var(--line); border-radius: 6px; }
.chart rect { transition: opacity .1s; }
.chart rect:hover { opacity: .65; }
.chart .axis { stroke: var(--line); stroke-width: 1; }
.chart-peak { position: absolute; top: .4rem; right: .6rem;
font-size: .72rem; color: var(--muted); }
.chart-row { display: grid; gap: 1rem; grid-template-columns: 1fr 1fr; }
@media (max-width: 720px) { .chart-row { grid-template-columns: 1fr; } }