diff --git a/.gitignore b/.gitignore index ec1b0f7..029a33b 100644 --- a/.gitignore +++ b/.gitignore @@ -51,6 +51,9 @@ packages.lock.tsv # Documentation site (generated) docs-site/node_modules/ + +# Design tooling scratch (local to a workstation, not shippable) +.impeccable/ docs-site/build/ docs-site/.docusaurus/ docs-site/docs/ diff --git a/PRODUCT.md b/PRODUCT.md new file mode 100644 index 0000000..b6e901f --- /dev/null +++ b/PRODUCT.md @@ -0,0 +1,69 @@ +# Product + + + +## Platform + +web + +Two surfaces share this record. The **central panel** is a web surface (browser, server-rendered). The **device console** is a native desktop surface: a Python/tkinter application that ships on each field PC and runs with no network. Visual work on the device console follows the native-platform guidance for its own platform; this value names the surface under design here. + +## Stack + +Python standard library only (`http.server` + server-rendered templates). No framework and no third-party dependency: the fleet installs offline from a committed package pool, and `fastapi`/`uvicorn`/`flask` are not in that pool. Adding one would mean the panel cannot be installed on the devices it manages. Confirmed with the user over FastAPI for this reason. + +## Users + +**Field technician** — a dealer/authorised service person who receives a Windows 10 PC and must turn it into a Blueforce Field OS device. Works at the device, often in a workshop or a customer site, with limited time and no developer background. Job: run the install, know whether it succeeded, and recover when it did not. + +**Central operator** — the person responsible for the whole fleet (~700 devices) from an office browser. Job: see which devices are installed or not, which rollout wave each is in, and push an approved update. Handles many devices per session and needs status, not detail. + +Both operate the same fleet from opposite ends: the technician from the device, the operator from the browser. + +## Product Purpose + +Convert a fleet of Windows 10 field PCs into machines running Blueforce Field OS, and keep them correct afterwards. Success means a technician finishes an install without internet access, without guessing, and without a silent partial state — and an operator can say at any moment which devices are ready and which are not. + +## Positioning + +The install is provably offline and fail-closed. The device carries its own signed package pool, the installer refuses to continue when the contract does not hold rather than half-installing, and every destructive step demands explicit approval with a root gate. Neither property can be copied by a neighbouring tool that assumes internet access or treats a failed check as a warning. + +## Operating Context + +- Fleet: ~700 Windows 10 field PCs, converted in waves LAB(2) → P1(5) → P2(20) → W1(50) → W2(100) → PROD, production behind a separate typed override. +- Install target: Ubuntu 26.04.1 LTS ("Resolute Raccoon"), GNOME desktop installed but not the boot default — the device boots to a terminal and the desktop is switched on when required. +- Network: the field estate is reached over WireGuard; four remote channels are supported (SSH-over-WG, xRDP, RustDesk OSS, MeshCentral). No commercial remote-desktop licence (AnyDesk was rejected). +- Fleet tooling: Ansible CLI + Semaphore Community for orchestration, Prometheus + Node Exporter + Grafana OSS + Uptime Kuma for monitoring, Docusaurus for documentation. +- Device state machine: `PROVISIONED_OFFLINE → ENROLLED → READY`, with no skipping and no going back; `READY` is refused unless every channel reports `verified`. +- Documentation set: 33 numbered Turkish documents plus README/SUMMARY, served through Docusaurus. + +## Capabilities and Constraints + +**Confirmed functionality** — the device console exposes 48 operations: 13 write (all requiring root) and 35 read-only. The installer is `scripts/install/blueforce-install.sh`, driven by an offline bundle at `provisioning/offline-repo/built/` (index + `pool/` of 1321 `.deb` files, ~887 MB). Evidence of fleet state is an inventory and a enrolment token per device. + +**Hard constraints** — no internet during install or normal operation; every user-visible string is Turkish (code, comments and commit messages stay English); destructive operations need an explicit typed confirmation and a root gate; a check that fails stops the operation, it never degrades to a warning. + +**Deliberately undecided** — where the central panel is hosted and how it authenticates to the fleet. Not invented here: the panel is being designed before its deployment model is fixed. + +## Brand Commitments + +Name: **Blueforce Field OS**. Existing operator documentation and console copy are Turkish and already carry the product voice — plain, factual, no marketing register. No logo or palette is binding; no visual identity exists yet. + +## Evidence on Hand + +- `docs/` — 33 numbered Turkish documents, including `01-ARCHITECTURE`, `24-DECISION-LOG` (K-01…K-14), `27-OFFLINE-PROVISIONING`, `33-OPERATOR-CONSOLE`. +- `bfos/` — the device console (`operations.py` catalogue of 48 operations, `runner.py`, `tui.py`, `ui_gui.py`). +- `provisioning/offline-repo/` — the committed package pool and its manifests. +- No analytics, no customer logos, no testimonials, no benchmark numbers, and no screenshots of a working web panel exist. Nothing of the sort may be fabricated. + +## Product Principles + +1. **Fail closed, never half-install.** A device that cannot be verified stays unfinished and says so; a degraded success is worse than a clear refusal. +2. **Offline is a design property, not a deployment accident.** Anything the fleet needs travels with the fleet. +3. **Destructive work is explicit.** Approval and root are gates, not warnings. +4. **State is legible at a glance.** Which wave, which stage, which channel — readable in seconds, from a device screen or across 700 devices. +5. **Turkish for operators, English for the codebase.** No mixed-language surface. + +## Accessibility & Inclusion + +Field devices are operated in workshops and customer sites, sometimes on small or poorly lit screens; the console is used with a keyboard more often than a mouse. Readable contrast and non-colour-only status signalling are requirements, not enhancements. No formal standard (WCAG level) was agreed; none is claimed. diff --git a/docs/DESIGN.md b/docs/DESIGN.md new file mode 100644 index 0000000..b1e0b62 --- /dev/null +++ b/docs/DESIGN.md @@ -0,0 +1,80 @@ +# Blueforce Field OS — Design System + +Two operator surfaces, one visual language: the **device console** (Python/tkinter, +ships on each field PC) and the **central panel** (Python stdlib `http.server`, +browser). Both describe the same fleet from opposite ends, so they share a status +vocabulary and a token set. Palette values live in `web/static/tokens.css`; the +console reads the same values through `bfos/ui_theme.py`. + +## Direction contract + +**THESIS.** The device's state is the subject, and it must be legible from a step +back — on a workshop laptop, in daylight, with someone waiting. This surface +refuses the category default it would otherwise inherit: a near-black "ops +console" with grey body text, six hues fighting for meaning, and status buried in +a log. Instead: a light ground, dark text with real contrast, and saturation +spent only where it carries a state. + +**OWN-WORLD.** Light neutral ground (`--bg-app #f6f7f8`) with white raised panels +and hairline borders; one dark region, the app header, which anchors the frame. +One action accent (`--accent #1d4ed8`) marking the action the operator came to +take and the selected row, nothing else. Four state severities — ok / warn / +danger / info — each with a strong and a soft form, so a badge or a row can be +built from one palette without inventing a second. Data (ids, counts, exit codes, +timestamps) set in monospace so columns align; UI text in the platform face. +Buttons and inputs share one height; rows share one height; a 4px space rhythm. + +**STORY.** The operator sees at a glance whether the device (or the fleet) is +ready, what stage it reached, and what the one next action is. A technician +understands an install's outcome without reading the log; an operator scanning +700 devices finds the ones that need attention without opening them. + +**FIRST VIEWPORT.** Header band (dark, 56px) carrying the product name, the +target identity, and privilege level on the right — privilege is never a buried +sentence. Below it, a two-column body: left, the fleet/device list with a state +badge per row, sorted so non-ready devices surface first, with a filter bar and a +summary strip of counts by state (the one large figure per view). Right, the +selected device's detail: state, target id, hostname, wave, and the channel +verification list. The primary action sits in the header, not below the fold. + +**FORM.** Operate surface. The world is a *control room board*: fixed regions, +legible readouts, one accent, state as signal. No decorative motion; the only +transition is a state change and a row selection. + +**FINISH.** unreviewed and undocumented is unfinished; this build ends with the +finish review, the verdict, DESIGN.md, and every shipping raster carrying its +provenance. + +## Status vocabulary + +One word per state, Turkish, used identically in both surfaces. + +| State | Token | Meaning | +|---|---|---| +| `KURULDU` | `--state-provisioned` | Provisioned offline, not yet enrolled | +| `KAYITLI` | `--state-enrolled` | Enrolled, channels not all verified | +| `HAZIR` | `--state-ready` | Every channel verified; the only shippable state | +| `HATA` | `--state-failed` | A check failed; the device needs a person | +| `YOK` | `--state-absent` | Not reachable / never seen | + +Colour is never the only carrier: every badge ships its word, and the fleet table +sorts non-ready first, so the list is readable in greyscale. + +## Privilege + +Read-only and full privilege are visually distinct states, not a log line: the +header states the current level, and the console disables rather than hides +write operations when unprivileged, with the reason on the control. + +## Rules + +1. Saturation carries meaning. A colour is a state or the action accent; it is + never decoration, and a fifth hue is a bug. +2. Badges carry words. Greyscale must not lose the state. +3. One loud figure per view. Everything else is quiet and aligned. +4. No webfont. The fleet installs offline; the UI face is whatever the platform + ships and the data face is monospace. +5. Destructive actions look different and ask twice. Approval and the root gate + are design, not an afterthought. +6. The log is evidence, not the interface. Outcomes surface as state and + structure first; raw output stays available, collapsed. diff --git a/web/static/tokens.css b/web/static/tokens.css new file mode 100644 index 0000000..dc9cf5b --- /dev/null +++ b/web/static/tokens.css @@ -0,0 +1,119 @@ +/* + * 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. */ + --state-provisioned: var(--info); + --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; +}