CI / test (push) Successful in 1m28s
The dashboard could say what the cluster was made of and not who was spending it. Usage was per key, which is the right unit for revoking something and the wrong one for the question actually being asked: one person holds a laptop key, a CI key and a notebook key, and adding three rows up by eye is a chore that gets skipped. Per person now, joined through the keys they own, and per model — counted by the model that answered rather than the one requested, since those differ whenever a fallback serves and what actually ran is the question. Headline figures are stat tiles with a sparkline: a single current value is a number, and plotting one is how a chart ends up with a single bar in it. The sparkline says only rising, falling or flat, which is the one thing the number cannot. Ranked horizontal bars, because the labels are email addresses and model names and the label is the point — the question is which person, not what shape the distribution has. Rotated or truncated axis labels say the chart matters more than reading it. Trimmed where drawn and complete on hover, so the plot keeps its geometry and a reader can still find out whose row it is. The palette is validated rather than chosen: every adjacent pair clears the colour-vision separation threshold, and the dark values are the same hues re-stepped for the dark surface instead of an automatic flip. Run through a validator, because that is the only way anyone can honestly claim a palette is colourblind-safe. Which turned up a bug in the existing chart code. cssVar is an allowlist of four names that silently returns --accent for anything else, and callers wrap it in var() themselves — so every new series would have rendered in one colour with accent-coloured text. Caught by a test written from the rule that chart text wears text tokens, never the series colour. A key with no owner is labelled "operator-issued keys" rather than unknown: it predates accounts and is doing its job, and "unknown" would imply something is wrong. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
169 lines
8.1 KiB
CSS
169 lines
8.1 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; } }
|
|
.chat-bar { display: flex; align-items: center; gap: 1rem; margin-bottom: .75rem; }
|
|
.inline-label { font-size: .85rem; font-weight: 600; margin: 0; }
|
|
.inline-label select { margin-left: .4rem; font-weight: 400; }
|
|
.chat-log { min-height: 16rem; max-height: 55vh; overflow-y: auto; padding: .5rem;
|
|
background: var(--card); border: 1px solid var(--line); border-radius: 6px; }
|
|
.bubble { white-space: pre-wrap; word-wrap: break-word; padding: .55rem .75rem;
|
|
margin: .4rem 0; border-radius: 8px; max-width: 46rem; }
|
|
.bubble.you { background: var(--accent); color: var(--bg); margin-left: auto; }
|
|
.bubble.assistant { background: var(--bg); border: 1px solid var(--line); }
|
|
.bubble.failed { color: var(--bad); border-color: var(--bad); }
|
|
.chat-composer { display: flex; gap: .6rem; margin-top: .75rem; align-items: flex-end; }
|
|
.chat-composer textarea { flex: 1; font: inherit; padding: .5rem .6rem; resize: vertical;
|
|
border: 1px solid var(--line); border-radius: 6px; background: var(--card); color: var(--fg); }
|
|
|
|
/* KaTeX sits inside a bubble that sets white-space: pre-wrap for plain text;
|
|
display maths needs to escape that or every fraction gets the newlines in
|
|
its own source. */
|
|
.bubble .katex-display { white-space: normal; margin: .6rem 0; overflow-x: auto; }
|
|
.bubble .katex { white-space: normal; }
|
|
.bubble.you .katex { color: inherit; }
|
|
/* A reply that stopped because it ran out of room, not because it was done. */
|
|
.note.truncated { color: var(--warn, #a15c00); margin: .2rem 0 .8rem; }
|
|
/* Offered under a reply that contains files worth keeping. */
|
|
.link.download { display: inline-block; margin: .3rem 0 .8rem; }
|
|
|
|
/* A usage bar inside a table cell. Drawn with two elements rather than a
|
|
gradient so the number stays selectable and readable by a screen reader. */
|
|
.meter { position: relative; min-width: 9rem; }
|
|
.meter > span {
|
|
display: block; height: .75rem; border-radius: 2px;
|
|
background: var(--accent); opacity: .55;
|
|
}
|
|
.meter > em { font-style: normal; font-size: .8rem; color: var(--muted); }
|
|
.bad { color: var(--bad); }
|
|
|
|
/* Chart palette.
|
|
*
|
|
* Roles rather than raw colours, so light and dark swap in one place and the
|
|
* charts are written against meaning. The values are a validated categorical
|
|
* set: every adjacent pair clears the colour-vision-deficiency separation
|
|
* threshold, and the dark column is the same hues re-stepped for the dark
|
|
* surface rather than an automatic flip of the light one.
|
|
*
|
|
* Checked with a validator rather than by eye, which is the only way anyone
|
|
* can honestly claim a palette is colourblind-safe. */
|
|
:root {
|
|
--viz-1: #2a78d6; /* blue */
|
|
--viz-2: #eb6834; /* orange */
|
|
--viz-3: #1baf7a; /* aqua */
|
|
--viz-4: #eda100; /* yellow */
|
|
--viz-grid: #e6e5e1;
|
|
}
|
|
@media (prefers-color-scheme: dark) {
|
|
:root:not([data-theme="light"]) {
|
|
--viz-1: #3987e5;
|
|
--viz-2: #d95926;
|
|
--viz-3: #199e70;
|
|
--viz-4: #c98500;
|
|
--viz-grid: #2e2e2c;
|
|
}
|
|
}
|
|
:root[data-theme="dark"] {
|
|
--viz-1: #3987e5;
|
|
--viz-2: #d95926;
|
|
--viz-3: #199e70;
|
|
--viz-4: #c98500;
|
|
--viz-grid: #2e2e2c;
|
|
}
|
|
|
|
/* Stat tiles: the headline figures, which are numbers rather than charts. */
|
|
.tiles { display: flex; flex-wrap: wrap; gap: .75rem; margin: .5rem 0 1.25rem; }
|
|
.tile {
|
|
flex: 1 1 8.5rem; min-width: 8.5rem; padding: .6rem .75rem;
|
|
border: 1px solid var(--viz-grid); border-radius: 6px; background: var(--card);
|
|
}
|
|
.tile .v { font-size: 1.5rem; line-height: 1.1; font-variant-numeric: tabular-nums; }
|
|
.tile .k { font-size: .8rem; color: var(--muted); }
|
|
.tile svg { display: block; margin-top: .35rem; }
|
|
.chart { margin: .25rem 0 1rem; }
|
|
.chart svg { display: block; max-width: 100%; height: auto; }
|