Blueforce-Linux/web/static/tokens.css
emirhanmamak-blueforce 337db15d5b feat(web): add the central fleet panel the operator asked for first
The fleet has ~700 devices and no way to see them: the device console manages one
machine from its own keyboard, and the question "which devices are installed, and
which wave is each in" had no surface at all. This adds it as the first screen.

Python standard library only -- http.server, html.escape, string.Template. No
framework and no third-party import, because the fleet installs offline from a
committed package pool that contains no web framework, so fastapi/uvicorn/flask
would have made the panel uninstallable on the devices it manages. That was
decided with the user over the FastAPI they first picked, and a test asserts it:
the module imports only stdlib plus its own siblings.

What it shows: the dark header with the target and the privilege level; a summary
of counts by state with one loud figure; the device list with a state badge per
row, monospace ids and hostnames so columns line up, non-ready devices sorted
first, and a filter bar; the selected device's detail with its four-channel
verification, since READY is refused until every channel is verified (docs/27).
Nothing is colour-only: every badge carries its Turkish state word.

Data has no live source yet, so a missing inventory file falls back to synthetic
records that the page labels as demo in two places -- the panel never presents
invented devices as real ones.

Two defects found by reviewing a real render, both fixed here with a test that
fails when the bug returns (proven by mutation: each assertion goes red against
the reintroduced bug, green after):
- --state-provisioned resolved to var(--info), and --info is --accent, so a
  KURULDU badge and the selected row were the same blue. The state now resolves
  to its own colour, and the test resolves var() aliases before comparing --
  comparing the raw declarations does not detect this, which is how the first
  version of the test passed against the bug.
- the wave cell printed the bare code and then a label that already contains it,
  so one device read "P2 P2 — PILOT-2". Each wave is named once, now.

Also fixes two entries in docs/27 that had become untrue: it claimed a clone
carries the packages and the installer downloads nothing, which stopped being the
case when the pool moved to Git LFS and bf-bootstrap began cloning without
smudge. The real flow -- light clone, payload from a local mirror or a targeted
`git lfs pull` -- is documented instead.

Tests: tests/test_web_static.py, 112 tests, collected by tests/run-all.sh
(12/12 PASS overall).
2026-09-16 22:39:50 +03:00

125 lines
5.4 KiB
CSS

/*
* Blueforce Field OS — design tokens
* ---------------------------------------------------------------------------
* One source for both operator surfaces:
* - the device console (Python/tkinter, bfos/ui_theme.py reads these values)
* - the central panel (stdlib http.server, web/templates/*)
*
* The surfaces share a status vocabulary because they describe the same fleet
* from two ends. A colour here means one thing everywhere: a device state, a
* severity, or an action weight. It never decorates.
*
* WHY LIGHT, NOT DARK
* The console is used in workshops, dealer service areas and customer sites --
* bright overhead light, daylight from a roller door, a 14" laptop panel. A
* near-black console loses its own body text there and its state colours
* collapse into equal-brightness smudges. So the ground is light, the text is
* dark, and saturation is spent only where it carries meaning: state.
*
* WHAT THE COLOURS HAVE TO CARRY
* The catalogue is 48 operations, 13 of which write. The state machine has
* three stages and a device can also be failed or absent. That is four
* severities plus one action colour, and no more -- a fifth hue would make the
* distinction something an operator has to read instead of see.
*/
:root {
/* -- Ground and structure -------------------------------------------- */
--bg-app: #f6f7f8; /* window ground */
--bg-panel: #ffffff; /* raised surfaces, cards, table body */
--bg-sunken: #eef0f2; /* wells: log output, code, inactive rows */
--bg-header: #1d2530; /* app header; the one dark region */
--border: #d5d9de; /* hairline between regions */
--border-strong: #b6bcc4; /* input borders, table rules */
/* -- Text ------------------------------------------------------------- */
--text: #1a1f26; /* body */
--text-muted: #5c6570; /* secondary lines, labels */
--text-faint: #8b939c; /* timestamps, hints, placeholders */
--text-on-dark: #e8ebee; /* header text */
--text-on-accent: #ffffff;
/* -- Action ----------------------------------------------------------- */
/* One accent. It marks the action the operator came to take and the
selection in a list; nothing else. */
--accent: #1d4ed8;
--accent-hover: #1a43bd;
--accent-active: #16389e;
--accent-soft: #e8eeff; /* selected row, focus ring fill */
--focus-ring: #1d4ed8;
/* -- State: the fleet vocabulary -------------------------------------- */
/* Four severities. Each has a strong form for text and a soft form for
fills, so a badge can be built from either without a second palette. */
--ok: #15803d;
--ok-soft: #e6f4ea;
--warn: #b45309;
--warn-soft: #fdf3e3;
--danger: #b91c1c;
--danger-soft: #fdecec;
--info: #1d4ed8;
--info-soft: #e8eeff;
--idle: #5c6570; /* not yet acted on: not a failure */
--idle-soft: #eef0f2;
/* Device state machine ----------------------------------------------
PROVISIONED_OFFLINE -> ENROLLED -> READY, and the failure/absence that sit
outside the sequence. Kept as named tokens so no surface invents a fifth
stage or repaints a stage by accident.
PROVISIONED is deliberately NOT the action accent. The accent means "the
action you came to take" and "the selected row"; a state that borrowed it
made a KURULDU badge and a selected row the same blue, so a list could not
say which of the two it was showing. This slate is distinct from both the
accent and the neutral --idle, and reads as one step along a path. */
--state-provisioned: #475569;
--state-enrolled: var(--warn);
--state-ready: var(--ok);
--state-failed: var(--danger);
--state-absent: var(--idle);
/* -- Type ------------------------------------------------------------- */
/* Workhorse faces only: this is an Operate surface, and the value of a
webfont here is negative -- the fleet installs offline from a committed
pool, so a font CDN is a broken page on every device. The UI face is
whatever the platform ships; the data face is monospace so device ids,
counts and exit codes line up in a column. */
--font-ui: system-ui, -apple-system, "Segoe UI", Roboto, "Noto Sans",
"DejaVu Sans", sans-serif;
--font-data: ui-monospace, "Cascadia Mono", "JetBrains Mono", Menlo,
Consolas, "DejaVu Sans Mono", monospace;
--size-xs: 12px; /* timestamps, table meta */
--size-sm: 13px; /* labels, table body */
--size-md: 15px; /* body, inputs, buttons */
--size-lg: 18px; /* card titles */
--size-xl: 22px; /* view titles */
--size-2xl: 30px; /* the one headline figure per view */
--leading-tight: 1.25;
--leading-normal: 1.5;
/* -- Space ------------------------------------------------------------ */
/* A 4px rhythm. Named, so a component never hand-picks 7px. */
--sp-1: 4px;
--sp-2: 8px;
--sp-3: 12px;
--sp-4: 16px;
--sp-5: 24px;
--sp-6: 32px;
--sp-7: 48px;
--radius: 6px;
--radius-lg: 10px;
/* -- Elevation -------------------------------------------------------- */
/* Two levels. More would separate regions that are not separate. */
--shadow-sm: 0 1px 2px rgba(16, 24, 40, .06);
--shadow-md: 0 4px 12px rgba(16, 24, 40, .10);
/* -- Metrics ---------------------------------------------------------- */
--row-h: 44px; /* fleet table row: comfortable at arm's length */
--control-h: 36px; /* buttons and inputs share one height */
--header-h: 56px;
--sidebar-w: 220px;
}