aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-07-07 18:24:12 -0500
committerCraig Jennings <c@cjennings.net>2026-07-07 18:24:12 -0500
commit7955be70ea7751c68d6a9b2ca3683ee697a38726 (patch)
treeb241c157d36238f9769660c0701ca24a2696abf2
parent96881fccae125c8803ac5c26b8ddb7ee77be363a (diff)
downloadarchsetup-7955be70ea7751c68d6a9b2ca3683ee697a38726.tar.gz
archsetup-7955be70ea7751c68d6a9b2ca3683ee697a38726.zip
docs: add maintenance console design + interactive prototypes
The design doc replaces the install-health framing in system-monitor-design-ideas.org with a maintenance console: dense metric subpanels behind a persistent selector, a doctor that streams into a results wall, determinate remedies only (guard-armed updates, curation lifecycles for noise, listeners, containers, and orphans), rotary band selectors on the tall subpanels, and a CLI-first build with a four-layer test strategy. Every decision is dated in the doc. Prototypes A-E5 keep the exploration path: five divergent layouts converging into E5, a fully interactive simulation whose GOOD/BAD snapshots become the build's test fixtures. todo.org gains the spec-create task, the panel keybinding-family task, and scope notes on the net/bt doctor retrofit.
-rw-r--r--docs/design/maintenance-console-design-ideas.org527
-rw-r--r--docs/prototypes/2026-07-07-maint-console-A-two-column.html335
-rw-r--r--docs/prototypes/2026-07-07-maint-console-B-domain-tiles.html382
-rw-r--r--docs/prototypes/2026-07-07-maint-console-C-severity-feed.html410
-rw-r--r--docs/prototypes/2026-07-07-maint-console-D-doctor-first.html364
-rw-r--r--docs/prototypes/2026-07-07-maint-console-E-instrument-dashboard.html475
-rw-r--r--docs/prototypes/2026-07-07-maint-console-E2-grouped-dashboard.html443
-rw-r--r--docs/prototypes/2026-07-07-maint-console-E3-hifi-drilldown.html488
-rw-r--r--docs/prototypes/2026-07-07-maint-console-E4-interactive.html546
-rw-r--r--docs/prototypes/2026-07-07-maint-console-E5-selector-subpanel.html1696
-rw-r--r--todo.org13
11 files changed, 5679 insertions, 0 deletions
diff --git a/docs/design/maintenance-console-design-ideas.org b/docs/design/maintenance-console-design-ideas.org
new file mode 100644
index 0000000..066c25d
--- /dev/null
+++ b/docs/design/maintenance-console-design-ideas.org
@@ -0,0 +1,527 @@
+#+TITLE: Maintenance Console — Design Ideas
+#+AUTHOR: Craig Jennings
+#+DATE: 2026-07-06
+
+* Direction
+
+A single-host maintenance console (GUI, waybar-launched) that surfaces every
+health metric for this Arch workstation and, where the remediation is safe,
+runs it. It reshapes the earlier install-health/workstation-contract framing
+(=system-monitor-design-ideas.org=) into a maintenance surface with a doctor
+action.
+
+The console is the glanceable, single-host version of the home project's
+=system-health-check.org= workflow (~1000 lines, capability-dispatched across
+ratio/velox/mybitch/truenas). The console owns the routine-maintenance and
+at-a-glance-status slice; the workflow stays as the escalation path for
+forensic investigation and as the only sanctioned way to run updates.
+
+* Scope: hosts (Q2 resolved 2026-07-06)
+
+v1 targets *both* daily drivers — ratio and velox — as first-class hosts, not
+ratio-first-velox-later. Consequences for v1:
+
+- *Capability dispatch is mandatory from day one*, not deferred. The console
+ probes the live host and runs only applicable checks (btrfs RAID1 on ratio vs
+ ZFS-primary on velox; AMD amd-pstate on ratio vs Intel intel_pstate on velox).
+ This mirrors the home workflow's Phase 0 probe.
+- *velox-only metrics ship in v1*: battery health (capacity / cycles) and the
+ unclean-shutdown / suspend-failure rate — both currently GAPs, both driven by
+ velox being the machine that travels and suspends.
+- The ZFS-primary remediation paths (scrub, snapshot retention, pool health) are
+ built and tested, not stubbed.
+
+* Thresholds source of truth (Q3 resolved 2026-07-06)
+
+One machine-readable file, =maintenance-thresholds.toml=, holds every severity
+value (cache-size trigger, disk %, scrub-age warn/crit, snapshot retention
+limits, temp bands, backup-staleness windows, etc.). archsetup owns and ships
+it; both consumers read the *installed* path, so neither reaches into the
+other's repo:
+
+- the maintenance console (dotfiles code) reads it at runtime,
+- the system-health-check workflow reads it instead of hardcoding severity
+ rules in prose.
+
+The workflow's hard-won values migrate into the TOML as the seed content (the
+snapshot MONTHLY limit that bit /home, the 10 GB cache trigger, the scrub-age
+bands). Install path is an implementation sub-question — a stable location both
+consumers reach (e.g. =~/.config/archsetup/maintenance-thresholds.toml=).
+
+* Workflow ownership — move system-health-check into archsetup (proposed 2026-07-06)
+
+Craig's call: the =system-health-check.org= workflow should live in archsetup,
+not the home project. Rationale — the home project is scoped to finances,
+health, and personal matters; system design, execution, and maintenance are
+archsetup's domain. Home only owns the workflow by inherited accident, not by
+fit. Moving it here also collapses the Q3 coupling: the TOML source-of-truth and
+its workflow consumer end up in the same project.
+
+Move scope (home → archsetup), to sequence when Craig gives the go:
+- =system-health-check.org= (the workflow itself)
+- =homelab-inventory/*.org= (ratio/velox/mybitch/truenas capability inventories
+ it cross-references)
+- any home-project references to the workflow (startup, project-workflows index)
+
+Cross-project mechanics: the archsetup side (receiving the files, wiring the
+TOML) is in-scope here; the home side (removing the originals, updating home's
+references) is the home project's scope — handled by a handoff note to home's
+inbox or a home session, not edited blind from here.
+
+* Panel shape
+
+Three regions, driven by the Automation column of the metrics table below:
+
+- *Actionable* (left) — metrics with a lever. Automation = Auto or Confirm.
+ Two doctor actions serve this column: "Clean up" fires every Auto metric
+ unattended; "Review & fix" opens a preview for the Confirm metrics and acts
+ only on approval.
+- *Diagnostic* (right) — read-only telemetry. Automation = None or Human. A
+ state color (green/amber/red) and the value; no button. Red here is the
+ signal to run the workflow.
+- *Updates* (quarantined strip) — Automation = Workflow. Shown as a count with
+ notable packages named; the only affordance is "run the workflow." The panel
+ never applies updates in place.
+
+*Bar glyph* tracks the worst *Diagnostic* state only — not the actionable
+count. A big package cache is boring; a SMART failure is a fire. Actionable
+clutter must not turn the bar red or it trains you to ignore it.
+
+** Doctor = live output wall (Q1 resolved 2026-07-06)
+
+"Clean up" and "Review & fix" are not fire-and-forget buttons. Running a doctor
+opens an *output wall* — one lamp per action, streaming in realtime as each
+runs:
+
+- *amber* while the action is running,
+- *green* on success (with the reclaimed amount / result inline),
+- *red* on failure.
+
+Feedback is always shown (not just when something was reclaimed) — you watch it
+happen. This is the same live-results shape that should back the other doctors
+(net, bluetooth), so every doctor in the system reads the same way. See the
+todo task to retrofit the net + bt doctors to realtime lamp output.
+
+* Automation legend
+
+| Value | Meaning |
+|----------+-----------------------------------------------------------------------------------------------------------|
+| Auto | Doctor "Clean up" button — fire unattended, reversible or harmless |
+|----------+-----------------------------------------------------------------------------------------------------------|
+| Confirm | Doctor "Review & fix" — preview the change, act only on one click |
+|----------+-----------------------------------------------------------------------------------------------------------|
+| Human | Panel nudges; a human decides and acts (no button, not the agent) |
+|----------+-----------------------------------------------------------------------------------------------------------|
+| Workflow | vLater — metric shown read-only; no in-panel button. Agent-workflow assistance deferred (decided |
+| | 2026-07-07, see below) |
+|----------+-----------------------------------------------------------------------------------------------------------|
+| None | Diagnostic only — no software remediation exists (hardware, telemetry) |
+|----------+-----------------------------------------------------------------------------------------------------------|
+
+*Workflow buttons removed (decided 2026-07-07, prototyping E5).* The panel
+carries no "run workflow" affordances — not on failed units, unclean boots,
+updates, or CVEs. The metrics stay on the board as read-only telemetry; AI
+assistance via the system-health-check workflow is a vLater feature. v1 ships
+only determinate remedies (fixed, scriptable actions with predictable
+outcomes).
+
+*Determinate remedies adopted (decided 2026-07-07).* Every remedy below is
+Confirm-tier; contextual levers appear only when the metric is off-nominal:
+
+- /Service restarts & enables/: failed units (systemctl restart + reset-failed),
+ fail2ban, cronie, chronyd (+ makestep), tailscaled, snapper timers, zram
+ config re-apply. DNS/NetworkManager delegates to the net panel's doctor
+ (deep-link, not a duplicate repair chain).
+- /Deterministic maintenance ops/: btrfs balance -dusage=50 (unallocated low),
+ reinstall owning packages on pacman -Qkk failures, rsyncshot RUN NOW (result
+ streams to the output well), smartctl -t short self-test, snapshot-retention
+ repair (write sane TIMELINE limits + cleanup — the /home lesson as a
+ one-press fix). btrfs device-error counter reset stays manual: resetting
+ without diagnosis masks a dying drive.
+- /Composite macro/: RECLAIM SPACE on the disk-usage cell — runs every reclaim
+ lever (cache, journal, coredumps, app logs, docker tier-1, snapper cleanup)
+ as one output-wall stream.
+- /Disruptive but determinate/: REBOOT behind arm-to-fire, offered when
+ running kernel != installed.
+- /Still read-only/: temps, throttling, battery, memory/OOM, taint, journal
+ error content, kernel/hw events, listeners, unclean-shutdown rate — physical
+ or investigative; vLater AI territory.
+
+*Updates join the Confirm layer (decided 2026-07-07).* Two levers on the
+updates strip, both behind the live-update guard (mesa/hyprland/wayland
+runtime in the pending set):
+
+- UPDATE — repo + AUR system update.
+- TOPGRADE — full ecosystem run. The panel's wrapper always passes
+ --disable git (topgrade's git step rebase-autostashes ~/code/*/ — never
+ under a live session).
+
+*MEM·PWR: evidence + two levers + expectation tags (decided 2026-07-07).*
+Mostly physics, so the category leans watch-only — with these additions:
+
+- /CPU mode selector/: a free segmented control (PERF · BAL · POWER) writing
+ the EPP hint — set the active mode to anything, not drift-repair against a
+ declared default. amd-pstate on ratio, intel_pstate on velox.
+- /Battery charge limit/ (velox-only, capability-gated): SET 80% writes
+ charge_control_end_threshold — the standard longevity cap. Battery *health*
+ (capacity vs design, cycles) stays watch-only hardware telemetry.
+- /Evidence drill-downs/ (digest idiom): top-5 RAM consumers under memory,
+ recent boots listed clean/unclean under the unclean-rate, and throttle/OOM
+ events with timestamps. Evidence makes the numbers actionable even where no
+ button exists.
+- /KILL on top-memory items/ (revised 2026-07-07 — Craig): arm-to-fire, four
+ guards: the arm shows the exact victim (name + size); SIGTERM not SIGKILL,
+ with the outcome reported to the wall; PID + process name revalidated at
+ fire time so a recycled PID can't be hit; session-critical names (systemd,
+ the compositor, the panel itself) render a disabled key — protected. A
+ SIGKILL escalation for TERM-survivors is vLater.
+- /Expectation-setting, panel-wide/: every leverless cell's sub-line carries an
+ explicit tag ("hardware — watch only", "evidence below"), and each subpanel
+ header shows the split — "N fixable · M watch" — so the user knows their
+ agency before reading a single cell.
+
+*Refresh cadence (decided 2026-07-07).* Four tiers, matching probe cost:
+
+- /Live group, panel open/: temps, memory free + top consumers, throttle
+ state — re-read every ~3 s while their subpanel is visible, gated on
+ panel-open exactly like the audio panel's meters. Stop when hidden.
+- /Fast local tier, panel open/: re-probed every ~30 s while the panel is up;
+ additionally, any metric re-probes immediately after an action that touches
+ it (fire CLEAN → cache re-measured, not assumed).
+- /On open/: the hydration tiers re-run (fast reads first, process probes
+ behind them — sub-second perceived).
+- /Network tier/: checkupdates / arch-audit / AUR / firmware stay on the
+ hourly systemd-timer cache with age shown; refreshed on demand only.
+- /Panel closed/: the waybar glyph is fed by a light background scan every
+ ~30 min (systemd timer writing the state file the glyph reads) — the bar
+ stays honest without the panel running.
+
+*Journal errors get a digest, not a fix (decided 2026-07-07).* No generic
+remedy exists — an error-priority line is a symptom of an arbitrary subsystem —
+so the panel ships four determinate assists instead:
+
+- /Digest/: the cell expands to errors grouped by syslog identifier — count,
+ message snippet, first/last seen, and the exact next command (journalctl -u
+ <unit> -b) when the identifier maps to a unit. Top-10 groups, read-only.
+- /MARK KNOWN with a full lifecycle/: arm-to-fire showing the exact pattern
+ before it stores; marked groups move to a dim KNOWN section (never vanish)
+ with per-row UNMARK; every mark/unmark logs to the results wall; marks carry
+ date + example. Patterns bind to identifier + message, never a whole unit —
+ a muted service's *new* errors still surface.
+- /Two noise layers/: shipped defaults (bluetoothd HFP, pixman, xkbcomp) in
+ the packaged TOML, user marks in a separate user file merged over it (a
+ template sync never eats curation). CLEAR MARKS (arm-to-fire) empties the
+ user layer and re-enables shipped defaults; unmarking a shipped default
+ records a disable flag in the user layer.
+- /OPEN JOURNAL/: launches a terminal running journalctl -p err -b — the same
+ delegation pattern as NET DOCTOR.
+
+Ruled out: auto-restarting units that log errors (error ≠ failed) and
+keyword-driven fix suggestions (vLater AI territory).
+
+*Full-sweep findings — all committed to v1 (decided 2026-07-07).* Every metric
+was audited against the converged checklist (honest label, evidence digest
+where a count hides detail, curation lifecycle where "expected" is config
+knowledge, guarded per-item remedies, cross-links, watch-only tags). Adopted,
+all prototyped:
+
+- /Storage/: disk top-consumers digest (evidence only — no file deletion
+ keys); per-device error rows on RAID1 when counters are nonzero,
+ cross-checked against SMART; SMART sub-line carries the last self-test
+ result. Spec note: a real scrub runs hours — the ring needs a running-%
+ state, not an instant reset.
+- /Packages/: orphan digest (name + size) with per-package REMOVE (armed) and
+ KEEP — the curation lifecycle encoding "intentional, not orphaned" (the
+ rust lesson); batch REMOVE ALL skips kept packages. Per-file pacnew rows
+ tagged safe-delete (reflector-managed) vs needs-merge, MERGE delegating to
+ a terminal diff. CVEs named: package · CVE id · severity. AUR and firmware
+ names spelled out.
+- /systemd/: failed units upgraded from a count-lever to a per-unit roster
+ (name · since · exit code · journalctl hint) with per-row RESTART + RESET;
+ is-system-running names its cause ("degraded — N failed units below");
+ taint letters decoded.
+- /Logs/: coredumps grouped by binary (count · last · coredumpctl hint),
+ cleared with the CLEAR action; kernel/hw events listed when not clean
+ (hardware — watch only).
+- /Services/: docker system df breakdown (images / containers / volumes /
+ build cache with per-type reclaimable); stopped containers upgraded to the
+ full signal/expected curation lifecycle (MARK EXPECTED / UNMARK, shipped
+ default: winvm) with per-container START; cron expected-entries drift
+ roster.
+- /Snapshots/: count split by type — timeline (auto-pruned) · single
+ (manual — escapes timeline cleanup, the pile-up risk) · pre/post — with
+ oldest-single named and DELETE STALE (armed, keeps newest 2) when singles
+ accumulate.
+- /Network minors/: fail2ban shows recent-ban count; NTP shows offset.
+
+Rationale for prototyping everything (Craig): real estate and complexity have
+bitten before — surface those limits in the disposable prototype, not after
+functionality exists behind the UI.
+
+*Vertical compression → rotary band selector (2026-07-07).* First attempt —
+the MEM·PWR three-column layout on Packages/Logs/Services — lost too much row
+detail to third-width truncation (Craig's verdict after use). Replaced by a
+*rotary band selector*: the amplifier input-selector idiom. A machined knob
+(click to cycle) whose needle swings to engraved band labels, one per evidence
+section (ORPHANS · PACNEW · ADVISORIES; SIGNAL · KNOWN NOISE · COREDUMPS ·
+KERNEL/HW; CONTAINERS · DOCKER DISK · CRON & BACKUPS). Each band carries its
+own status lamp (section health at a glance without switching) plus a count;
+the selected band gets a gold underline and the needle. One section renders at
+a time at full width, restoring complete row detail. Deliberately distinct
+from the category tiles, console keys, and the CPU-mode segmented control —
+each selection idiom in the panel now has its own visual voice. MEM·PWR keeps
+its three-column evidence strip (short rows fit fine at third-width).
+
+*Listeners get the same treatment (decided 2026-07-07).* The count becomes
+"unexpected listeners" — evidence digest (process · port · bind address from
+ss -tlnp), expected-list curation with the full MARK EXPECTED / UNMARK /
+CLEAR lifecycle (shipped defaults: sshd, mpd, tailscaled; user marks in the
+user layer), and guarded per-socket remedies: STOP (systemctl stop, armed)
+when a unit owns the socket, KILL (SIGTERM, armed) otherwise. Severity keys
+on unexpected AND public-bind (0.0.0.0/::) — a loopback listener warns, an
+exposed one fails — and when ufw is down the signal header names the exposure
+("ufw down — N public binds exposed"). Stopped containers likewise gained a
+contextual START lever (allowlist: winvm), and the firewall its ENABLE.
+
+*Updates strip border is state-tiered (decided 2026-07-07).* Green when
+nothing pending, amber for ordinary pending/AUR/firmware counts, red when
+CVEs exist or pending exceeds the "a lot" threshold (or the update cache has
+gone stale — staleness window in the TOML). The CVE badge renders only when
+the count is nonzero.
+
+*Guard arms instead of blocking (revised 2026-07-07).* When the guard trips,
+the key arms (red, "press again to run anyway — or apply from a TTY") rather
+than hard-refusing. The user decides; the footgun is acknowledged and
+deliberately handed over. After a system update lands, the panel offers a
+reboot: a REBOOT key (arm-to-fire) appears on the updates strip and the
+reboot-required metric flips.
+
+*No per-ecosystem update metrics.* Topgrade's step set (yay, rustup, cargo,
+pipx, npm/pnpm, gem, go, flatpak, fwupd, tmux/zsh/nvim plugins, git repos) has
+no cheap offline "updates available?" probe — mirroring it means a network
+round-trip per registry at panel-open. Instead: one *topgrade freshness*
+metric (wrapper stamps last-run time; threshold in the TOML) whose remedy is
+the TOPGRADE lever, plus a *firmware updates* count in the updates strip
+(fwupd refreshes metadata on its own timer; the panel reads the cache).
+
+Rationale for the hard lines: system updates are Workflow, never Auto — the
+2026-06-07 Hyprland crash was a live -Syu swapping mesa+hyprland under the
+running session, and the standing rule is never -Syu live under Hyprland when
+the mesa/hyprland/wayland runtime is in the set. Hardware findings (SMART, MCE,
+thermal) are None — the fix is replacing a drive or clearing a fan, not
+software.
+
+* Metrics — Storage & filesystem integrity
+
+| Metric | Fix / lever | Automation | Notes |
+|----------------------------------+----------------------------------+------------+-----------------------------------|
+| Package cache size | paccache -r / -ruk0 | Auto | Reclaim, all re-downloadable |
+|----------------------------------+----------------------------------+------------+-----------------------------------|
+| paccache -rk1 (keep 1 version) | paccache -rk1 | Confirm | Frees most; kills downgrade |
+| | | | headroom |
+|----------------------------------+----------------------------------+------------+-----------------------------------|
+| Disk usage (df) | — | None | Fix is cache/snapshot/prune |
+| | | | levers |
+|----------------------------------+----------------------------------+------------+-----------------------------------|
+| Btrfs unallocated space | — | None | Chunk headroom; diagnostic |
+|----------------------------------+----------------------------------+------------+-----------------------------------|
+| Btrfs scrub age | btrfs scrub start | Confirm | GAP; ZFS has this, btrfs doesn't. |
+| | | | IO-heavy, on-demand |
+|----------------------------------+----------------------------------+------------+-----------------------------------|
+| Btrfs device stats (per-drive | btrfs device stats --reset | None | GAP; RAID1 early-warning ahead of |
+| error counters) | | | SMART; reset after review |
+|----------------------------------+----------------------------------+------------+-----------------------------------|
+| ZFS pool health / errors | — | None | CRITICAL if state != ONLINE or |
+| | | | errors > 0 |
+|----------------------------------+----------------------------------+------------+-----------------------------------|
+| ZFS scrub age | zpool scrub | Confirm | Covered |
+|----------------------------------+----------------------------------+------------+-----------------------------------|
+| ZFS capacity | — | None | Perf degrades > 80%; the ZFS |
+| | | | headroom metric (no unallocated |
+| | | | concept) |
+|----------------------------------+----------------------------------+------------+-----------------------------------|
+| ZFS fragmentation (FRAG) | — | None | GAP; no defrag exists — the |
+| | | | remedy is snapshot pruning + |
+| | | | staying under 80% |
+|----------------------------------+----------------------------------+------------+-----------------------------------|
+| ZFS autotrim (SSD TRIM) | zpool set autotrim=on / zpool | Confirm | GAP; velox; the fstrim.timer |
+| | trim | | counterpart on ZFS |
+|----------------------------------+----------------------------------+------------+-----------------------------------|
+| fstrim.timer enabled + firing | systemctl enable --now | Confirm | GAP; standard SSD hygiene |
+| | fstrim.timer | | |
+|----------------------------------+----------------------------------+------------+-----------------------------------|
+| pacman file integrity (Qkk) | reinstall package | Workflow | GAP; modified/missing files need |
+| | | | judgment |
+|----------------------------------+----------------------------------+------------+-----------------------------------|
+| SMART health | — | None | Hardware; replace drive |
+|----------------------------------+----------------------------------+------------+-----------------------------------|
+| SMART wear / temperature | — | None | Diagnostic |
+|----------------------------------+----------------------------------+------------+-----------------------------------|
+
+* Metrics — Snapshots
+
+| Metric | Fix / lever | Automation | Notes |
+|--------------------------------+--------------------------+------------+---------------------------------------------|
+| Snapper count / retention | snapper cleanup / delete | Confirm | Manual (single) snapshots need explicit |
+| | | | choice |
+|--------------------------------+--------------------------+------------+---------------------------------------------|
+| ZFS snapshot count / retention | zfs destroy | Confirm | Runaway retention |
+|--------------------------------+--------------------------+------------+---------------------------------------------|
+| Snapshot auto-timer running | systemctl enable timer | Confirm | Is the auto-snapshot service firing |
+|--------------------------------+--------------------------+------------+---------------------------------------------|
+
+* Metrics — Packages & security
+
+| Metric | Fix / lever | Automation | Notes |
+|-----------------------------------+------------------------------+------------+--------------------------------------|
+| Orphaned packages | pacman -Rns (named args) | Confirm | Review first (rust looked orphaned, |
+| | | | was intentional) |
+|-----------------------------------+------------------------------+------------+--------------------------------------|
+| Pending updates count | — | Workflow | Never auto; workflow-only |
+|-----------------------------------+------------------------------+------------+--------------------------------------|
+| .pacnew files | diff + merge / delete | Confirm | Allowlist auto-deletes mirrorlist / |
+| | | | locale.gen |
+|-----------------------------------+------------------------------+------------+--------------------------------------|
+| arch-audit CVEs | reviewed update | Workflow | GAP; top-priority add. Fix is an |
+| | | | update |
+|-----------------------------------+------------------------------+------------+--------------------------------------|
+| Keyring freshness | pacman -Sy archlinux-keyring | Confirm | GAP; stale keyring breaks update |
+| | | | signatures |
+|-----------------------------------+------------------------------+------------+--------------------------------------|
+| AUR / foreign staleness (Qm) | paru -Sua | Workflow | GAP; AUR updates reviewed, not auto |
+|-----------------------------------+------------------------------+------------+--------------------------------------|
+| Reboot required (kernel mismatch) | reboot | Human | GAP; uname -r vs /usr/lib/modules. |
+| | | | You pick when |
+|-----------------------------------+------------------------------+------------+--------------------------------------|
+
+* Metrics — systemd & boot
+
+| Metric | Fix / lever | Automation | Notes |
+|--------------------------------+------------------------+------------+-----------------------------------------------|
+| is-system-running (degraded?) | — | None | GAP; one-token whole-system verdict; |
+| | | | candidate for the bar glyph |
+|--------------------------------+------------------------+------------+-----------------------------------------------|
+| Failed systemd units | restart / investigate | Workflow | Depends why it failed |
+|--------------------------------+------------------------+------------+-----------------------------------------------|
+| Maintenance timers enabled + | systemctl enable timer | Confirm | GAP; meta-metric — are |
+| firing | | | paccache/btrfs-scrub/fstrim/reflector/snapper |
+| | | | firing |
+|--------------------------------+------------------------+------------+-----------------------------------------------|
+| Kernel taint flag | — | None | GAP; tainted != 0 = proprietary module or |
+| | | | prior crash |
+|--------------------------------+------------------------+------------+-----------------------------------------------|
+
+* Metrics — Logs & coredumps
+
+| Metric | Fix / lever | Automation | Notes |
+|------------------------------------+---------------------------------+------------+----------------------------------|
+| Coredump count | coredumpctl clean (keep recent) | Auto | Keep last few days for forensics |
+|------------------------------------+---------------------------------+------------+----------------------------------|
+| Journald disk usage | journalctl --vacuum-size/time | Auto | Bounded journal |
+|------------------------------------+---------------------------------+------------+----------------------------------|
+| App-log cleanup (>7d) | log-cleanup cron trigger | Auto | Already a cron; manual trigger |
+|------------------------------------+---------------------------------+------------+----------------------------------|
+| Journal error count (real vs | — | Workflow | Forensic; noise-filtered count |
+| noise) | | | |
+|------------------------------------+---------------------------------+------------+----------------------------------|
+| Kernel/hardware events | — | None | Forensic, hardware |
+| (MCE/USB/thermal/GPU) | | | |
+|------------------------------------+---------------------------------+------------+----------------------------------|
+
+* Metrics — Memory, thermal, power
+
+| Metric | Fix / lever | Automation | Notes |
+|-----------------------------------------+-------------+------------+--------------------------------------------|
+| Memory free / OOM kills | — | Workflow | OOM = investigate |
+|-----------------------------------------+-------------+------------+--------------------------------------------|
+| Swap / zram present + healthy | — | None | Diagnostic |
+|-----------------------------------------+-------------+------------+--------------------------------------------|
+| CPU / GPU temperatures | — | None | Hardware |
+|-----------------------------------------+-------------+------------+--------------------------------------------|
+| Thermal throttling active | — | None | Cooling issue |
+|-----------------------------------------+-------------+------------+--------------------------------------------|
+| Battery health (capacity / cycles) | — | None | GAP; laptop; ties to open suspend todo |
+|-----------------------------------------+-------------+------------+--------------------------------------------|
+| Unclean-shutdown / suspend-failure rate | — | Workflow | GAP; ratio flagged ~75% unclean 2026-06-08 |
+|-----------------------------------------+-------------+------------+--------------------------------------------|
+
+* Metrics — Network & security posture
+
+| Metric | Fix / lever | Automation | Notes |
+|-----------------------------------+---------------------------+------------+-----------------------|
+| DNS / NetworkManager reachability | restart NM | Workflow | CRITICAL if down |
+|-----------------------------------+---------------------------+------------+-----------------------|
+| Firewall active (ufw / nftables) | ufw enable | Confirm | GAP; security posture |
+|-----------------------------------+---------------------------+------------+-----------------------|
+| Unexpected listeners (ss -tlnp) | — | None | GAP; security review |
+|-----------------------------------+---------------------------+------------+-----------------------|
+| Tailscale peers | tailscale up | Confirm | Covered |
+|-----------------------------------+---------------------------+------------+-----------------------|
+| fail2ban running + bans | systemctl start | Confirm | Covered |
+|-----------------------------------+---------------------------+------------+-----------------------|
+| NTP sync (chrony) | systemctl restart chronyd | Confirm | Covered |
+|-----------------------------------+---------------------------+------------+-----------------------|
+
+* Metrics — Services, backups, virt
+
+| Metric | Fix / lever | Automation | Notes |
+|---------------------------------+--------------------------+------------+--------------------------------------------|
+| rsyncshot backup freshness | — | Workflow | CRITICAL if daily > 48h; investigate |
+| | | | failure |
+|---------------------------------+--------------------------+------------+--------------------------------------------|
+| Docker/podman reclaimable | prune tier 1 / tiers 2-3 | Confirm | Tier 1 nearly Auto; 2-3 destructive |
+|---------------------------------+--------------------------+------------+--------------------------------------------|
+| Docker stopped containers | — | None | Mostly expected (WinVM on-demand) |
+|---------------------------------+--------------------------+------------+--------------------------------------------|
+| libvirt VM state | — | None | Expected off |
+|---------------------------------+--------------------------+------------+--------------------------------------------|
+| Cron running + expected entries | systemctl enable cronie | Confirm | rsyncshot + log-cleanup entries |
+|---------------------------------+--------------------------+------------+--------------------------------------------|
+
+* Architecture & testing (decided 2026-07-07)
+
+*CLI-first, GUI as a face.* The console ships as the fourth panel sibling: a
+=maint= Python package in dotfiles (like =net/=, =bt/=, =audio/=) with probe
+modules (read-only collectors), a remedies module, =cli.py=, and =gui.py=
+driving the same code. =maint status --json= is the contract; =maint fix
+<thing>= is every lever. The GUI never does anything the CLI can't.
+
+Safety mechanics baked into the CLI:
+- global =--dry-run= prints the exact command instead of executing — free test
+ surface, and the GUI's arm-press can display it ("this will run: …").
+- hard read/write split: collectors never elevate; every remedy is an
+ allowlisted exact argv in one small auditable module.
+
+*Four test layers (safest → scariest):*
+
+1. /Unit, fake binaries — no VM, no root (~90% of surface)./ Probes are
+ parsers over command output: feed canned smartctl/btrfs/journalctl/pacman/
+ ss/docker output via fakes on PATH (the net suite's fake-curl and audio's
+ fake-parec pattern). Remedies tested as command construction (assert the
+ argv, don't run it). The live-update guard is a pure function over a
+ package list.
+2. /Read-only integration on the live machine./ All collectors are read-only
+ by design — =maint status --json= runs safely against real hosts.
+3. /Remedies in a VM — archsetup's existing harness./
+ =scripts/testing/run-test.sh= boots the installer VM; a maint scenario
+ breaks things deliberately over ssh (stop cronie, mask fstrim, orphan
+ packages, fill the cache), runs =maint fix …=, asserts post-state. No GUI
+ in the VM. Add qcow2 snapshot/restore between remedy tests so each starts
+ pristine and destructive remedies can't contaminate each other. Pure
+ pacman-level tests may use a throwaway systemd-nspawn container instead
+ (lighter); the VM stays for systemd/btrfs/reboot territory.
+4. /GUI on the host, never in the VM./ AT-SPI smoke like the sibling panels,
+ driven by fixture data. The prototype's GOOD/BAD snapshots become those
+ fixtures — =MAINT_PANEL_FIXTURE=bad= renders the degraded board without a
+ degraded machine, conforming to the =maint status --json= schema.
+
+* Open questions
+
+- RESOLVED 2026-07-06 — live output wall (amber running / green done / red fail),
+ realtime, always shown. See "Doctor = live output wall" above.
+- RESOLVED 2026-07-06 — velox is a first-class v1 target alongside ratio. See
+ "Scope: hosts" below.
+- RESOLVED 2026-07-06 — single machine-readable thresholds file
+ (=maintenance-thresholds.toml=) is the source of truth, *owned by archsetup*.
+ Both the console and the system-health-check workflow read it, so they can
+ never drift. See "Thresholds source of truth" and "Workflow ownership" below.
diff --git a/docs/prototypes/2026-07-07-maint-console-A-two-column.html b/docs/prototypes/2026-07-07-maint-console-A-two-column.html
new file mode 100644
index 0000000..23d0e60
--- /dev/null
+++ b/docs/prototypes/2026-07-07-maint-console-A-two-column.html
@@ -0,0 +1,335 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>Maintenance console — Approach A · two-column board</title>
+<style>
+:root{
+ --ground:#0a0c0d; --panel:#100f0f; --well:#0a0c0d;
+ --gold:#dab53d; --gold-hi:#ffd75f; --silver:#bfc4d0; --cream:#f3e7c5;
+ --dim:#7c838a; --slate:#424f5e; --slate-hi:#54677d; --wash:#2c2f32;
+ --ok:#74932f; --warn:#dab53d; --fail:#cb6b4d;
+ --mono:"BerkeleyMono Nerd Font","Berkeley Mono",monospace;
+}
+*{box-sizing:border-box;margin:0;padding:0}
+html{background:var(--ground)}
+body{font-family:var(--mono);color:var(--silver);line-height:1.4;
+ padding:2.2rem 1.4rem 4rem;
+ background:radial-gradient(1100px 560px at 72% -12%,#141210 0%,transparent 60%),var(--ground);
+ display:flex;flex-direction:column;align-items:center;gap:1.5rem}
+
+/* ---- capsule ---- */
+.capsule{width:1000px;max-width:100%;background:var(--panel);border:1px solid var(--gold);
+ border-radius:16px;padding:18px 20px;box-shadow:0 20px 55px rgba(0,0,0,.6)}
+
+/* ---- header strip ---- */
+.hdr{display:flex;align-items:center;gap:14px;padding-bottom:12px;border-bottom:1px solid var(--wash)}
+.hdr .mark{color:var(--gold);font-weight:700;font-size:1.15rem;letter-spacing:.12em}
+.hdr .mark .host{color:var(--cream)}
+.hdr .verdict{margin-left:auto;display:flex;align-items:center;gap:8px;font-size:.92rem}
+.hdr .verdict .word{color:var(--ok);font-weight:700;letter-spacing:.08em;text-transform:uppercase}
+.subhdr{color:var(--dim);font-size:.72rem;letter-spacing:.24em;text-transform:uppercase;margin-top:8px}
+
+/* ---- lamps ---- */
+.lamp{font-size:.72rem;line-height:1;flex:0 0 auto;position:relative;top:-.5px}
+.lamp.ok{color:var(--ok)}
+.lamp.warn{color:var(--warn)}
+.lamp.fail{color:var(--fail)}
+.lamp.off{color:var(--dim)}
+.lamp.run{color:var(--warn)}
+.lamp.big{font-size:.85rem}
+
+/* ---- board ---- */
+.board{display:grid;grid-template-columns:1fr 1fr;gap:20px;margin-top:16px}
+.col{min-width:0}
+.col-h{display:flex;align-items:baseline;gap:10px;margin-bottom:10px}
+.col-h .tag{color:var(--gold);font-weight:700;font-size:.86rem;letter-spacing:.22em}
+.col-h .sub{color:var(--dim);font-size:.68rem;letter-spacing:.04em}
+
+/* doctor buttons */
+.doctor{display:flex;gap:10px;margin-bottom:14px}
+.btn{font:inherit;cursor:pointer;border-radius:10px;border:1px solid transparent;
+ background:var(--slate);color:var(--cream);padding:.5rem .9rem;font-size:.82rem;letter-spacing:.02em;
+ transition:background .12s,border-color .12s}
+.btn:hover{background:var(--slate-hi)}
+.btn.big{flex:1;text-align:center;padding:.62rem .8rem;font-size:.86rem}
+.btn.big .k{display:block;color:var(--cream);font-weight:700}
+.btn.big .d{display:block;color:var(--silver);font-size:.66rem;letter-spacing:.02em;margin-top:2px;opacity:.85}
+.btn.ghost{background:transparent;border-color:var(--wash);color:var(--silver)}
+.btn.ghost:hover{border-color:var(--slate-hi);background:rgba(66,79,94,.18)}
+.btn.lever{padding:.28rem .66rem;font-size:.74rem;border:1px solid var(--slate);background:rgba(66,79,94,.32);color:var(--cream)}
+.btn.lever:hover{background:var(--slate);border-color:var(--slate-hi)}
+.btn.done{background:var(--slate);color:var(--cream);padding:.5rem 1.4rem}
+
+/* inset group */
+.group{background:var(--well);border:1px solid var(--wash);border-radius:10px;padding:8px 10px 6px;margin-bottom:11px}
+.group-h{color:var(--gold);font-weight:700;font-size:.7rem;letter-spacing:.2em;text-transform:uppercase;
+ display:flex;align-items:center;gap:8px;padding:1px 2px 7px}
+.group-h::after{content:"";height:1px;background:var(--wash);flex:1}
+
+/* metric row */
+.row{display:grid;grid-template-columns:auto 1fr auto auto;align-items:center;gap:9px;
+ padding:4px 4px;border-radius:6px;font-size:.82rem}
+.row:hover{background:rgba(44,47,50,.4)}
+.row .name{color:var(--silver);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
+.row .cls{color:var(--dim);font-size:.6rem;letter-spacing:.14em;border:1px solid var(--wash);
+ border-radius:4px;padding:0 4px;flex:0 0 auto;text-transform:uppercase}
+.row .val{color:var(--dim);text-align:right;white-space:nowrap;font-size:.78rem}
+.row .val.warn{color:var(--warn)}
+.row .val.fail{color:var(--fail)}
+.row .lev-slot{justify-self:end;min-width:0}
+.wf-link{font:inherit;background:transparent;border:0;cursor:pointer;color:var(--warn);
+ font-size:.72rem;letter-spacing:.02em;white-space:nowrap;padding:.1rem 0}
+.wf-link:hover{color:var(--gold-hi);text-decoration:underline}
+.wf-link.fail{color:var(--fail)}
+.dash{color:var(--wash);font-size:.8rem;padding-right:6px}
+
+/* ---- updates quarantine strip ---- */
+.updates{margin-top:6px;padding-top:15px;border-top:1px solid var(--gold);
+ display:flex;align-items:center;gap:16px;flex-wrap:wrap}
+.updates .u-tag{color:var(--gold);font-weight:700;font-size:.8rem;letter-spacing:.22em}
+.updates .u-body{color:var(--silver);font-size:.82rem;flex:1;min-width:260px}
+.updates .u-body .n{color:var(--cream);font-weight:700}
+.updates .u-body .cve{color:var(--fail);font-weight:700}
+.updates .u-body .pk{color:var(--dim)}
+.updates .btn{white-space:nowrap}
+
+/* ---- running / output-wall capsule ---- */
+.run-cap .hdr .mark .state{color:var(--warn)}
+.wall{margin-top:14px;background:var(--well);border:1px solid var(--wash);border-radius:10px;padding:12px 14px;
+ display:flex;flex-direction:column;gap:9px}
+.wall .act{display:grid;grid-template-columns:auto 1fr auto;align-items:center;gap:10px;font-size:.84rem}
+.wall .act .step{color:var(--silver)}
+.wall .act .dots{color:var(--wash);letter-spacing:.1em;overflow:hidden}
+.wall .act .res{color:var(--dim);font-size:.76rem;white-space:nowrap;text-align:right}
+.wall .act.running .step{color:var(--cream)}
+.wall .act.running .res{color:var(--warn)}
+.wall .act.done .res{color:var(--ok)}
+.wall .act.queued .step{color:var(--dim)}
+.run-foot{display:flex;align-items:center;justify-content:space-between;margin-top:14px}
+.run-foot .prog{color:var(--dim);font-size:.76rem;letter-spacing:.04em}
+.run-foot .prog b{color:var(--cream);font-weight:700}
+
+/* ---- legend ---- */
+.legend{width:1000px;max-width:100%;display:flex;gap:18px;flex-wrap:wrap;
+ font-size:.7rem;color:var(--dim);padding:0 4px}
+.legend .item b{color:var(--silver);font-weight:700}
+.legend .item{white-space:nowrap}
+.legend .swatch{color:var(--gold)}
+
+@media(max-width:1040px){
+ .capsule,.legend{width:100%}
+ .board{grid-template-columns:1fr}
+}
+</style>
+</head>
+<body>
+
+<!-- =================== RESTING BOARD =================== -->
+<section class="capsule">
+ <div class="hdr">
+ <div class="mark">MAINT · <span class="host">ratio</span></div>
+ <div class="verdict">
+ <span class="lamp ok big">&#9679;</span>
+ <span class="word">healthy</span>
+ </div>
+ </div>
+ <div class="subhdr">Approach A — Two-column board</div>
+
+ <div class="board">
+
+ <!-- ============ LEFT / FIX ============ -->
+ <div class="col">
+ <div class="col-h"><span class="tag">FIX</span><span class="sub">actionable — auto &amp; confirm</span></div>
+
+ <div class="doctor">
+ <button class="btn big">
+ <span class="k">Clean up</span><span class="d">runs the Auto set unattended</span>
+ </button>
+ <button class="btn big ghost">
+ <span class="k">Review &amp; fix</span><span class="d">preview the Confirm set</span>
+ </button>
+ </div>
+
+ <!-- Storage -->
+ <div class="group">
+ <div class="group-h">Storage</div>
+ <div class="row"><span class="lamp warn">&#9679;</span><span class="name">cache size</span><span class="cls">Auto</span><span class="lev-slot"><button class="btn lever">Clean</button></span></div>
+ <div class="row"><span class="lamp warn">&#9679;</span><span class="name">btrfs scrub age</span><span class="cls">Conf</span><span class="lev-slot"><button class="btn lever">Scrub</button></span></div>
+ <div class="row"><span class="lamp fail">&#9679;</span><span class="name">fstrim.timer</span><span class="cls">Conf</span><span class="lev-slot"><button class="btn lever">Enable</button></span></div>
+ <div class="row"><span class="lamp ok">&#9679;</span><span class="name">deep trim (keep-1)</span><span class="cls">Conf</span><span class="lev-slot"><button class="btn lever">Deep trim</button></span></div>
+ </div>
+
+ <!-- Snapshots -->
+ <div class="group">
+ <div class="group-h">Snapshots</div>
+ <div class="row"><span class="lamp ok">&#9679;</span><span class="name">snapper count/retention</span><span class="cls">Conf</span><span class="lev-slot"><button class="btn lever">Prune</button></span></div>
+ <div class="row"><span class="lamp ok">&#9679;</span><span class="name">snapshot auto-timer</span><span class="cls">Conf</span><span class="val">active</span></div>
+ </div>
+
+ <!-- Packages -->
+ <div class="group">
+ <div class="group-h">Packages</div>
+ <div class="row"><span class="lamp warn">&#9679;</span><span class="name">orphaned packages</span><span class="cls">Conf</span><span class="lev-slot"><button class="btn lever">Review</button></span></div>
+ <div class="row"><span class="lamp warn">&#9679;</span><span class="name">.pacnew files</span><span class="cls">Conf</span><span class="lev-slot"><button class="btn lever">Review</button></span></div>
+ <div class="row"><span class="lamp ok">&#9679;</span><span class="name">keyring freshness</span><span class="cls">Conf</span><span class="lev-slot"><button class="btn lever">Refresh</button></span></div>
+ </div>
+
+ <!-- Logs -->
+ <div class="group">
+ <div class="group-h">Logs &amp; coredumps</div>
+ <div class="row"><span class="lamp warn">&#9679;</span><span class="name">coredumps</span><span class="cls">Auto</span><span class="lev-slot"><button class="btn lever">Clear</button></span></div>
+ <div class="row"><span class="lamp ok">&#9679;</span><span class="name">journald disk usage</span><span class="cls">Auto</span><span class="lev-slot"><button class="btn lever">Vacuum</button></span></div>
+ <div class="row"><span class="lamp ok">&#9679;</span><span class="name">app-log cleanup</span><span class="cls">Auto</span><span class="lev-slot"><button class="btn lever">Run</button></span></div>
+ </div>
+
+ <!-- systemd -->
+ <div class="group">
+ <div class="group-h">systemd</div>
+ <div class="row"><span class="lamp warn">&#9679;</span><span class="name">maintenance timers firing</span><span class="cls">Conf</span><span class="lev-slot"><button class="btn lever">Enable</button></span></div>
+ </div>
+
+ <!-- Network confirm -->
+ <div class="group">
+ <div class="group-h">Network</div>
+ <div class="row"><span class="lamp ok">&#9679;</span><span class="name">firewall active</span><span class="cls">Conf</span><span class="val">active</span></div>
+ <div class="row"><span class="lamp ok">&#9679;</span><span class="name">tailscale peers</span><span class="cls">Conf</span><span class="val">4/4</span></div>
+ <div class="row"><span class="lamp ok">&#9679;</span><span class="name">fail2ban</span><span class="cls">Conf</span><span class="val">active</span></div>
+ <div class="row"><span class="lamp ok">&#9679;</span><span class="name">NTP sync</span><span class="cls">Conf</span><span class="val">synced</span></div>
+ </div>
+
+ <!-- Services confirm -->
+ <div class="group">
+ <div class="group-h">Services</div>
+ <div class="row"><span class="lamp warn">&#9679;</span><span class="name">docker reclaimable</span><span class="cls">Conf</span><span class="lev-slot"><button class="btn lever">Prune</button></span></div>
+ <div class="row"><span class="lamp ok">&#9679;</span><span class="name">cron</span><span class="cls">Conf</span><span class="val">ok</span></div>
+ </div>
+ </div>
+
+ <!-- ============ RIGHT / WATCH ============ -->
+ <div class="col">
+ <div class="col-h"><span class="tag">WATCH</span><span class="sub">read-only — none, human &amp; forensic</span></div>
+
+ <!-- Hardware -->
+ <div class="group">
+ <div class="group-h">Hardware</div>
+ <div class="row"><span class="lamp ok">&#9679;</span><span class="name">SMART health</span><span class="cls">None</span><span class="val">PASSED</span></div>
+ <div class="row"><span class="lamp ok">&#9679;</span><span class="name">SMART wear</span><span class="cls">None</span><span class="val">0%</span></div>
+ <div class="row"><span class="lamp ok">&#9679;</span><span class="name">btrfs device errors</span><span class="cls">None</span><span class="val">0/0</span></div>
+ <div class="row"><span class="lamp ok">&#9679;</span><span class="name">CPU/GPU temps</span><span class="cls">None</span><span class="val">61&deg;/54&deg;</span></div>
+ <div class="row"><span class="lamp ok">&#9679;</span><span class="name">thermal throttling</span><span class="cls">None</span><span class="val">no</span></div>
+ <div class="row"><span class="lamp ok">&#9679;</span><span class="name">kernel/hardware events</span><span class="cls">None</span><span class="val">clean</span></div>
+ </div>
+
+ <!-- System -->
+ <div class="group">
+ <div class="group-h">System &amp; disk</div>
+ <div class="row"><span class="lamp ok">&#9679;</span><span class="name">is-system-running</span><span class="cls">None</span><span class="val">running</span></div>
+ <div class="row"><span class="lamp ok">&#9679;</span><span class="name">disk usage</span><span class="cls">None</span><span class="val">69%</span></div>
+ <div class="row"><span class="lamp ok">&#9679;</span><span class="name">btrfs unallocated</span><span class="cls">None</span><span class="val">118 GiB</span></div>
+ <div class="row"><span class="lamp ok">&#9679;</span><span class="name">kernel taint flag</span><span class="cls">None</span><span class="val">0</span></div>
+ <div class="row"><span class="lamp ok">&#9679;</span><span class="name">pacman file integrity</span><span class="cls">Wflow</span><span class="val">clean</span></div>
+ <div class="row"><span class="lamp ok">&#9679;</span><span class="name">reboot required</span><span class="cls">Human</span><span class="val">no</span></div>
+ </div>
+
+ <!-- Forensic / systemd -->
+ <div class="group">
+ <div class="group-h">Forensic (workflow)</div>
+ <div class="row"><span class="lamp warn">&#9679;</span><span class="name">failed units</span><span class="cls">Wflow</span><span class="val warn">1</span><span class="lev-slot"><button class="wf-link">&rarr; workflow</button></span></div>
+ <div class="row"><span class="lamp warn">&#9679;</span><span class="name">unclean-shutdown rate</span><span class="cls">Wflow</span><span class="val warn">75%</span><span class="lev-slot"><button class="wf-link">&rarr; workflow</button></span></div>
+ <div class="row"><span class="lamp ok">&#9679;</span><span class="name">journal error count</span><span class="cls">Wflow</span><span class="val">12 real</span></div>
+ <div class="row"><span class="lamp ok">&#9679;</span><span class="name">memory free / OOM</span><span class="cls">Wflow</span><span class="val">104 GB free</span></div>
+ <div class="row"><span class="lamp ok">&#9679;</span><span class="name">rsyncshot backup</span><span class="cls">Wflow</span><span class="val">3h ago</span></div>
+ <div class="row"><span class="lamp ok">&#9679;</span><span class="name">DNS / NetworkManager</span><span class="cls">Wflow</span><span class="val">ok</span></div>
+ </div>
+
+ <!-- Power / memory -->
+ <div class="group">
+ <div class="group-h">Memory &amp; power</div>
+ <div class="row"><span class="lamp ok">&#9679;</span><span class="name">swap / zram</span><span class="cls">None</span><span class="val">16 GiB</span></div>
+ <div class="row"><span class="lamp off">&#9675;</span><span class="name">battery health</span><span class="cls">None</span><span class="val">n/a</span></div>
+ </div>
+
+ <!-- Network posture -->
+ <div class="group">
+ <div class="group-h">Network posture</div>
+ <div class="row"><span class="lamp ok">&#9679;</span><span class="name">unexpected listeners</span><span class="cls">None</span><span class="val">3</span></div>
+ </div>
+
+ <!-- Services / virt -->
+ <div class="group">
+ <div class="group-h">Services &amp; virt</div>
+ <div class="row"><span class="lamp ok">&#9679;</span><span class="name">docker stopped containers</span><span class="cls">None</span><span class="val">2</span></div>
+ <div class="row"><span class="lamp off">&#9675;</span><span class="name">libvirt VMs</span><span class="cls">None</span><span class="val">off</span></div>
+ </div>
+ </div>
+
+ </div><!-- /board -->
+
+ <!-- quarantined updates strip -->
+ <div class="updates">
+ <span class="u-tag">UPDATES</span>
+ <span class="u-body"><span class="n">47 pending</span> &middot; <span class="pk">mesa, linux-lts, systemd</span> &middot; <span class="cve">3 CVEs</span></span>
+ <button class="btn">Run workflow</button>
+ </div>
+</section>
+
+<!-- =================== RUNNING / OUTPUT WALL =================== -->
+<section class="capsule run-cap">
+ <div class="hdr">
+ <div class="mark">MAINT · <span class="host">ratio</span> · <span class="state">CLEANING</span></div>
+ <div class="verdict">
+ <span class="lamp run big">&#9680;</span>
+ <span class="word" style="color:var(--warn)">working</span>
+ </div>
+ </div>
+ <div class="subhdr">Doctor has the surface — Auto set streaming</div>
+
+ <div class="wall">
+ <div class="act running">
+ <span class="lamp run">&#9680;</span>
+ <span class="step">cache trim</span>
+ <span class="res">running</span>
+ </div>
+ <div class="act done">
+ <span class="lamp ok">&#9679;</span>
+ <span class="step">journal vacuum</span>
+ <span class="res">done &middot; reclaimed 0.9 GB</span>
+ </div>
+ <div class="act done">
+ <span class="lamp ok">&#9679;</span>
+ <span class="step">coredump clear</span>
+ <span class="res">done &middot; 12 cleared</span>
+ </div>
+ <div class="act queued">
+ <span class="lamp off">&#9675;</span>
+ <span class="step">app-log cleanup</span>
+ <span class="res">queued</span>
+ </div>
+ </div>
+
+ <div class="run-foot">
+ <span class="prog"><b>2</b> of 4 done &middot; 1 running &middot; 1 queued</span>
+ <button class="btn done">Done</button>
+ </div>
+</section>
+
+<!-- =================== AUTOMATION-CLASS LEGEND =================== -->
+<div class="legend">
+ <span class="item"><span class="swatch">&#9679;</span> <b>Auto</b> — doctor "Clean up", fires unattended</span>
+ <span class="item"><span class="swatch">&#9679;</span> <b>Confirm</b> — doctor "Review &amp; fix", preview then one click</span>
+ <span class="item"><span class="swatch">&#9679;</span> <b>Human</b> — panel nudges, you act</span>
+ <span class="item"><span class="swatch">&#9679;</span> <b>Workflow</b> — escalate to the agent health-check</span>
+ <span class="item"><span class="swatch">&#9679;</span> <b>None</b> — diagnostic only, no fix</span>
+</div>
+<div class="legend" style="margin-top:-8px">
+ <span class="item"><span class="lamp ok">&#9679;</span> ok/pass</span>
+ <span class="item"><span class="lamp warn">&#9679;</span> warn</span>
+ <span class="item"><span class="lamp fail">&#9679;</span> fail</span>
+ <span class="item"><span class="lamp run">&#9680;</span> running</span>
+ <span class="item"><span class="lamp off">&#9675;</span> off / n/a</span>
+</div>
+
+</body>
+</html>
diff --git a/docs/prototypes/2026-07-07-maint-console-B-domain-tiles.html b/docs/prototypes/2026-07-07-maint-console-B-domain-tiles.html
new file mode 100644
index 0000000..ebc3acf
--- /dev/null
+++ b/docs/prototypes/2026-07-07-maint-console-B-domain-tiles.html
@@ -0,0 +1,382 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>MAINT · ratio — Approach B</title>
+<style>
+ :root{
+ --bg:#0a0c0d; --surface:#100f0f; --gold:#dab53d; --silver:#bfc4d0;
+ --dim:#7c838a; --cream:#f3e7c5; --slate:#424f5e; --slate-h:#54677d;
+ --ok:#74932f; --warn:#dab53d; --fail:#cb6b4d; --na:#7c838a;
+ --inset:#0a0c0d; --inset-bd:#2c2f32;
+ }
+ *{box-sizing:border-box}
+ body{
+ margin:0; padding:34px 16px; background:var(--bg);
+ font-family:"BerkeleyMono Nerd Font", ui-monospace, "SF Mono", Menlo, Consolas, monospace;
+ color:var(--silver); font-size:13px; line-height:1.45;
+ display:flex; flex-direction:column; align-items:center; gap:20px;
+ }
+ .console{
+ width:1000px; max-width:100%; background:var(--surface);
+ border:1px solid var(--gold); border-radius:16px; padding:18px 20px 22px;
+ }
+ /* header */
+ .hdr{display:flex; align-items:baseline; justify-content:space-between; gap:16px;}
+ .hdr .title{color:var(--cream); font-size:16px; letter-spacing:.5px;}
+ .hdr .title b{color:var(--gold);}
+ .hdr .verdict{font-size:14px; color:var(--ok); font-weight:bold; letter-spacing:.5px;}
+ .sub{color:var(--dim); font-size:12px; margin-top:2px; margin-bottom:14px;}
+
+ /* global action bar */
+ .actionbar{
+ display:flex; align-items:center; gap:10px; flex-wrap:wrap;
+ padding:11px 13px; margin-bottom:16px;
+ background:var(--inset); border:1px solid var(--inset-bd); border-radius:10px;
+ }
+ .actionbar .grp{display:flex; gap:8px;}
+ .actionbar .spacer{flex:1 1 auto;}
+ button{
+ font-family:inherit; font-size:12px; cursor:pointer;
+ background:var(--slate); color:var(--cream); border:1px solid #33404d;
+ border-radius:10px; padding:6px 12px;
+ }
+ button:hover{background:var(--slate-h);}
+ button.ghost{background:transparent; color:var(--silver); border-color:var(--inset-bd); padding:5px 10px;}
+ button.ghost:hover{background:#161a1d; color:var(--cream);}
+ button.tiny{font-size:11px; padding:3px 9px; border-radius:8px;}
+
+ /* updates chip */
+ .updates-chip{
+ display:flex; align-items:center; gap:12px; margin-left:auto;
+ padding:6px 12px; border:1px solid var(--fail); border-radius:999px;
+ background:rgba(203,107,77,.09);
+ }
+ .updates-chip .u-lamp{color:var(--fail);}
+ .updates-chip .u-txt{color:var(--cream); font-size:12px;}
+ .updates-chip .u-txt b{color:var(--warn);}
+
+ /* grid */
+ .grid{
+ display:grid; grid-template-columns:repeat(3, 1fr); gap:12px;
+ }
+ .tile{
+ background:var(--inset); border:1px solid var(--inset-bd); border-radius:10px;
+ padding:11px 12px 12px; display:flex; flex-direction:column; gap:8px;
+ cursor:pointer; transition:border-color .12s;
+ }
+ .tile:hover{border-color:#3c4a58;}
+ .tile.expanded{grid-column:1 / -1; cursor:default; border-color:var(--slate-h);}
+ .tile-top{display:flex; align-items:flex-start; gap:9px;}
+ .rollup{font-size:16px; line-height:1.1; margin-top:1px;}
+ .tile-head{flex:1 1 auto; min-width:0;}
+ .tile-name{color:var(--gold); font-weight:bold; letter-spacing:.3px;}
+ .tile-finding{color:var(--silver); font-size:12px; margin-top:2px;
+ white-space:nowrap; overflow:hidden; text-overflow:ellipsis;}
+ .tile-finding.warnt{color:var(--warn);}
+ .tile-finding.failt{color:var(--fail);}
+ .tile-foot{display:flex; align-items:center; gap:8px; justify-content:space-between;}
+ .counts{color:var(--dim); font-size:11px;}
+ .counts .fix{color:var(--silver);}
+ .expander{color:var(--dim); font-size:11px;}
+
+ /* lamps */
+ .lamp{font-size:13px;}
+ .l-ok{color:var(--ok);} .l-warn{color:var(--warn);} .l-fail{color:var(--fail);}
+ .l-na{color:var(--na);} .l-prog{color:var(--warn);}
+
+ /* expanded detail */
+ .detail{border-top:1px solid var(--inset-bd); padding-top:9px; margin-top:2px;}
+ .detail-hint{color:var(--dim); font-size:11px; margin-bottom:7px;}
+ .row{
+ display:grid; grid-template-columns:16px 1fr auto auto;
+ align-items:center; gap:10px; padding:4px 4px; border-radius:6px;
+ }
+ .row:hover{background:#141719;}
+ .row + .row{border-top:1px solid #17191b;}
+ .r-name{color:var(--silver);}
+ .r-val{color:var(--dim); font-size:12px; text-align:right; white-space:nowrap;}
+ .r-lever{justify-self:end; min-width:96px; text-align:right;}
+ .r-lever .none{color:#4d5358;}
+ .r-auto{font-size:10px; color:var(--dim); text-transform:uppercase; letter-spacing:.5px;
+ border:1px solid var(--inset-bd); border-radius:5px; padding:1px 5px; margin-left:6px;}
+
+ /* running capsule */
+ .capsule{
+ width:1000px; max-width:100%; background:var(--surface);
+ border:1px solid var(--gold); border-radius:16px; padding:14px 18px 16px;
+ }
+ .cap-hdr{display:flex; align-items:center; justify-content:space-between; margin-bottom:10px;}
+ .cap-hdr .title{color:var(--cream); font-size:14px; letter-spacing:.5px;}
+ .cap-hdr .title b{color:var(--gold);}
+ .cap-hdr .running{color:var(--warn); font-weight:bold;}
+ .wall{
+ background:var(--inset); border:1px solid var(--inset-bd); border-radius:10px;
+ padding:10px 12px; display:flex; flex-direction:column; gap:6px;
+ }
+ .wall .line{display:flex; align-items:center; gap:10px;}
+ .wall .line .glyph{width:14px; text-align:center;}
+ .wall .line .act{color:var(--silver); min-width:180px;}
+ .wall .line .st{color:var(--dim); font-size:12px;}
+ .wall .line .st b{color:var(--ok);}
+ .wall .line.done .act{color:var(--dim);}
+ .wall .line.queued .act{color:var(--dim);}
+ .cap-foot{display:flex; justify-content:flex-end; margin-top:11px;}
+ .legend{
+ width:1000px; max-width:100%; color:var(--dim); font-size:11px;
+ padding:0 6px; line-height:1.6;
+ }
+ .legend b{color:var(--silver); font-weight:normal;}
+</style>
+</head>
+<body>
+
+<div class="console">
+ <div class="hdr">
+ <div>
+ <div class="title">MAINT · <b>ratio</b></div>
+ </div>
+ <div class="verdict">● healthy</div>
+ </div>
+ <div class="sub">Approach B — Domain-tile grid</div>
+
+ <!-- global actions -->
+ <div class="actionbar">
+ <div class="grp">
+ <button>Clean up</button>
+ <button class="ghost">Review &amp; fix</button>
+ </div>
+ <div class="spacer"></div>
+ <div class="updates-chip">
+ <span class="u-lamp">●</span>
+ <span class="u-txt"><b>47 pending</b> · 3 CVE · mesa,linux-lts</span>
+ <button class="tiny">Run workflow</button>
+ </div>
+ </div>
+
+ <!-- domain grid -->
+ <div class="grid">
+
+ <!-- STORAGE (expanded) -->
+ <div class="tile expanded" id="t-storage">
+ <div class="tile-top">
+ <span class="rollup l-fail">●</span>
+ <div class="tile-head">
+ <div class="tile-name">Storage</div>
+ <div class="tile-finding failt">fstrim off · scrub 34d</div>
+ </div>
+ <div class="tile-foot" style="flex-direction:column; align-items:flex-end; gap:6px;">
+ <button class="tiny">Clean</button>
+ <span class="counts"><span class="fix">4 fix</span> · 6 watch</span>
+ </div>
+ </div>
+ <div class="detail">
+ <div class="detail-hint">10 checks — lever shown where the doctor can act</div>
+
+ <div class="row"><span class="lamp l-warn">●</span><span class="r-name">cache<span class="r-auto">auto</span></span><span class="r-val">8.8 GB</span><span class="r-lever"><button class="tiny">Clean</button></span></div>
+ <div class="row"><span class="lamp l-ok">●</span><span class="r-name">deep-trim<span class="r-auto">confirm</span></span><span class="r-val">keep-3</span><span class="r-lever"><button class="tiny">Deep trim</button></span></div>
+ <div class="row"><span class="lamp l-ok">●</span><span class="r-name">disk</span><span class="r-val">69%</span><span class="r-lever"><span class="none">—</span></span></div>
+ <div class="row"><span class="lamp l-ok">●</span><span class="r-name">btrfs unalloc</span><span class="r-val">118 GiB</span><span class="r-lever"><span class="none">—</span></span></div>
+ <div class="row"><span class="lamp l-warn">●</span><span class="r-name">btrfs scrub<span class="r-auto">confirm</span></span><span class="r-val">34 d</span><span class="r-lever"><button class="tiny">Scrub</button></span></div>
+ <div class="row"><span class="lamp l-ok">●</span><span class="r-name">btrfs device-err</span><span class="r-val">0 / 0</span><span class="r-lever"><span class="none">—</span></span></div>
+ <div class="row"><span class="lamp l-ok">●</span><span class="r-name">SMART</span><span class="r-val">PASSED</span><span class="r-lever"><span class="none">—</span></span></div>
+ <div class="row"><span class="lamp l-ok">●</span><span class="r-name">SMART wear</span><span class="r-val">0%</span><span class="r-lever"><span class="none">—</span></span></div>
+ <div class="row"><span class="lamp l-fail">●</span><span class="r-name">fstrim.timer<span class="r-auto">confirm</span></span><span class="r-val">off</span><span class="r-lever"><button class="tiny">Enable</button></span></div>
+ <div class="row"><span class="lamp l-ok">●</span><span class="r-name">pacman integrity<span class="r-auto">workflow</span></span><span class="r-val">clean</span><span class="r-lever"><span class="none">—</span></span></div>
+ </div>
+ </div>
+
+ <!-- SNAPSHOTS -->
+ <div class="tile" onclick="toggle('snap')" id="t-snap">
+ <div class="tile-top">
+ <span class="rollup l-ok">●</span>
+ <div class="tile-head">
+ <div class="tile-name">Snapshots</div>
+ <div class="tile-finding">42 snaps · timer active</div>
+ </div>
+ </div>
+ <div class="tile-foot">
+ <span class="counts"><span class="fix">1 fix</span> · 1 watch</span>
+ <span class="expander">▸ detail</span>
+ </div>
+ <div class="detail" id="d-snap" style="display:none;">
+ <div class="row"><span class="lamp l-ok">●</span><span class="r-name">snapper count<span class="r-auto">confirm</span></span><span class="r-val">42</span><span class="r-lever"><button class="tiny">Prune</button></span></div>
+ <div class="row"><span class="lamp l-ok">●</span><span class="r-name">snapshot timer<span class="r-auto">confirm</span></span><span class="r-val">active</span><span class="r-lever"><span class="none">—</span></span></div>
+ </div>
+ </div>
+
+ <!-- PACKAGES & SECURITY -->
+ <div class="tile" onclick="toggle('pkg')" id="t-pkg">
+ <div class="tile-top">
+ <span class="rollup l-fail">●</span>
+ <div class="tile-head">
+ <div class="tile-name">Packages &amp; Security</div>
+ <div class="tile-finding failt">3 CVE · 47 updates</div>
+ </div>
+ </div>
+ <div class="tile-foot">
+ <span class="counts"><span class="fix">3 fix</span> · 4 watch</span>
+ <span class="expander">▸ detail</span>
+ </div>
+ <div class="detail" id="d-pkg" style="display:none;">
+ <div class="row"><span class="lamp l-warn">●</span><span class="r-name">orphans<span class="r-auto">confirm</span></span><span class="r-val">13</span><span class="r-lever"><button class="tiny">Review</button></span></div>
+ <div class="row"><span class="lamp l-warn">●</span><span class="r-name">.pacnew<span class="r-auto">confirm</span></span><span class="r-val">2</span><span class="r-lever"><button class="tiny">Review</button></span></div>
+ <div class="row"><span class="lamp l-ok">●</span><span class="r-name">keyring<span class="r-auto">confirm</span></span><span class="r-val">12 d</span><span class="r-lever"><button class="tiny">Refresh</button></span></div>
+ <div class="row"><span class="lamp l-warn">●</span><span class="r-name">pending updates<span class="r-auto">workflow</span></span><span class="r-val">47</span><span class="r-lever"><button class="tiny">Updates</button></span></div>
+ <div class="row"><span class="lamp l-fail">●</span><span class="r-name">arch-audit<span class="r-auto">workflow</span></span><span class="r-val">3 CVE</span><span class="r-lever"><button class="tiny">Updates</button></span></div>
+ <div class="row"><span class="lamp l-ok">●</span><span class="r-name">AUR staleness<span class="r-auto">workflow</span></span><span class="r-val">5</span><span class="r-lever"><span class="none">—</span></span></div>
+ <div class="row"><span class="lamp l-ok">●</span><span class="r-name">reboot-required<span class="r-auto">human</span></span><span class="r-val">no</span><span class="r-lever"><span class="none">—</span></span></div>
+ </div>
+ </div>
+
+ <!-- SYSTEMD & BOOT -->
+ <div class="tile" onclick="toggle('sd')" id="t-sd">
+ <div class="tile-top">
+ <span class="rollup l-warn">●</span>
+ <div class="tile-head">
+ <div class="tile-name">systemd &amp; Boot</div>
+ <div class="tile-finding warnt">1 failed · timers 4/5</div>
+ </div>
+ </div>
+ <div class="tile-foot">
+ <span class="counts"><span class="fix">1 fix</span> · 3 watch</span>
+ <span class="expander">▸ detail</span>
+ </div>
+ <div class="detail" id="d-sd" style="display:none;">
+ <div class="row"><span class="lamp l-ok">●</span><span class="r-name">is-system-running</span><span class="r-val">running</span><span class="r-lever"><span class="none">—</span></span></div>
+ <div class="row"><span class="lamp l-warn">●</span><span class="r-name">failed units<span class="r-auto">workflow</span></span><span class="r-val">1</span><span class="r-lever"><button class="tiny">Run workflow</button></span></div>
+ <div class="row"><span class="lamp l-warn">●</span><span class="r-name">maintenance timers<span class="r-auto">confirm</span></span><span class="r-val">4 / 5</span><span class="r-lever"><button class="tiny">Enable</button></span></div>
+ <div class="row"><span class="lamp l-ok">●</span><span class="r-name">kernel taint</span><span class="r-val">0</span><span class="r-lever"><span class="none">—</span></span></div>
+ </div>
+ </div>
+
+ <!-- LOGS & COREDUMPS -->
+ <div class="tile" onclick="toggle('log')" id="t-log">
+ <div class="tile-top">
+ <span class="rollup l-warn">●</span>
+ <div class="tile-head">
+ <div class="tile-name">Logs &amp; Coredumps</div>
+ <div class="tile-finding warnt">18 coredumps / 7d</div>
+ </div>
+ <div class="tile-foot" style="flex-direction:column; align-items:flex-end; gap:6px;">
+ <button class="tiny" onclick="event.stopPropagation()">Clean</button>
+ </div>
+ </div>
+ <div class="tile-foot">
+ <span class="counts"><span class="fix">3 fix</span> · 2 watch</span>
+ <span class="expander">▸ detail</span>
+ </div>
+ <div class="detail" id="d-log" style="display:none;">
+ <div class="row"><span class="lamp l-warn">●</span><span class="r-name">coredumps<span class="r-auto">auto</span></span><span class="r-val">18 / 7d</span><span class="r-lever"><button class="tiny">Clear</button></span></div>
+ <div class="row"><span class="lamp l-ok">●</span><span class="r-name">journald<span class="r-auto">auto</span></span><span class="r-val">1.2 GB</span><span class="r-lever"><button class="tiny">Vacuum</button></span></div>
+ <div class="row"><span class="lamp l-ok">●</span><span class="r-name">app-log cleanup<span class="r-auto">auto</span></span><span class="r-val">ok</span><span class="r-lever"><button class="tiny">Run</button></span></div>
+ <div class="row"><span class="lamp l-ok">●</span><span class="r-name">journal errors<span class="r-auto">workflow</span></span><span class="r-val">12 real</span><span class="r-lever"><span class="none">—</span></span></div>
+ <div class="row"><span class="lamp l-ok">●</span><span class="r-name">kernel/hw events</span><span class="r-val">clean</span><span class="r-lever"><span class="none">—</span></span></div>
+ </div>
+ </div>
+
+ <!-- MEMORY / THERMAL / POWER -->
+ <div class="tile" onclick="toggle('mem')" id="t-mem">
+ <div class="tile-top">
+ <span class="rollup l-warn">●</span>
+ <div class="tile-head">
+ <div class="tile-name">Memory / Thermal / Power</div>
+ <div class="tile-finding warnt">unclean-shutdown 75%</div>
+ </div>
+ </div>
+ <div class="tile-foot">
+ <span class="counts"><span class="fix">0 fix</span> · 6 watch</span>
+ <span class="expander">▸ detail</span>
+ </div>
+ <div class="detail" id="d-mem" style="display:none;">
+ <div class="row"><span class="lamp l-ok">●</span><span class="r-name">memory<span class="r-auto">workflow</span></span><span class="r-val">104 GB free</span><span class="r-lever"><span class="none">—</span></span></div>
+ <div class="row"><span class="lamp l-ok">●</span><span class="r-name">swap / zram</span><span class="r-val">16 GiB</span><span class="r-lever"><span class="none">—</span></span></div>
+ <div class="row"><span class="lamp l-ok">●</span><span class="r-name">temps</span><span class="r-val">61° / 54°</span><span class="r-lever"><span class="none">—</span></span></div>
+ <div class="row"><span class="lamp l-ok">●</span><span class="r-name">thermal throttle</span><span class="r-val">no</span><span class="r-lever"><span class="none">—</span></span></div>
+ <div class="row"><span class="lamp l-na">○</span><span class="r-name">battery</span><span class="r-val">n/a</span><span class="r-lever"><span class="none">—</span></span></div>
+ <div class="row"><span class="lamp l-warn">●</span><span class="r-name">unclean-shutdown<span class="r-auto">workflow</span></span><span class="r-val">75%</span><span class="r-lever"><span class="none">—</span></span></div>
+ </div>
+ </div>
+
+ <!-- NETWORK & POSTURE -->
+ <div class="tile" onclick="toggle('net')" id="t-net">
+ <div class="tile-top">
+ <span class="rollup l-ok">●</span>
+ <div class="tile-head">
+ <div class="tile-name">Network &amp; Posture</div>
+ <div class="tile-finding">all posture green</div>
+ </div>
+ </div>
+ <div class="tile-foot">
+ <span class="counts"><span class="fix">4 fix</span> · 2 watch</span>
+ <span class="expander">▸ detail</span>
+ </div>
+ <div class="detail" id="d-net" style="display:none;">
+ <div class="row"><span class="lamp l-ok">●</span><span class="r-name">DNS / NM<span class="r-auto">workflow</span></span><span class="r-val">ok</span><span class="r-lever"><span class="none">—</span></span></div>
+ <div class="row"><span class="lamp l-ok">●</span><span class="r-name">firewall<span class="r-auto">confirm</span></span><span class="r-val">active</span><span class="r-lever"><span class="none">—</span></span></div>
+ <div class="row"><span class="lamp l-ok">●</span><span class="r-name">listeners</span><span class="r-val">3</span><span class="r-lever"><span class="none">—</span></span></div>
+ <div class="row"><span class="lamp l-ok">●</span><span class="r-name">tailscale<span class="r-auto">confirm</span></span><span class="r-val">4 / 4</span><span class="r-lever"><span class="none">—</span></span></div>
+ <div class="row"><span class="lamp l-ok">●</span><span class="r-name">fail2ban<span class="r-auto">confirm</span></span><span class="r-val">active</span><span class="r-lever"><span class="none">—</span></span></div>
+ <div class="row"><span class="lamp l-ok">●</span><span class="r-name">NTP<span class="r-auto">confirm</span></span><span class="r-val">synced</span><span class="r-lever"><span class="none">—</span></span></div>
+ </div>
+ </div>
+
+ <!-- SERVICES / BACKUPS / VIRT -->
+ <div class="tile" onclick="toggle('svc')" id="t-svc">
+ <div class="tile-top">
+ <span class="rollup l-warn">●</span>
+ <div class="tile-head">
+ <div class="tile-name">Services &amp; Backups</div>
+ <div class="tile-finding warnt">docker reclaim 3 GB</div>
+ </div>
+ </div>
+ <div class="tile-foot">
+ <span class="counts"><span class="fix">2 fix</span> · 3 watch</span>
+ <span class="expander">▸ detail</span>
+ </div>
+ <div class="detail" id="d-svc" style="display:none;">
+ <div class="row"><span class="lamp l-ok">●</span><span class="r-name">rsyncshot<span class="r-auto">workflow</span></span><span class="r-val">3h ago</span><span class="r-lever"><span class="none">—</span></span></div>
+ <div class="row"><span class="lamp l-warn">●</span><span class="r-name">docker reclaim<span class="r-auto">confirm</span></span><span class="r-val">3 GB</span><span class="r-lever"><button class="tiny">Prune</button></span></div>
+ <div class="row"><span class="lamp l-ok">●</span><span class="r-name">docker stopped</span><span class="r-val">2</span><span class="r-lever"><span class="none">—</span></span></div>
+ <div class="row"><span class="lamp l-na">○</span><span class="r-name">libvirt</span><span class="r-val">off</span><span class="r-lever"><span class="none">—</span></span></div>
+ <div class="row"><span class="lamp l-ok">●</span><span class="r-name">cron<span class="r-auto">confirm</span></span><span class="r-val">ok</span><span class="r-lever"><span class="none">—</span></span></div>
+ </div>
+ </div>
+
+ </div>
+</div>
+
+<!-- running / output-wall capsule -->
+<div class="capsule">
+ <div class="cap-hdr">
+ <div class="title">MAINT · <b>ratio</b> · <span class="running">CLEANING ◐</span></div>
+ <span style="color:var(--dim); font-size:12px;">4 Auto actions</span>
+ </div>
+ <div class="wall">
+ <div class="line"><span class="glyph l-prog">◐</span><span class="act">cache trim</span><span class="st">running…</span></div>
+ <div class="line done"><span class="glyph l-ok">●</span><span class="act">journal vacuum</span><span class="st">done · reclaimed <b>0.9 GB</b></span></div>
+ <div class="line done"><span class="glyph l-ok">●</span><span class="act">coredump clear</span><span class="st">done · <b>12 cleared</b></span></div>
+ <div class="line queued"><span class="glyph l-na">○</span><span class="act">app-log cleanup</span><span class="st">queued</span></div>
+ </div>
+ <div class="cap-foot"><button>Done</button></div>
+</div>
+
+<div class="legend">
+ Automation — <b>Auto</b> doctor cleans unattended · <b>Confirm</b> doctor previews then you click · <b>Human</b> panel nudges you to act · <b>Workflow</b> escalates to an agent workflow · <b>None</b> diagnostic only.
+</div>
+
+<script>
+ function toggle(id){
+ var t = document.getElementById('t-'+id);
+ var d = document.getElementById('d-'+id);
+ if(!d) return;
+ var open = d.style.display !== 'none';
+ d.style.display = open ? 'none' : 'block';
+ t.classList.toggle('expanded', !open);
+ var ex = t.querySelector('.expander');
+ if(ex) ex.textContent = open ? '▸ detail' : '▾ detail';
+ }
+</script>
+</body>
+</html>
diff --git a/docs/prototypes/2026-07-07-maint-console-C-severity-feed.html b/docs/prototypes/2026-07-07-maint-console-C-severity-feed.html
new file mode 100644
index 0000000..90182d6
--- /dev/null
+++ b/docs/prototypes/2026-07-07-maint-console-C-severity-feed.html
@@ -0,0 +1,410 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>MAINT · ratio — Approach C · Severity feed</title>
+<style>
+ :root{
+ --page:#0a0c0d; --surface:#100f0f; --inset:#0a0c0d;
+ --gold:#dab53d; --silver:#bfc4d0; --dim:#7c838a; --cream:#f3e7c5;
+ --slate:#424f5e; --slate-hi:#54677d; --line:#2c2f32;
+ --ok:#74932f; --warn:#dab53d; --fail:#cb6b4d;
+ }
+ *{box-sizing:border-box}
+ html,body{margin:0;padding:0}
+ body{
+ background:var(--page);
+ color:var(--silver);
+ font-family:"BerkeleyMono Nerd Font","Berkeley Mono",monospace;
+ font-size:13px; line-height:1.45;
+ display:flex; flex-direction:column; align-items:center;
+ padding:28px 16px 60px;
+ -webkit-font-smoothing:antialiased;
+ }
+ .console{
+ width:1000px; max-width:100%;
+ background:var(--surface);
+ border:1px solid var(--gold);
+ border-radius:16px;
+ padding:18px;
+ margin-bottom:20px;
+ }
+
+ /* ---- header ---- */
+ .top{display:flex; align-items:baseline; justify-content:space-between; gap:12px}
+ .title{font-weight:700; color:var(--cream); font-size:16px; letter-spacing:.5px}
+ .title .sub-host{color:var(--dim); font-weight:400; font-size:13px}
+ .verdict{font-weight:700; font-size:14px; color:var(--ok)}
+ .subtitle{color:var(--dim); font-size:12px; margin-top:2px; letter-spacing:.3px}
+
+ /* ---- action bar ---- */
+ .actionbar{
+ display:flex; align-items:center; gap:10px; flex-wrap:wrap;
+ margin:14px 0 6px;
+ }
+ .btn{
+ background:var(--slate); color:var(--cream);
+ border:1px solid #2f3a45; border-radius:10px;
+ padding:7px 14px; font-family:inherit; font-size:12px; font-weight:600;
+ cursor:pointer; letter-spacing:.3px;
+ }
+ .btn:hover{background:var(--slate-hi)}
+ .btn.small{padding:4px 10px; font-size:11px; font-weight:500}
+ .btn.wide{padding:7px 18px}
+ .spacer{flex:1 1 auto}
+
+ /* quarantined updates pill */
+ .updates-pill{
+ display:flex; align-items:center; gap:12px;
+ background:var(--inset); border:1px solid var(--line);
+ border-left:3px solid var(--warn);
+ border-radius:10px; padding:6px 8px 6px 12px;
+ }
+ .updates-pill .u-text{font-size:12px; color:var(--silver)}
+ .updates-pill .u-text b{color:var(--cream); font-weight:600}
+ .updates-pill .u-cve{color:var(--fail); font-weight:600}
+ .updates-pill .u-pkgs{color:var(--dim)}
+
+ /* ---- feed ---- */
+ .feed{margin-top:14px}
+ .band-head{
+ display:flex; align-items:center; gap:10px;
+ color:var(--gold); font-weight:700; font-size:12px;
+ letter-spacing:1.2px; text-transform:uppercase;
+ margin:16px 0 6px; padding-bottom:5px;
+ border-bottom:1px solid var(--line);
+ }
+ .band-head .count{color:var(--dim); font-weight:400; letter-spacing:.3px; text-transform:none}
+ .band-head .accent{
+ display:inline-block; width:8px; height:8px; border-radius:2px;
+ }
+ .band-fail .accent{background:var(--fail)}
+ .band-warn .accent{background:var(--warn)}
+ .band-ok .accent{background:var(--ok)}
+
+ .rows{
+ background:var(--inset); border:1px solid var(--line);
+ border-radius:10px; padding:4px 2px;
+ }
+
+ .row{
+ display:flex; align-items:center; gap:10px;
+ padding:6px 12px;
+ border-bottom:1px solid rgba(44,47,50,.5);
+ }
+ .row:last-child{border-bottom:none}
+ .row .lamp{flex:0 0 auto; font-size:13px; width:14px; text-align:center}
+ .row .name{flex:0 0 auto; color:var(--silver); white-space:nowrap}
+ .row .dots{
+ flex:1 1 auto; min-width:16px;
+ border-bottom:1px dotted #3a3f43;
+ transform:translateY(-3px);
+ margin:0 4px;
+ }
+ .row .value{flex:0 0 auto; color:var(--cream); white-space:nowrap; text-align:right}
+ .row .value.warnval{color:var(--warn)}
+ .row .value.failval{color:var(--fail)}
+ .row .chip{
+ flex:0 0 auto; width:64px; text-align:center;
+ font-size:10px; letter-spacing:.4px; text-transform:uppercase;
+ padding:2px 0; border-radius:6px; border:1px solid var(--line);
+ color:var(--dim);
+ }
+ .chip.auto{color:#a9c05a; border-color:#3c4a24}
+ .chip.confirm{color:var(--gold); border-color:#4a4023}
+ .chip.workflow{color:var(--slate-hi); border-color:#33414f}
+ .chip.human{color:var(--cream); border-color:#4a4436}
+ .chip.none{color:var(--dim); border-color:var(--line)}
+ .row .control{flex:0 0 auto; width:118px; text-align:right}
+ .row .control .lever{
+ background:var(--slate); color:var(--cream);
+ border:1px solid #2f3a45; border-radius:8px;
+ padding:3px 10px; font-family:inherit; font-size:11px; cursor:pointer;
+ }
+ .row .control .lever:hover{background:var(--slate-hi)}
+ .row .control .wflink{color:var(--slate-hi); font-size:11px; cursor:pointer}
+ .row .control .wflink:hover{color:var(--cream); text-decoration:underline}
+ .row .control .nolever{color:#4a5054; font-size:11px}
+
+ /* lamps */
+ .l-ok{color:var(--ok)}
+ .l-warn{color:var(--warn)}
+ .l-fail{color:var(--fail)}
+ .l-off{color:var(--dim)}
+ .l-prog{color:var(--warn)}
+
+ /* OK band quieting */
+ .band-ok-summary{
+ display:flex; align-items:center; gap:8px;
+ color:var(--dim); font-size:12px; cursor:pointer;
+ margin:16px 0 6px; padding-bottom:5px;
+ border-bottom:1px solid var(--line);
+ }
+ .band-ok-summary .tri{color:var(--gold)}
+ .band-ok .rows{opacity:.72}
+ .band-ok .row{padding:4px 12px}
+ .band-ok .row .name{color:var(--dim)}
+ .band-ok .row .value{color:var(--silver)}
+
+ /* ---- running capsule ---- */
+ .capsule{
+ width:1000px; max-width:100%;
+ background:var(--surface);
+ border:1px solid var(--gold);
+ border-radius:16px; padding:16px 18px;
+ }
+ .cap-head{
+ display:flex; align-items:center; justify-content:space-between;
+ color:var(--cream); font-weight:700; font-size:13px; letter-spacing:.5px;
+ margin-bottom:10px;
+ }
+ .cap-head .run{color:var(--warn)}
+ .wall{
+ background:var(--inset); border:1px solid var(--line); border-radius:10px;
+ padding:10px 14px; font-size:12px;
+ }
+ .wall .line{display:flex; align-items:center; gap:10px; padding:3px 0}
+ .wall .line .lamp{width:14px; text-align:center}
+ .wall .line .act{color:var(--silver)}
+ .wall .line .act .done{color:var(--dim)}
+ .wall .line .res{color:var(--ok); margin-left:auto}
+ .wall .line.queued .act{color:var(--dim)}
+ .cap-foot{display:flex; justify-content:flex-end; margin-top:12px}
+
+ .legend{
+ width:1000px; max-width:100%;
+ color:var(--dim); font-size:11px; line-height:1.6;
+ margin-top:16px; padding:0 4px;
+ }
+ .legend b{color:var(--silver); font-weight:600}
+</style>
+</head>
+<body>
+
+<div class="console">
+ <div class="top">
+ <div>
+ <div class="title">MAINT <span class="sub-host">· ratio</span></div>
+ <div class="subtitle">Approach C — Severity feed</div>
+ </div>
+ <div class="verdict">● healthy</div>
+ </div>
+
+ <!-- action bar -->
+ <div class="actionbar">
+ <button class="btn wide">Clean up <span style="color:var(--dim);font-weight:400">· 4 Auto</span></button>
+ <button class="btn wide">Review &amp; fix <span style="color:var(--dim);font-weight:400">· 15 Confirm</span></button>
+ <div class="spacer"></div>
+ <div class="updates-pill">
+ <span class="u-text"><b>47 pending</b> · <span class="u-cve">3 CVE</span> · <span class="u-pkgs">mesa, linux-lts</span></span>
+ <button class="btn small">Run workflow</button>
+ </div>
+ </div>
+
+ <div class="feed">
+
+ <!-- ===================== FAIL ===================== -->
+ <div class="band-head band-fail"><span class="accent"></span>Fail <span class="count">— act now · 2</span></div>
+ <div class="rows">
+ <div class="row">
+ <span class="lamp l-fail">●</span>
+ <span class="name">fstrim.timer</span>
+ <span class="dots"></span>
+ <span class="value failval">off</span>
+ <span class="chip confirm">Confirm</span>
+ <span class="control"><button class="lever">Enable</button></span>
+ </div>
+ <div class="row">
+ <span class="lamp l-fail">●</span>
+ <span class="name">arch-audit</span>
+ <span class="dots"></span>
+ <span class="value failval">3 CVE</span>
+ <span class="chip workflow">Workflow</span>
+ <span class="control"><span class="wflink">→ workflow</span></span>
+ </div>
+ </div>
+
+ <!-- ===================== WARN ===================== -->
+ <div class="band-head band-warn"><span class="accent"></span>Warn <span class="count">— review · 13</span></div>
+ <div class="rows">
+ <div class="row">
+ <span class="lamp l-warn">●</span>
+ <span class="name">cache</span>
+ <span class="dots"></span>
+ <span class="value warnval">8.8&nbsp;GB</span>
+ <span class="chip auto">Auto</span>
+ <span class="control"><button class="lever">Clean</button></span>
+ </div>
+ <div class="row">
+ <span class="lamp l-warn">●</span>
+ <span class="name">btrfs scrub</span>
+ <span class="dots"></span>
+ <span class="value warnval">34d ago</span>
+ <span class="chip confirm">Confirm</span>
+ <span class="control"><button class="lever">Scrub</button></span>
+ </div>
+ <div class="row">
+ <span class="lamp l-warn">●</span>
+ <span class="name">orphans</span>
+ <span class="dots"></span>
+ <span class="value warnval">13</span>
+ <span class="chip confirm">Confirm</span>
+ <span class="control"><button class="lever">Review</button></span>
+ </div>
+ <div class="row">
+ <span class="lamp l-warn">●</span>
+ <span class="name">.pacnew files</span>
+ <span class="dots"></span>
+ <span class="value warnval">2</span>
+ <span class="chip confirm">Confirm</span>
+ <span class="control"><button class="lever">Review</button></span>
+ </div>
+ <div class="row">
+ <span class="lamp l-warn">●</span>
+ <span class="name">pending updates</span>
+ <span class="dots"></span>
+ <span class="value warnval">47</span>
+ <span class="chip workflow">Workflow</span>
+ <span class="control"><span class="wflink">→ workflow</span></span>
+ </div>
+ <div class="row">
+ <span class="lamp l-warn">●</span>
+ <span class="name">failed units</span>
+ <span class="dots"></span>
+ <span class="value warnval">1</span>
+ <span class="chip workflow">Workflow</span>
+ <span class="control"><span class="wflink">→ workflow</span></span>
+ </div>
+ <div class="row">
+ <span class="lamp l-warn">●</span>
+ <span class="name">maintenance timers</span>
+ <span class="dots"></span>
+ <span class="value warnval">4/5</span>
+ <span class="chip confirm">Confirm</span>
+ <span class="control"><button class="lever">Enable</button></span>
+ </div>
+ <div class="row">
+ <span class="lamp l-warn">●</span>
+ <span class="name">coredumps</span>
+ <span class="dots"></span>
+ <span class="value warnval">18 / 7d</span>
+ <span class="chip auto">Auto</span>
+ <span class="control"><button class="lever">Clear</button></span>
+ </div>
+ <div class="row">
+ <span class="lamp l-warn">●</span>
+ <span class="name">unclean-shutdown</span>
+ <span class="dots"></span>
+ <span class="value warnval">75%</span>
+ <span class="chip workflow">Workflow</span>
+ <span class="control"><span class="wflink">→ workflow</span></span>
+ </div>
+ <div class="row">
+ <span class="lamp l-warn">●</span>
+ <span class="name">docker reclaim</span>
+ <span class="dots"></span>
+ <span class="value warnval">3&nbsp;GB</span>
+ <span class="chip confirm">Confirm</span>
+ <span class="control"><button class="lever">Prune</button></span>
+ </div>
+ </div>
+
+ <!-- ===================== OK (quieted) ===================== -->
+ <div class="band-ok" id="okband">
+ <div class="band-ok-summary" onclick="var b=document.getElementById('okrows');b.style.display=b.style.display==='none'?'block':'none';this.querySelector('.tri').textContent=b.style.display==='none'?'▸':'▾';">
+ <span class="tri">▾</span>
+ <span style="color:var(--ok)">●</span>
+ <span>29 OK — clean, no action</span>
+ <span style="flex:1"></span>
+ <span style="color:#565c60">collapse</span>
+ </div>
+ <div class="rows" id="okrows">
+ <div class="row"><span class="lamp l-ok">●</span><span class="name">deep-trim keep-3</span><span class="dots"></span><span class="value">available</span><span class="chip confirm">Confirm</span><span class="control"><button class="lever">Deep trim</button></span></div>
+ <div class="row"><span class="lamp l-ok">●</span><span class="name">disk usage</span><span class="dots"></span><span class="value">69%</span><span class="chip none">None</span><span class="control"><span class="nolever">—</span></span></div>
+ <div class="row"><span class="lamp l-ok">●</span><span class="name">btrfs unalloc</span><span class="dots"></span><span class="value">118&nbsp;GiB</span><span class="chip none">None</span><span class="control"><span class="nolever">—</span></span></div>
+ <div class="row"><span class="lamp l-ok">●</span><span class="name">btrfs device-err</span><span class="dots"></span><span class="value">0 / 0</span><span class="chip none">None</span><span class="control"><span class="nolever">—</span></span></div>
+ <div class="row"><span class="lamp l-ok">●</span><span class="name">SMART status</span><span class="dots"></span><span class="value">PASSED</span><span class="chip none">None</span><span class="control"><span class="nolever">—</span></span></div>
+ <div class="row"><span class="lamp l-ok">●</span><span class="name">SMART wear</span><span class="dots"></span><span class="value">0%</span><span class="chip none">None</span><span class="control"><span class="nolever">—</span></span></div>
+ <div class="row"><span class="lamp l-ok">●</span><span class="name">pacman integrity</span><span class="dots"></span><span class="value">clean</span><span class="chip workflow">Workflow</span><span class="control"><span class="nolever">—</span></span></div>
+ <div class="row"><span class="lamp l-ok">●</span><span class="name">snapper count</span><span class="dots"></span><span class="value">42</span><span class="chip confirm">Confirm</span><span class="control"><button class="lever">Prune</button></span></div>
+ <div class="row"><span class="lamp l-ok">●</span><span class="name">snapshot timer</span><span class="dots"></span><span class="value">active</span><span class="chip confirm">Confirm</span><span class="control"><span class="nolever">—</span></span></div>
+ <div class="row"><span class="lamp l-ok">●</span><span class="name">keyring</span><span class="dots"></span><span class="value">12d</span><span class="chip confirm">Confirm</span><span class="control"><button class="lever">Refresh</button></span></div>
+ <div class="row"><span class="lamp l-ok">●</span><span class="name">AUR staleness</span><span class="dots"></span><span class="value">5</span><span class="chip workflow">Workflow</span><span class="control"><span class="wflink">→ workflow</span></span></div>
+ <div class="row"><span class="lamp l-ok">●</span><span class="name">reboot-required</span><span class="dots"></span><span class="value">no</span><span class="chip human">Human</span><span class="control"><span class="nolever">—</span></span></div>
+ <div class="row"><span class="lamp l-ok">●</span><span class="name">is-system-running</span><span class="dots"></span><span class="value">running</span><span class="chip none">None</span><span class="control"><span class="nolever">—</span></span></div>
+ <div class="row"><span class="lamp l-ok">●</span><span class="name">kernel taint</span><span class="dots"></span><span class="value">0</span><span class="chip none">None</span><span class="control"><span class="nolever">—</span></span></div>
+ <div class="row"><span class="lamp l-ok">●</span><span class="name">journald size</span><span class="dots"></span><span class="value">1.2&nbsp;GB</span><span class="chip auto">Auto</span><span class="control"><button class="lever">Vacuum</button></span></div>
+ <div class="row"><span class="lamp l-ok">●</span><span class="name">app-log cleanup</span><span class="dots"></span><span class="value">ok</span><span class="chip auto">Auto</span><span class="control"><button class="lever">Run</button></span></div>
+ <div class="row"><span class="lamp l-ok">●</span><span class="name">journal errors</span><span class="dots"></span><span class="value">12 real</span><span class="chip workflow">Workflow</span><span class="control"><span class="nolever">—</span></span></div>
+ <div class="row"><span class="lamp l-ok">●</span><span class="name">kernel/hw events</span><span class="dots"></span><span class="value">clean</span><span class="chip none">None</span><span class="control"><span class="nolever">—</span></span></div>
+ <div class="row"><span class="lamp l-ok">●</span><span class="name">memory</span><span class="dots"></span><span class="value">104&nbsp;GB free</span><span class="chip workflow">Workflow</span><span class="control"><span class="nolever">—</span></span></div>
+ <div class="row"><span class="lamp l-ok">●</span><span class="name">swap/zram</span><span class="dots"></span><span class="value">16&nbsp;GiB</span><span class="chip none">None</span><span class="control"><span class="nolever">—</span></span></div>
+ <div class="row"><span class="lamp l-ok">●</span><span class="name">temps</span><span class="dots"></span><span class="value">61° / 54°</span><span class="chip none">None</span><span class="control"><span class="nolever">—</span></span></div>
+ <div class="row"><span class="lamp l-ok">●</span><span class="name">thermal throttle</span><span class="dots"></span><span class="value">no</span><span class="chip none">None</span><span class="control"><span class="nolever">—</span></span></div>
+ <div class="row"><span class="lamp l-ok">●</span><span class="name">DNS / NM</span><span class="dots"></span><span class="value">ok</span><span class="chip workflow">Workflow</span><span class="control"><span class="nolever">—</span></span></div>
+ <div class="row"><span class="lamp l-ok">●</span><span class="name">firewall</span><span class="dots"></span><span class="value">active</span><span class="chip confirm">Confirm</span><span class="control"><span class="nolever">—</span></span></div>
+ <div class="row"><span class="lamp l-ok">●</span><span class="name">listeners</span><span class="dots"></span><span class="value">3</span><span class="chip none">None</span><span class="control"><span class="nolever">—</span></span></div>
+ <div class="row"><span class="lamp l-ok">●</span><span class="name">tailscale</span><span class="dots"></span><span class="value">4/4</span><span class="chip confirm">Confirm</span><span class="control"><span class="nolever">—</span></span></div>
+ <div class="row"><span class="lamp l-ok">●</span><span class="name">fail2ban</span><span class="dots"></span><span class="value">active</span><span class="chip confirm">Confirm</span><span class="control"><span class="nolever">—</span></span></div>
+ <div class="row"><span class="lamp l-ok">●</span><span class="name">NTP</span><span class="dots"></span><span class="value">synced</span><span class="chip confirm">Confirm</span><span class="control"><span class="nolever">—</span></span></div>
+ <div class="row"><span class="lamp l-ok">●</span><span class="name">rsyncshot</span><span class="dots"></span><span class="value">3h ago</span><span class="chip workflow">Workflow</span><span class="control"><span class="nolever">—</span></span></div>
+ <div class="row"><span class="lamp l-ok">●</span><span class="name">docker stopped</span><span class="dots"></span><span class="value">2</span><span class="chip none">None</span><span class="control"><span class="nolever">—</span></span></div>
+ <div class="row"><span class="lamp l-ok">●</span><span class="name">cron</span><span class="dots"></span><span class="value">ok</span><span class="chip confirm">Confirm</span><span class="control"><span class="nolever">—</span></span></div>
+ </div>
+ </div>
+
+ <!-- ===================== OFF / N-A ===================== -->
+ <div class="band-ok" style="margin-top:6px">
+ <div class="rows" style="opacity:.55">
+ <div class="row"><span class="lamp l-off">○</span><span class="name" style="color:var(--dim)">battery</span><span class="dots"></span><span class="value" style="color:var(--dim)">n/a</span><span class="chip none">None</span><span class="control"><span class="nolever">—</span></span></div>
+ <div class="row"><span class="lamp l-off">○</span><span class="name" style="color:var(--dim)">libvirt</span><span class="dots"></span><span class="value" style="color:var(--dim)">off</span><span class="chip none">None</span><span class="control"><span class="nolever">—</span></span></div>
+ </div>
+ </div>
+
+ </div>
+</div>
+
+<!-- =============== RUNNING / OUTPUT-WALL capsule =============== -->
+<div class="capsule">
+ <div class="cap-head">
+ <span>MAINT · ratio · CLEANING <span class="run">◐</span></span>
+ <span style="color:var(--dim); font-weight:400; font-size:12px">4 Auto actions</span>
+ </div>
+ <div class="wall">
+ <div class="line">
+ <span class="lamp l-prog">◐</span>
+ <span class="act">cache trim <span style="color:var(--dim)">running…</span></span>
+ </div>
+ <div class="line">
+ <span class="lamp l-ok">●</span>
+ <span class="act"><span class="done">journal vacuum — done</span></span>
+ <span class="res">reclaimed 0.9&nbsp;GB</span>
+ </div>
+ <div class="line">
+ <span class="lamp l-ok">●</span>
+ <span class="act"><span class="done">coredump clear — done</span></span>
+ <span class="res">12 cleared</span>
+ </div>
+ <div class="line queued">
+ <span class="lamp l-off">○</span>
+ <span class="act">app-log cleanup <span style="color:#565c60">queued</span></span>
+ </div>
+ </div>
+ <div class="cap-foot">
+ <button class="btn">Done</button>
+ </div>
+</div>
+
+<div class="legend">
+ <b>Automation classes:</b>
+ <b>Auto</b> — doctor "Clean up" runs unattended ·
+ <b>Confirm</b> — doctor "Review &amp; fix" preview then click ·
+ <b>Human</b> — you act ·
+ <b>Workflow</b> — escalate to an agent workflow ·
+ <b>None</b> — diagnostic only.
+ &nbsp;&nbsp;<b>Lamps:</b> ● ok / warn / fail · ◐ in-progress · ○ off-or-n/a. ZFS rows omitted (btrfs host).
+</div>
+
+</body>
+</html>
diff --git a/docs/prototypes/2026-07-07-maint-console-D-doctor-first.html b/docs/prototypes/2026-07-07-maint-console-D-doctor-first.html
new file mode 100644
index 0000000..7f2a86e
--- /dev/null
+++ b/docs/prototypes/2026-07-07-maint-console-D-doctor-first.html
@@ -0,0 +1,364 @@
+<!-- Approach D — Doctor-first maintenance console mockup (ratio) -->
+<title>MAINT · ratio — Approach D Doctor-first</title>
+<style>
+ :root{
+ --bg:#0a0c0d; --surface:#100f0f; --gold:#dab53d; --silver:#bfc4d0;
+ --dim:#7c838a; --cream:#f3e7c5; --slate:#424f5e; --slate-h:#54677d;
+ --inset:#0a0c0d; --line:#2c2f32;
+ --olive:#74932f; --amber:#dab53d; --terra:#cb6b4d;
+ }
+ *{box-sizing:border-box}
+ html,body{margin:0;padding:0}
+ body{
+ background:var(--bg);
+ color:var(--silver);
+ font-family:"BerkeleyMono Nerd Font", ui-monospace, monospace;
+ font-size:13px; line-height:1.45;
+ display:flex; justify-content:center;
+ padding:28px 16px 60px;
+ -webkit-font-smoothing:antialiased;
+ }
+ .console{
+ width:1000px; max-width:100%;
+ background:var(--surface);
+ border:1px solid var(--gold);
+ border-radius:16px;
+ padding:18px;
+ }
+
+ /* ---- header ---- */
+ .head{display:flex; align-items:baseline; justify-content:space-between; gap:12px}
+ .head .title{color:var(--cream); font-size:15px; letter-spacing:.06em; font-weight:700}
+ .head .title .sub{display:block; color:var(--dim); font-size:11px; font-weight:400; letter-spacing:.02em; margin-top:2px}
+ .verdict{font-size:14px; font-weight:700; letter-spacing:.03em}
+ .verdict .lamp{font-size:13px}
+
+ .rule{height:1px; background:var(--line); border:0; margin:14px 0}
+
+ /* ---- lamps ---- */
+ .lamp{font-style:normal}
+ .ok{color:var(--olive)}
+ .warn{color:var(--amber)}
+ .fail{color:var(--terra)}
+ .na{color:var(--dim)}
+ .run{color:var(--amber)}
+
+ /* ---- generic inset ---- */
+ .inset{background:var(--inset); border:1px solid var(--line); border-radius:10px; padding:14px}
+ .sec-h{color:var(--gold); font-weight:700; letter-spacing:.05em; font-size:12px; text-transform:uppercase; margin:0 0 10px}
+
+ /* ---- buttons ---- */
+ button{font-family:inherit; cursor:pointer}
+ .btn{
+ background:var(--slate); color:var(--cream); border:1px solid #33414d;
+ border-radius:10px; padding:9px 16px; font-size:12px; letter-spacing:.03em;
+ transition:background .12s;
+ }
+ .btn:hover{background:var(--slate-h)}
+ .btn-lever{padding:4px 10px; font-size:11px; border-radius:8px}
+
+ /* ================= HERO ================= */
+ .hero{
+ background:linear-gradient(180deg,#141312,#0d0c0c);
+ border:1px solid var(--line); border-radius:14px;
+ padding:22px 22px 24px; text-align:center;
+ }
+ .hero .stethoscope{font-size:26px; color:var(--gold); letter-spacing:.3em; margin-bottom:6px}
+ .hero .stat-line{color:var(--silver); font-size:13px; letter-spacing:.04em}
+ .hero .stat-line b{color:var(--cream); font-weight:700}
+ .hero .stat-line .fail{font-weight:700}
+
+ /* summary lamp strip */
+ .strip{display:flex; flex-wrap:wrap; gap:3px; justify-content:center; margin:14px auto 4px; max-width:720px}
+ .strip .dot{font-size:11px; line-height:1}
+
+ .dbig{display:flex; gap:16px; justify-content:center; margin-top:20px; flex-wrap:wrap}
+ .doc-btn{
+ background:var(--slate); color:var(--cream);
+ border:1px solid #33414d; border-radius:12px;
+ padding:16px 26px; min-width:280px; text-align:center;
+ transition:background .12s;
+ }
+ .doc-btn:hover{background:var(--slate-h)}
+ .doc-btn .big{display:block; font-size:17px; font-weight:700; letter-spacing:.08em; color:var(--cream)}
+ .doc-btn .sub{display:block; font-size:11px; color:#cdd7cf; margin-top:6px; letter-spacing:.02em; opacity:.85}
+ .doc-btn.primary{border-color:var(--gold); box-shadow:0 0 0 1px rgba(218,181,61,.25) inset}
+ .doc-btn.primary .big{color:var(--cream)}
+
+ .hero-foot{color:var(--dim); font-size:11px; margin-top:18px; letter-spacing:.02em}
+
+ /* ================= DETAILS DISCLOSURE ================= */
+ .disc{margin-top:16px}
+ .disc-head{
+ display:flex; align-items:center; gap:8px;
+ color:var(--gold); font-weight:700; font-size:12px; letter-spacing:.05em;
+ cursor:pointer; user-select:none; padding:4px 2px;
+ }
+ .disc-head .caret{transition:transform .12s}
+ .disc-body{margin-top:12px}
+
+ .domain{margin-bottom:14px}
+ .domain-h{color:var(--dim); font-size:10px; letter-spacing:.14em; text-transform:uppercase; margin:0 0 6px; border-bottom:1px solid var(--line); padding-bottom:4px}
+ table.metrics{width:100%; border-collapse:collapse}
+ table.metrics td{padding:3px 8px; vertical-align:middle; white-space:nowrap}
+ table.metrics tr:hover{background:rgba(66,79,94,.14)}
+ td.mlamp{width:16px; text-align:center}
+ td.mname{color:var(--silver)}
+ td.mval{color:var(--dim); text-align:right; width:150px}
+ td.mlever{width:110px; text-align:right}
+ .auto-tag{font-size:9px; letter-spacing:.08em; color:var(--dim); text-transform:uppercase; padding-right:6px}
+
+ /* ================= UPDATES QUARANTINE ================= */
+ .updates{margin-top:16px; border:1px dashed #4a4535; background:#12100c}
+ .updates .sec-h{color:var(--amber)}
+ .updates-row{display:flex; align-items:center; justify-content:space-between; gap:14px; flex-wrap:wrap}
+ .updates-info b{color:var(--cream)}
+ .updates-info .cve{color:var(--terra); font-weight:700}
+ .updates-info .pkgs{color:var(--dim)}
+ .updates-note{color:var(--dim); font-size:10px; margin-top:8px; letter-spacing:.02em}
+
+ /* ================= OUTPUT WALL ================= */
+ .wall{
+ margin-top:18px;
+ background:#0b0a09; border:1px solid var(--gold); border-radius:14px;
+ padding:18px 20px;
+ }
+ .wall-h{display:flex; align-items:center; justify-content:space-between; margin-bottom:4px}
+ .wall-h .t{color:var(--gold); font-weight:700; letter-spacing:.06em; font-size:13px}
+ .wall-h .meta{color:var(--dim); font-size:11px}
+ .wall-sub{color:var(--dim); font-size:11px; margin-bottom:12px}
+ .prog{height:6px; background:#1a1712; border:1px solid var(--line); border-radius:4px; overflow:hidden; margin-bottom:14px}
+ .prog i{display:block; height:100%; width:62%; background:linear-gradient(90deg,#74932f,#dab53d)}
+ .stream{font-size:12.5px; line-height:1.9}
+ .srow{display:flex; align-items:baseline; gap:10px}
+ .srow .l{width:14px; text-align:center; flex:0 0 auto}
+ .srow .task{color:var(--silver); flex:0 0 200px}
+ .srow .status{color:var(--dim)}
+ .srow.done .task{color:var(--silver)}
+ .srow.done .status{color:var(--olive)}
+ .srow.run .task{color:var(--cream)}
+ .srow.run .status{color:var(--amber)}
+ .srow.queued .task{color:var(--dim)}
+ .srow.queued .status{color:var(--dim)}
+ .srow .result{color:var(--cream); opacity:.85}
+ .wall-foot{display:flex; justify-content:space-between; align-items:center; margin-top:16px}
+ .wall-foot .summary{color:var(--dim); font-size:11px}
+ .wall-foot .summary b{color:var(--olive)}
+
+ .stagelabel{color:var(--dim); font-size:10px; letter-spacing:.18em; text-transform:uppercase; margin:26px 0 8px; text-align:center}
+</style>
+
+<div class="console">
+
+ <!-- ================= HEADER ================= -->
+ <div class="head">
+ <div class="title">MAINT · ratio
+ <span class="sub">Approach D — Doctor-first</span>
+ </div>
+ <div class="verdict ok"><span class="lamp">●</span> healthy</div>
+ </div>
+ <hr class="rule">
+
+ <div class="stagelabel">— resting state · the doctor is what you see first —</div>
+
+ <!-- ================= HERO (resting) ================= -->
+ <div class="hero">
+ <div class="stethoscope">✚ ✚ ✚</div>
+ <div class="stat-line">
+ <b>ratio</b> · <span class="fail">1 fail</span> · <span class="warn">8 warnings</span> · <span class="ok">34 ok</span>
+ </div>
+
+ <!-- summary lamp strip: one tiny lamp per metric -->
+ <div class="strip" title="one lamp per metric, colored by state">
+ <!-- 44 dots; mostly olive, some amber, one terracotta -->
+ <span class="dot warn">●</span><span class="dot ok">●</span><span class="dot ok">●</span>
+ <span class="dot warn">●</span><span class="dot ok">●</span><span class="dot ok">●</span>
+ <span class="dot ok">●</span><span class="dot ok">●</span><span class="dot fail">●</span>
+ <span class="dot ok">●</span><span class="dot ok">●</span><span class="dot ok">●</span>
+ <span class="dot warn">●</span><span class="dot warn">●</span><span class="dot ok">●</span>
+ <span class="dot warn">●</span><span class="dot fail">●</span><span class="dot ok">●</span>
+ <span class="dot ok">●</span><span class="dot ok">●</span><span class="dot warn">●</span>
+ <span class="dot warn">●</span><span class="dot ok">●</span><span class="dot warn">●</span>
+ <span class="dot ok">●</span><span class="dot ok">●</span><span class="dot ok">●</span>
+ <span class="dot ok">●</span><span class="dot ok">●</span><span class="dot ok">●</span>
+ <span class="dot ok">●</span><span class="dot na">○</span><span class="dot warn">●</span>
+ <span class="dot ok">●</span><span class="dot ok">●</span><span class="dot ok">●</span>
+ <span class="dot ok">●</span><span class="dot ok">●</span><span class="dot ok">●</span>
+ <span class="dot warn">●</span><span class="dot ok">●</span><span class="dot na">○</span>
+ <span class="dot ok">●</span><span class="dot ok">●</span>
+ </div>
+
+ <!-- big doctor actions -->
+ <div class="dbig">
+ <button class="doc-btn primary" onclick="document.getElementById('wall').scrollIntoView({behavior:'smooth'})">
+ <span class="big">CLEAN UP</span>
+ <span class="sub">4 safe tasks · reclaims ~2 GB</span>
+ </button>
+ <button class="doc-btn">
+ <span class="big">REVIEW &amp; FIX</span>
+ <span class="sub">9 items need a look</span>
+ </button>
+ </div>
+
+ <div class="hero-foot">
+ "Should I run the doctor?" — press Clean up for the 4 unattended tasks, or Review &amp; fix to preview the 9 confirm items.
+ </div>
+ </div>
+
+ <!-- ================= DETAILS DISCLOSURE (shown expanded statically) ================= -->
+ <div class="disc inset">
+ <div class="disc-head" onclick="var b=document.getElementById('discbody'); var c=this.querySelector('.caret'); if(b.style.display==='none'){b.style.display='block'; c.textContent='▾';} else {b.style.display='none'; c.textContent='▸';}">
+ <span class="caret">▾</span> 44 metrics · the fine print
+ </div>
+ <div class="disc-body" id="discbody">
+
+ <!-- STORAGE / FILESYSTEM -->
+ <div class="domain">
+ <div class="domain-h">Storage &amp; filesystem</div>
+ <table class="metrics">
+ <tr><td class="mlamp warn">●</td><td class="mname">cache</td><td class="mval">8.8 GB</td><td class="mlever"><span class="auto-tag">auto</span><button class="btn btn-lever">Clean</button></td></tr>
+ <tr><td class="mlamp ok">●</td><td class="mname">deep-trim keep-3</td><td class="mval">ready</td><td class="mlever"><span class="auto-tag">confirm</span><button class="btn btn-lever">Deep trim</button></td></tr>
+ <tr><td class="mlamp ok">●</td><td class="mname">disk</td><td class="mval">69%</td><td class="mlever">—</td></tr>
+ <tr><td class="mlamp ok">●</td><td class="mname">btrfs unalloc</td><td class="mval">118 GiB</td><td class="mlever">—</td></tr>
+ <tr><td class="mlamp warn">●</td><td class="mname">btrfs scrub</td><td class="mval">34 d ago</td><td class="mlever"><span class="auto-tag">confirm</span><button class="btn btn-lever">Scrub</button></td></tr>
+ <tr><td class="mlamp ok">●</td><td class="mname">btrfs device-err</td><td class="mval">0 / 0</td><td class="mlever">—</td></tr>
+ <tr><td class="mlamp ok">●</td><td class="mname">SMART</td><td class="mval">PASSED</td><td class="mlever">—</td></tr>
+ <tr><td class="mlamp ok">●</td><td class="mname">SMART wear</td><td class="mval">0%</td><td class="mlever">—</td></tr>
+ <tr><td class="mlamp fail">●</td><td class="mname">fstrim.timer</td><td class="mval">off</td><td class="mlever"><span class="auto-tag">confirm</span><button class="btn btn-lever">Enable</button></td></tr>
+ </table>
+ </div>
+
+ <!-- PACKAGES -->
+ <div class="domain">
+ <div class="domain-h">Packages &amp; snapshots</div>
+ <table class="metrics">
+ <tr><td class="mlamp ok">●</td><td class="mname">pacman integrity</td><td class="mval">clean</td><td class="mlever"><span class="auto-tag">workflow</span>—</td></tr>
+ <tr><td class="mlamp ok">●</td><td class="mname">snapper count</td><td class="mval">42</td><td class="mlever"><span class="auto-tag">confirm</span><button class="btn btn-lever">Prune</button></td></tr>
+ <tr><td class="mlamp ok">●</td><td class="mname">snapshot timer</td><td class="mval">active</td><td class="mlever"><span class="auto-tag">confirm</span>—</td></tr>
+ <tr><td class="mlamp warn">●</td><td class="mname">orphans</td><td class="mval">13</td><td class="mlever"><span class="auto-tag">confirm</span><button class="btn btn-lever">Review</button></td></tr>
+ <tr><td class="mlamp warn">●</td><td class="mname">.pacnew</td><td class="mval">2</td><td class="mlever"><span class="auto-tag">confirm</span><button class="btn btn-lever">Review</button></td></tr>
+ <tr><td class="mlamp ok">●</td><td class="mname">keyring</td><td class="mval">12 d</td><td class="mlever"><span class="auto-tag">confirm</span><button class="btn btn-lever">Refresh</button></td></tr>
+ <tr><td class="mlamp warn">●</td><td class="mname">pending updates</td><td class="mval">47</td><td class="mlever"><span class="auto-tag">workflow</span>updates</td></tr>
+ <tr><td class="mlamp fail">●</td><td class="mname">arch-audit</td><td class="mval">3 CVE</td><td class="mlever"><span class="auto-tag">workflow</span>updates</td></tr>
+ <tr><td class="mlamp ok">●</td><td class="mname">AUR staleness</td><td class="mval">5</td><td class="mlever"><span class="auto-tag">workflow</span>updates</td></tr>
+ </table>
+ </div>
+
+ <!-- SYSTEM STATE -->
+ <div class="domain">
+ <div class="domain-h">System state</div>
+ <table class="metrics">
+ <tr><td class="mlamp ok">●</td><td class="mname">reboot-required</td><td class="mval">no</td><td class="mlever"><span class="auto-tag">human</span>—</td></tr>
+ <tr><td class="mlamp ok">●</td><td class="mname">is-system-running</td><td class="mval">running</td><td class="mlever">—</td></tr>
+ <tr><td class="mlamp warn">●</td><td class="mname">failed units</td><td class="mval">1</td><td class="mlever"><span class="auto-tag">workflow</span><button class="btn btn-lever">Run</button></td></tr>
+ <tr><td class="mlamp warn">●</td><td class="mname">maintenance timers</td><td class="mval">4 / 5</td><td class="mlever"><span class="auto-tag">confirm</span><button class="btn btn-lever">Enable</button></td></tr>
+ <tr><td class="mlamp ok">●</td><td class="mname">kernel taint</td><td class="mval">0</td><td class="mlever">—</td></tr>
+ </table>
+ </div>
+
+ <!-- LOGS -->
+ <div class="domain">
+ <div class="domain-h">Logs &amp; coredumps</div>
+ <table class="metrics">
+ <tr><td class="mlamp warn">●</td><td class="mname">coredumps</td><td class="mval">18 / 7 d</td><td class="mlever"><span class="auto-tag">auto</span><button class="btn btn-lever">Clear</button></td></tr>
+ <tr><td class="mlamp ok">●</td><td class="mname">journald</td><td class="mval">1.2 GB</td><td class="mlever"><span class="auto-tag">auto</span><button class="btn btn-lever">Vacuum</button></td></tr>
+ <tr><td class="mlamp ok">●</td><td class="mname">app-log cleanup</td><td class="mval">ok</td><td class="mlever"><span class="auto-tag">auto</span><button class="btn btn-lever">Run</button></td></tr>
+ <tr><td class="mlamp ok">●</td><td class="mname">journal errors</td><td class="mval">12 real</td><td class="mlever"><span class="auto-tag">workflow</span>—</td></tr>
+ <tr><td class="mlamp ok">●</td><td class="mname">kernel/hw events</td><td class="mval">clean</td><td class="mlever">—</td></tr>
+ </table>
+ </div>
+
+ <!-- RESOURCES -->
+ <div class="domain">
+ <div class="domain-h">Resources &amp; thermals</div>
+ <table class="metrics">
+ <tr><td class="mlamp ok">●</td><td class="mname">memory</td><td class="mval">104 GB free</td><td class="mlever"><span class="auto-tag">workflow</span>—</td></tr>
+ <tr><td class="mlamp ok">●</td><td class="mname">swap / zram</td><td class="mval">16 GiB</td><td class="mlever">—</td></tr>
+ <tr><td class="mlamp ok">●</td><td class="mname">temps</td><td class="mval">61° / 54°</td><td class="mlever">—</td></tr>
+ <tr><td class="mlamp ok">●</td><td class="mname">thermal throttle</td><td class="mval">no</td><td class="mlever">—</td></tr>
+ <tr><td class="mlamp na">○</td><td class="mname na">battery</td><td class="mval">n/a</td><td class="mlever">—</td></tr>
+ <tr><td class="mlamp warn">●</td><td class="mname">unclean-shutdown</td><td class="mval">75%</td><td class="mlever"><span class="auto-tag">workflow</span>—</td></tr>
+ </table>
+ </div>
+
+ <!-- NETWORK / SECURITY -->
+ <div class="domain">
+ <div class="domain-h">Network &amp; security</div>
+ <table class="metrics">
+ <tr><td class="mlamp ok">●</td><td class="mname">DNS / NM</td><td class="mval">ok</td><td class="mlever"><span class="auto-tag">workflow</span>—</td></tr>
+ <tr><td class="mlamp ok">●</td><td class="mname">firewall</td><td class="mval">active</td><td class="mlever"><span class="auto-tag">confirm</span>—</td></tr>
+ <tr><td class="mlamp ok">●</td><td class="mname">listeners</td><td class="mval">3</td><td class="mlever">—</td></tr>
+ <tr><td class="mlamp ok">●</td><td class="mname">tailscale</td><td class="mval">4 / 4</td><td class="mlever"><span class="auto-tag">confirm</span>—</td></tr>
+ <tr><td class="mlamp ok">●</td><td class="mname">fail2ban</td><td class="mval">active</td><td class="mlever"><span class="auto-tag">confirm</span>—</td></tr>
+ <tr><td class="mlamp ok">●</td><td class="mname">NTP</td><td class="mval">synced</td><td class="mlever"><span class="auto-tag">confirm</span>—</td></tr>
+ </table>
+ </div>
+
+ <!-- SERVICES / BACKUP -->
+ <div class="domain">
+ <div class="domain-h">Backup &amp; services</div>
+ <table class="metrics">
+ <tr><td class="mlamp ok">●</td><td class="mname">rsyncshot</td><td class="mval">3 h ago</td><td class="mlever"><span class="auto-tag">workflow</span>—</td></tr>
+ <tr><td class="mlamp warn">●</td><td class="mname">docker reclaim</td><td class="mval">3 GB</td><td class="mlever"><span class="auto-tag">confirm</span><button class="btn btn-lever">Prune</button></td></tr>
+ <tr><td class="mlamp ok">●</td><td class="mname">docker stopped</td><td class="mval">2</td><td class="mlever">—</td></tr>
+ <tr><td class="mlamp na">○</td><td class="mname na">libvirt</td><td class="mval">off</td><td class="mlever">—</td></tr>
+ <tr><td class="mlamp ok">●</td><td class="mname">cron</td><td class="mval">ok</td><td class="mlever"><span class="auto-tag">confirm</span>—</td></tr>
+ </table>
+ </div>
+
+ </div>
+ </div>
+
+ <!-- ================= UPDATES QUARANTINE ================= -->
+ <div class="updates inset">
+ <div class="sec-h">Updates — quarantined from the doctor</div>
+ <div class="updates-row">
+ <div class="updates-info">
+ <b>47 pending</b> · <span class="cve">3 CVE</span> · <span class="pkgs">mesa, linux-lts, systemd</span>
+ </div>
+ <button class="btn">Run workflow</button>
+ </div>
+ <div class="updates-note">Updates run through the workflow, never the doctor. Clean up will not touch these.</div>
+ </div>
+
+ <div class="stagelabel">— running state · press CLEAN UP and the hero becomes the output wall —</div>
+
+ <!-- ================= OUTPUT WALL (the star) ================= -->
+ <div class="wall" id="wall">
+ <div class="wall-h">
+ <span class="t">✚ DOCTOR · CLEAN UP</span>
+ <span class="meta">4 tasks · unattended · started 14:22:07</span>
+ </div>
+ <div class="wall-sub">Running the 4 safe (auto) tasks. Confirm items and updates are untouched.</div>
+ <div class="prog"><i></i></div>
+
+ <div class="stream">
+ <div class="srow run">
+ <span class="l run">◐</span>
+ <span class="task">cache trim</span>
+ <span class="status">running… 8.8 GB → clearing</span>
+ </div>
+ <div class="srow done">
+ <span class="l ok">●</span>
+ <span class="task">journald vacuum</span>
+ <span class="status">done · <span class="result">reclaimed 0.9 GB</span></span>
+ </div>
+ <div class="srow done">
+ <span class="l ok">●</span>
+ <span class="task">coredump clear</span>
+ <span class="status">done · <span class="result">12 cleared</span></span>
+ </div>
+ <div class="srow queued">
+ <span class="l na">○</span>
+ <span class="task">app-log cleanup</span>
+ <span class="status">queued</span>
+ </div>
+ </div>
+
+ <div class="wall-foot">
+ <span class="summary">3 of 4 complete · <b>reclaimed 0.9 GB</b> so far · ~1.1 GB pending</span>
+ <button class="btn">Done</button>
+ </div>
+ </div>
+
+</div>
diff --git a/docs/prototypes/2026-07-07-maint-console-E-instrument-dashboard.html b/docs/prototypes/2026-07-07-maint-console-E-instrument-dashboard.html
new file mode 100644
index 0000000..52ecfad
--- /dev/null
+++ b/docs/prototypes/2026-07-07-maint-console-E-instrument-dashboard.html
@@ -0,0 +1,475 @@
+<meta charset="utf-8">
+<title>MAINT · ratio — Approach E · Instrument Dashboard</title>
+<style>
+ :root{
+ --page:#0a0c0d; --surface:#100f0f; --inset:#0a0c0d;
+ --gold:#dab53d; --silver:#bfc4d0; --dim:#7c838a; --cream:#f3e7c5;
+ --slate:#424f5e; --slate-hi:#54677d; --line:#2c2f32;
+ --olive:#74932f; --amber:#dab53d; --terra:#cb6b4d;
+ --font:"BerkeleyMono Nerd Font", ui-monospace, monospace;
+ }
+ *{box-sizing:border-box}
+ html,body{margin:0;background:var(--page);}
+ body{
+ font-family:var(--font); color:var(--silver);
+ display:flex; justify-content:center; padding:26px 14px 60px;
+ -webkit-font-smoothing:antialiased;
+ }
+ .console{
+ width:1050px; max-width:100%;
+ background:var(--surface);
+ border:1px solid var(--gold); border-radius:16px;
+ padding:18px;
+ }
+
+ /* ---- header ---- */
+ .head{display:flex; align-items:baseline; justify-content:space-between; gap:16px;}
+ .head .title{font-size:19px; color:var(--cream); letter-spacing:.5px;}
+ .head .title b{color:var(--gold); font-weight:700;}
+ .verdict{font-size:15px; color:var(--olive); font-weight:700; white-space:nowrap;}
+ .sub{color:var(--dim); font-size:12px; margin-top:2px; letter-spacing:.4px;}
+
+ /* ---- top action bar ---- */
+ .actionbar{
+ display:flex; align-items:center; gap:10px;
+ margin:14px 0 16px; padding:10px 12px;
+ background:var(--inset); border:1px solid var(--line); border-radius:10px;
+ }
+ .actionbar .lbl{color:var(--dim); font-size:11.5px; letter-spacing:.5px; text-transform:uppercase; margin-right:2px;}
+ .btn{
+ font-family:var(--font); cursor:pointer;
+ background:var(--slate); color:var(--cream);
+ border:none; border-radius:10px; padding:7px 14px; font-size:13px;
+ }
+ .btn:hover{background:var(--slate-hi);}
+ .btn.lever{padding:3px 9px; font-size:11px; border-radius:8px;}
+ .actionbar .spacer{flex:1;}
+ .legend{color:var(--dim); font-size:11px; line-height:1.5;}
+ .legend b{color:var(--silver); font-weight:400;}
+
+ /* ---- grid ---- */
+ .cluster{display:grid; grid-template-columns:repeat(12,1fr); gap:10px;}
+ .card{
+ background:var(--inset); border:1px solid var(--line); border-radius:10px;
+ padding:11px 12px 12px; position:relative; min-height:96px;
+ display:flex; flex-direction:column;
+ }
+ .card .lever{position:absolute; top:9px; right:9px;}
+ .card .cap{
+ color:var(--dim); font-size:10.5px; letter-spacing:.6px;
+ text-transform:uppercase; margin-bottom:8px; padding-right:56px;
+ }
+ .card .big{color:var(--cream); font-size:23px; line-height:1; font-weight:700;}
+ .card .unit{color:var(--dim); font-size:12px; font-weight:400;}
+ .card .note{color:var(--dim); font-size:11px; margin-top:6px;}
+
+ /* domain band label */
+ .band{
+ grid-column:1/-1; color:var(--gold); font-weight:700; font-size:12px;
+ letter-spacing:1.2px; text-transform:uppercase; margin:6px 2px 0;
+ display:flex; align-items:center; gap:10px;
+ }
+ .band::after{content:""; flex:1; height:1px; background:var(--line);}
+
+ /* span helpers */
+ .s3{grid-column:span 3;} .s4{grid-column:span 4;}
+ .s5{grid-column:span 5;} .s6{grid-column:span 6;}
+ .s7{grid-column:span 7;} .s8{grid-column:span 8;}
+ .s9{grid-column:span 9;} .s12{grid-column:span 12;}
+
+ /* ---- horizontal capacity meter ---- */
+ .meter{margin-top:auto;}
+ .meter .track{
+ position:relative; height:14px; background:#07090a;
+ border:1px solid var(--line); border-radius:7px; overflow:hidden;
+ }
+ .meter .fill{height:100%; border-radius:6px 0 0 6px;}
+ .fill.ok{background:linear-gradient(90deg,#5c7626,#74932f);}
+ .fill.warn{background:linear-gradient(90deg,#a98a2c,#dab53d);}
+ .fill.fail{background:linear-gradient(90deg,#a8543b,#cb6b4d);}
+ .meter .tick{position:absolute; top:-2px; bottom:-2px; width:2px; background:var(--gold);}
+ .meter .zone{position:absolute; top:0; bottom:0; opacity:.16;}
+ .zone.amber{background:var(--amber);} .zone.terra{background:var(--terra);}
+ .meter .scale{display:flex; justify-content:space-between; color:var(--dim); font-size:9.5px; margin-top:4px;}
+
+ /* ---- lamps ---- */
+ .lamp{display:inline-block; width:1em; text-align:center;}
+ .l-ok{color:var(--olive);} .l-warn{color:var(--amber);}
+ .l-fail{color:var(--terra);} .l-off{color:var(--dim);} .l-prog{color:var(--amber);}
+
+ /* ---- lamp board ---- */
+ .lampboard{
+ grid-column:1/-1; background:var(--inset); border:1px solid var(--line);
+ border-radius:10px; padding:12px 14px;
+ display:grid; grid-template-columns:repeat(4,1fr); gap:7px 18px;
+ }
+ .lampboard .row{display:flex; align-items:center; gap:8px; font-size:12px;}
+ .lampboard .row .nm{color:var(--silver);}
+ .lampboard .row .vv{color:var(--dim); margin-left:auto; font-size:11px;}
+
+ /* ---- counter tile ---- */
+ .tile .row{display:flex; align-items:center; gap:8px;}
+ .tile .big{font-size:26px;}
+
+ /* ---- updates quarantine ---- */
+ .updates{
+ grid-column:1/-1; border:1px solid var(--amber);
+ background:linear-gradient(180deg,#141210,#0a0c0d);
+ border-radius:10px; padding:13px 16px;
+ display:flex; align-items:center; gap:26px; flex-wrap:wrap;
+ }
+ .updates .q{
+ color:var(--amber); font-size:10.5px; letter-spacing:1.5px;
+ text-transform:uppercase; border:1px solid var(--amber);
+ padding:3px 8px; border-radius:6px; align-self:flex-start;
+ }
+ .updates .metric{display:flex; flex-direction:column; gap:2px;}
+ .updates .metric .n{font-size:30px; color:var(--cream); font-weight:700; line-height:1;}
+ .updates .metric .n.cve{color:var(--terra);}
+ .updates .metric .k{color:var(--dim); font-size:10.5px; text-transform:uppercase; letter-spacing:.6px;}
+ .updates .spacer{flex:1;}
+
+ /* ---- svg arc/dial labels ---- */
+ .gaugewrap{display:flex; gap:14px; margin-top:2px;}
+ .gauge{display:flex; flex-direction:column; align-items:center; gap:3px;}
+ .gauge .rd{color:var(--cream); font-size:13px; font-weight:700;}
+ .gauge .lb{color:var(--dim); font-size:10px;}
+ .dialwrap{display:flex; justify-content:center; margin-top:2px;}
+
+ /* ---- output wall ---- */
+ .wall{
+ margin-top:16px; background:var(--inset);
+ border:1px solid var(--line); border-radius:10px; padding:13px 15px;
+ }
+ .wall .wh{
+ display:flex; align-items:center; justify-content:space-between;
+ color:var(--cream); font-size:13px; letter-spacing:.5px; margin-bottom:10px;
+ }
+ .wall .wh .l{color:var(--gold);}
+ .wall .stream{font-size:12.5px; line-height:1.85;}
+ .wall .stream .ln{display:flex; gap:9px; align-items:baseline;}
+ .wall .stream .msg{color:var(--silver);}
+ .wall .stream .meta{color:var(--dim);}
+ .wall .stream .queued .msg{color:var(--dim);}
+ .wall .wfoot{margin-top:11px; display:flex; justify-content:flex-end;}
+
+ @keyframes pulse{0%,100%{opacity:1}50%{opacity:.35}}
+ .spin{animation:pulse 1.1s ease-in-out infinite;}
+</style>
+
+<div class="console">
+
+ <!-- HEADER -->
+ <div class="head">
+ <div>
+ <div class="title">MAINT · <b>ratio</b></div>
+ <div class="sub">Approach E — Instrument dashboard</div>
+ </div>
+ <div class="verdict"><span class="lamp l-ok">●</span> healthy</div>
+ </div>
+
+ <!-- ACTION BAR -->
+ <div class="actionbar">
+ <span class="lbl">Doctor</span>
+ <button class="btn">Clean up</button>
+ <button class="btn">Review &amp; fix</button>
+ <span class="spacer"></span>
+ <span class="legend">
+ <b>Auto</b> unattended · <b>Confirm</b> preview+click · <b>Human</b> you act · <b>Workflow</b> escalate · <b>None</b> diagnostic
+ </span>
+ </div>
+
+ <!-- CLUSTER -->
+ <div class="cluster">
+
+ <!-- STORAGE & FILESYSTEM -->
+ <div class="band">Storage &amp; Filesystem</div>
+
+ <div class="card s4">
+ <div class="cap">Disk usage</div>
+ <div class="big">69<span class="unit">%</span></div>
+ <div class="note">root · btrfs</div>
+ <div class="meter">
+ <div class="track">
+ <div class="zone amber" style="left:80%;right:10%"></div>
+ <div class="zone terra" style="left:90%;right:0"></div>
+ <div class="fill ok" style="width:69%"></div>
+ </div>
+ <div class="scale"><span>0</span><span>80</span><span>90</span><span>100%</span></div>
+ </div>
+ </div>
+
+ <div class="card s4">
+ <button class="btn lever">Clean</button>
+ <div class="cap">Package cache <span class="lamp l-warn">●</span></div>
+ <div class="big">8.8<span class="unit"> GB</span></div>
+ <div class="note">Auto · threshold ~10 GB</div>
+ <div class="meter">
+ <div class="track">
+ <div class="tick" style="left:88%"></div>
+ <div class="fill warn" style="width:88%"></div>
+ </div>
+ <div class="scale"><span>0</span><span>threshold 10 GB</span></div>
+ </div>
+ </div>
+
+ <div class="card s4">
+ <button class="btn lever">Deep trim</button>
+ <div class="cap">Deep-trim keep-3 <span class="lamp l-ok">●</span></div>
+ <div class="big">keep 3</div>
+ <div class="note">Confirm · retain 3 versions</div>
+ <div class="meter">
+ <div class="track"><div class="fill ok" style="width:100%"></div></div>
+ <div class="scale"><span>staged &amp; ready</span></div>
+ </div>
+ </div>
+
+ <div class="card s3">
+ <button class="btn lever">Scrub</button>
+ <div class="cap">btrfs scrub</div>
+ <div class="dialwrap">
+ <svg width="98" height="98" viewBox="0 0 98 98">
+ <circle cx="49" cy="49" r="40" fill="none" stroke="#1a1d1f" stroke-width="8"/>
+ <!-- 34d of ~30d cadence => over => amber, ~0.62 of ring -->
+ <circle cx="49" cy="49" r="40" fill="none" stroke="#dab53d" stroke-width="8"
+ stroke-linecap="round" stroke-dasharray="251.3" stroke-dashoffset="95"
+ transform="rotate(-90 49 49)"/>
+ <text x="49" y="46" text-anchor="middle" fill="#f3e7c5" font-family="monospace" font-size="20" font-weight="700">34</text>
+ <text x="49" y="63" text-anchor="middle" fill="#7c838a" font-family="monospace" font-size="10">days</text>
+ </svg>
+ </div>
+ <div class="note" style="text-align:center"><span class="lamp l-warn">●</span> past 30d cadence</div>
+ </div>
+
+ <div class="card s3">
+ <div class="cap">btrfs unalloc <span class="lamp l-ok">●</span></div>
+ <div class="big">118<span class="unit"> GiB</span></div>
+ <div class="note">unallocated headroom</div>
+ <div class="meter">
+ <div class="track"><div class="fill ok" style="width:46%"></div></div>
+ <div class="scale"><span>ample slack</span></div>
+ </div>
+ </div>
+
+ <div class="card s3">
+ <div class="cap">btrfs device-err <span class="lamp l-ok">●</span></div>
+ <div class="big">0 / 0</div>
+ <div class="note">read / write errors</div>
+ </div>
+
+ <div class="card s3">
+ <button class="btn lever">Enable</button>
+ <div class="cap">fstrim.timer <span class="lamp l-fail">●</span></div>
+ <div class="big" style="color:var(--terra)">OFF</div>
+ <div class="note">Confirm · SSD discard disabled</div>
+ </div>
+
+ <!-- THERMAL & POWER -->
+ <div class="band">Thermal &amp; Power</div>
+
+ <div class="card s5">
+ <div class="cap">Temperatures <span class="lamp l-ok">●</span></div>
+ <div class="gaugewrap">
+ <div class="gauge">
+ <svg width="112" height="64" viewBox="0 0 112 64">
+ <path d="M8 60 A48 48 0 0 1 104 60" fill="none" stroke="#1a1d1f" stroke-width="9"/>
+ <path d="M8 60 A48 48 0 0 1 79 21" fill="none" stroke="#74932f" stroke-width="9" stroke-linecap="round"/>
+ <path d="M79 21 A48 48 0 0 1 96 34" fill="none" stroke="#dab53d" stroke-width="9"/>
+ <path d="M96 34 A48 48 0 0 1 104 60" fill="none" stroke="#cb6b4d" stroke-width="9"/>
+ <!-- needle @ 61C (~61% of arc) -->
+ <line x1="56" y1="60" x2="86" y2="30" stroke="#f3e7c5" stroke-width="2.5"/>
+ <circle cx="56" cy="60" r="4" fill="#dab53d"/>
+ </svg>
+ <div class="rd">61°C</div><div class="lb">CPU package</div>
+ </div>
+ <div class="gauge">
+ <svg width="112" height="64" viewBox="0 0 112 64">
+ <path d="M8 60 A48 48 0 0 1 104 60" fill="none" stroke="#1a1d1f" stroke-width="9"/>
+ <path d="M8 60 A48 48 0 0 1 79 21" fill="none" stroke="#74932f" stroke-width="9" stroke-linecap="round"/>
+ <path d="M79 21 A48 48 0 0 1 96 34" fill="none" stroke="#dab53d" stroke-width="9"/>
+ <path d="M96 34 A48 48 0 0 1 104 60" fill="none" stroke="#cb6b4d" stroke-width="9"/>
+ <!-- needle @ 54C (~54%) -->
+ <line x1="56" y1="60" x2="79" y2="34" stroke="#f3e7c5" stroke-width="2.5"/>
+ <circle cx="56" cy="60" r="4" fill="#dab53d"/>
+ </svg>
+ <div class="rd">54°C</div><div class="lb">NVMe</div>
+ </div>
+ </div>
+ </div>
+
+ <div class="card s3">
+ <div class="cap">Thermal throttle <span class="lamp l-ok">●</span></div>
+ <div class="big">NO</div>
+ <div class="note">no throttle events</div>
+ </div>
+
+ <div class="card s2 s3" style="grid-column:span 2;">
+ <div class="cap">Battery <span class="lamp l-off">○</span></div>
+ <div class="big" style="color:var(--dim)">n/a</div>
+ <div class="note">desktop</div>
+ </div>
+
+ <div class="card s2 s3" style="grid-column:span 2;">
+ <div class="cap">Swap / zram <span class="lamp l-ok">●</span></div>
+ <div class="big">16<span class="unit"> GiB</span></div>
+ <div class="note">zram active</div>
+ </div>
+
+ <!-- MEMORY -->
+ <div class="card s6">
+ <div class="cap">Memory free <span class="lamp l-ok">●</span></div>
+ <div class="big">104<span class="unit"> GB free</span></div>
+ <div class="note">of 125 GB total · Workflow</div>
+ <div class="meter">
+ <div class="track"><div class="fill ok" style="width:83%"></div></div>
+ <div class="scale"><span>0</span><span>free 104 / 125 GB</span></div>
+ </div>
+ </div>
+
+ <div class="card s6">
+ <div class="cap">Unclean-shutdown ratio <span class="lamp l-warn">●</span></div>
+ <div class="big" style="color:var(--amber)">75<span class="unit">%</span></div>
+ <div class="note">clean boots · Workflow — investigate</div>
+ <div class="meter">
+ <div class="track">
+ <div class="zone amber" style="left:0;right:0"></div>
+ <div class="fill warn" style="width:75%"></div>
+ </div>
+ <div class="scale"><span>0</span><span>clean-boot fraction</span><span>100%</span></div>
+ </div>
+ </div>
+
+ <!-- UPDATES QUARANTINE -->
+ <div class="band">Updates — quarantined</div>
+ <div class="updates">
+ <div class="q">Updates</div>
+ <div class="metric"><span class="n">47</span><span class="k">pending</span></div>
+ <div class="metric"><span class="n cve">3</span><span class="k">CVE · arch-audit</span></div>
+ <div class="metric"><span class="n">5</span><span class="k">AUR stale</span></div>
+ <div class="spacer"></div>
+ <div class="note" style="max-width:230px; margin:0;">
+ <span class="lamp l-fail">●</span> Security advisories present.
+ Applying updates escalates to the update workflow — not an unattended clean.
+ </div>
+ <button class="btn">Open updates</button>
+ </div>
+
+ <!-- PACKAGES & JOURNAL COUNTERS -->
+ <div class="band">Packages · Logs · Counters</div>
+
+ <div class="card s3 tile">
+ <button class="btn lever">Review</button>
+ <div class="cap">Orphans</div>
+ <div class="row"><span class="lamp l-warn">●</span><span class="big">13</span></div>
+ <div class="note">Confirm · unowned pkgs</div>
+ </div>
+
+ <div class="card s3 tile">
+ <button class="btn lever">Review</button>
+ <div class="cap">.pacnew files</div>
+ <div class="row"><span class="lamp l-warn">●</span><span class="big">2</span></div>
+ <div class="note">Confirm · config merges</div>
+ </div>
+
+ <div class="card s3 tile">
+ <button class="btn lever">Clear</button>
+ <div class="cap">Coredumps 7d</div>
+ <div class="row"><span class="lamp l-warn">●</span><span class="big">18</span></div>
+ <div class="note">Auto · clearable</div>
+ </div>
+
+ <div class="card s3 tile">
+ <button class="btn lever">Run workflow</button>
+ <div class="cap">Failed units</div>
+ <div class="row"><span class="lamp l-warn">●</span><span class="big">1</span></div>
+ <div class="note">Workflow · systemd</div>
+ </div>
+
+ <div class="card s3">
+ <button class="btn lever">Prune</button>
+ <div class="cap">Snapper snapshots <span class="lamp l-ok">●</span></div>
+ <div class="big">42</div>
+ <div class="note">Confirm · retention ok</div>
+ <div class="meter">
+ <div class="track"><div class="fill ok" style="width:52%"></div></div>
+ <div class="scale"><span>within budget</span></div>
+ </div>
+ </div>
+
+ <div class="card s3">
+ <button class="btn lever">Refresh</button>
+ <div class="cap">Keyring age</div>
+ <div class="dialwrap">
+ <svg width="90" height="90" viewBox="0 0 90 90">
+ <circle cx="45" cy="45" r="36" fill="none" stroke="#1a1d1f" stroke-width="7"/>
+ <circle cx="45" cy="45" r="36" fill="none" stroke="#74932f" stroke-width="7"
+ stroke-linecap="round" stroke-dasharray="226.2" stroke-dashoffset="176"
+ transform="rotate(-90 45 45)"/>
+ <text x="45" y="43" text-anchor="middle" fill="#f3e7c5" font-family="monospace" font-size="18" font-weight="700">12</text>
+ <text x="45" y="59" text-anchor="middle" fill="#7c838a" font-family="monospace" font-size="9">days</text>
+ </svg>
+ </div>
+ <div class="note" style="text-align:center"><span class="lamp l-ok">●</span> Confirm · fresh</div>
+ </div>
+
+ <div class="card s3 tile">
+ <div class="cap">Journal errors</div>
+ <div class="row"><span class="lamp l-ok">●</span><span class="big">12</span></div>
+ <div class="note">Workflow · real errors</div>
+ </div>
+
+ <div class="card s3 tile">
+ <div class="cap">Listeners</div>
+ <div class="row"><span class="lamp l-ok">●</span><span class="big">3</span></div>
+ <div class="note">None · open sockets</div>
+ </div>
+
+ <!-- LAMP BOARD -->
+ <div class="band">Status board — binary &amp; diagnostic</div>
+ <div class="lampboard">
+ <div class="row"><span class="lamp l-ok">●</span><span class="nm">SMART</span><span class="vv">PASSED</span></div>
+ <div class="row"><span class="lamp l-ok">●</span><span class="nm">SMART wear</span><span class="vv">0%</span></div>
+ <div class="row"><span class="lamp l-ok">●</span><span class="nm">pacman integrity</span><span class="vv">clean</span></div>
+ <div class="row"><span class="lamp l-ok">●</span><span class="nm">snapshot timer</span><span class="vv">active</span></div>
+
+ <div class="row"><span class="lamp l-ok">●</span><span class="nm">is-system-running</span><span class="vv">running</span></div>
+ <div class="row"><span class="lamp l-ok">●</span><span class="nm">reboot-required</span><span class="vv">no</span></div>
+ <div class="row"><span class="lamp l-ok">●</span><span class="nm">kernel taint</span><span class="vv">0</span></div>
+ <div class="row"><span class="lamp l-ok">●</span><span class="nm">kernel/hw events</span><span class="vv">clean</span></div>
+
+ <div class="row"><span class="lamp l-ok">●</span><span class="nm">firewall</span><span class="vv">active</span></div>
+ <div class="row"><span class="lamp l-ok">●</span><span class="nm">fail2ban</span><span class="vv">active</span></div>
+ <div class="row"><span class="lamp l-ok">●</span><span class="nm">tailscale</span><span class="vv">4/4</span></div>
+ <div class="row"><span class="lamp l-ok">●</span><span class="nm">NTP</span><span class="vv">synced</span></div>
+
+ <div class="row"><span class="lamp l-ok">●</span><span class="nm">DNS / NM</span><span class="vv">ok</span></div>
+ <div class="row"><span class="lamp l-ok">●</span><span class="nm">rsyncshot</span><span class="vv">3h ago</span></div>
+ <div class="row"><span class="lamp l-ok">●</span><span class="nm">cron</span><span class="vv">ok</span></div>
+ <div class="row"><span class="lamp l-ok">●</span><span class="nm">app-log cleanup</span><span class="vv">ok</span></div>
+
+ <div class="row"><span class="lamp l-ok">●</span><span class="nm">journald size</span><span class="vv">1.2 GB</span></div>
+ <div class="row"><span class="lamp l-ok">●</span><span class="nm">docker stopped</span><span class="vv">2</span></div>
+ <div class="row"><span class="lamp l-warn">●</span><span class="nm">maintenance timers</span><span class="vv">4/5</span></div>
+ <div class="row"><span class="lamp l-warn">●</span><span class="nm">docker reclaim</span><span class="vv">3 GB · Prune</span></div>
+
+ <div class="row"><span class="lamp l-off">○</span><span class="nm">libvirt</span><span class="vv">off</span></div>
+ </div>
+
+ </div><!-- /cluster -->
+
+ <!-- RUNNING / OUTPUT WALL -->
+ <div class="wall">
+ <div class="wh">
+ <span>MAINT · <span class="l">ratio</span> · CLEANING <span class="lamp l-prog spin">◐</span></span>
+ <span class="meta" style="font-size:11px">4 Auto actions</span>
+ </div>
+ <div class="stream">
+ <div class="ln"><span class="lamp l-prog spin">◐</span><span class="msg">cache trim</span><span class="meta">running…</span></div>
+ <div class="ln"><span class="lamp l-ok">●</span><span class="msg">journal vacuum</span><span class="meta">done · reclaimed 0.9 GB</span></div>
+ <div class="ln"><span class="lamp l-ok">●</span><span class="msg">coredump clear</span><span class="meta">done · 12 cleared</span></div>
+ <div class="ln queued"><span class="lamp l-off">○</span><span class="msg">app-log cleanup</span><span class="meta">queued</span></div>
+ </div>
+ <div class="wfoot"><button class="btn">Done</button></div>
+ </div>
+
+</div>
diff --git a/docs/prototypes/2026-07-07-maint-console-E2-grouped-dashboard.html b/docs/prototypes/2026-07-07-maint-console-E2-grouped-dashboard.html
new file mode 100644
index 0000000..b818006
--- /dev/null
+++ b/docs/prototypes/2026-07-07-maint-console-E2-grouped-dashboard.html
@@ -0,0 +1,443 @@
+<meta charset="utf-8">
+<title>MAINT · ratio — Approach E2 · Grouped Instrument Dashboard</title>
+<style>
+ :root{
+ --page:#0a0c0d; --surface:#100f0f; --inset:#0a0c0d;
+ --gold:#dab53d; --silver:#bfc4d0; --dim:#7c838a; --cream:#f3e7c5;
+ --slate:#424f5e; --slate-hi:#54677d; --line:#2c2f32;
+ --olive:#74932f; --amber:#dab53d; --terra:#cb6b4d;
+ --font:"BerkeleyMono Nerd Font", ui-monospace, monospace;
+ }
+ *{box-sizing:border-box}
+ html,body{margin:0;background:var(--page);}
+ body{
+ font-family:var(--font); color:var(--silver);
+ display:flex; justify-content:center; padding:26px 14px 60px;
+ -webkit-font-smoothing:antialiased;
+ }
+ .console{ width:1050px; max-width:100%; background:var(--surface);
+ border:1px solid var(--gold); border-radius:16px; padding:18px; }
+
+ /* header */
+ .head{display:flex; align-items:baseline; justify-content:space-between; gap:16px;}
+ .head .title{font-size:19px; color:var(--cream); letter-spacing:.5px;}
+ .head .title b{color:var(--gold); font-weight:700;}
+ .verdict{font-size:15px; color:var(--olive); font-weight:700; white-space:nowrap;}
+ .sub{color:var(--dim); font-size:12px; margin-top:2px; letter-spacing:.4px;}
+
+ /* action bar */
+ .actionbar{display:flex; align-items:center; gap:10px; margin:14px 0 14px;
+ padding:10px 12px; background:var(--inset); border:1px solid var(--line); border-radius:10px;}
+ .actionbar .lbl{color:var(--dim); font-size:11.5px; letter-spacing:.5px; text-transform:uppercase; margin-right:2px;}
+ .btn{font-family:var(--font); cursor:pointer; background:var(--slate); color:var(--cream);
+ border:none; border-radius:10px; padding:7px 14px; font-size:13px;}
+ .btn:hover{background:var(--slate-hi);}
+ .btn.lever{padding:3px 9px; font-size:11px; border-radius:8px;}
+ .btn.mini{padding:2px 8px; font-size:10.5px; border-radius:7px;}
+ .actionbar .spacer{flex:1;}
+ .legend{color:var(--dim); font-size:11px; line-height:1.5;}
+ .legend b{color:var(--silver); font-weight:400;}
+
+ /* updates quarantine */
+ .updates{border:1px solid var(--amber);
+ background:linear-gradient(180deg,#141210,#0a0c0d);
+ border-radius:10px; padding:12px 16px; margin-bottom:14px;
+ display:flex; align-items:center; gap:26px; flex-wrap:wrap;}
+ .updates .q{color:var(--amber); font-size:10.5px; letter-spacing:1.5px; text-transform:uppercase;
+ border:1px solid var(--amber); padding:3px 8px; border-radius:6px; align-self:flex-start;}
+ .updates .metric{display:flex; flex-direction:column; gap:2px;}
+ .updates .metric .n{font-size:28px; color:var(--cream); font-weight:700; line-height:1;}
+ .updates .metric .n.cve{color:var(--terra);}
+ .updates .metric .k{color:var(--dim); font-size:10.5px; text-transform:uppercase; letter-spacing:.6px;}
+ .updates .spacer{flex:1;}
+
+ /* ---- category panel ---- */
+ .panel{border:1px solid var(--line); border-left:3px solid var(--line);
+ border-radius:11px; margin-bottom:10px; overflow:hidden; background:#0d0c0c;}
+ .panel.attn{border-left-color:var(--amber);}
+ .panel.crit{border-left-color:var(--terra);}
+ .panel.clear{border-left-color:var(--olive); opacity:.85;}
+ .phead{display:flex; align-items:center; gap:11px; padding:9px 14px; cursor:pointer;}
+ .phead:hover{background:#141313;}
+ .phead .nm{color:var(--gold); font-weight:700; letter-spacing:.9px; text-transform:uppercase; font-size:12.5px;}
+ .phead .cnt{color:var(--dim); font-size:11px; margin-left:auto;}
+ .phead .chev{color:var(--dim); font-size:12px; width:1em; text-align:center;}
+ .phead .attnword{color:var(--amber); font-size:11px;}
+ .phead .attnword.crit{color:var(--terra);}
+ .phead .okword{color:var(--olive); font-size:11px;}
+ .pbody{padding:2px 14px 13px;}
+
+ .heroes{display:grid; grid-template-columns:repeat(12,1fr); gap:10px; margin-bottom:11px;}
+ .card{background:var(--inset); border:1px solid var(--line); border-radius:10px;
+ padding:10px 12px 11px; position:relative; min-height:88px; display:flex; flex-direction:column;}
+ .card .lever{position:absolute; top:8px; right:8px;}
+ .card .cap{color:var(--dim); font-size:10.5px; letter-spacing:.6px; text-transform:uppercase;
+ margin-bottom:7px; padding-right:52px;}
+ .card .big{color:var(--cream); font-size:22px; line-height:1; font-weight:700;}
+ .card .unit{color:var(--dim); font-size:12px; font-weight:400;}
+ .card .note{color:var(--dim); font-size:11px; margin-top:6px;}
+ .s2{grid-column:span 2;} .s3{grid-column:span 3;} .s4{grid-column:span 4;}
+ .s5{grid-column:span 5;} .s6{grid-column:span 6;}
+
+ /* meter */
+ .meter{margin-top:auto;}
+ .meter .track{position:relative; height:13px; background:#07090a; border:1px solid var(--line);
+ border-radius:7px; overflow:hidden;}
+ .meter .fill{height:100%; border-radius:6px 0 0 6px;}
+ .fill.ok{background:linear-gradient(90deg,#5c7626,#74932f);}
+ .fill.warn{background:linear-gradient(90deg,#a98a2c,#dab53d);}
+ .fill.fail{background:linear-gradient(90deg,#a8543b,#cb6b4d);}
+ .meter .tick{position:absolute; top:-2px; bottom:-2px; width:2px; background:var(--gold);}
+ .meter .zone{position:absolute; top:0; bottom:0; opacity:.16;}
+ .zone.amber{background:var(--amber);} .zone.terra{background:var(--terra);}
+ .meter .scale{display:flex; justify-content:space-between; color:var(--dim); font-size:9.5px; margin-top:4px;}
+
+ /* lamps */
+ .lamp{display:inline-block; width:1em; text-align:center;}
+ .l-ok{color:var(--olive);} .l-warn{color:var(--amber);}
+ .l-fail{color:var(--terra);} .l-off{color:var(--dim);} .l-prog{color:var(--amber);}
+
+ .tile .row{display:flex; align-items:center; gap:8px;}
+ .tile .big{font-size:25px;}
+
+ .gaugewrap{display:flex; gap:14px; margin-top:2px;}
+ .gauge{display:flex; flex-direction:column; align-items:center; gap:3px;}
+ .gauge .rd{color:var(--cream); font-size:13px; font-weight:700;}
+ .gauge .lb{color:var(--dim); font-size:10px;}
+ .dialwrap{display:flex; justify-content:center; margin-top:2px;}
+
+ /* compact strip (the boring/green/binary metrics) */
+ .strip{display:grid; grid-template-columns:repeat(3,1fr); gap:5px 22px;
+ padding-top:9px; border-top:1px solid var(--line);}
+ .strip .row{display:flex; align-items:center; gap:8px; font-size:12px; min-height:22px;}
+ .strip .row .nm2{color:var(--silver);}
+ .strip .row .vv{color:var(--dim); margin-left:auto; font-size:11px;}
+ .strip .row .lever{margin-left:auto;}
+ .strip .row .lever + .vv{margin-left:8px;}
+
+ /* output wall */
+ .wall{margin-top:16px; background:var(--inset); border:1px solid var(--line);
+ border-radius:10px; padding:13px 15px;}
+ .wall .wh{display:flex; align-items:center; justify-content:space-between;
+ color:var(--cream); font-size:13px; letter-spacing:.5px; margin-bottom:10px;}
+ .wall .wh .l{color:var(--gold);}
+ .wall .stream{font-size:12.5px; line-height:1.85;}
+ .wall .stream .ln{display:flex; gap:9px; align-items:baseline;}
+ .wall .stream .msg{color:var(--silver);}
+ .wall .stream .meta{color:var(--dim);}
+ .wall .stream .queued .msg{color:var(--dim);}
+ .wall .wfoot{margin-top:11px; display:flex; justify-content:flex-end;}
+
+ .footnote{color:var(--dim); font-size:11px; margin-top:14px; line-height:1.6;}
+ @keyframes pulse{0%,100%{opacity:1}50%{opacity:.35}}
+ .spin{animation:pulse 1.1s ease-in-out infinite;}
+</style>
+
+<div class="console">
+
+ <!-- HEADER -->
+ <div class="head">
+ <div>
+ <div class="title">MAINT · <b>ratio</b></div>
+ <div class="sub">Approach E2 — Grouped instrument dashboard (categories collapse when clean)</div>
+ </div>
+ <div class="verdict"><span class="lamp l-ok">●</span> healthy</div>
+ </div>
+
+ <!-- ACTION BAR -->
+ <div class="actionbar">
+ <span class="lbl">Doctor</span>
+ <button class="btn">Clean up</button>
+ <button class="btn">Review &amp; fix</button>
+ <span class="spacer"></span>
+ <span class="legend">
+ <b>Auto</b> unattended · <b>Confirm</b> preview+click · <b>Human</b> you act · <b>Workflow</b> escalate · <b>None</b> diagnostic
+ </span>
+ </div>
+
+ <!-- UPDATES (pulled up: the 3 CVEs are the one real fail) -->
+ <div class="updates">
+ <div class="q">Updates</div>
+ <div class="metric"><span class="n">47</span><span class="k">pending</span></div>
+ <div class="metric"><span class="n cve">3</span><span class="k">CVE · arch-audit</span></div>
+ <div class="metric"><span class="n">5</span><span class="k">AUR stale</span></div>
+ <div class="spacer"></div>
+ <div class="note" style="max-width:250px; margin:0; color:var(--dim); font-size:11px;">
+ <span class="lamp l-fail">●</span> Security advisories present. Updates run through the workflow — never an unattended clean.
+ </div>
+ <button class="btn">Run workflow</button>
+ </div>
+
+ <!-- ============ CATEGORY PANELS (severity-ordered) ============ -->
+
+ <!-- STORAGE (has a fail: fstrim) -->
+ <div class="panel crit">
+ <div class="phead">
+ <span class="chev">▾</span><span class="lamp l-fail">●</span>
+ <span class="nm">Storage &amp; Filesystem</span>
+ <span class="cnt"><span class="attnword crit">3 need attention</span> · 6 ok</span>
+ </div>
+ <div class="pbody">
+ <div class="heroes">
+ <div class="card s3">
+ <div class="cap">Disk usage</div>
+ <div class="big">69<span class="unit">%</span></div>
+ <div class="note">root · btrfs</div>
+ <div class="meter"><div class="track">
+ <div class="zone amber" style="left:80%;right:10%"></div>
+ <div class="zone terra" style="left:90%;right:0"></div>
+ <div class="fill ok" style="width:69%"></div></div>
+ <div class="scale"><span>0</span><span>80</span><span>90</span><span>100</span></div>
+ </div>
+ </div>
+ <div class="card s3">
+ <button class="btn lever">Clean</button>
+ <div class="cap">Cache <span class="lamp l-warn">●</span></div>
+ <div class="big">8.8<span class="unit"> GB</span></div>
+ <div class="note">Auto · thr ~10 GB</div>
+ <div class="meter"><div class="track"><div class="tick" style="left:88%"></div>
+ <div class="fill warn" style="width:88%"></div></div>
+ <div class="scale"><span>0</span><span>10 GB</span></div>
+ </div>
+ </div>
+ <div class="card s3">
+ <button class="btn lever">Scrub</button>
+ <div class="cap">btrfs scrub</div>
+ <div class="dialwrap">
+ <svg width="86" height="86" viewBox="0 0 98 98">
+ <circle cx="49" cy="49" r="40" fill="none" stroke="#1a1d1f" stroke-width="8"/>
+ <circle cx="49" cy="49" r="40" fill="none" stroke="#dab53d" stroke-width="8"
+ stroke-linecap="round" stroke-dasharray="251.3" stroke-dashoffset="95" transform="rotate(-90 49 49)"/>
+ <text x="49" y="46" text-anchor="middle" fill="#f3e7c5" font-family="monospace" font-size="20" font-weight="700">34</text>
+ <text x="49" y="63" text-anchor="middle" fill="#7c838a" font-family="monospace" font-size="10">days</text>
+ </svg>
+ </div>
+ <div class="note" style="text-align:center"><span class="lamp l-warn">●</span> past 30d</div>
+ </div>
+ <div class="card s3">
+ <button class="btn lever">Enable</button>
+ <div class="cap">fstrim.timer <span class="lamp l-fail">●</span></div>
+ <div class="big" style="color:var(--terra)">OFF</div>
+ <div class="note">Confirm · SSD discard off</div>
+ </div>
+ </div>
+ <div class="strip">
+ <div class="row"><span class="lamp l-ok">●</span><span class="nm2">SMART</span><span class="vv">PASSED</span></div>
+ <div class="row"><span class="lamp l-ok">●</span><span class="nm2">SMART wear</span><span class="vv">0%</span></div>
+ <div class="row"><span class="lamp l-ok">●</span><span class="nm2">device errors</span><span class="vv">0 / 0</span></div>
+ <div class="row"><span class="lamp l-ok">●</span><span class="nm2">btrfs unalloc</span><span class="vv">118 GiB</span></div>
+ <div class="row"><span class="lamp l-ok">●</span><span class="nm2">pacman integrity</span><span class="vv">clean</span></div>
+ <div class="row"><span class="lamp l-ok">●</span><span class="nm2">deep-trim</span><button class="btn mini lever" style="position:static">Deep trim</button><span class="vv">keep 3</span></div>
+ </div>
+ </div>
+ </div>
+
+ <!-- PACKAGES & SECURITY -->
+ <div class="panel attn">
+ <div class="phead">
+ <span class="chev">▾</span><span class="lamp l-warn">●</span>
+ <span class="nm">Packages &amp; Security</span>
+ <span class="cnt"><span class="attnword">2 need attention</span> · 2 ok · updates above</span>
+ </div>
+ <div class="pbody">
+ <div class="heroes">
+ <div class="card s3 tile">
+ <button class="btn lever">Review</button>
+ <div class="cap">Orphans</div>
+ <div class="row"><span class="lamp l-warn">●</span><span class="big">13</span></div>
+ <div class="note">Confirm · unowned pkgs</div>
+ </div>
+ <div class="card s3 tile">
+ <button class="btn lever">Review</button>
+ <div class="cap">.pacnew files</div>
+ <div class="row"><span class="lamp l-warn">●</span><span class="big">2</span></div>
+ <div class="note">Confirm · config merges</div>
+ </div>
+ </div>
+ <div class="strip">
+ <div class="row"><span class="lamp l-ok">●</span><span class="nm2">keyring age</span><button class="btn mini lever" style="position:static">Refresh</button><span class="vv">12d</span></div>
+ <div class="row"><span class="lamp l-ok">●</span><span class="nm2">reboot required</span><span class="vv">no</span></div>
+ </div>
+ </div>
+ </div>
+
+ <!-- SYSTEMD & BOOT -->
+ <div class="panel attn">
+ <div class="phead">
+ <span class="chev">▾</span><span class="lamp l-warn">●</span>
+ <span class="nm">systemd &amp; Boot</span>
+ <span class="cnt"><span class="attnword">2 need attention</span> · 2 ok</span>
+ </div>
+ <div class="pbody">
+ <div class="heroes">
+ <div class="card s3 tile">
+ <button class="btn lever">Run workflow</button>
+ <div class="cap">Failed units</div>
+ <div class="row"><span class="lamp l-warn">●</span><span class="big">1</span></div>
+ <div class="note">Workflow · systemd</div>
+ </div>
+ <div class="card s3 tile">
+ <button class="btn lever">Enable</button>
+ <div class="cap">Maintenance timers</div>
+ <div class="row"><span class="lamp l-warn">●</span><span class="big">4/5</span></div>
+ <div class="note">Confirm · one not firing</div>
+ </div>
+ </div>
+ <div class="strip">
+ <div class="row"><span class="lamp l-ok">●</span><span class="nm2">is-system-running</span><span class="vv">running</span></div>
+ <div class="row"><span class="lamp l-ok">●</span><span class="nm2">kernel taint</span><span class="vv">0</span></div>
+ </div>
+ </div>
+ </div>
+
+ <!-- LOGS & COREDUMPS -->
+ <div class="panel attn">
+ <div class="phead">
+ <span class="chev">▾</span><span class="lamp l-warn">●</span>
+ <span class="nm">Logs &amp; Coredumps</span>
+ <span class="cnt"><span class="attnword">1 needs attention</span> · 4 ok</span>
+ </div>
+ <div class="pbody">
+ <div class="heroes">
+ <div class="card s3 tile">
+ <button class="btn lever">Clear</button>
+ <div class="cap">Coredumps 7d</div>
+ <div class="row"><span class="lamp l-warn">●</span><span class="big">18</span></div>
+ <div class="note">Auto · clearable</div>
+ </div>
+ </div>
+ <div class="strip">
+ <div class="row"><span class="lamp l-ok">●</span><span class="nm2">journald size</span><button class="btn mini lever" style="position:static">Vacuum</button><span class="vv">1.2 GB</span></div>
+ <div class="row"><span class="lamp l-ok">●</span><span class="nm2">app-log cleanup</span><button class="btn mini lever" style="position:static">Run</button><span class="vv">ok</span></div>
+ <div class="row"><span class="lamp l-ok">●</span><span class="nm2">journal errors</span><span class="vv">12 real</span></div>
+ <div class="row"><span class="lamp l-ok">●</span><span class="nm2">kernel/hw events</span><span class="vv">clean</span></div>
+ </div>
+ </div>
+ </div>
+
+ <!-- MEMORY / THERMAL / POWER -->
+ <div class="panel attn">
+ <div class="phead">
+ <span class="chev">▾</span><span class="lamp l-warn">●</span>
+ <span class="nm">Memory · Thermal · Power</span>
+ <span class="cnt"><span class="attnword">1 needs attention</span> · 4 ok</span>
+ </div>
+ <div class="pbody">
+ <div class="heroes">
+ <div class="card s5">
+ <div class="cap">Temperatures <span class="lamp l-ok">●</span></div>
+ <div class="gaugewrap">
+ <div class="gauge">
+ <svg width="104" height="60" viewBox="0 0 112 64">
+ <path d="M8 60 A48 48 0 0 1 104 60" fill="none" stroke="#1a1d1f" stroke-width="9"/>
+ <path d="M8 60 A48 48 0 0 1 79 21" fill="none" stroke="#74932f" stroke-width="9" stroke-linecap="round"/>
+ <path d="M79 21 A48 48 0 0 1 96 34" fill="none" stroke="#dab53d" stroke-width="9"/>
+ <path d="M96 34 A48 48 0 0 1 104 60" fill="none" stroke="#cb6b4d" stroke-width="9"/>
+ <line x1="56" y1="60" x2="86" y2="30" stroke="#f3e7c5" stroke-width="2.5"/>
+ <circle cx="56" cy="60" r="4" fill="#dab53d"/>
+ </svg>
+ <div class="rd">61°C</div><div class="lb">CPU</div>
+ </div>
+ <div class="gauge">
+ <svg width="104" height="60" viewBox="0 0 112 64">
+ <path d="M8 60 A48 48 0 0 1 104 60" fill="none" stroke="#1a1d1f" stroke-width="9"/>
+ <path d="M8 60 A48 48 0 0 1 79 21" fill="none" stroke="#74932f" stroke-width="9" stroke-linecap="round"/>
+ <path d="M79 21 A48 48 0 0 1 96 34" fill="none" stroke="#dab53d" stroke-width="9"/>
+ <path d="M96 34 A48 48 0 0 1 104 60" fill="none" stroke="#cb6b4d" stroke-width="9"/>
+ <line x1="56" y1="60" x2="79" y2="34" stroke="#f3e7c5" stroke-width="2.5"/>
+ <circle cx="56" cy="60" r="4" fill="#dab53d"/>
+ </svg>
+ <div class="rd">54°C</div><div class="lb">NVMe</div>
+ </div>
+ </div>
+ </div>
+ <div class="card s4">
+ <div class="cap">Memory free <span class="lamp l-ok">●</span></div>
+ <div class="big">104<span class="unit"> GB</span></div>
+ <div class="note">of 125 GB · Workflow</div>
+ <div class="meter"><div class="track"><div class="fill ok" style="width:83%"></div></div>
+ <div class="scale"><span>0</span><span>free 104 / 125</span></div></div>
+ </div>
+ <div class="card s3">
+ <div class="cap">Unclean boot <span class="lamp l-warn">●</span></div>
+ <div class="big" style="color:var(--amber)">75<span class="unit">%</span></div>
+ <div class="note">Workflow · investigate</div>
+ <div class="meter"><div class="track"><div class="zone amber" style="left:0;right:0"></div>
+ <div class="fill warn" style="width:75%"></div></div>
+ <div class="scale"><span>0</span><span>100</span></div></div>
+ </div>
+ </div>
+ <div class="strip">
+ <div class="row"><span class="lamp l-ok">●</span><span class="nm2">swap / zram</span><span class="vv">16 GiB</span></div>
+ <div class="row"><span class="lamp l-ok">●</span><span class="nm2">thermal throttle</span><span class="vv">no</span></div>
+ <div class="row"><span class="lamp l-off">○</span><span class="nm2">battery</span><span class="vv">n/a · desktop</span></div>
+ </div>
+ </div>
+ </div>
+
+ <!-- SERVICES & BACKUPS -->
+ <div class="panel attn">
+ <div class="phead">
+ <span class="chev">▾</span><span class="lamp l-warn">●</span>
+ <span class="nm">Services &amp; Backups</span>
+ <span class="cnt"><span class="attnword">1 needs attention</span> · 3 ok</span>
+ </div>
+ <div class="pbody">
+ <div class="heroes">
+ <div class="card s3 tile">
+ <button class="btn lever">Prune</button>
+ <div class="cap">Docker reclaim</div>
+ <div class="row"><span class="lamp l-warn">●</span><span class="big">3<span class="unit"> GB</span></span></div>
+ <div class="note">Confirm · reclaimable</div>
+ </div>
+ </div>
+ <div class="strip">
+ <div class="row"><span class="lamp l-ok">●</span><span class="nm2">rsyncshot backup</span><span class="vv">3h ago</span></div>
+ <div class="row"><span class="lamp l-ok">●</span><span class="nm2">docker stopped</span><span class="vv">2</span></div>
+ <div class="row"><span class="lamp l-ok">●</span><span class="nm2">cron</span><span class="vv">ok</span></div>
+ <div class="row"><span class="lamp l-off">○</span><span class="nm2">libvirt VMs</span><span class="vv">off</span></div>
+ </div>
+ </div>
+ </div>
+
+ <!-- ===== COLLAPSED (all-green) categories — the space win ===== -->
+
+ <div class="panel clear">
+ <div class="phead">
+ <span class="chev">▸</span><span class="lamp l-ok">●</span>
+ <span class="nm">Snapshots</span>
+ <span class="cnt"><span class="okword">all clear · 2 ✓</span></span>
+ </div>
+ </div>
+
+ <div class="panel clear">
+ <div class="phead">
+ <span class="chev">▸</span><span class="lamp l-ok">●</span>
+ <span class="nm">Network &amp; Posture</span>
+ <span class="cnt"><span class="okword">all clear · 6 ✓</span></span>
+ </div>
+ </div>
+
+ <div class="footnote">
+ Collapsed panels are all-green — one header line each (chevron ▸ to expand). On a fully-healthy day every
+ panel collapses to this and the console is ~4 header rows plus the updates strip. The board's height tracks
+ how much needs you, not how many metrics exist. Verdict lamp (top-right) reads the worst <i>diagnostic</i>
+ state only — the fstrim fail and the 3 CVEs are actionable/updates, so the box still reads "healthy."
+ </div>
+
+ <!-- RUNNING / OUTPUT WALL -->
+ <div class="wall">
+ <div class="wh">
+ <span>MAINT · <span class="l">ratio</span> · CLEANING <span class="lamp l-prog spin">◐</span></span>
+ <span class="meta" style="font-size:11px">4 Auto actions</span>
+ </div>
+ <div class="stream">
+ <div class="ln"><span class="lamp l-prog spin">◐</span><span class="msg">cache trim</span><span class="meta">running…</span></div>
+ <div class="ln"><span class="lamp l-ok">●</span><span class="msg">journal vacuum</span><span class="meta">done · reclaimed 0.9 GB</span></div>
+ <div class="ln"><span class="lamp l-ok">●</span><span class="msg">coredump clear</span><span class="meta">done · 12 cleared</span></div>
+ <div class="ln queued"><span class="lamp l-off">○</span><span class="msg">app-log cleanup</span><span class="meta">queued</span></div>
+ </div>
+ <div class="wfoot"><button class="btn">Done</button></div>
+ </div>
+
+</div>
diff --git a/docs/prototypes/2026-07-07-maint-console-E3-hifi-drilldown.html b/docs/prototypes/2026-07-07-maint-console-E3-hifi-drilldown.html
new file mode 100644
index 0000000..fbfb03d
--- /dev/null
+++ b/docs/prototypes/2026-07-07-maint-console-E3-hifi-drilldown.html
@@ -0,0 +1,488 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>MAINT · ratio — Approach E3 · Hi-fi drill-down console</title>
+<style>
+:root{
+ --ground:#151311; --panel:#100f0f; --well:#0a0c0d; --raise:#1a1917;
+ --gold:#dab53d; --gold-hi:#ffd75f; --silver:#bfc4d0; --cream:#f3e7c5;
+ --steel:#969385; --dim:#7c838a; --slate:#424f5e; --slate-hi:#54677d;
+ --wash:#2c2f32; --pass:#74932f; --amber:#dab53d; --fail:#cb6b4d;
+ --mono:"BerkeleyMono Nerd Font","Berkeley Mono",monospace;
+}
+*{box-sizing:border-box;margin:0;padding:0}
+html{background:var(--ground)}
+body{font-family:var(--mono);color:var(--silver);line-height:1.45;
+ padding:2rem 1.4rem 5rem;
+ background:radial-gradient(1200px 600px at 70% -10%,#1c1915 0%,transparent 60%),var(--ground);
+ display:flex;justify-content:center}
+.console{width:1080px;max-width:100%}
+
+/* ---- shared primitives (from the widget gallery) ---- */
+.lamp{width:10px;height:10px;border-radius:50%;background:var(--pass);
+ box-shadow:0 0 6px 1px rgba(116,147,47,.55);display:inline-block;flex:none}
+.lamp.gold{background:var(--gold);box-shadow:0 0 6px 1px rgba(218,181,61,.6)}
+.lamp.red{background:var(--fail);box-shadow:0 0 7px 1px rgba(203,107,77,.6)}
+.lamp.off{background:var(--wash);box-shadow:none}
+.lamp.busy{background:var(--gold);animation:pulse .7s ease-in-out infinite}
+@keyframes pulse{50%{opacity:.25}}
+
+.key{font:inherit;font-size:11.5px;letter-spacing:.06em;color:var(--silver);cursor:pointer;
+ background:linear-gradient(180deg,#23211e,#191715);border:1px solid #33302b;border-bottom-color:#0c0b0a;
+ border-radius:8px;padding:6px 11px;box-shadow:inset 0 1px 0 rgba(255,255,255,.04),0 2px 3px rgba(0,0,0,.4)}
+.key:hover{color:var(--gold);border-color:var(--gold)}
+.key:active{transform:translateY(1px)}
+.key.on{color:var(--panel);background:linear-gradient(180deg,#f0d879,var(--gold));border-color:var(--gold-hi);font-weight:700}
+.key.red{color:var(--cream);background:linear-gradient(180deg,#d98a6f,var(--fail));border-color:var(--fail)}
+.key.sm{font-size:10.5px;padding:4px 9px;border-radius:7px}
+
+.badge{font-size:.62rem;letter-spacing:.16em;color:var(--panel);background:var(--gold);border-radius:4px;padding:1px 6px}
+.badge.red{background:var(--fail);color:var(--cream)}
+.badge.ghost{background:transparent;border:1px solid var(--slate);color:var(--silver)}
+
+.bar{width:100%;height:12px;background:#0d0f10;border:1px solid #231f18;border-radius:6px;overflow:hidden;position:relative}
+.bar>span{position:absolute;left:0;top:0;bottom:0;background:linear-gradient(90deg,#8a7524,var(--gold));border-radius:6px}
+.bar.warn>span{background:linear-gradient(90deg,#a35a3f,var(--fail))}
+.bar .tk{position:absolute;top:-2px;bottom:-2px;width:2px;background:var(--gold-hi)}
+
+.ring{width:58px;height:58px;border-radius:50%;
+ background:conic-gradient(var(--rc,var(--gold)) calc(var(--p)*1%),var(--wash) 0);
+ display:grid;place-items:center;position:relative;flex:none}
+.ring::before{content:"";position:absolute;inset:6px;border-radius:50%;background:var(--well)}
+.ring b{position:relative;color:var(--cream);font-size:12px;font-weight:700;font-variant-numeric:tabular-nums;line-height:1}
+.ring small{position:relative;color:var(--dim);font-size:8.5px}
+
+.readout{color:var(--cream);font-size:22px;font-weight:700;font-variant-numeric:tabular-nums;letter-spacing:.03em}
+.readout small{color:var(--dim);font-size:11px;font-weight:400}
+
+/* engraved section label */
+.engrave{color:var(--steel);font-size:.66rem;letter-spacing:.28em;text-transform:uppercase;
+ display:flex;align-items:center;gap:10px;flex:1}
+.engrave::after{content:"";height:1px;background:var(--wash);flex:1}
+.engrave .cnt{color:var(--dim);letter-spacing:.08em;text-transform:none;font-size:.7rem}
+
+/* lamp row (list item) */
+.lrow{display:flex;align-items:center;gap:9px;padding:6px 9px;border-radius:7px;background:#141210;font-size:12.5px}
+.lrow .who{color:var(--silver)} .lrow .who b{color:var(--cream)}
+.lrow .what{margin-left:auto;color:var(--dim);font-size:11px;display:flex;align-items:center;gap:8px}
+
+/* faceplate card */
+.plate{background:linear-gradient(180deg,var(--raise),var(--panel));border:1px solid #262320;border-radius:12px;
+ box-shadow:inset 0 1px 0 rgba(255,255,255,.04),0 6px 14px rgba(0,0,0,.4)}
+
+/* ---- masthead ---- */
+.mast{padding:15px 18px;margin-bottom:14px;display:flex;align-items:center;gap:18px;flex-wrap:wrap}
+.mast .brand{display:flex;flex-direction:column;gap:2px}
+.mast .brand .ey{color:var(--steel);font-size:.64rem;letter-spacing:.28em;text-transform:uppercase}
+.mast .brand .ti{color:var(--cream);font-size:19px;letter-spacing:.5px}
+.mast .brand .ti b{color:var(--gold)}
+.mast .verdict{display:flex;align-items:center;gap:8px;color:var(--pass);font-size:14px;font-weight:700}
+.mast .spacer{flex:1}
+.mast .doctor{display:flex;align-items:center;gap:8px}
+.mast .doctor .lbl{color:var(--steel);font-size:.62rem;letter-spacing:.22em;text-transform:uppercase;margin-right:2px}
+
+/* updates readout inline in masthead */
+.updbar{display:flex;align-items:center;gap:16px;padding:9px 18px;margin-bottom:16px;
+ border:1px solid var(--fail);border-radius:11px;
+ background:linear-gradient(180deg,#181210,#0a0c0d)}
+.updbar .q{color:var(--fail);font-size:.6rem;letter-spacing:.2em;text-transform:uppercase;
+ border:1px solid var(--fail);border-radius:5px;padding:2px 7px}
+.updbar .rd{display:flex;flex-direction:column;line-height:1.05}
+.updbar .rd .n{color:var(--cream);font-size:20px;font-weight:700;font-variant-numeric:tabular-nums}
+.updbar .rd .n.cve{color:var(--fail)}
+.updbar .rd .k{color:var(--dim);font-size:.6rem;letter-spacing:.12em;text-transform:uppercase}
+.updbar .spacer{flex:1}
+.updbar .msg{color:var(--dim);font-size:11px;max-width:300px}
+
+/* ---- OVERVIEW: station grid ---- */
+.ovhead{margin:0 4px 9px;display:flex;align-items:center;gap:10px}
+.overview{display:grid;grid-template-columns:repeat(4,1fr);gap:11px;margin-bottom:22px}
+.station{padding:12px 13px;cursor:pointer;display:flex;flex-direction:column;gap:9px;
+ transition:border-color .12s,transform .06s;border-left:3px solid var(--wash)}
+.station:hover{border-color:#3a352c;transform:translateY(-1px)}
+.station.attn{border-left-color:var(--amber)}
+.station.crit{border-left-color:var(--fail)}
+.station.clear{border-left-color:var(--pass)}
+.station .top{display:flex;align-items:center;gap:9px}
+.station .top .nm{color:var(--cream);font-size:12.5px;font-weight:700;letter-spacing:.02em}
+.station .top .chev{margin-left:auto;color:var(--dim);font-size:11px}
+.station .st{display:flex;align-items:center;gap:8px;font-size:11px;color:var(--dim)}
+.station .st .att{color:var(--amber)} .station .st .att.crit{color:var(--fail)}
+.station .st .okc{color:var(--pass)}
+.station .spark{margin-top:1px}
+
+/* ---- CATEGORY drill-down sections ---- */
+.cat{margin-bottom:11px;overflow:hidden}
+.cathead{display:flex;align-items:center;gap:12px;padding:11px 16px;cursor:pointer;user-select:none}
+.cathead:hover{background:#161412}
+.cathead .chev{color:var(--gold);font-size:12px;width:1em;text-align:center;transition:transform .15s}
+.cat.open .cathead .chev{transform:rotate(90deg)}
+.cathead .nm{color:var(--gold);font-weight:700;letter-spacing:.14em;text-transform:uppercase;font-size:12.5px}
+.cathead .cnt{margin-left:auto;color:var(--dim);font-size:11px;display:flex;gap:10px;align-items:center}
+.cathead .cnt .att{color:var(--amber)} .cathead .cnt .att.crit{color:var(--fail)}
+.catbody{display:none;padding:2px 16px 15px}
+.cat.open .catbody{display:block}
+
+.heroes{display:grid;grid-template-columns:repeat(12,1fr);gap:11px;margin-bottom:12px}
+.inst{background:var(--well);border:1px solid #201d17;border-radius:9px;padding:11px 12px;
+ display:flex;flex-direction:column;gap:8px;position:relative;min-height:84px}
+.inst .cap{color:var(--steel);font-size:.62rem;letter-spacing:.16em;text-transform:uppercase;
+ display:flex;align-items:center;gap:7px}
+.inst .cap .key{position:absolute;top:9px;right:9px}
+.inst .val{display:flex;align-items:baseline;gap:8px;margin-top:auto}
+.inst.s3{grid-column:span 3} .inst.s4{grid-column:span 4} .inst.s5{grid-column:span 5} .inst.s6{grid-column:span 6}
+.inst .sub{color:var(--dim);font-size:10.5px}
+.gaugepair{display:flex;gap:16px;align-items:center;justify-content:center;margin-top:auto}
+.gaugepair .g{display:flex;flex-direction:column;align-items:center;gap:3px}
+.gaugepair .g .lb{color:var(--steel);font-size:9px;letter-spacing:.1em}
+
+.strip{display:grid;grid-template-columns:repeat(3,1fr);gap:6px 12px}
+
+/* ---- output well ---- */
+.owell{margin-top:18px;padding:13px 15px}
+.owell .oh{display:flex;align-items:center;justify-content:space-between;margin-bottom:11px;
+ color:var(--cream);font-size:13px}
+.owell .oh .l{color:var(--gold)}
+.owell .ostep{display:flex;gap:9px;align-items:flex-start;padding:3px 0;font-size:12.5px}
+.owell .ostep .lamp{margin-top:3px;width:8px;height:8px}
+.owell .ostep b{color:var(--cream);font-weight:700}
+.owell .ostep .ev{color:var(--steel);font-size:11px}
+.owell .queued b{color:var(--dim)}
+.owell .ofoot{margin-top:11px;display:flex;justify-content:flex-end}
+
+.hint{color:var(--dim);font-size:11px;margin:0 4px 14px;display:flex;align-items:center;gap:8px}
+.controls-row{display:flex;gap:8px;margin:0 2px 12px}
+.footnote{color:var(--dim);font-size:11px;margin-top:16px;line-height:1.6;padding:0 4px}
+.footnote i{color:var(--steel);font-style:normal}
+@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
+</style>
+</head>
+<body>
+<div class="console">
+
+ <!-- ===== MASTHEAD ===== -->
+ <div class="plate mast">
+ <div class="brand">
+ <div class="ey">archsetup · maintenance console</div>
+ <div class="ti">MAINT · <b>ratio</b></div>
+ </div>
+ <div class="verdict"><span class="lamp"></span> HEALTHY</div>
+ <div class="spacer"></div>
+ <div class="doctor">
+ <span class="lbl">Doctor</span>
+ <button class="key" onclick="runDoctor()">CLEAN UP</button>
+ <button class="key">REVIEW &amp; FIX</button>
+ </div>
+ </div>
+
+ <!-- ===== UPDATES (quarantined, top billing for the 3 CVEs) ===== -->
+ <div class="updbar">
+ <span class="q">Updates</span>
+ <div class="rd"><span class="n">47</span><span class="k">pending</span></div>
+ <div class="rd"><span class="n cve">3</span><span class="k">CVE</span></div>
+ <div class="rd"><span class="n">5</span><span class="k">AUR stale</span></div>
+ <span class="msg"><span class="lamp red" style="width:8px;height:8px"></span> Security advisories present — updates run through the workflow, never the doctor.</span>
+ <span class="spacer"></span>
+ <button class="key red">RUN WORKFLOW</button>
+ </div>
+
+ <!-- ===== OVERVIEW: all categories at a glance ===== -->
+ <div class="ovhead"><span class="engrave">Overview<span class="cnt">· 8 systems</span></span></div>
+ <div class="overview" id="overview"></div>
+
+ <!-- ===== DRILL-DOWN CATEGORY SECTIONS ===== -->
+ <div class="controls-row">
+ <button class="key sm" onclick="allCats(true)">EXPAND ALL</button>
+ <button class="key sm" onclick="allCats(false)">COLLAPSE ALL</button>
+ <span class="hint" style="margin:0 0 0 6px">click any category header — or its overview tile — to drill in</span>
+ </div>
+
+ <!-- STORAGE -->
+ <div class="cat plate open" id="cat-storage">
+ <div class="cathead" onclick="toggleCat('storage')">
+ <span class="chev">▸</span><span class="lamp red"></span>
+ <span class="nm">Storage &amp; Filesystem</span>
+ <span class="cnt"><span class="att crit">3 attention</span> · 6 ok</span>
+ </div>
+ <div class="catbody">
+ <div class="heroes">
+ <div class="inst s3">
+ <div class="cap">Disk usage</div>
+ <div class="val"><span class="readout">69<small>%</small></span></div>
+ <div class="bar"><span style="width:69%"></span></div>
+ <div class="sub">root · btrfs · thr 80 / 90</div>
+ </div>
+ <div class="inst s3">
+ <button class="key sm">CLEAN</button>
+ <div class="cap"><span class="lamp gold"></span> Package cache</div>
+ <div class="val"><span class="readout">8.8<small> GB</small></span></div>
+ <div class="bar warn"><span style="width:88%"></span><span class="tk" style="left:100%"></span></div>
+ <div class="sub">Auto · threshold ~10 GB</div>
+ </div>
+ <div class="inst s3">
+ <button class="key sm">SCRUB</button>
+ <div class="cap"><span class="lamp gold"></span> btrfs scrub</div>
+ <div style="display:flex;justify-content:center;margin-top:auto">
+ <span class="ring" style="--p:62;--rc:var(--amber)"><b>34</b><small>days</small></span>
+ </div>
+ <div class="sub" style="text-align:center">past 30d cadence</div>
+ </div>
+ <div class="inst s3">
+ <button class="key sm red">ENABLE</button>
+ <div class="cap"><span class="lamp red"></span> fstrim.timer</div>
+ <div class="val"><span class="readout" style="color:var(--fail)">OFF</span></div>
+ <div class="sub">Confirm · SSD discard disabled</div>
+ </div>
+ </div>
+ <div class="strip">
+ <div class="lrow"><span class="lamp"></span><span class="who">SMART</span><span class="what">PASSED</span></div>
+ <div class="lrow"><span class="lamp"></span><span class="who">SMART wear</span><span class="what">0%</span></div>
+ <div class="lrow"><span class="lamp"></span><span class="who">device errors</span><span class="what">0 / 0</span></div>
+ <div class="lrow"><span class="lamp"></span><span class="who">btrfs unalloc</span><span class="what">118 GiB</span></div>
+ <div class="lrow"><span class="lamp"></span><span class="who">pacman integrity</span><span class="what">clean</span></div>
+ <div class="lrow"><span class="lamp"></span><span class="who">deep-trim</span><span class="what"><button class="key sm">DEEP TRIM</button> keep 3</span></div>
+ </div>
+ </div>
+ </div>
+
+ <!-- PACKAGES -->
+ <div class="cat plate open" id="cat-packages">
+ <div class="cathead" onclick="toggleCat('packages')">
+ <span class="chev">▸</span><span class="lamp gold"></span>
+ <span class="nm">Packages &amp; Security</span>
+ <span class="cnt"><span class="att">2 attention</span> · 2 ok · updates above</span>
+ </div>
+ <div class="catbody">
+ <div class="heroes">
+ <div class="inst s3">
+ <button class="key sm">REVIEW</button>
+ <div class="cap"><span class="lamp gold"></span> Orphans</div>
+ <div class="val"><span class="readout">13</span></div>
+ <div class="sub">Confirm · unowned pkgs</div>
+ </div>
+ <div class="inst s3">
+ <button class="key sm">REVIEW</button>
+ <div class="cap"><span class="lamp gold"></span> .pacnew files</div>
+ <div class="val"><span class="readout">2</span></div>
+ <div class="sub">Confirm · config merges</div>
+ </div>
+ </div>
+ <div class="strip">
+ <div class="lrow"><span class="lamp"></span><span class="who">keyring age</span><span class="what"><button class="key sm">REFRESH</button> 12d</span></div>
+ <div class="lrow"><span class="lamp"></span><span class="who">reboot required</span><span class="what">no</span></div>
+ </div>
+ </div>
+ </div>
+
+ <!-- SYSTEMD -->
+ <div class="cat plate open" id="cat-systemd">
+ <div class="cathead" onclick="toggleCat('systemd')">
+ <span class="chev">▸</span><span class="lamp gold"></span>
+ <span class="nm">systemd &amp; Boot</span>
+ <span class="cnt"><span class="att">2 attention</span> · 2 ok</span>
+ </div>
+ <div class="catbody">
+ <div class="heroes">
+ <div class="inst s3">
+ <button class="key sm">WORKFLOW</button>
+ <div class="cap"><span class="lamp gold"></span> Failed units</div>
+ <div class="val"><span class="readout">1</span></div>
+ <div class="sub">Workflow · systemd</div>
+ </div>
+ <div class="inst s3">
+ <button class="key sm">ENABLE</button>
+ <div class="cap"><span class="lamp gold"></span> Maint timers</div>
+ <div class="val"><span class="readout">4<small>/5</small></span></div>
+ <div class="sub">Confirm · one not firing</div>
+ </div>
+ </div>
+ <div class="strip">
+ <div class="lrow"><span class="lamp"></span><span class="who">is-system-running</span><span class="what">running</span></div>
+ <div class="lrow"><span class="lamp"></span><span class="who">kernel taint</span><span class="what">0</span></div>
+ </div>
+ </div>
+ </div>
+
+ <!-- LOGS -->
+ <div class="cat plate open" id="cat-logs">
+ <div class="cathead" onclick="toggleCat('logs')">
+ <span class="chev">▸</span><span class="lamp gold"></span>
+ <span class="nm">Logs &amp; Coredumps</span>
+ <span class="cnt"><span class="att">1 attention</span> · 4 ok</span>
+ </div>
+ <div class="catbody">
+ <div class="heroes">
+ <div class="inst s3">
+ <button class="key sm">CLEAR</button>
+ <div class="cap"><span class="lamp gold"></span> Coredumps 7d</div>
+ <div class="val"><span class="readout">18</span></div>
+ <div class="sub">Auto · clearable</div>
+ </div>
+ </div>
+ <div class="strip">
+ <div class="lrow"><span class="lamp"></span><span class="who">journald size</span><span class="what"><button class="key sm">VACUUM</button> 1.2 GB</span></div>
+ <div class="lrow"><span class="lamp"></span><span class="who">app-log cleanup</span><span class="what"><button class="key sm">RUN</button> ok</span></div>
+ <div class="lrow"><span class="lamp"></span><span class="who">journal errors</span><span class="what">12 real</span></div>
+ <div class="lrow"><span class="lamp"></span><span class="who">kernel/hw events</span><span class="what">clean</span></div>
+ </div>
+ </div>
+ </div>
+
+ <!-- MEMORY/THERMAL/POWER -->
+ <div class="cat plate open" id="cat-power">
+ <div class="cathead" onclick="toggleCat('power')">
+ <span class="chev">▸</span><span class="lamp gold"></span>
+ <span class="nm">Memory · Thermal · Power</span>
+ <span class="cnt"><span class="att">1 attention</span> · 4 ok</span>
+ </div>
+ <div class="catbody">
+ <div class="heroes">
+ <div class="inst s5">
+ <div class="cap"><span class="lamp"></span> Temperatures</div>
+ <div class="gaugepair">
+ <div class="g"><span class="ring" style="--p:61;--rc:var(--pass)"><b>61°</b></span><span class="lb">CPU</span></div>
+ <div class="g"><span class="ring" style="--p:54;--rc:var(--pass)"><b>54°</b></span><span class="lb">NVMe</span></div>
+ </div>
+ </div>
+ <div class="inst s4">
+ <div class="cap"><span class="lamp"></span> Memory free</div>
+ <div class="val"><span class="readout">104<small> GB</small></span></div>
+ <div class="bar"><span style="width:83%"></span></div>
+ <div class="sub">of 125 GB · 0 OOM</div>
+ </div>
+ <div class="inst s3">
+ <div class="cap"><span class="lamp gold"></span> Unclean boot</div>
+ <div class="val"><span class="readout" style="color:var(--amber)">75<small>%</small></span></div>
+ <div class="bar warn"><span style="width:75%"></span></div>
+ <div class="sub">Workflow · investigate</div>
+ </div>
+ </div>
+ <div class="strip">
+ <div class="lrow"><span class="lamp"></span><span class="who">swap / zram</span><span class="what">16 GiB</span></div>
+ <div class="lrow"><span class="lamp"></span><span class="who">thermal throttle</span><span class="what">no</span></div>
+ <div class="lrow"><span class="lamp off"></span><span class="who">battery</span><span class="what">n/a · desktop</span></div>
+ </div>
+ </div>
+ </div>
+
+ <!-- SERVICES -->
+ <div class="cat plate open" id="cat-services">
+ <div class="cathead" onclick="toggleCat('services')">
+ <span class="chev">▸</span><span class="lamp gold"></span>
+ <span class="nm">Services &amp; Backups</span>
+ <span class="cnt"><span class="att">1 attention</span> · 3 ok</span>
+ </div>
+ <div class="catbody">
+ <div class="heroes">
+ <div class="inst s3">
+ <button class="key sm">PRUNE</button>
+ <div class="cap"><span class="lamp gold"></span> Docker reclaim</div>
+ <div class="val"><span class="readout">3<small> GB</small></span></div>
+ <div class="sub">Confirm · reclaimable</div>
+ </div>
+ </div>
+ <div class="strip">
+ <div class="lrow"><span class="lamp"></span><span class="who">rsyncshot backup</span><span class="what">3h ago</span></div>
+ <div class="lrow"><span class="lamp"></span><span class="who">docker stopped</span><span class="what">2</span></div>
+ <div class="lrow"><span class="lamp"></span><span class="who">cron</span><span class="what">ok</span></div>
+ <div class="lrow"><span class="lamp off"></span><span class="who">libvirt VMs</span><span class="what">off</span></div>
+ </div>
+ </div>
+ </div>
+
+ <!-- SNAPSHOTS (collapsed by default — all green) -->
+ <div class="cat plate" id="cat-snapshots">
+ <div class="cathead" onclick="toggleCat('snapshots')">
+ <span class="chev">▸</span><span class="lamp"></span>
+ <span class="nm">Snapshots</span>
+ <span class="cnt"><span class="okc" style="color:var(--pass)">all clear · 2 ✓</span></span>
+ </div>
+ <div class="catbody">
+ <div class="strip">
+ <div class="lrow"><span class="lamp"></span><span class="who">snapper count</span><span class="what"><button class="key sm">PRUNE</button> 42 · retention ok</span></div>
+ <div class="lrow"><span class="lamp"></span><span class="who">snapshot timer</span><span class="what">active</span></div>
+ </div>
+ </div>
+ </div>
+
+ <!-- NETWORK (collapsed by default — all green) -->
+ <div class="cat plate" id="cat-network">
+ <div class="cathead" onclick="toggleCat('network')">
+ <span class="chev">▸</span><span class="lamp"></span>
+ <span class="nm">Network &amp; Posture</span>
+ <span class="cnt"><span class="okc" style="color:var(--pass)">all clear · 6 ✓</span></span>
+ </div>
+ <div class="catbody">
+ <div class="strip">
+ <div class="lrow"><span class="lamp"></span><span class="who">firewall</span><span class="what">active</span></div>
+ <div class="lrow"><span class="lamp"></span><span class="who">unexpected listeners</span><span class="what">3</span></div>
+ <div class="lrow"><span class="lamp"></span><span class="who">tailscale</span><span class="what">4/4</span></div>
+ <div class="lrow"><span class="lamp"></span><span class="who">fail2ban</span><span class="what">active</span></div>
+ <div class="lrow"><span class="lamp"></span><span class="who">NTP sync</span><span class="what">synced</span></div>
+ <div class="lrow"><span class="lamp"></span><span class="who">DNS / NM</span><span class="what">ok</span></div>
+ </div>
+ </div>
+ </div>
+
+ <!-- ===== OUTPUT WELL (doctor running) ===== -->
+ <div class="plate owell" id="owell" style="display:none">
+ <div class="oh"><span>MAINT · <span class="l">ratio</span> · CLEANING <span class="lamp busy"></span></span>
+ <span style="color:var(--dim);font-size:11px">4 Auto actions</span></div>
+ <div class="ostep"><span class="lamp busy"></span><span><b>cache trim</b> <span class="ev">running…</span></span></div>
+ <div class="ostep"><span class="lamp"></span><span><b>journal vacuum</b> <span class="ev">done · reclaimed 0.9 GB</span></span></div>
+ <div class="ostep"><span class="lamp"></span><span><b>coredump clear</b> <span class="ev">done · 12 cleared</span></span></div>
+ <div class="ostep queued"><span class="lamp off"></span><span><b>app-log cleanup</b> <span class="ev">queued</span></span></div>
+ <div class="ofoot"><button class="key" onclick="document.getElementById('owell').style.display='none'">DONE</button></div>
+ </div>
+
+ <div class="footnote">
+ Two levels: the <i>Overview</i> grid is every system at a glance — click a tile to jump to and open its section.
+ Each <i>category</i> below is a drill-down you expand or collapse (click the header, or use Expand/Collapse all).
+ All-green categories (Snapshots, Network) start collapsed — on a fully-healthy day every section collapses and the
+ console is the masthead + overview. Verdict lamp reads worst <i>diagnostic</i> state only — the fstrim fail and the
+ 3 CVEs are actionable/updates, so the box still reads HEALTHY.
+ </div>
+
+</div>
+
+<script>
+const CATS = [
+ {id:'storage', name:'Storage & FS', cls:'crit', att:'3', ok:'6', crit:true},
+ {id:'packages', name:'Packages & Sec', cls:'attn', att:'2', ok:'2'},
+ {id:'systemd', name:'systemd & Boot', cls:'attn', att:'2', ok:'2'},
+ {id:'logs', name:'Logs & Cores', cls:'attn', att:'1', ok:'4'},
+ {id:'power', name:'Mem·Therm·Power', cls:'attn', att:'1', ok:'4'},
+ {id:'services', name:'Services & Backup', cls:'attn', att:'1', ok:'3'},
+ {id:'snapshots',name:'Snapshots', cls:'clear', att:'0', ok:'2'},
+ {id:'network', name:'Network & Posture',cls:'clear', att:'0', ok:'6'},
+];
+const lampFor = c => c.cls==='crit'?'red':c.cls==='attn'?'gold':'';
+// build overview station tiles
+const ov = document.getElementById('overview');
+CATS.forEach(c=>{
+ const el=document.createElement('div');
+ el.className='plate station '+c.cls;
+ const att = c.att!=='0'
+ ? `<span class="att ${c.crit?'crit':''}">${c.att} attention</span> · <span class="okc">${c.ok} ok</span>`
+ : `<span class="okc">all clear · ${c.ok} ✓</span>`;
+ el.innerHTML =
+ `<div class="top"><span class="lamp ${lampFor(c)}"></span><span class="nm">${c.name}</span><span class="chev">▸</span></div>
+ <div class="st">${att}</div>`;
+ el.onclick=()=>openCat(c.id);
+ ov.appendChild(el);
+});
+function toggleCat(id){ document.getElementById('cat-'+id).classList.toggle('open'); }
+function openCat(id){
+ const el=document.getElementById('cat-'+id);
+ el.classList.add('open');
+ el.scrollIntoView({behavior:'smooth',block:'center'});
+}
+function allCats(open){ CATS.forEach(c=>{
+ document.getElementById('cat-'+c.id).classList.toggle('open',open); }); }
+function runDoctor(){ document.getElementById('owell').style.display='block';
+ document.getElementById('owell').scrollIntoView({behavior:'smooth',block:'center'}); }
+</script>
+</body>
+</html>
diff --git a/docs/prototypes/2026-07-07-maint-console-E4-interactive.html b/docs/prototypes/2026-07-07-maint-console-E4-interactive.html
new file mode 100644
index 0000000..c678a8d
--- /dev/null
+++ b/docs/prototypes/2026-07-07-maint-console-E4-interactive.html
@@ -0,0 +1,546 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>MAINT · ratio — Approach E4 · Interactive view-swap console</title>
+<style>
+:root{
+ --ground:#151311; --panel:#100f0f; --well:#0a0c0d; --raise:#1a1917;
+ --gold:#dab53d; --gold-hi:#ffd75f; --silver:#bfc4d0; --cream:#f3e7c5;
+ --steel:#969385; --dim:#7c838a; --slate:#424f5e; --slate-hi:#54677d;
+ --wash:#2c2f32; --pass:#74932f; --amber:#dab53d; --fail:#cb6b4d;
+ --mono:"BerkeleyMono Nerd Font","Berkeley Mono",monospace;
+}
+*{box-sizing:border-box;margin:0;padding:0}
+html{background:var(--ground)}
+body{font-family:var(--mono);color:var(--silver);line-height:1.45;
+ padding:2rem 1.4rem 5rem;
+ background:radial-gradient(1200px 600px at 70% -10%,#1c1915 0%,transparent 60%),var(--ground);
+ display:flex;justify-content:center}
+.console{width:1060px;max-width:100%}
+
+/* ---- primitives (widget-gallery kit) ---- */
+.lamp{width:10px;height:10px;border-radius:50%;background:var(--pass);
+ box-shadow:0 0 6px 1px rgba(116,147,47,.55);display:inline-block;flex:none}
+.lamp.gold{background:var(--gold);box-shadow:0 0 6px 1px rgba(218,181,61,.6)}
+.lamp.red{background:var(--fail);box-shadow:0 0 7px 1px rgba(203,107,77,.6)}
+.lamp.off{background:var(--wash);box-shadow:none}
+.lamp.busy{background:var(--gold);animation:pulse .7s ease-in-out infinite}
+@keyframes pulse{50%{opacity:.25}}
+
+.key{font:inherit;font-size:11.5px;letter-spacing:.06em;color:var(--silver);cursor:pointer;
+ background:linear-gradient(180deg,#23211e,#191715);border:1px solid #33302b;border-bottom-color:#0c0b0a;
+ border-radius:8px;padding:6px 11px;box-shadow:inset 0 1px 0 rgba(255,255,255,.04),0 2px 3px rgba(0,0,0,.4);
+ white-space:nowrap}
+.key:hover{color:var(--gold);border-color:var(--gold)}
+.key:active{transform:translateY(1px)}
+.key.on{color:var(--panel);background:linear-gradient(180deg,#f0d879,var(--gold));border-color:var(--gold-hi);font-weight:700}
+.key.red{color:var(--cream);background:linear-gradient(180deg,#d98a6f,var(--fail));border-color:var(--fail)}
+.key.armed{background:rgba(203,107,77,.15);border-color:var(--fail);color:var(--fail)}
+.key.sm{font-size:10.5px;padding:4px 9px;border-radius:7px}
+.key.done{opacity:.45;pointer-events:none}
+
+.bar{width:100%;height:12px;background:#0d0f10;border:1px solid #231f18;border-radius:6px;overflow:hidden;position:relative}
+.bar>span{position:absolute;left:0;top:0;bottom:0;background:linear-gradient(90deg,#8a7524,var(--gold));border-radius:6px;
+ transition:width .5s ease}
+.bar.warn>span{background:linear-gradient(90deg,#a35a3f,var(--fail))}
+.bar.okc>span{background:linear-gradient(90deg,#5c7626,var(--pass))}
+.bar .tk{position:absolute;top:-2px;bottom:-2px;width:2px;background:var(--gold-hi)}
+
+.ring{width:58px;height:58px;border-radius:50%;
+ background:conic-gradient(var(--rc,var(--gold)) calc(var(--p)*1%),var(--wash) 0);
+ display:grid;place-items:center;position:relative;flex:none}
+.ring::before{content:"";position:absolute;inset:6px;border-radius:50%;background:var(--well)}
+.ring b{position:relative;color:var(--cream);font-size:12px;font-weight:700;font-variant-numeric:tabular-nums;line-height:1}
+.ring small{position:relative;color:var(--dim);font-size:8.5px}
+
+.readout{color:var(--cream);font-size:22px;font-weight:700;font-variant-numeric:tabular-nums;letter-spacing:.03em}
+.readout small{color:var(--dim);font-size:11px;font-weight:400}
+
+.engrave{color:var(--steel);font-size:.66rem;letter-spacing:.28em;text-transform:uppercase;
+ display:flex;align-items:center;gap:10px;flex:1}
+.engrave::after{content:"";height:1px;background:var(--wash);flex:1}
+.engrave .cnt{color:var(--dim);letter-spacing:.08em;text-transform:none;font-size:.7rem}
+
+.lrow{display:flex;align-items:center;gap:9px;padding:6px 9px;border-radius:7px;background:#141210;font-size:12.5px}
+.lrow .who{color:var(--silver)} .lrow .who b{color:var(--cream)}
+.lrow .what{margin-left:auto;color:var(--dim);font-size:11px;display:flex;align-items:center;gap:8px}
+
+.plate{background:linear-gradient(180deg,var(--raise),var(--panel));border:1px solid #262320;border-radius:12px;
+ box-shadow:inset 0 1px 0 rgba(255,255,255,.04),0 6px 14px rgba(0,0,0,.4)}
+
+/* masthead */
+.mast{padding:15px 18px;margin-bottom:14px;display:flex;align-items:center;gap:16px;flex-wrap:wrap}
+.mast .brand{display:flex;flex-direction:column;gap:2px}
+.mast .brand .ey{color:var(--steel);font-size:.64rem;letter-spacing:.28em;text-transform:uppercase}
+.mast .brand .ti{color:var(--cream);font-size:19px;letter-spacing:.5px}
+.mast .brand .ti b{color:var(--gold)}
+.mast .verdict{display:flex;align-items:center;gap:8px;color:var(--pass);font-size:14px;font-weight:700}
+.mast .spacer{flex:1}
+.mast .doctor{display:flex;align-items:center;gap:8px}
+.mast .doctor .lbl{color:var(--steel);font-size:.62rem;letter-spacing:.22em;text-transform:uppercase;margin-right:2px}
+
+/* updates */
+.updbar{display:flex;align-items:center;gap:16px;padding:9px 18px;margin-bottom:16px;
+ border:1px solid var(--fail);border-radius:11px;
+ background:linear-gradient(180deg,#181210,#0a0c0d)}
+.updbar .q{color:var(--fail);font-size:.6rem;letter-spacing:.2em;text-transform:uppercase;
+ border:1px solid var(--fail);border-radius:5px;padding:2px 7px}
+.updbar .rd{display:flex;flex-direction:column;line-height:1.05}
+.updbar .rd .n{color:var(--cream);font-size:20px;font-weight:700;font-variant-numeric:tabular-nums}
+.updbar .rd .n.cve{color:var(--fail)}
+.updbar .rd .k{color:var(--dim);font-size:.6rem;letter-spacing:.12em;text-transform:uppercase}
+.updbar .spacer{flex:1}
+.updbar .msg{color:var(--dim);font-size:11px;max-width:300px}
+
+/* overview stations */
+.overview{display:grid;grid-template-columns:repeat(4,1fr);gap:11px}
+.station{padding:12px 13px;cursor:pointer;display:flex;flex-direction:column;gap:9px;
+ transition:border-color .12s,transform .06s;border-left:3px solid var(--wash)}
+.station:hover{border-color:#3a352c;transform:translateY(-1px)}
+.station.attn{border-left-color:var(--amber)}
+.station.crit{border-left-color:var(--fail)}
+.station.clear{border-left-color:var(--pass)}
+.station .top{display:flex;align-items:center;gap:9px}
+.station .top .nm{color:var(--cream);font-size:12.5px;font-weight:700}
+.station .top .chev{margin-left:auto;color:var(--dim);font-size:11px}
+.station .st{display:flex;align-items:center;gap:8px;font-size:11px;color:var(--dim)}
+.station .st .att{color:var(--amber)} .station .st .att.crit{color:var(--fail)}
+.station .st .okc{color:var(--pass)}
+.ovhead{margin:0 4px 9px;display:flex;align-items:center;gap:10px}
+
+/* category detail view */
+.catplate{padding:0 0 15px}
+.cathead{display:flex;align-items:center;gap:12px;padding:12px 16px;border-bottom:1px solid #201d17;margin-bottom:13px}
+.cathead .nm{color:var(--gold);font-weight:700;letter-spacing:.14em;text-transform:uppercase;font-size:13px}
+.cathead .cnt{color:var(--dim);font-size:11px;display:flex;gap:10px;align-items:center}
+.cathead .cnt .att{color:var(--amber)} .cathead .cnt .att.crit{color:var(--fail)}
+.cathead .cnt .okc{color:var(--pass)}
+.cathead .navs{margin-left:auto;display:flex;gap:7px}
+.catbody{padding:0 16px}
+.heroes{display:grid;grid-template-columns:repeat(12,1fr);gap:11px;margin-bottom:12px}
+.inst{background:var(--well);border:1px solid #201d17;border-radius:9px;padding:11px 12px;
+ display:flex;flex-direction:column;gap:8px;position:relative;min-height:86px}
+.inst .cap{color:var(--steel);font-size:.62rem;letter-spacing:.16em;text-transform:uppercase;
+ display:flex;align-items:center;gap:7px;padding-right:70px}
+.inst .lever{position:absolute;top:9px;right:9px}
+.inst .val{display:flex;align-items:baseline;gap:8px;margin-top:auto}
+.inst.s3{grid-column:span 3} .inst.s4{grid-column:span 4} .inst.s5{grid-column:span 5} .inst.s6{grid-column:span 6}
+.inst .sub{color:var(--dim);font-size:10.5px}
+.gaugepair{display:flex;gap:16px;align-items:center;justify-content:center;margin-top:auto}
+.gaugepair .g{display:flex;flex-direction:column;align-items:center;gap:3px}
+.gaugepair .g .lb{color:var(--steel);font-size:9px;letter-spacing:.1em}
+.strip{display:grid;grid-template-columns:repeat(3,1fr);gap:6px 12px}
+
+/* doctor / review views */
+.owell{padding:13px 16px}
+.owell .oh{display:flex;align-items:center;justify-content:space-between;margin-bottom:11px;
+ color:var(--cream);font-size:13px}
+.owell .oh .l{color:var(--gold)}
+.ostep{display:flex;gap:9px;align-items:flex-start;padding:4px 0;font-size:12.5px}
+.ostep .lamp{margin-top:3px;width:8px;height:8px}
+.ostep b{color:var(--cream);font-weight:700}
+.ostep .ev{color:var(--steel);font-size:11px;display:block}
+.ostep.queued b{color:var(--dim)}
+.ofoot{margin-top:12px;display:flex;justify-content:flex-end;gap:8px}
+
+.rvrow{display:flex;align-items:center;gap:10px;padding:8px 10px;border-radius:8px;background:#141210;
+ font-size:12.5px;margin-bottom:6px}
+.rvrow .who{color:var(--cream);font-weight:700}
+.rvrow .why{color:var(--dim);font-size:11px}
+.rvrow .keys{margin-left:auto;display:flex;gap:7px;align-items:center}
+.rvrow .res{color:var(--pass);font-size:11px}
+.rvnote{color:var(--dim);font-size:11px;margin:10px 2px 0;line-height:1.5}
+
+/* toast */
+#toast{position:fixed;bottom:26px;right:26px;display:flex;flex-direction:column;gap:8px;z-index:9}
+.toastw{font-size:11.5px;color:var(--cream);background:var(--slate);border-radius:7px;padding:6px 11px;
+ box-shadow:0 4px 12px rgba(0,0,0,.5);animation:tin .18s ease}
+.toastw.err{background:#5e3a2e}
+@keyframes tin{from{opacity:0;transform:translateY(6px)}}
+
+.crumb{color:var(--dim);font-size:11px;margin:0 4px 9px;display:flex;gap:6px;align-items:center}
+.crumb a{color:var(--gold);cursor:pointer;text-decoration:none}
+.footnote{color:var(--dim);font-size:11px;margin-top:16px;line-height:1.6;padding:0 4px}
+@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
+</style>
+</head>
+<body>
+<div class="console">
+
+ <!-- persistent masthead -->
+ <div class="plate mast">
+ <div class="brand">
+ <div class="ey">archsetup · maintenance console · E4 interactive</div>
+ <div class="ti">MAINT · <b>ratio</b></div>
+ </div>
+ <div class="verdict" id="verdict"><span class="lamp"></span> HEALTHY</div>
+ <div class="spacer"></div>
+ <div class="doctor">
+ <button class="key" id="homekey" onclick="go('home')">HOME</button>
+ <span class="lbl" style="margin-left:8px">Doctor</span>
+ <button class="key" onclick="go('doctor')">CLEAN UP</button>
+ <button class="key" onclick="go('review')">REVIEW &amp; FIX</button>
+ </div>
+ </div>
+
+ <!-- swapped view -->
+ <div id="view"></div>
+
+ <div class="footnote">
+ Fully interactive: views replace each other (no inline collapse). HOME = overview of all systems;
+ click a station to drill in; PREV/NEXT walk the categories; CLEAN UP streams the output well and
+ actually updates the metrics; REVIEW &amp; FIX lists every Confirm item — destructive ones arm on
+ first press, fire on second. Workflow items always escalate (toast) — the console never runs them.
+ </div>
+</div>
+<div id="toast"></div>
+
+<script>
+/* ================= state ================= */
+const S = {
+ cache:{v:8.8, st:'warn'}, scrub:{v:34, st:'warn'}, fstrim:{v:'OFF', st:'fail'},
+ disk:{v:69, st:'ok'}, smart:{v:'PASSED', st:'ok'}, wear:{v:'0%', st:'ok'},
+ deverr:{v:'0 / 0', st:'ok'}, unalloc:{v:'118 GiB', st:'ok'}, integ:{v:'clean', st:'ok'},
+ deeptrim:{v:'keep 3', st:'ok'},
+ orphans:{v:13, st:'warn'}, pacnew:{v:2, st:'warn'}, keyring:{v:12, st:'ok'}, reboot:{v:'no', st:'ok'},
+ failed:{v:1, st:'warn'}, timers:{v:'4/5', st:'warn'}, sysrun:{v:'running', st:'ok'}, taint:{v:0, st:'ok'},
+ cores:{v:18, st:'warn'}, journald:{v:1.2, st:'ok'}, applog:{v:'ok', st:'ok'},
+ jerr:{v:'12 real', st:'ok'}, khw:{v:'clean', st:'ok'},
+ temps:{v:'61/54', st:'ok'}, mem:{v:104, st:'ok'}, unclean:{v:75, st:'warn'},
+ zram:{v:'16 GiB', st:'ok'}, throttle:{v:'no', st:'ok'}, battery:{v:'n/a', st:'off'},
+ docker:{v:3, st:'warn'}, rsync:{v:'3h ago', st:'ok'}, dstopped:{v:2, st:'ok'},
+ cron:{v:'ok', st:'ok'}, libvirt:{v:'off', st:'off'},
+ snapper:{v:42, st:'ok'}, stimer:{v:'active', st:'ok'},
+ fw:{v:'active', st:'ok'}, listen:{v:3, st:'ok'}, ts:{v:'4/4', st:'ok'},
+ f2b:{v:'active', st:'ok'}, ntp:{v:'synced', st:'ok'}, dns:{v:'ok', st:'ok'},
+};
+
+const CATS = [
+ {id:'storage', name:'Storage & Filesystem', ids:['disk','cache','scrub','fstrim','smart','wear','deverr','unalloc','integ','deeptrim']},
+ {id:'packages', name:'Packages & Security', ids:['orphans','pacnew','keyring','reboot']},
+ {id:'systemd', name:'systemd & Boot', ids:['failed','timers','sysrun','taint']},
+ {id:'logs', name:'Logs & Coredumps', ids:['cores','journald','applog','jerr','khw']},
+ {id:'power', name:'Memory · Thermal · Power', ids:['temps','mem','unclean','zram','throttle','battery']},
+ {id:'services', name:'Services & Backups', ids:['docker','rsync','dstopped','cron','libvirt']},
+ {id:'snapshots',name:'Snapshots', ids:['snapper','stimer']},
+ {id:'network', name:'Network & Posture', ids:['fw','listen','ts','f2b','ntp','dns']},
+];
+
+let view = 'home';
+let armed = null; // id of armed destructive key
+let doctorTimer = null;
+
+/* ================= helpers ================= */
+const $ = id => document.getElementById(id);
+function counts(cat){
+ let attn=0, ok=0, crit=false;
+ cat.ids.forEach(i=>{ const st=S[i].st;
+ if(st==='warn'){attn++} else if(st==='fail'){attn++;crit=true} else ok++; });
+ return {attn, ok, crit};
+}
+function lampCls(st){ return st==='ok'?'':st==='warn'?'gold':st==='fail'?'red':'off'; }
+function catLamp(cat){ const c=counts(cat); return c.crit?'red':c.attn?'gold':''; }
+function toast(msg, err){
+ const t=document.createElement('div'); t.className='toastw'+(err?' err':''); t.textContent=msg;
+ $('toast').appendChild(t); setTimeout(()=>t.remove(), 2600);
+}
+function esc(id){ toast('escalates to the system-health-check workflow — not run from the console', true); }
+
+/* generic lever runner: busy → mutate → re-render */
+function fire(act){
+ const a = ACTIONS[act];
+ if(a.arm && armed!==act){ armed=act; render(); toast(a.armMsg||'press again to confirm', true); return; }
+ armed=null;
+ toast(a.busy||'running…');
+ setTimeout(()=>{ a.run(); toast(a.doneMsg); render(); }, a.ms||800);
+}
+
+const ACTIONS = {
+ clean_cache:{ busy:'paccache -r / -ruk0 …', ms:1000, doneMsg:'cache trim — reclaimed 8.4 GB',
+ run(){ S.cache={v:0.4, st:'ok'}; } },
+ scrub:{ busy:'btrfs scrub started on / …', ms:1400, doneMsg:'scrub running — age resets on completion',
+ run(){ S.scrub={v:0, st:'ok'}; } },
+ fstrim:{ busy:'systemctl enable --now fstrim.timer …', ms:900, doneMsg:'fstrim.timer enabled — weekly TRIM active',
+ run(){ S.fstrim={v:'ON', st:'ok'}; S.timers={v:'5/5', st:'ok'}; } },
+ deeptrim:{ arm:true, armMsg:'keep only 1 version of every pkg — press DEEP TRIM again', busy:'paccache -rk1 …', ms:1100,
+ doneMsg:'deep trim — kept 1 version, reclaimed 3.1 GB more', run(){ S.deeptrim={v:'keep 1', st:'ok'}; if(S.cache.st==='ok') S.cache={v:0.2,st:'ok'}; } },
+ orphans:{ arm:true, armMsg:'remove 13 orphaned packages — press again', busy:'pacman -Rns (13 pkgs) …', ms:1200,
+ doneMsg:'orphans removed — 13 pkgs', run(){ S.orphans={v:0, st:'ok'}; } },
+ pacnew:{ busy:'diffing 2 .pacnew files …', ms:900, doneMsg:'.pacnew resolved — mirrorlist deleted, locale.gen merged',
+ run(){ S.pacnew={v:0, st:'ok'}; } },
+ keyring:{ busy:'pacman -Sy archlinux-keyring …', ms:900, doneMsg:'keyring refreshed',
+ run(){ S.keyring={v:0, st:'ok'}; } },
+ timers:{ busy:'systemctl enable --now (1 timer) …', ms:800, doneMsg:'all 5 maintenance timers firing',
+ run(){ S.timers={v:'5/5', st:'ok'}; S.fstrim.st==='fail' && (S.fstrim={v:'ON',st:'ok'}); } },
+ cores:{ busy:'coredumpctl clean (keep 3d) …', ms:800, doneMsg:'coredumps cleared — 18 removed',
+ run(){ S.cores={v:0, st:'ok'}; } },
+ journald:{ busy:'journalctl --vacuum-size=300M …', ms:800, doneMsg:'journal vacuumed — reclaimed 0.9 GB',
+ run(){ S.journald={v:0.3, st:'ok'}; } },
+ applog:{ busy:'log-cleanup …', ms:600, doneMsg:'app logs — nothing older than 7d', run(){} },
+ docker:{ arm:true, armMsg:'docker prune tier 1+2 — press again', busy:'docker container/image prune …', ms:1200,
+ doneMsg:'docker pruned — reclaimed 2.8 GB', run(){ S.docker={v:0.2, st:'ok'}; } },
+ snapper:{ arm:true, armMsg:'prune stale manual snapshots — press again', busy:'snapper cleanup …', ms:900,
+ doneMsg:'snapper — retention applied', run(){ S.snapper={v:31, st:'ok'}; } },
+};
+
+/* ================= view renderers ================= */
+function lever(act, label, cls){
+ const armedNow = armed===act;
+ return `<button class="key sm ${cls||''} ${armedNow?'armed':''}" onclick="event.stopPropagation();fire('${act}')">${armedNow?label+'?':label}</button>`;
+}
+function wkey(label){ return `<button class="key sm" onclick="event.stopPropagation();esc()">${label||'WORKFLOW'}</button>`; }
+function lrow(id, name, extra){
+ const m=S[id];
+ return `<div class="lrow"><span class="lamp ${lampCls(m.st)}"></span><span class="who">${name}</span>
+ <span class="what">${extra||''}${m.v}</span></div>`;
+}
+
+function renderHome(){
+ const stations = CATS.map(c=>{
+ const k=counts(c);
+ const st = k.attn ? `<span class="att ${k.crit?'crit':''}">${k.attn} attention</span> · <span class="okc">${k.ok} ok</span>`
+ : `<span class="okc">all clear · ${k.ok} ✓</span>`;
+ return `<div class="plate station ${k.crit?'crit':k.attn?'attn':'clear'}" onclick="go('cat:${c.id}')">
+ <div class="top"><span class="lamp ${catLamp(c)}"></span><span class="nm">${c.name}</span><span class="chev">▸</span></div>
+ <div class="st">${st}</div></div>`;
+ }).join('');
+ return `
+ <div class="updbar">
+ <span class="q">Updates</span>
+ <div class="rd"><span class="n">47</span><span class="k">pending</span></div>
+ <div class="rd"><span class="n cve">3</span><span class="k">CVE</span></div>
+ <div class="rd"><span class="n">5</span><span class="k">AUR stale</span></div>
+ <span class="msg"><span class="lamp red" style="width:8px;height:8px"></span> Security advisories present — updates run through the workflow, never the doctor.</span>
+ <span class="spacer"></span>
+ <button class="key red" onclick="esc()">RUN WORKFLOW</button>
+ </div>
+ <div class="ovhead"><span class="engrave">Overview<span class="cnt">· ${CATS.length} systems</span></span></div>
+ <div class="overview">${stations}</div>`;
+}
+
+function heroBar(id, cap, unit, pct, opts){
+ const m=S[id]; const o=opts||{};
+ return `<div class="inst s3">
+ ${o.lever||''}
+ <div class="cap"><span class="lamp ${lampCls(m.st)}"></span> ${cap}</div>
+ <div class="val"><span class="readout">${m.v}<small> ${unit||''}</small></span></div>
+ <div class="bar ${m.st==='warn'?'warn':m.st==='ok'?'okc':''}">${o.tick?'<span class="tk" style="left:'+o.tick+'%"></span>':''}<span style="width:${pct}%"></span></div>
+ <div class="sub">${o.sub||''}</div></div>`;
+}
+function heroCount(id, cap, opts){
+ const m=S[id]; const o=opts||{};
+ return `<div class="inst s3">
+ ${o.lever||''}
+ <div class="cap"><span class="lamp ${lampCls(m.st)}"></span> ${cap}</div>
+ <div class="val"><span class="readout" ${m.st==='fail'?'style="color:var(--fail)"':m.st==='warn'&&o.tint?'style="color:var(--amber)"':''}>${m.v}${o.unit?'<small>'+o.unit+'</small>':''}</span></div>
+ <div class="sub">${o.sub||''}</div></div>`;
+}
+function heroRing(id, cap, pct, color, small, opts){
+ const m=S[id]; const o=opts||{};
+ return `<div class="inst s3">
+ ${o.lever||''}
+ <div class="cap"><span class="lamp ${lampCls(m.st)}"></span> ${cap}</div>
+ <div style="display:flex;justify-content:center;margin-top:auto">
+ <span class="ring" style="--p:${pct};--rc:${color}"><b>${m.v}</b><small>${small}</small></span></div>
+ <div class="sub" style="text-align:center">${o.sub||''}</div></div>`;
+}
+
+const CATVIEW = {
+ storage(){ return {
+ heroes:
+ heroBar('disk','Disk usage','%',S.disk.v,{sub:'root · btrfs · thr 80 / 90'})+
+ heroBar('cache','Package cache','GB',Math.min(100,S.cache.v/10*100),{lever:`<span class="lever">${lever('clean_cache','CLEAN')}</span>`,tick:100,sub:'Auto · threshold ~10 GB'})+
+ heroRing('scrub','btrfs scrub',Math.min(100,S.scrub.v/55*100),S.scrub.st==='ok'?'var(--pass)':'var(--amber)','days',{lever:`<span class="lever">${lever('scrub','SCRUB')}</span>`,sub:S.scrub.st==='ok'?'scrub fresh':'past 30d cadence'})+
+ heroCount('fstrim','fstrim.timer',{lever:`<span class="lever">${lever('fstrim','ENABLE',S.fstrim.st==='fail'?'red':'')}</span>`,sub:'Confirm · weekly SSD TRIM'}),
+ strip:
+ lrow('smart','SMART')+lrow('wear','SMART wear')+lrow('deverr','device errors')+
+ lrow('unalloc','btrfs unalloc')+lrow('integ','pacman integrity')+
+ lrow('deeptrim','deep-trim', lever('deeptrim','DEEP TRIM')+' ')
+ };},
+ packages(){ return {
+ heroes:
+ heroCount('orphans','Orphans',{lever:`<span class="lever">${lever('orphans','REMOVE')}</span>`,sub:'Confirm · unowned pkgs',tint:1})+
+ heroCount('pacnew','.pacnew files',{lever:`<span class="lever">${lever('pacnew','REVIEW')}</span>`,sub:'Confirm · config merges',tint:1}),
+ strip:
+ lrow('keyring','keyring age (d)', lever('keyring','REFRESH')+' ')+
+ lrow('reboot','reboot required')
+ };},
+ systemd(){ return {
+ heroes:
+ heroCount('failed','Failed units',{lever:`<span class="lever">${wkey()}</span>`,sub:'Workflow · investigate why',tint:1})+
+ heroCount('timers','Maint timers',{lever:`<span class="lever">${lever('timers','ENABLE')}</span>`,sub:'Confirm · paccache/scrub/fstrim/…',tint:1}),
+ strip: lrow('sysrun','is-system-running')+lrow('taint','kernel taint')
+ };},
+ logs(){ return {
+ heroes:
+ heroCount('cores','Coredumps 7d',{lever:`<span class="lever">${lever('cores','CLEAR')}</span>`,sub:'Auto · keeps last 3 days',tint:1}),
+ strip:
+ lrow('journald','journald size (GB)', lever('journald','VACUUM')+' ')+
+ lrow('applog','app-log cleanup', lever('applog','RUN')+' ')+
+ lrow('jerr','journal errors')+lrow('khw','kernel/hw events')
+ };},
+ power(){ return {
+ heroes:
+ `<div class="inst s5"><div class="cap"><span class="lamp"></span> Temperatures</div>
+ <div class="gaugepair">
+ <div class="g"><span class="ring" style="--p:61;--rc:var(--pass)"><b>61°</b></span><span class="lb">CPU</span></div>
+ <div class="g"><span class="ring" style="--p:54;--rc:var(--pass)"><b>54°</b></span><span class="lb">NVMe</span></div>
+ </div></div>`+
+ `<div class="inst s4"><div class="cap"><span class="lamp"></span> Memory free</div>
+ <div class="val"><span class="readout">104<small> GB</small></span></div>
+ <div class="bar okc"><span style="width:83%"></span></div><div class="sub">of 125 GB · 0 OOM</div></div>`+
+ `<div class="inst s3"><span class="lever">${wkey()}</span><div class="cap"><span class="lamp gold"></span> Unclean boot</div>
+ <div class="val"><span class="readout" style="color:var(--amber)">75<small>%</small></span></div>
+ <div class="bar warn"><span style="width:75%"></span></div><div class="sub">Workflow · investigate</div></div>`,
+ strip: lrow('zram','swap / zram')+lrow('throttle','thermal throttle')+lrow('battery','battery')
+ };},
+ services(){ return {
+ heroes:
+ heroCount('docker','Docker reclaim',{lever:`<span class="lever">${lever('docker','PRUNE')}</span>`,unit:' GB',sub:'Confirm · tiers 1+2',tint:1}),
+ strip:
+ lrow('rsync','rsyncshot backup')+lrow('dstopped','docker stopped')+
+ lrow('cron','cron')+lrow('libvirt','libvirt VMs')
+ };},
+ snapshots(){ return {
+ heroes:'',
+ strip:
+ lrow('snapper','snapper count', lever('snapper','PRUNE')+' ')+
+ lrow('stimer','snapshot timer')
+ };},
+ network(){ return {
+ heroes:'',
+ strip:
+ lrow('fw','firewall')+lrow('listen','unexpected listeners')+lrow('ts','tailscale')+
+ lrow('f2b','fail2ban')+lrow('ntp','NTP sync')+lrow('dns','DNS / NM')
+ };},
+};
+
+function renderCat(cid){
+ const idx = CATS.findIndex(c=>c.id===cid);
+ const cat = CATS[idx];
+ const k = counts(cat);
+ const prev = CATS[(idx+CATS.length-1)%CATS.length].id;
+ const next = CATS[(idx+1)%CATS.length].id;
+ const body = CATVIEW[cid]();
+ const cnt = k.attn ? `<span class="att ${k.crit?'crit':''}">${k.attn} attention</span> · <span class="okc">${k.ok} ok</span>`
+ : `<span class="okc">all clear · ${k.ok} ✓</span>`;
+ return `
+ <div class="crumb"><a onclick="go('home')">overview</a> ▸ ${cat.name.toLowerCase()}</div>
+ <div class="plate catplate">
+ <div class="cathead">
+ <span class="lamp ${catLamp(cat)}"></span>
+ <span class="nm">${cat.name}</span>
+ <span class="cnt">${cnt}</span>
+ <span class="navs">
+ <button class="key sm" onclick="go('cat:${prev}')">◂ PREV</button>
+ <button class="key sm" onclick="go('home')">HOME</button>
+ <button class="key sm" onclick="go('cat:${next}')">NEXT ▸</button>
+ </span>
+ </div>
+ <div class="catbody">
+ ${body.heroes?`<div class="heroes">${body.heroes}</div>`:''}
+ <div class="strip">${body.strip}</div>
+ </div>
+ </div>`;
+}
+
+/* ---- doctor (clean up) ---- */
+let docSteps = [];
+function renderDoctor(){
+ const rows = docSteps.map(s=>{
+ const lamp = s.st==='run'?'busy':s.st==='done'?'':'off';
+ return `<div class="ostep ${s.st==='wait'?'queued':''}"><span class="lamp ${lamp}"></span>
+ <span><b>${s.name}</b> <span class="ev">${s.ev}</span></span></div>`;
+ }).join('');
+ const running = docSteps.some(s=>s.st!=='done');
+ return `
+ <div class="crumb"><a onclick="go('home')">overview</a> ▸ doctor · clean up</div>
+ <div class="plate owell">
+ <div class="oh"><span>MAINT · <span class="l">ratio</span> · CLEANING ${running?'<span class="lamp busy"></span>':'<span class="lamp"></span>'}</span>
+ <span style="color:var(--dim);font-size:11px">${docSteps.length} Auto actions</span></div>
+ ${rows}
+ <div class="ofoot"><button class="key ${running?'done':''}" onclick="go('home')">DONE</button></div>
+ </div>`;
+}
+function startDoctor(){
+ clearTimeout(doctorTimer);
+ docSteps = [
+ {name:'cache trim', act:'clean_cache', st:'wait', ev:'queued',
+ res:()=> S.cache.st==='warn' ? (S.cache={v:0.4,st:'ok'}, 'done · reclaimed 8.4 GB') : 'done · nothing to reclaim'},
+ {name:'journal vacuum',act:'journald', st:'wait', ev:'queued',
+ res:()=> S.journald.v>0.5 ? (S.journald={v:0.3,st:'ok'}, 'done · reclaimed 0.9 GB') : 'done · already tight'},
+ {name:'coredump clear',act:'cores', st:'wait', ev:'queued',
+ res:()=> S.cores.v>0 ? (()=>{const n=S.cores.v; S.cores={v:0,st:'ok'}; return 'done · '+n+' cleared';})() : 'done · none to clear'},
+ {name:'app-log cleanup',act:'applog', st:'wait', ev:'queued',
+ res:()=> 'done · nothing older than 7d'},
+ ];
+ let i = 0;
+ function step(){
+ if(i>0){ docSteps[i-1].st='done'; docSteps[i-1].ev=docSteps[i-1].res(); }
+ if(i<docSteps.length){ docSteps[i].st='run'; docSteps[i].ev='running…'; i++;
+ if(view==='doctor') $('view').innerHTML=renderDoctor();
+ doctorTimer=setTimeout(step, 1000);
+ } else {
+ if(view==='doctor') $('view').innerHTML=renderDoctor();
+ toast('clean up complete');
+ }
+ }
+ step();
+}
+
+/* ---- review & fix ---- */
+const REVIEW = [
+ {act:'fstrim', who:'fstrim.timer', why:'weekly SSD TRIM disabled', id:'fstrim', okv:()=>S.fstrim.st==='ok'},
+ {act:'scrub', who:'btrfs scrub', why:'34d since last scrub (30d cadence)', id:'scrub', okv:()=>S.scrub.st==='ok'},
+ {act:'orphans', who:'orphans', why:'13 unowned packages', id:'orphans', okv:()=>S.orphans.st==='ok'},
+ {act:'pacnew', who:'.pacnew files', why:'2 configs awaiting merge', id:'pacnew', okv:()=>S.pacnew.st==='ok'},
+ {act:'timers', who:'maint timers', why:'1 of 5 not firing', id:'timers', okv:()=>S.timers.st==='ok'},
+ {act:'keyring', who:'keyring', why:'refresh before next update', id:'keyring', okv:()=>S.keyring.st==='ok'},
+ {act:'docker', who:'docker reclaim', why:'3 GB reclaimable (tiers 1+2)', id:'docker', okv:()=>S.docker.st==='ok'},
+ {act:'snapper', who:'snapper', why:'retention within budget — optional', id:'snapper', okv:()=>S.snapper.v<42},
+ {act:'deeptrim',who:'deep-trim', why:'keep 1 version — frees most, less downgrade headroom', id:'deeptrim', okv:()=>S.deeptrim.v==='keep 1'},
+];
+function renderReview(){
+ const rows = REVIEW.map(r=>{
+ const done = r.okv();
+ return `<div class="rvrow">
+ <span class="lamp ${done?'':lampCls(S[r.id].st)}"></span>
+ <span class="who">${r.who}</span><span class="why">${r.why}</span>
+ <span class="keys">${done?'<span class="res">done ✓</span>':lever(r.act, ACTIONS[r.act].arm?'FIX':'FIX')}</span>
+ </div>`;
+ }).join('');
+ return `
+ <div class="crumb"><a onclick="go('home')">overview</a> ▸ doctor · review &amp; fix</div>
+ <div class="plate owell">
+ <div class="oh"><span>MAINT · <span class="l">ratio</span> · REVIEW &amp; FIX</span>
+ <span style="color:var(--dim);font-size:11px">${REVIEW.filter(r=>!r.okv()).length} Confirm items</span></div>
+ ${rows}
+ <div class="rvnote">Destructive fixes (orphan removal, deep-trim, prunes) arm on first press and fire on the
+ second. Workflow items — failed units, unclean boots, updates, CVEs — are deliberately absent here: they
+ escalate to the health-check workflow.</div>
+ <div class="ofoot"><button class="key" onclick="go('home')">DONE</button></div>
+ </div>`;
+}
+
+/* ================= router ================= */
+function go(v){
+ armed = null;
+ view = v;
+ if(v==='doctor') startDoctor();
+ render();
+ window.scrollTo({top:0, behavior:'smooth'});
+}
+function render(){
+ $('homekey').classList.toggle('on', view==='home');
+ const el = $('view');
+ if(view==='home') el.innerHTML = renderHome();
+ else if(view==='doctor') el.innerHTML = renderDoctor();
+ else if(view==='review') el.innerHTML = renderReview();
+ else if(view.startsWith('cat:')) el.innerHTML = renderCat(view.slice(4));
+}
+render();
+</script>
+</body>
+</html>
diff --git a/docs/prototypes/2026-07-07-maint-console-E5-selector-subpanel.html b/docs/prototypes/2026-07-07-maint-console-E5-selector-subpanel.html
new file mode 100644
index 0000000..95ec4c9
--- /dev/null
+++ b/docs/prototypes/2026-07-07-maint-console-E5-selector-subpanel.html
@@ -0,0 +1,1696 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>MAINT · ratio — E5 · Selector + dense subpanel</title>
+<style>
+:root{
+ --ground:#151311; --panel:#100f0f; --well:#0a0c0d; --raise:#1a1917;
+ --gold:#dab53d; --gold-hi:#ffd75f; --silver:#bfc4d0; --cream:#f3e7c5;
+ --steel:#969385; --dim:#7c838a; --slate:#424f5e; --slate-hi:#54677d;
+ --wash:#2c2f32; --pass:#74932f; --amber:#dab53d; --fail:#cb6b4d;
+ --mono:"BerkeleyMono Nerd Font","Berkeley Mono",monospace;
+}
+*{box-sizing:border-box;margin:0;padding:0}
+html{background:var(--ground)}
+body{font-family:var(--mono);color:var(--silver);line-height:1.45;
+ padding:1.6rem 1.2rem 4rem;
+ background:radial-gradient(1200px 600px at 70% -10%,#1c1915 0%,transparent 60%),var(--ground);
+ display:flex;justify-content:center;align-items:flex-start}
+
+.capsule{width:960px;max-width:100%;background:var(--panel);color:var(--silver);
+ border:1.6px solid var(--gold);border-radius:16px;padding:17px 19px;
+ box-shadow:0 18px 50px rgba(0,0,0,.55);position:relative}
+
+/* ---- primitives ---- */
+.lamp{width:10px;height:10px;border-radius:50%;background:var(--pass);
+ box-shadow:0 0 6px 1px rgba(116,147,47,.55);display:inline-block;flex:none}
+.lamp.gold{background:var(--gold);box-shadow:0 0 6px 1px rgba(218,181,61,.6)}
+.lamp.red{background:var(--fail);box-shadow:0 0 7px 1px rgba(203,107,77,.6)}
+.lamp.off{background:var(--wash);box-shadow:none}
+.lamp.busy{background:var(--gold);animation:pulse .7s ease-in-out infinite;
+ box-shadow:0 0 6px 1px rgba(218,181,61,.6)}
+@keyframes pulse{50%{opacity:.25}}
+
+.key{font:inherit;font-size:11.5px;letter-spacing:.06em;color:var(--silver);cursor:pointer;
+ background:linear-gradient(180deg,#23211e,#191715);border:1px solid #33302b;border-bottom-color:#0c0b0a;
+ border-radius:8px;padding:6px 11px;box-shadow:inset 0 1px 0 rgba(255,255,255,.04),0 2px 3px rgba(0,0,0,.4);
+ white-space:nowrap}
+.key:hover{color:var(--gold);border-color:var(--gold)}
+.key:active{transform:translateY(1px)}
+.key.on{color:var(--panel);background:linear-gradient(180deg,#f0d879,var(--gold));border-color:var(--gold-hi);font-weight:700}
+.key.red{color:var(--cream);background:linear-gradient(180deg,#d98a6f,var(--fail));border-color:var(--fail)}
+.key.armed{background:rgba(203,107,77,.15);border-color:var(--fail);color:var(--fail)}
+.key.dis{opacity:.3;cursor:not-allowed}
+.key.dis:hover{color:var(--silver);border-color:#33302b}
+.key.sm{font-size:10px;padding:3px 8px;border-radius:7px}
+
+.badge{font-size:.6rem;letter-spacing:.16em;color:var(--panel);background:var(--gold);border-radius:4px;padding:1px 6px;white-space:nowrap}
+.badge.red{background:var(--fail);color:var(--cream)}
+.badge.ghost{background:transparent;border:1px solid var(--slate);color:var(--silver)}
+.badge.pass{background:var(--pass);color:var(--cream)}
+
+.bar{width:100%;height:11px;background:#0d0f10;border:1px solid #231f18;border-radius:6px;overflow:hidden;position:relative}
+.bar>span{position:absolute;left:0;top:0;bottom:0;background:linear-gradient(90deg,#8a7524,var(--gold));border-radius:6px;transition:width .5s ease}
+.bar.warn>span{background:linear-gradient(90deg,#a35a3f,var(--fail))}
+.bar.okc>span{background:linear-gradient(90deg,#5c7626,var(--pass))}
+.bar .tk{position:absolute;top:-2px;bottom:-2px;width:2px;background:var(--gold-hi)}
+
+.ring{width:54px;height:54px;border-radius:50%;
+ background:conic-gradient(var(--rc,var(--gold)) calc(var(--p)*1%),var(--wash) 0);
+ display:grid;place-items:center;position:relative;flex:none}
+.ring::before{content:"";position:absolute;inset:6px;border-radius:50%;background:var(--well)}
+.ring b{position:relative;color:var(--cream);font-size:12px;font-weight:700;font-variant-numeric:tabular-nums;line-height:1}
+.ring small{position:relative;color:var(--dim);font-size:8px}
+
+.readout{color:var(--cream);font-size:21px;font-weight:700;font-variant-numeric:tabular-nums;letter-spacing:.03em;line-height:1}
+.readout small{color:var(--dim);font-size:11px;font-weight:400}
+
+.engrave{color:var(--steel);font-size:.64rem;letter-spacing:.26em;text-transform:uppercase;
+ display:flex;align-items:center;gap:10px}
+.engrave::after{content:"";height:1px;background:var(--wash);flex:1}
+.engrave .cnt{color:var(--dim);letter-spacing:.08em;text-transform:none;font-size:.68rem}
+
+.lrow{display:flex;align-items:center;gap:8px;padding:5px 8px;border-radius:7px;background:#141210;font-size:12px}
+.lrow .who{color:var(--silver)}
+.lrow .what{margin-left:auto;color:var(--dim);font-size:10.5px;display:flex;align-items:center;gap:7px}
+
+.ladder{display:inline-flex;gap:3px;align-items:flex-end;height:16px}
+.ladder i{width:5px;background:var(--wash);border-radius:1px}
+.ladder i:nth-child(1){height:5px}.ladder i:nth-child(2){height:9px}
+.ladder i:nth-child(3){height:12px}.ladder i:nth-child(4){height:16px}
+.ladder i.on{background:var(--pass)}
+.ladder.bad i.on{background:var(--fail)}
+
+/* faceplate */
+.face{display:flex;align-items:center;gap:11px;padding:11px 13px;margin-bottom:11px;
+ background:linear-gradient(180deg,var(--raise),var(--panel));border:1px solid #262320;border-radius:11px;
+ box-shadow:inset 0 1px 0 rgba(255,255,255,.04)}
+.face .word{color:var(--cream);font-size:15.5px;font-weight:700;letter-spacing:.22em}
+.face .spacer{flex:1}
+.face .unit{color:var(--steel);font-size:.72rem;letter-spacing:.1em}
+.face .xbtn{width:24px;height:24px;border-radius:50%;border:1px solid #33302b;background:transparent;
+ color:var(--dim);cursor:pointer;font-size:12px;line-height:1;display:grid;place-items:center;font-family:var(--mono)}
+.face .xbtn:hover{color:var(--fail);border-color:var(--fail)}
+
+.ctlrow{display:flex;align-items:center;gap:8px;margin-bottom:11px;padding:0 2px}
+.ctlrow .lbl{color:var(--steel);font-size:.6rem;letter-spacing:.22em;text-transform:uppercase;margin-right:3px}
+.ctlrow .spacer{flex:1}
+
+.updbar{display:flex;align-items:center;gap:13px;padding:7px 12px;margin-bottom:12px;
+ border:1px solid var(--wash);border-radius:9px;background:linear-gradient(180deg,#141310,#0a0c0d);font-size:11.5px}
+.updbar .q{color:var(--dim);font-size:.58rem;letter-spacing:.2em;text-transform:uppercase;
+ border:1px solid var(--wash);border-radius:5px;padding:1px 6px}
+.updbar.ok{border-color:var(--pass)} .updbar.ok .q{color:var(--pass);border-color:var(--pass)}
+.updbar.warn{border-color:var(--amber)} .updbar.warn .q{color:var(--amber);border-color:var(--amber)}
+.updbar.crit{border-color:var(--fail)} .updbar.crit .q{color:var(--fail);border-color:var(--fail)}
+.updbar b{color:var(--cream);font-variant-numeric:tabular-nums}
+.updbar .cve{color:var(--fail);font-weight:700}
+.updbar .spacer{flex:1}
+.updbar .dimtx{color:var(--dim);font-size:10.5px}
+
+/* selector */
+.selector{display:grid;grid-template-columns:repeat(4,1fr);gap:7px;margin-bottom:13px}
+.selbtn{display:flex;align-items:center;gap:8px;padding:8px 10px;cursor:pointer;border-radius:9px;
+ background:linear-gradient(180deg,#191715,#131110);border:1px solid #262320;border-left:3px solid var(--wash);
+ transition:border-color .1s}
+.selbtn:hover{border-color:#3a352c}
+.selbtn .nm{color:var(--silver);font-size:11.5px;font-weight:700;letter-spacing:.02em}
+.selbtn .ct{margin-left:auto;font-size:10px;color:var(--dim);font-variant-numeric:tabular-nums;white-space:nowrap}
+.selbtn .ct .a{color:var(--amber)} .selbtn .ct .a.crit{color:var(--fail)} .selbtn .ct .k{color:var(--pass)}
+.selbtn.attn{border-left-color:var(--amber)}
+.selbtn.crit{border-left-color:var(--fail)}
+.selbtn.clear{border-left-color:var(--pass)}
+.selbtn.loading{border-left-color:var(--amber)}
+.selbtn.sel{background:linear-gradient(180deg,#4a5768,var(--slate));border-color:var(--gold);border-left-width:3px}
+.selbtn.sel .nm{color:var(--gold)}
+.selbtn.sel .ct, .selbtn.sel .ct .a, .selbtn.sel .ct .k{color:var(--cream)}
+
+/* subpanel */
+.sub{background:var(--well);border:1px solid #201d17;border-radius:11px;padding:13px 14px;min-height:330px}
+.subhead{display:flex;align-items:center;gap:10px;margin-bottom:12px}
+.subhead .nm{color:var(--gold);font-weight:700;letter-spacing:.16em;text-transform:uppercase;font-size:12.5px}
+.subhead .cnt{color:var(--dim);font-size:11px;margin-left:auto;display:flex;gap:9px;align-items:center}
+.subhead .cnt .att{color:var(--amber)} .subhead .cnt .att.crit{color:var(--fail)} .subhead .cnt .okc{color:var(--pass)}
+
+.wgrid{display:grid;grid-template-columns:repeat(4,1fr);gap:9px;margin-bottom:10px}
+.cell{background:#100f0e;border:1px solid #1f1c18;border-radius:9px;padding:9px 10px;
+ display:flex;flex-direction:column;gap:7px;position:relative;min-height:76px}
+.cell .cap{color:var(--steel);font-size:.58rem;letter-spacing:.14em;text-transform:uppercase;
+ display:flex;align-items:center;gap:6px;padding-right:56px}
+.cell .lever{position:absolute;top:7px;right:7px}
+.cell .val{display:flex;align-items:baseline;gap:7px;margin-top:auto}
+.cell .sub2{color:var(--dim);font-size:10px}
+.cell .center{display:flex;justify-content:center;margin-top:auto}
+.gpair{display:flex;gap:14px;align-items:center;justify-content:center;margin-top:auto}
+.gpair .g{display:flex;flex-direction:column;align-items:center;gap:2px}
+.gpair .g .lb{color:var(--steel);font-size:8.5px;letter-spacing:.1em}
+
+.striph{margin:9px 0 7px}
+.strip{display:grid;grid-template-columns:repeat(3,1fr);gap:5px 9px}
+.strip.two{grid-template-columns:repeat(2,1fr)}
+
+/* segmented selector (gallery widget 06) */
+.seg{display:flex;border:1px solid #33302b;border-radius:8px;overflow:hidden;width:max-content}
+.seg button{font:inherit;font-size:10px;color:var(--silver);background:#191715;border:0;
+ border-right:1px solid #33302b;padding:5px 9px;cursor:pointer;letter-spacing:.06em}
+.seg button:last-child{border-right:0}
+.seg button:hover{color:var(--gold)}
+.seg button.on{background:linear-gradient(180deg,#f0d879,var(--gold));color:var(--panel);font-weight:700}
+
+/* evidence column headers */
+.evh{color:var(--steel);font-size:.58rem;letter-spacing:.14em;text-transform:uppercase;margin-bottom:4px}
+.colh{display:flex;align-items:center;gap:8px;min-width:0}
+.colh .engrave{flex:1;min-width:0}
+
+/* ===== rotary band selector (amplifier input-selector idiom) ===== */
+.deck{display:flex;align-items:center;gap:22px;margin:13px 4px 11px;padding:9px 14px;
+ background:linear-gradient(180deg,#161412,#100f0e);border:1px solid #201d17;border-radius:10px}
+.knob{width:46px;height:46px;border-radius:50%;position:relative;cursor:pointer;flex:none;
+ background:radial-gradient(circle at 40% 35%,#2a2622,#141210);border:1px solid #3a352c;
+ box-shadow:inset 0 2px 3px rgba(255,255,255,.05),0 3px 6px rgba(0,0,0,.5)}
+.knob:hover{border-color:var(--gold)}
+.knob .ind{position:absolute;left:50%;top:4px;width:2px;height:14px;background:var(--gold-hi);
+ margin-left:-1px;transform-origin:50% 19px;border-radius:1px;
+ box-shadow:0 0 5px rgba(255,215,95,.6);transition:transform .25s cubic-bezier(.3,1.3,.5,1)}
+.bands{display:flex;gap:26px;align-items:flex-end}
+.band{display:flex;flex-direction:column;align-items:center;gap:4px;cursor:pointer;user-select:none}
+.band .lamp{width:7px;height:7px}
+.band .bl{color:var(--steel);font-size:.6rem;letter-spacing:.18em;text-transform:uppercase;white-space:nowrap}
+.band:hover .bl{color:var(--silver)}
+.band.on .bl{color:var(--cream)}
+.band .ul{height:2px;width:100%;border-radius:1px;background:transparent;transition:background .15s}
+.band.on .ul{background:var(--gold);box-shadow:0 0 6px rgba(218,181,61,.5)}
+
+/* journal digest rows — capped at ~3.5 rows; the half row signals scrollability */
+/* rows are fixed 30px so the 3.5-row cap is exact: 3×30 + 3×4 gap + 15 half = 117 */
+.jlist{display:flex;flex-direction:column;gap:4px;max-height:117px;overflow-y:auto;padding-right:4px}
+.jlist .jrow{height:30px;min-height:30px;padding:2px 8px;flex:none}
+
+/* dark scrollbars, consistent with the panel family (slate thumb on dark track) */
+.capsule ::-webkit-scrollbar{width:8px;height:8px}
+.capsule ::-webkit-scrollbar-track{background:#0a0c0d;border-radius:4px}
+.capsule ::-webkit-scrollbar-thumb{background:#2c2f32;border-radius:4px}
+.capsule ::-webkit-scrollbar-thumb:hover{background:#54677d}
+.capsule *{scrollbar-width:thin;scrollbar-color:#2c2f32 #0a0c0d}
+.jrow{gap:8px}
+.jrow .jmsg{color:var(--dim);font-size:10.5px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0}
+.jrow .what{margin-left:0;flex:none}
+.jrow.known .who{color:var(--dim)}
+
+.skel{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:12px;
+ min-height:260px;color:var(--dim);font-size:11px;letter-spacing:.24em;text-transform:uppercase}
+
+/* doctor / review */
+.ostep{display:flex;gap:9px;align-items:flex-start;padding:4px 0;font-size:12.5px}
+.ostep .lamp{margin-top:3px;width:8px;height:8px}
+.ostep b{color:var(--cream);font-weight:700}
+.ostep .ev{color:var(--steel);font-size:11px;display:block}
+.ostep .ev.evfail{color:var(--fail)}
+.ostep.queued b{color:var(--dim)}
+.ofoot{margin-top:12px;display:flex;justify-content:flex-end;gap:8px}
+/* review list: fixed 32px rows + 5px gap (37 stride); cap = 8.5 rows so the
+ panel holds its shape and the half row cues the scroll */
+.rvlist{max-height:312px;overflow-y:auto;padding-right:4px}
+.rvrow{display:flex;align-items:center;gap:10px;padding:2px 10px;border-radius:8px;background:#141210;
+ font-size:12px;margin-bottom:5px;height:32px;min-height:32px}
+.rvrow .who{color:var(--cream);font-weight:700}
+.rvrow .why{color:var(--dim);font-size:10.5px}
+.rvrow .why.errtx{color:var(--fail)}
+.rvrow .keys{margin-left:auto;display:flex;gap:7px;align-items:center}
+.rvrow .res{color:var(--pass);font-size:11px}
+.rvnote{color:var(--dim);font-size:10.5px;margin:9px 2px 0;line-height:1.5}
+
+.errline{color:var(--fail);font-size:10px;line-height:1.35;margin-top:3px}
+
+/* results wall (output-well idiom from the net/bt panels) */
+.rwall{margin-top:12px;background:var(--well);border:1px solid #201d17;border-radius:11px;padding:10px 14px 12px}
+.rwall .rhead{display:flex;align-items:center;gap:10px;margin-bottom:4px}
+.rwall .rhead .keys{margin-left:auto;display:flex;gap:6px}
+/* rows fixed 30px so the 3.5-entry cap is exact: 3.5 × 30 = 105 */
+.rwall .rbody{max-height:105px;overflow-y:auto;padding-right:4px}
+.rwall .rstep{display:flex;gap:9px;align-items:center;height:30px;min-height:30px;font-size:12px}
+.rwall .rstep .tm{color:var(--dim);font-size:10px;font-variant-numeric:tabular-nums;flex:none;
+ display:flex;flex-direction:column;line-height:1.2}
+.rwall .rstep .tm .dt{font-size:8.5px;color:#5d636a;letter-spacing:.04em}
+.rwall .rstep .lamp{width:8px;height:8px}
+.rwall .rstep .rtxt{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
+.rwall .rstep b{color:var(--cream);font-weight:700}
+.rwall .rstep .ev{color:var(--steel);font-size:11px}
+.rwall .rstep .ev.evfail{color:var(--fail)}
+
+#toast{position:absolute;bottom:16px;right:18px;display:flex;flex-direction:column;gap:8px;z-index:9;
+ align-items:flex-end;pointer-events:none;max-width:70%}
+.toastw{font-size:11.5px;color:var(--cream);background:var(--slate);border-radius:7px;padding:6px 11px;
+ box-shadow:0 4px 12px rgba(0,0,0,.5);animation:tin .18s ease}
+.toastw.err{background:#5e3a2e}
+@keyframes tin{from{opacity:0;transform:translateY(6px)}}
+
+#closed{display:none;color:var(--dim);font-size:12px;border:1px dashed var(--wash);
+ border-radius:10px;padding:14px 20px;cursor:pointer}
+#closed:hover{color:var(--gold);border-color:var(--gold)}
+
+/* page-level prototype chrome (NOT part of the panel) */
+.pagectl{max-width:960px;margin:0 0 12px;display:flex;align-items:center;gap:8px;
+ color:var(--dim);font-size:10.5px;letter-spacing:.1em;text-transform:uppercase}
+.footnote{color:var(--dim);font-size:10.5px;margin-top:14px;line-height:1.55;max-width:960px}
+@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
+</style>
+</head>
+<body>
+<div>
+
+<!-- ===== prototype controls (page level, not panel UI) ===== -->
+<div class="pagectl">
+ prototype controls —
+ <button class="key sm" id="k-fail" onclick="toggleFail()" title="make every action fail so the error UX is visible">SIM FAIL</button>
+ <button class="key sm" id="k-bad" onclick="toggleBad()" title="load the degraded-machine snapshot: every metric in its warn/fail state">SIM BAD DAY</button>
+ <button class="key sm" id="k-zfs" onclick="toggleZfs()" title="show the velox/ZFS storage capability set instead of btrfs">SIM ZFS</button>
+ <button class="key sm" onclick="reloadPanel()" title="replay the open-and-hydrate sequence">SIM RELOAD</button>
+</div>
+
+<div class="capsule" id="capsule">
+
+ <!-- faceplate -->
+ <div class="face">
+ <span class="lamp" id="facelamp"></span>
+ <span class="word">MAINTENANCE</span>
+ <span class="badge red" id="cvebadge">3 CVE</span>
+ <span class="badge ghost" id="attbadge">9 ATTN</span>
+ <span class="badge red" id="errbadge" style="display:none">0 FAILED</span>
+ <span class="spacer"></span>
+ <span class="unit">MNT·01</span>
+ <button class="xbtn" title="Close (Esc)" onclick="closePanel()">✕</button>
+ </div>
+
+ <!-- doctor control row -->
+ <div class="ctlrow">
+ <span class="lbl">Doctor</span>
+ <button class="key" id="k-clean" onclick="go('doctor')">CLEAN UP</button>
+ <button class="key" id="k-review" onclick="go('review')">REVIEW &amp; FIX</button>
+ <span class="spacer"></span>
+ <span class="lbl" style="margin-right:0" id="lastscan">last scan 07:41 · 44 checks · 0.9s</span>
+ </div>
+
+ <!-- updates quarantine -->
+ <div class="updbar" id="updbar">
+ <span class="q">Updates</span>
+ <span><b id="upd-p">47</b> pending</span>
+ <span class="cve" id="upd-c">3 CVE</span>
+ <span><b id="upd-a">5</b> AUR stale</span>
+ <span><b id="upd-f">0</b> firmware</span>
+ <span class="dimtx">mesa · linux-lts · systemd — guard-checked before any apply</span>
+ <span class="spacer"></span>
+ <span id="upd-rb"></span>
+ <button class="key sm" id="k-update" onclick="fire('sysupdate')">UPDATE</button>
+ <button class="key sm" id="k-topgrade" onclick="fire('topgrade')">TOPGRADE</button>
+ </div>
+
+ <!-- persistent selector -->
+ <div class="selector" id="selector"></div>
+
+ <!-- swapped subpanel -->
+ <div class="sub" id="sub"></div>
+
+ <!-- results wall: appears on first action; toggle / copy / close -->
+ <div class="rwall" id="rwall" style="display:none">
+ <div class="rhead">
+ <span class="engrave" style="flex:1">Results<span class="cnt" id="rcount">· 0</span></span>
+ <span class="keys">
+ <button class="key sm" id="rtoggle" onclick="toggleWall()">HIDE</button>
+ <button class="key sm" onclick="copyWall()">COPY</button>
+ </span>
+ </div>
+ <div class="rbody" id="rbody"></div>
+ </div>
+
+ <div id="toast"></div>
+</div>
+
+<div id="closed" onclick="openPanel()">MNT·01 closed — click to reopen (waybar: click the maint tag)</div>
+
+<div class="footnote">
+ Prototype notes (not part of the panel): selector stays put — the gold/slate tile is the selection; the
+ subpanel below swaps. ✕ or Esc closes (chip reopens; reopening replays the hydrate sequence). CLEAN UP
+ streams into the subpanel and mutates real state; REVIEW &amp; FIX lists every Confirm lever, destructive
+ ones arm-then-fire. Metrics without a determinate lever are read-only telemetry (agent/workflow assistance
+ is vLater). SIM FAIL makes every action fail (red lamp, error line, RETRY key, FAILED badge); SIM BAD DAY
+ loads a degraded-machine snapshot so every metric shows its warn/fail face, doctor or not; SIM RELOAD
+ replays the open: tiles pulse amber per tier (local reads land first, process probes next, package scans
+ last) and settle individually — cached values would fill the board instantly in the real build, with lamps
+ confirming freshness as probes return.
+</div>
+</div>
+
+<script>
+/* ================= state snapshots ================= */
+function GOOD(){ return {
+ cache:{v:8.8, st:'warn'}, scrub:{v:34, st:'warn'}, fstrim:{v:'OFF', st:'fail'},
+ disk:{v:69, st:'ok'}, smart:{v:'PASSED', st:'ok'}, wear:{v:0, st:'ok'},
+ deverr:{v:'0 / 0', st:'ok'}, unalloc:{v:'118', st:'ok'}, integ:{v:'clean', st:'ok'},
+ deeptrim:{v:'keep 3', st:'ok'}, smartlast:'passed 2026-07-01', devrows:[],
+ dtop:[['~/videos','620 GB'],['~/music','240 GB'],['~/pictures','180 GB'],['/var/lib/docker','38 GB'],['~/.cache','22 GB']],
+ orphans:{v:13, st:'warn'}, pacnew:{v:2, st:'warn'}, keyring:{v:12, st:'ok'}, reboot:{v:'NO', st:'ok'},
+ orphlist:[['rust','1.2 GB'],['electron28','210 MB'],['python-build','84 MB'],['go-tools','61 MB'],
+ ['libplacebo','44 MB'],['ocaml','38 MB'],['ruby-rake','21 MB'],['libfoo','12 MB'],['perl-clone','9 MB'],
+ ['xcb-util-old','7 MB'],['libdazzle','6 MB'],['gtest','5 MB'],['orc-old','4 MB']],
+ pacnews:[{f:'/etc/pacman.d/mirrorlist.pacnew', safe:true, why:'reflector-managed'},
+ {f:'/etc/locale.gen.pacnew', safe:false, why:'needs merge'}],
+ cves:[['openssh','CVE-2026-2892','high'],['mesa','CVE-2026-1104','medium'],['libxml2','CVE-2026-0517','low']],
+ aurnames:'zoom · spotify · ttf-berkeley-mono +2', fwnames:'',
+ failed:{v:1, st:'warn'}, timers:{v:'4/5', st:'warn'}, sysrun:{v:'running', st:'ok'}, taint:{v:0, st:'ok'},
+ funits:[{u:'systemd-oomd', since:'09:12', info:'exit 217'}],
+ cores:{v:18, st:'warn'}, journald:{v:1.2, st:'ok'}, applog:{v:'ok', st:'ok'},
+ jerr:{v:12, st:'ok'}, khw:{v:'clean', st:'ok'},
+ coregroups:[['chrome',12,'13:02'],['hyprland',3,'06-07'],['hypridle',2,'11:40'],['dunst',1,'10:05']],
+ khwev:[],
+ jgroups:[
+ {id:'bluetoothd', msg:'Hands-Free Voice gateway connect failed', n:26, first:'07:02', last:'13:58', unit:'bluetooth.service'},
+ {id:'pixman', msg:'_pixman_log_error: BadDrawable', n:140, first:'07:02', last:'13:59', unit:null},
+ {id:'xkbcomp', msg:'Errors from xkbcomp are not fatal', n:8, first:'07:02', last:'12:10', unit:null},
+ {id:'insync', msg:'QSocketNotifier: invalid socket', n:6, first:'08:15', last:'13:20', unit:null},
+ {id:'gammastep', msg:'Wayland connection experienced a fatal error', n:3, first:'07:02', last:'07:03', unit:'gammastep.service'},
+ {id:'hypridle', msg:'Failed to inhibit idle', n:2, first:'09:41', last:'11:12', unit:'hypridle.service'},
+ {id:'dunst', msg:'gdk_pixbuf assertion failure', n:1, first:'10:05', last:'10:05', unit:'dunst.service'},
+ ],
+ temps:{v:'61/54', st:'ok'}, mem:{v:104, st:'ok'}, unclean:{v:75, st:'warn'},
+ zram:{v:'16 GiB', st:'ok'}, throttle:{v:'NO', st:'ok'}, battery:{v:'n/a', st:'off'},
+ cpumode:{v:'power', st:'ok'}, batt:{v:94, st:'ok'}, blimit:{v:'none', st:'warn'},
+ memtop:[['chrome','4.2 GB'],['emacs','1.9 GB'],['insync','0.9 GB'],['waybar','0.3 GB'],['mpd','0.2 GB']],
+ boots:[['07-07 07:01','clean'],['07-06 08:12','unclean'],['07-05 09:45','unclean'],['07-04 07:58','clean'],['07-03 08:30','unclean']],
+ pevents:[],
+ docker:{v:3, st:'warn'}, rsync:{v:'3h', st:'ok'}, dstopped:{v:2, st:'ok'},
+ cron:{v:'ok', st:'ok'}, libvirt:{v:'off', st:'off'},
+ ddf:[['images','14.2 GB','1.8 GB'],['containers','0.4 GB','0.1 GB'],['volumes','6.8 GB','0.3 GB'],['build cache','0.8 GB','0.8 GB']],
+ ctrs:[{name:'winvm', when:'2d ago', code:0},{name:'deepsat-pg', when:'6h ago', code:0}],
+ snapper:{v:42, st:'ok'}, stimer:{v:'active', st:'ok'},
+ snaptypes:{timeline:31, single:8, prepost:3, oldsingle:'2026-06-12'},
+ oldest:{v:8, st:'ok'}, retention:{v:'H6 D7 W2 M2 Y0', st:'ok'},
+ fw:{v:'active', st:'ok'}, listen:{v:0, st:'ok'}, ts:{v:'4/4', st:'ok'}, tsdown:[],
+ lsn:[
+ {proc:'sshd', port:22, bind:'0.0.0.0', unit:'sshd'},
+ {proc:'mpd', port:6600, bind:'127.0.0.1', unit:'mpd'},
+ {proc:'tailscaled', port:41641, bind:'0.0.0.0', unit:'tailscaled'},
+ ],
+ f2b:{v:0, st:'ok'}, ntp:{v:'synced', st:'ok'}, dns:{v:'ok', st:'ok'},
+ tgage:{v:9, st:'warn'},
+ upd:{p:47, c:3, a:5, f:0},
+ zcap:{v:62, st:'ok'}, zfrag:{v:31, st:'ok'}, ztrim:{v:'OFF', st:'fail'},
+ zhealth:{v:'ONLINE', st:'ok'}, zerr:{v:'0 / 0 / 0', st:'ok'},
+};}
+function BAD(){ return {
+ cache:{v:17, st:'warn'}, scrub:{v:74, st:'fail'}, fstrim:{v:'OFF', st:'fail'},
+ disk:{v:92, st:'fail'}, smart:{v:'FAILING', st:'fail'}, wear:{v:91, st:'warn'},
+ deverr:{v:'14 / 2', st:'fail'}, unalloc:{v:'9', st:'warn'}, integ:{v:'3 modified', st:'warn'},
+ deeptrim:{v:'never run', st:'warn'}, smartlast:'failed 2026-07-06 — reallocated sectors',
+ devrows:[['nvme0n1','12 read · 2 write · 3 corruption'],['nvme1n1','2 read · 0 write · 1 corruption']],
+ dtop:[['~/videos','1.4 TB'],['/var/lib/docker','160 GB'],['~/music','240 GB'],['~/.cache','96 GB'],['~/downloads','88 GB']],
+ orphans:{v:51, st:'warn'}, pacnew:{v:7, st:'warn'}, keyring:{v:94, st:'warn'}, reboot:{v:'YES', st:'warn'},
+ orphlist:[['rust','1.2 GB'],['cuda-11','2.8 GB'],['electron28','210 MB'],['electron25','195 MB'],
+ ['python-build','84 MB'],['go-tools','61 MB'],['libplacebo','44 MB'],['ocaml','38 MB'],
+ ['texlive-extra','1.1 GB'],['ruby-rake','21 MB'],['libfoo','12 MB'],['perl-clone','9 MB']],
+ pacnews:[{f:'/etc/pacman.d/mirrorlist.pacnew', safe:true, why:'reflector-managed'},
+ {f:'/etc/sudoers.pacnew', safe:false, why:'needs merge — review carefully'},
+ {f:'/etc/ssh/sshd_config.pacnew', safe:false, why:'needs merge'},
+ {f:'/etc/locale.gen.pacnew', safe:false, why:'needs merge'}],
+ cves:[['openssh','CVE-2026-2892','high'],['sudo','CVE-2026-3011','high'],['mesa','CVE-2026-1104','medium'],
+ ['curl','CVE-2026-2440','medium'],['libxml2','CVE-2026-0517','low']],
+ aurnames:'zoom · spotify · ttf-berkeley-mono · paru-git +8', fwnames:'UEFI 3.07→3.09 · NVMe fw',
+ failed:{v:4, st:'fail'}, timers:{v:'2/5', st:'fail'}, sysrun:{v:'degraded', st:'fail'}, taint:{v:'W', st:'warn'},
+ funits:[{u:'systemd-oomd', since:'09:12', info:'exit 217'},
+ {u:'fail2ban', since:'07:44', info:'exit 1 — bad jail'},
+ {u:'cronie', since:'07:02', info:'exit 203'},
+ {u:'smartd', since:'07:02', info:'exit 1'}],
+ cores:{v:112, st:'fail'}, journald:{v:3.8, st:'warn'}, applog:{v:'stale', st:'warn'},
+ jerr:{v:214, st:'warn'}, khw:{v:'MCE + I/O', st:'fail'},
+ coregroups:[['chrome',61,'13:41'],['hyprland',23,'12:15'],['electron',17,'11:03'],['dunst',11,'13:10']],
+ khwev:[['12:44','MCE: CPU 3 bank 5 — corrected'],['11:02','nvme0n1: I/O error, sector 88121'],['10:58','thermal: package throttle 91°C']],
+ jgroups:[
+ {id:'bluetoothd', msg:'Hands-Free Voice gateway connect failed', n:31, first:'07:02', last:'13:58', unit:'bluetooth.service'},
+ {id:'pixman', msg:'_pixman_log_error: BadDrawable', n:180, first:'07:02', last:'13:59', unit:null},
+ {id:'xkbcomp', msg:'Errors from xkbcomp are not fatal', n:9, first:'07:02', last:'12:10', unit:null},
+ {id:'systemd-oomd', msg:'Failed to acquire memory pressure info', n:88, first:'07:05', last:'13:55', unit:'systemd-oomd.service'},
+ {id:'wpa_supplicant', msg:'bgscan simple: Failed to enable signal monitoring', n:61, first:'07:02', last:'13:59', unit:'wpa_supplicant.service'},
+ {id:'hyprland', msg:'GLES2 framebuffer incomplete', n:31, first:'08:11', last:'13:40', unit:null},
+ {id:'rsyncshot', msg:'rsync error: partial transfer (code 23)', n:23, first:'02:30', last:'13:30', unit:'rsyncshot.service'},
+ {id:'dunst', msg:'gdk_pixbuf assertion failure', n:11, first:'07:20', last:'13:10', unit:'dunst.service'},
+ ],
+ temps:{v:'91/78', st:'fail'}, mem:{v:6, st:'warn'}, unclean:{v:82, st:'warn'},
+ zram:{v:'none', st:'warn'}, throttle:{v:'YES', st:'fail'}, battery:{v:'n/a', st:'off'},
+ cpumode:{v:'perf', st:'ok'}, batt:{v:71, st:'warn'}, blimit:{v:'none', st:'warn'},
+ memtop:[['chrome','38.4 GB'],['java (deepsat)','22.1 GB'],['emacs','2.1 GB'],['Hyprland','1.9 GB'],['insync','1.4 GB'],['dockerd','1.1 GB']],
+ boots:[['07-07 07:05','unclean'],['07-06 21:12','unclean'],['07-06 08:12','unclean'],['07-05 09:45','clean'],['07-04 07:58','unclean']],
+ pevents:[['13:41','OOM killed chrome (38 GB)'],['12:20','thermal throttle 91°C · 340 s'],['09:15','OOM killed java (22 GB)']],
+ docker:{v:22, st:'warn'}, rsync:{v:'52h', st:'fail'}, dstopped:{v:5, st:'warn'},
+ cron:{v:'down', st:'fail'}, libvirt:{v:'off', st:'off'},
+ ddf:[['images','38.1 GB','18.2 GB'],['containers','1.2 GB','0.4 GB'],['volumes','9.8 GB','2.1 GB'],['build cache','1.3 GB','1.3 GB']],
+ ctrs:[{name:'winvm', when:'2d ago', code:0},{name:'deepsat-pg', when:'6h ago', code:0},
+ {name:'jellyfin', when:'2h ago', code:137},{name:'pihole', when:'9h ago', code:1},
+ {name:'registry', when:'3d ago', code:0}],
+ snapper:{v:214, st:'warn'}, stimer:{v:'inactive', st:'fail'},
+ snaptypes:{timeline:62, single:140, prepost:12, oldsingle:'2026-01-03'},
+ oldest:{v:194, st:'fail'}, retention:{v:'H6 D7 W2 M10 Y2', st:'warn'},
+ fw:{v:'inactive', st:'fail'}, listen:{v:9, st:'warn'}, ts:{v:'1/4', st:'fail'},
+ tsdown:['truenas','worker','cjennings'],
+ lsn:[
+ {proc:'sshd', port:22, bind:'0.0.0.0', unit:'sshd'},
+ {proc:'mpd', port:6600, bind:'127.0.0.1', unit:'mpd'},
+ {proc:'tailscaled', port:41641, bind:'0.0.0.0', unit:'tailscaled'},
+ {proc:'python', port:8000, bind:'0.0.0.0', unit:null},
+ {proc:'ncat', port:4444, bind:'0.0.0.0', unit:null},
+ {proc:'smbd', port:445, bind:'0.0.0.0', unit:'smb'},
+ {proc:'node', port:3000, bind:'127.0.0.1', unit:null},
+ {proc:'postgres', port:5432, bind:'127.0.0.1', unit:'postgresql'},
+ {proc:'cupsd', port:631, bind:'127.0.0.1', unit:'cups'},
+ ],
+ f2b:{v:'down', st:'fail'}, ntp:{v:'unsynced', st:'warn'}, dns:{v:'FAIL', st:'fail'},
+ tgage:{v:34, st:'fail'},
+ upd:{p:214, c:9, a:12, f:2},
+ zcap:{v:91, st:'fail'}, zfrag:{v:68, st:'warn'}, ztrim:{v:'OFF', st:'fail'},
+ zhealth:{v:'DEGRADED', st:'fail'}, zerr:{v:'3 / 0 / 1', st:'fail'},
+};}
+let S = GOOD();
+
+const CATS = [
+ {id:'storage', name:'STORAGE', ids:['disk','cache','scrub','fstrim','smart','wear','deverr','unalloc','integ','deeptrim']},
+ {id:'packages', name:'PACKAGES', ids:['orphans','pacnew','keyring','reboot','tgage']},
+ {id:'systemd', name:'SYSTEMD', ids:['failed','timers','sysrun','taint']},
+ {id:'logs', name:'LOGS', ids:['cores','journald','applog','jerr','khw']},
+ {id:'power', name:'MEM·PWR', ids:['temps','mem','unclean','zram','throttle','battery']},
+ {id:'services', name:'SERVICES', ids:['docker','rsync','dstopped','cron','libvirt']},
+ {id:'snapshots',name:'SNAPSHOTS', ids:['snapper','stimer','oldest','retention']},
+ {id:'network', name:'NETWORK', ids:['fw','listen','ts','f2b','ntp','dns']},
+];
+/* diagnostic (no lever here) set — drives the faceplate verdict lamp */
+const DIAG = ['disk','smart','wear','deverr','unalloc','integ','sysrun','taint','jerr','khw',
+ 'temps','mem','zram','throttle','battery','listen','dstopped','libvirt','oldest','retention',
+ 'failed','unclean','rsync','dns','zcap','zfrag','zhealth','zerr','batt'];
+
+let view = 'cat:storage';
+let armed = null;
+let doctorTimer = null;
+let failMode = false;
+let badMode = false;
+const ERR = {};
+const loaded = new Set(); // categories whose probes have landed
+let loadTimers = [];
+
+/* ================= helpers ================= */
+const $ = id => document.getElementById(id);
+const BTRFS_IDS = ['disk','cache','scrub','fstrim','smart','wear','deverr','unalloc','integ','deeptrim'];
+const ZFS_IDS = ['zcap','cache','scrub','ztrim','smart','wear','zhealth','zerr','zfrag','integ','deeptrim'];
+const PWR_RATIO = ['temps','mem','unclean','zram','throttle','battery','cpumode'];
+const PWR_VELOX = ['temps','mem','unclean','zram','throttle','batt','blimit','cpumode'];
+function catIds(cat){
+ if(cat.id==='storage') return zfsMode?ZFS_IDS:BTRFS_IDS;
+ if(cat.id==='power') return zfsMode?PWR_VELOX:PWR_RATIO;
+ return cat.ids;
+}
+/* fixable/watch split per category — sets expectations before a single cell is read */
+function fixwatch(cid){
+ switch(cid){
+ case 'storage': return zfsMode?[7,4]:[8,2];
+ case 'packages': return [5,0];
+ case 'systemd': return [2,2];
+ case 'logs': return [4,1];
+ case 'power': return zfsMode?[3,5]:[2,5];
+ case 'services': return [4,1];
+ case 'snapshots':return [3,1];
+ case 'network': return [6,0];
+ }
+}
+function counts(cat){
+ let attn=0, ok=0, crit=false;
+ catIds(cat).forEach(i=>{ const st=S[i].st;
+ if(st==='warn'){attn++} else if(st==='fail'){attn++;crit=true} else ok++; });
+ return {attn, ok, crit};
+}
+function lampCls(st){ return st==='ok'?'':st==='warn'?'gold':st==='fail'?'red':'off'; }
+function toast(msg, err){
+ const t=document.createElement('div'); t.className='toastw'+(err?' err':''); t.textContent=msg;
+ $('toast').appendChild(t); setTimeout(()=>t.remove(), 2600);
+}
+
+/* ---- prototype toggles ---- */
+function toggleFail(){
+ failMode = !failMode;
+ const k=$('k-fail'); k.classList.toggle('red', failMode);
+ k.textContent = failMode ? 'SIM FAIL ON' : 'SIM FAIL';
+ toast(failMode ? 'failure simulation ON — every action will fail' : 'failure simulation off', failMode);
+}
+function toggleBad(){
+ badMode = !badMode;
+ const k=$('k-bad'); k.classList.toggle('red', badMode);
+ k.textContent = badMode ? 'BAD DAY ON' : 'SIM BAD DAY';
+ S = badMode ? BAD() : GOOD();
+ Object.keys(ERR).forEach(k2=>delete ERR[k2]);
+ render();
+ toast(badMode ? 'degraded snapshot loaded — every metric at its warn/fail face' : 'healthy snapshot restored', badMode);
+}
+/* ---- journal noise list: shipped defaults + user marks (two layers) ---- */
+const NOISE = [
+ {id:'bluetoothd', pat:'Hands-Free Voice gateway connect failed', date:'2026-02-27', shipped:true, disabled:false},
+ {id:'pixman', pat:'_pixman_log_error: BadDrawable', date:'2026-02-27', shipped:true, disabled:false},
+ {id:'xkbcomp', pat:'Errors from xkbcomp are not fatal', date:'2026-03-24', shipped:true, disabled:false},
+];
+const nkey = g => g.id+'|'+(g.pat||g.msg);
+function noiseFor(g){ return NOISE.find(n=>!n.disabled && n.id===g.id && n.pat===g.msg); }
+function jSignal(){ return S.jgroups.filter(g=>!noiseFor(g)).sort((a,b)=>b.n-a.n); }
+function jKnown(){ return S.jgroups.filter(g=>noiseFor(g)); }
+function recalcJerr(){
+ const sum = jSignal().reduce((s,g)=>s+g.n,0);
+ S.jerr = {v:sum, st: sum===0?'ok': sum<=25?'ok':'warn'};
+}
+function markKnown(key){
+ const g = S.jgroups.find(x=>nkey(x)===key); if(!g) return;
+ if(armed!=='mark:'+key){ armed='mark:'+key; render();
+ toast('mark known: '+g.id+' · "'+g.msg.slice(0,40)+'…" — press again', true); return; }
+ armed=null;
+ NOISE.push({id:g.id, pat:g.msg, date:today(), shipped:false, disabled:false});
+ resAdd('mark known', g.id+' · '+g.msg.slice(0,50)+' ('+g.n+' this boot)', 'done');
+ render();
+}
+function unmarkKnown(key){
+ const g = S.jgroups.find(x=>nkey(x)===key); if(!g) return;
+ const n = noiseFor(g); if(!n) return;
+ if(n.shipped) n.disabled = true;
+ else NOISE.splice(NOISE.indexOf(n),1);
+ resAdd('unmark', g.id+' — back in signal'+(n.shipped?' (shipped default disabled)':''), 'done');
+ render();
+}
+function clearMarks(){
+ if(armed!=='clearmarks'){ armed='clearmarks'; render();
+ toast('clear all user marks + re-enable shipped defaults — press again', true); return; }
+ armed=null;
+ const removed = NOISE.filter(n=>!n.shipped).length;
+ for(let i=NOISE.length-1;i>=0;i--){ if(!NOISE[i].shipped) NOISE.splice(i,1); else NOISE[i].disabled=false; }
+ resAdd('clear marks', removed+' user mark(s) removed · shipped defaults restored', 'done');
+ render();
+}
+
+/* ---- listeners: expected-list curation + guarded per-socket remedies ---- */
+const LEXPECT = [
+ {proc:'sshd', port:22, date:'2026-02-27', shipped:true, disabled:false},
+ {proc:'mpd', port:6600, date:'2026-02-27', shipped:true, disabled:false},
+ {proc:'tailscaled', port:41641, date:'2026-02-27', shipped:true, disabled:false},
+];
+const lkey = r => r.proc+'|'+r.port;
+const isPub = b => b==='0.0.0.0'||b==='::';
+function lexpFor(r){ return LEXPECT.find(p=>!p.disabled && p.proc===r.proc && p.port===r.port); }
+function lsnSignal(){ return S.lsn.filter(r=>!lexpFor(r)); }
+function recalcListen(){
+ const sig = lsnSignal();
+ const pub = sig.filter(r=>isPub(r.bind)).length;
+ S.listen = {v:sig.length, st: sig.length===0?'ok': pub?'fail':'warn'};
+}
+function markExpected(key){
+ const r = S.lsn.find(x=>lkey(x)===key); if(!r) return;
+ if(armed!=='lexp:'+key){ armed='lexp:'+key; render();
+ toast('mark expected: '+r.proc+' :'+r.port+' — press again', true); return; }
+ armed=null;
+ LEXPECT.push({proc:r.proc, port:r.port, date:today(), shipped:false, disabled:false});
+ resAdd('mark expected', r.proc+' :'+r.port+' ('+r.bind+')', 'done');
+ render();
+}
+function unmarkExpected(key){
+ const p = LEXPECT.find(x=>!x.disabled && x.proc+'|'+x.port===key); if(!p) return;
+ if(p.shipped) p.disabled = true; else LEXPECT.splice(LEXPECT.indexOf(p),1);
+ resAdd('unmark expected', p.proc+' :'+p.port+' — back in signal'+(p.shipped?' (shipped default disabled)':''), 'done');
+ render();
+}
+function clearLsn(){
+ if(armed!=='clearlsn'){ armed='clearlsn'; render();
+ toast('clear user expected-marks + re-enable shipped — press again', true); return; }
+ armed=null;
+ const removed = LEXPECT.filter(p=>!p.shipped).length;
+ for(let i=LEXPECT.length-1;i>=0;i--){ if(!LEXPECT[i].shipped) LEXPECT.splice(i,1); else LEXPECT[i].disabled=false; }
+ resAdd('clear expected', removed+' user mark(s) removed · shipped defaults restored', 'done');
+ render();
+}
+function lsnFix(key){
+ const r = S.lsn.find(x=>lkey(x)===key); if(!r) return;
+ const verb = r.unit ? 'systemctl stop '+r.unit : 'SIGTERM '+r.proc;
+ if(armed!=='lsnfix:'+key){ armed='lsnfix:'+key; render();
+ toast((r.unit?'STOP ':'KILL ')+r.proc+' :'+r.port+' — '+verb+' · press again', true); return; }
+ armed=null;
+ const e = resAdd(r.unit?'stop unit':'kill listener', verb+' …');
+ setTimeout(()=>{
+ if(failMode){ resDone(e,'fail', r.unit?'systemctl: unit busy — stop refused':r.proc+' respawned — supervised? check its unit'); }
+ else { S.lsn.splice(S.lsn.indexOf(r),1);
+ resDone(e,'done', r.proc+' stopped — port '+r.port+' closed'); }
+ render();
+ }, 800);
+}
+
+/* ---- rotary sub-section selector state ---- */
+const SUBSEC = {packages:'orphans', logs:'signal', services:'containers'};
+const DECKDEFS = {};
+function setSubsec(cat,id){ SUBSEC[cat]=id; armed=null; render(); }
+function cycleSubsec(cat){
+ const defs = DECKDEFS[cat]; if(!defs||!defs.length) return;
+ const i = defs.findIndex(d=>d.id===SUBSEC[cat]);
+ setSubsec(cat, defs[(i+1)%defs.length].id);
+}
+function deck(cat, defs){
+ DECKDEFS[cat] = defs;
+ const cur = SUBSEC[cat];
+ const i = Math.max(0, defs.findIndex(d=>d.id===cur));
+ const ang = defs.length>1 ? -50 + i*(100/(defs.length-1)) : 0;
+ const bands = defs.map(d=>`<span class="band ${d.id===cur?'on':''}" onclick="setSubsec('${cat}','${d.id}')">
+ <span class="lamp ${d.lamp||''}"></span>
+ <span class="bl">${d.label}${d.count!==undefined&&d.count!==''?' · '+d.count:''}</span>
+ <span class="ul"></span></span>`).join('');
+ return `<div class="deck">
+ <span class="knob" onclick="cycleSubsec('${cat}')" title="cycle sections">
+ <span class="ind" style="transform:rotate(${ang}deg)"></span></span>
+ <span class="bands">${bands}</span></div>`;
+}
+
+/* ---- orphans: KEEP curation (the rust lesson) + per-package REMOVE ---- */
+const OKEEP = []; // user layer: packages marked intentional
+const okept = name => OKEEP.some(k=>k.name===name);
+function orphKeep(name){
+ if(okept(name)) return;
+ OKEEP.push({name, date:today()});
+ S.orphans.v = Math.max(0, S.orphans.v-1);
+ if(S.orphans.v===0) S.orphans.st='ok';
+ resAdd('keep', name+' marked intentional — excluded from removal', 'done');
+ render();
+}
+function orphUnkeep(name){
+ const k = OKEEP.find(x=>x.name===name); if(!k) return;
+ OKEEP.splice(OKEEP.indexOf(k),1);
+ S.orphans.v++; S.orphans.st='warn';
+ resAdd('unkeep', name+' — back in the orphan set', 'done');
+ render();
+}
+function orphRemove(name){
+ const row = S.orphlist.find(r=>r[0]===name); if(!row) return;
+ if(armed!=='orem:'+name){ armed='orem:'+name; render();
+ toast('pacman -Rns '+name+' ('+row[1]+') — press again', true); return; }
+ armed=null;
+ const e = resAdd('orphan remove', 'pacman -Rns '+name+' …');
+ setTimeout(()=>{
+ if(failMode){ resDone(e,'fail','pacman: breaking dependency — '+name+' required by another package'); }
+ else { S.orphlist.splice(S.orphlist.indexOf(row),1);
+ S.orphans.v = Math.max(0, S.orphans.v-1);
+ if(S.orphans.v===0) S.orphans.st='ok';
+ resDone(e,'done',name+' removed — '+row[1]+' freed'); }
+ render();
+ }, 900);
+}
+
+/* ---- pacnew: per-file DELETE (safe) / MERGE (terminal delegation) ---- */
+function pacnewAct(file){
+ const p = S.pacnews.find(x=>x.f===file); if(!p) return;
+ const e = resAdd(p.safe?'pacnew delete':'pacnew merge',
+ p.safe ? 'rm '+file+' — '+p.why+' …' : 'opening diff in terminal — '+file+' …');
+ setTimeout(()=>{
+ if(failMode){ resDone(e,'fail', p.safe?'rm: permission denied':'diff tool not found'); }
+ else { S.pacnews.splice(S.pacnews.indexOf(p),1);
+ S.pacnew.v = Math.max(0, S.pacnew.v-1);
+ if(S.pacnew.v===0) S.pacnew.st='ok';
+ resDone(e,'done', p.safe ? file+' deleted — current config is authoritative'
+ : file+' merged in terminal · pacnew deleted'); }
+ render();
+ }, 700);
+}
+
+/* ---- failed units: per-unit roster remedies ---- */
+function funitAct(u, reset){
+ const r = S.funits.find(x=>x.u===u); if(!r) return;
+ const e = resAdd(reset?'reset-failed':'unit restart',
+ (reset?'systemctl reset-failed ':'systemctl restart ')+u+' …');
+ setTimeout(()=>{
+ if(failMode){ resDone(e,'fail', u+': start request repeated too quickly'); }
+ else { S.funits.splice(S.funits.indexOf(r),1);
+ S.failed.v = Math.max(0, S.failed.v-1);
+ if(S.failed.v===0){ S.failed.st='ok'; if(S.sysrun.st!=='ok') S.sysrun={v:'running', st:'ok'}; }
+ resDone(e,'done', u+(reset?' cleared from failed list':' restarted — running')); }
+ render();
+ }, 800);
+}
+
+/* ---- containers: expected-list curation + per-container START ---- */
+const CEXPECT = [
+ {name:'winvm', date:'2026-02-27', shipped:true, disabled:false},
+ {name:'deepsat-pg', date:'2026-06-20', shipped:false, disabled:false},
+];
+const cexp = name => CEXPECT.some(p=>!p.disabled && p.name===name);
+function recalcCtrs(){
+ const unexp = S.ctrs.filter(c=>!cexp(c.name)).length;
+ S.dstopped = {v:S.ctrs.length, st: unexp?'warn':'ok'};
+}
+function ctrStartOne(name){
+ const c = S.ctrs.find(x=>x.name===name); if(!c) return;
+ const e = resAdd('container start', 'docker start '+name+' …');
+ setTimeout(()=>{
+ if(failMode){ resDone(e,'fail', name+' exits immediately (code 1) — see docker logs'); }
+ else { S.ctrs.splice(S.ctrs.indexOf(c),1);
+ resDone(e,'done', name+' running'); }
+ render();
+ }, 800);
+}
+function ctrMark(name){
+ if(armed!=='cexp:'+name){ armed='cexp:'+name; render();
+ toast('mark expected: '+name+' stays stopped by design — press again', true); return; }
+ armed=null;
+ CEXPECT.push({name, date:today(), shipped:false, disabled:false});
+ resAdd('mark expected', name+' — expected-stopped (on-demand)', 'done');
+ render();
+}
+function ctrUnmark(name){
+ const p = CEXPECT.find(x=>!x.disabled && x.name===name); if(!p) return;
+ if(p.shipped) p.disabled=true; else CEXPECT.splice(CEXPECT.indexOf(p),1);
+ resAdd('unmark expected', name+' — back in signal', 'done');
+ render();
+}
+
+/* ---- snapshots: stale manual (single) snapshot delete ---- */
+function snapDelStale(){
+ if(armed!=='snapstale'){ armed='snapstale'; render();
+ toast('delete '+(S.snaptypes.single-2)+' stale manual snapshots (keep newest 2) — press again', true); return; }
+ armed=null;
+ const n = S.snaptypes.single-2;
+ const e = resAdd('stale snapshots', 'snapper delete ('+n+' singles) …');
+ setTimeout(()=>{
+ if(failMode){ resDone(e,'fail','snapper: config busy — snapshot in progress'); }
+ else { S.snaptypes.single=2; S.snaptypes.oldsingle='recent';
+ S.snapper.v = S.snaptypes.timeline+S.snaptypes.single+S.snaptypes.prepost;
+ if(S.snapper.v<100) S.snapper.st='ok';
+ resDone(e,'done', n+' manual snapshots deleted — space freed on next cleanup'); }
+ render();
+ }, 900);
+}
+
+/* kill process: arm-to-fire SIGTERM on a top-memory item; session-critical names protected */
+const PROTECTED = ['systemd','Hyprland','maintenance-panel'];
+function killProc(name){
+ const row = S.memtop.find(r=>r[0]===name); if(!row) return;
+ if(armed!=='kill:'+name){ armed='kill:'+name; render();
+ toast('KILL '+name+' ('+row[1]+') — SIGTERM · press again', true); return; }
+ armed=null;
+ const e = resAdd('kill process', 'SIGTERM '+name+' ('+row[1]+') …');
+ setTimeout(()=>{
+ if(failMode){ resDone(e,'fail',name+' ignored SIGTERM — state D (uninterruptible IO)'); }
+ else {
+ S.memtop.splice(S.memtop.indexOf(row),1);
+ const gb = parseFloat(row[1])||0;
+ S.mem.v = Math.min(125, Math.round((S.mem.v+gb)*10)/10);
+ if(S.mem.v>16 && S.mem.st!=='ok') S.mem={v:S.mem.v, st:'ok'};
+ resDone(e,'done',name+' exited cleanly · '+row[1]+' freed');
+ }
+ render();
+ }, 700);
+}
+
+/* cpu mode: free selector — set the active EPP mode to anything */
+const MODE_LABELS = {perf:'performance', bal:'balance_performance', power:'power'};
+function setMode(m){
+ if(S.cpumode.v===m) return;
+ const e = resAdd('cpu mode', 'writing EPP hint: '+MODE_LABELS[m]+' …');
+ setTimeout(()=>{
+ if(failMode){ resDone(e,'fail','sysfs: energy_performance_preference write refused'); }
+ else { S.cpumode.v=m; resDone(e,'done','EPP → '+MODE_LABELS[m]+' (all cores)'); }
+ render();
+ }, 500);
+}
+
+let zfsMode = false;
+function toggleZfs(){
+ zfsMode = !zfsMode;
+ const k=$('k-zfs'); k.classList.toggle('on', zfsMode);
+ k.textContent = zfsMode ? 'ZFS ON' : 'SIM ZFS';
+ Object.keys(ERR).forEach(k2=>{ if(ACT2CAT[k2]==='storage') delete ERR[k2]; });
+ render();
+ toast(zfsMode ? 'storage shows the velox/ZFS capability set' : 'storage shows the ratio/btrfs capability set');
+}
+function reloadPanel(){ openPanel(); }
+
+/* ---- actions ---- */
+/* ---- results wall ---- */
+const RES = [];
+let wallHidden = false;
+const ANAMES = { clean_cache:'cache trim', scrub:'btrfs scrub', fstrim:'fstrim enable',
+ deeptrim:'deep trim', orphans:'orphan removal', pacnew:'pacnew merge', keyring:'keyring refresh',
+ timers:'timer enable', cores:'coredump clear', journald:'journal vacuum', applog:'app-log cleanup',
+ docker:'docker prune', snapper:'snapper cleanup', unit_restart:'unit restart',
+ f2b_fix:'fail2ban restart', cron_fix:'cronie start', ntp_fix:'chrony restart',
+ ts_fix:'tailscaled restart', stimer_fix:'snapshot timers', zram_fix:'zram enable',
+ netdoc:'net doctor', balance:'btrfs balance', reinstall:'pkg reinstall', rsyncnow:'backup run',
+ smarttest:'smart self-test', retention:'retention repair', reboot:'reboot',
+ sysupdate:'system update', topgrade:'topgrade', ztrim_fix:'zpool autotrim',
+ openjournal:'open journal', bat_limit:'charge limit',
+ ctr_start:'container start', fw_fix:'firewall enable' };
+function now(){ return new Date().toTimeString().slice(0,8); }
+function today(){ const d=new Date();
+ return d.getFullYear()+'-'+String(d.getMonth()+1).padStart(2,'0')+'-'+String(d.getDate()).padStart(2,'0'); }
+function resAdd(name, ev, st){
+ const e = {d:today(), t:now(), name, ev, st:st||'run'};
+ RES.push(e); renderWall(); return e;
+}
+function resDone(e, st, ev){ e.st=st; e.ev=ev; e.d=today(); e.t=now(); renderWall(); }
+function renderWall(){
+ const w = $('rwall');
+ if(!RES.length){ w.style.display='none'; return; }
+ w.style.display='block';
+ $('rcount').textContent = '· '+RES.length;
+ $('rtoggle').textContent = wallHidden ? 'SHOW' : 'HIDE';
+ const b = $('rbody');
+ b.style.display = wallHidden ? 'none' : 'block';
+ b.innerHTML = RES.map(e=>{
+ const lamp = e.st==='run'?'busy':e.st==='fail'?'red':'';
+ return `<div class="rstep"><span class="tm"><span class="dt">${e.d}</span>${e.t}</span><span class="lamp ${lamp}"></span>
+ <span class="rtxt" title="${e.name} — ${e.ev}"><b>${e.name}</b> <span class="ev ${e.st==='fail'?'evfail':''}">${e.ev}</span></span></div>`;
+ }).join('');
+ b.scrollTop = b.scrollHeight;
+}
+function toggleWall(){ wallHidden=!wallHidden; renderWall(); }
+function copyWall(){
+ const txt = RES.map(e=>`${e.d} ${e.t} ${e.st.toUpperCase().padEnd(4)} ${e.name} — ${e.ev}`).join('\n');
+ (navigator.clipboard ? navigator.clipboard.writeText(txt) : Promise.reject())
+ .then(()=>toast('copied '+RES.length+' result lines'))
+ .catch(()=>toast('clipboard unavailable', true));
+}
+
+function fire(act){
+ const a = ACTIONS[act];
+ // guard-armed updates: first press runs the guard check and arms; second press overrides
+ if(a.guardArm && armed!==act){
+ armed = act;
+ const e = resAdd(ANAMES[act]||act, a.busy||'checking guard…');
+ setTimeout(()=>{
+ resDone(e,'fail','guard: mesa in update set — press again to run anyway, or apply from a TTY');
+ render();
+ }, 600);
+ render();
+ return;
+ }
+ if(a.arm && armed!==act && !ERR[act]){ armed=act; render(); toast(a.armMsg||'press again to confirm', true); return; }
+ const overridden = a.guardArm && armed===act;
+ armed=null;
+ const e = resAdd(ANAMES[act]||act, (overridden && a.busy2) ? a.busy2 : (a.busy||'running…'));
+ setTimeout(()=>{
+ if(failMode){ ERR[act]=a.err||'exited 1'; resDone(e,'fail',ERR[act]); }
+ else { delete ERR[act]; a.run(); resDone(e,'done',a.doneMsg);
+ if(a.guardArm && S.reboot.v==='YES')
+ resAdd('reboot offer','kernel/mesa updated — REBOOT key armed on the updates strip','done');
+ }
+ render();
+ }, a.ms||800);
+}
+const ACTIONS = {
+ clean_cache:{ busy:'paccache -r / -ruk0 …', ms:1000, doneMsg:'cache trim — reclaimed 8.4 GB',
+ err:'paccache: /var/cache/pacman/pkg — permission denied (polkit dismissed)',
+ run(){ S.cache={v:0.4, st:'ok'}; } },
+ scrub:{ busy:'btrfs scrub started on / …', ms:1400, doneMsg:'scrub running — age resets on completion',
+ err:'btrfs: scrub already running on / — see dmesg',
+ run(){ S.scrub={v:0, st:'ok'}; } },
+ fstrim:{ busy:'systemctl enable --now fstrim.timer …', ms:900, doneMsg:'fstrim.timer enabled — weekly TRIM active',
+ err:'systemctl: unit fstrim.timer is masked',
+ run(){ S.fstrim={v:'ON', st:'ok'}; if(S.timers.st==='warn') S.timers={v:'5/5', st:'ok'}; } },
+ deeptrim:{ arm:true, armMsg:'keep only 1 version of every pkg — press again', busy:'paccache -rk1 …', ms:1100,
+ doneMsg:'deep trim — kept 1 version, reclaimed 3.1 GB more',
+ err:'paccache -rk1: exited 1 — cache dir busy (pacman lock held)',
+ run(){ S.deeptrim={v:'keep 1', st:'ok'}; if(S.cache.st==='ok') S.cache={v:0.2,st:'ok'}; } },
+ orphans:{ arm:true, armMsg:'remove all unkept orphans — press again', busy:'pacman -Rns (unkept set) …', ms:1200,
+ doneMsg:'orphans removed — kept packages skipped',
+ err:'pacman -Rns: breaking dependency — rust required by cargo-audit',
+ run(){ S.orphlist = S.orphlist.filter(r=>okept(r[0])); S.orphans={v:0, st:'ok'}; } },
+ pacnew:{ busy:'diffing .pacnew files …', ms:900, doneMsg:'.pacnew resolved — mirrorlist deleted, locale.gen merged',
+ err:'merge aborted — locale.gen.pacnew conflicts, needs manual merge',
+ run(){ S.pacnew={v:0, st:'ok'}; } },
+ keyring:{ busy:'pacman -Sy archlinux-keyring …', ms:900, doneMsg:'keyring refreshed',
+ err:'pacman -Sy: mirror timeout — no network?',
+ run(){ S.keyring={v:0, st:'ok'}; } },
+ timers:{ busy:'systemctl enable --now …', ms:800, doneMsg:'all 5 maintenance timers firing',
+ err:'systemctl: failed to enable — unit file not found',
+ run(){ S.timers={v:'5/5', st:'ok'}; if(S.fstrim.st==='fail') S.fstrim={v:'ON',st:'ok'}; } },
+ cores:{ busy:'coredumpctl clean (keep 3d) …', ms:800, doneMsg:'coredumps cleared',
+ err:'coredumpctl: permission denied — /var/lib/systemd/coredump',
+ run(){ S.cores={v:0, st:'ok'}; S.coregroups=[]; } },
+ journald:{ busy:'journalctl --vacuum-size=300M …', ms:800, doneMsg:'journal vacuumed — reclaimed 0.9 GB',
+ err:'journalctl: vacuum failed — rotation in progress, retry',
+ run(){ S.journald={v:0.3, st:'ok'}; } },
+ applog:{ busy:'log-cleanup …', ms:600, doneMsg:'app logs — nothing older than 7d',
+ err:'log-cleanup: exited 1 — ~/.local/var/log not writable',
+ run(){ S.applog={v:'ok', st:'ok'}; } },
+ docker:{ arm:true, armMsg:'docker prune tier 1+2 — press again', busy:'docker container/image prune …', ms:1200,
+ doneMsg:'docker pruned — reclaimed 2.8 GB',
+ err:'docker: cannot connect to daemon — is docker.service running?',
+ run(){ S.docker={v:0.2, st:'ok'}; S.ddf = S.ddf.map(r=>[r[0], r[1], '0 GB']); } },
+ snapper:{ arm:true, armMsg:'prune stale manual snapshots — press again', busy:'snapper cleanup …', ms:900,
+ doneMsg:'snapper — retention applied',
+ err:'snapper: config busy — snapshot in progress',
+ run(){ S.snapper={v:Math.min(S.snapper.v,31), st:'ok'}; } },
+ /* --- group A: service restarts / enables --- */
+ unit_restart:{ busy:'systemctl restart (all failed units) …', ms:900, doneMsg:'units restarted — running',
+ err:'systemd-oomd: start request repeated too quickly',
+ run(){ S.funits=[]; S.failed={v:0, st:'ok'}; if(S.sysrun.st!=='ok') S.sysrun={v:'running', st:'ok'}; } },
+ f2b_fix:{ busy:'systemctl restart fail2ban …', ms:800, doneMsg:'fail2ban running — sshd jail active',
+ err:'fail2ban: failed to start — bad jail config',
+ run(){ S.f2b={v:0, st:'ok'}; } },
+ cron_fix:{ busy:'systemctl start cronie …', ms:700, doneMsg:'cronie running — entries live',
+ err:'cronie: unit masked',
+ run(){ S.cron={v:'ok', st:'ok'}; } },
+ ntp_fix:{ busy:'systemctl restart chronyd + makestep …', ms:800, doneMsg:'clock stepped — synchronized',
+ err:'chronyd: no reachable sources',
+ run(){ S.ntp={v:'synced', st:'ok'}; } },
+ ts_fix:{ busy:'systemctl restart tailscaled …', ms:900, doneMsg:'tailscaled up — peers reconnecting',
+ err:'tailscaled: bind: address already in use',
+ run(){ S.ts={v:'4/4', st:'ok'}; S.tsdown=[]; } },
+ stimer_fix:{ busy:'systemctl enable --now snapper timers …', ms:700, doneMsg:'snapshot timers firing',
+ err:'systemctl: unit not found',
+ run(){ S.stimer={v:'active', st:'ok'}; } },
+ zram_fix:{ busy:'re-applying zram-generator config …', ms:800, doneMsg:'zram active — 16 GiB zstd',
+ err:'zram-generator: module not loaded',
+ run(){ S.zram={v:'16 GiB', st:'ok'}; } },
+ netdoc:{ busy:'opening NET·01 …', ms:400, doneMsg:'delegated to the net panel doctor (NET·01)',
+ err:'net panel not running',
+ run(){} },
+ /* --- group B: deterministic maintenance ops --- */
+ balance:{ arm:true, armMsg:'btrfs balance is IO-heavy and slow — press again', busy:'btrfs balance -dusage=50 …', ms:1500,
+ doneMsg:'balance complete — unallocated recovered',
+ err:'balance cancelled — ENOSPC during relocation',
+ run(){ S.unalloc={v:'118', st:'ok'}; } },
+ reinstall:{ busy:'pacman -S (owning packages) …', ms:1100, doneMsg:'modified files restored — integrity clean',
+ err:'pacman: failed to commit transaction',
+ run(){ S.integ={v:'clean', st:'ok'}; } },
+ rsyncnow:{ busy:'rsyncshot manual run …', ms:1400, doneMsg:'backup complete — synced to truenas',
+ err:'rsync exit 23 — partial transfer (permissions)',
+ run(){ S.rsync={v:'now', st:'ok'}; } },
+ smarttest:{ busy:'smartctl -t short (both NVMe) …', ms:900, doneMsg:'short self-test started — results in ~2 min',
+ err:'smartctl: device busy',
+ run(){ S.smartlast='passed just now (short test)'; } },
+ retention:{ busy:'writing sane retention (M2 Y0) + cleanup …', ms:1000, doneMsg:'retention repaired — old chain pruned',
+ err:'snapperd: config locked',
+ run(){ S.retention={v:'H6 D7 W2 M2 Y0', st:'ok'}; S.oldest={v:8, st:'ok'}; S.snapper={v:Math.min(S.snapper.v,42), st:'ok'}; } },
+ /* --- group D: disruptive but determinate --- */
+ reboot:{ arm:true, armMsg:'reboot the machine — press again', busy:'rebooting … (simulated)', ms:1200,
+ doneMsg:'rebooted — new kernel running, taint cleared',
+ err:'logind: inhibitor lock held (backup in progress)',
+ run(){ S.reboot={v:'NO', st:'ok'}; S.taint={v:0, st:'ok'}; if(S.sysrun.st!=='ok') S.sysrun={v:'running', st:'ok'}; } },
+ ctr_start:{ busy:'docker start (all unexpected exited) …', ms:1000,
+ doneMsg:'containers started — running again',
+ err:'docker: container exits immediately (code 1) — see docker logs',
+ run(){ S.ctrs = S.ctrs.filter(c=>cexp(c.name)); } },
+ fw_fix:{ busy:'ufw enable …', ms:800,
+ doneMsg:'firewall active — mosh rule loaded',
+ err:'ufw: iptables lock held — retry',
+ run(){ S.fw={v:'active', st:'ok'}; } },
+ bat_limit:{ busy:'writing charge_control_end_threshold=80 …', ms:700,
+ doneMsg:'charge limit set — 80% (battery longevity)',
+ err:'sysfs: permission denied',
+ run(){ S.blimit={v:'80%', st:'ok'}; } },
+ openjournal:{ busy:'spawning terminal: journalctl -p err -b …', ms:400,
+ doneMsg:'opened in terminal — journalctl -p err -b',
+ err:'no terminal available',
+ run(){} },
+ /* --- zfs (velox capability set) --- */
+ ztrim_fix:{ busy:'zpool set autotrim=on + zpool trim …', ms:1000,
+ doneMsg:'autotrim enabled — initial trim running',
+ err:'zpool: pool is busy (resilver in progress)',
+ run(){ S.ztrim={v:'ON', st:'ok'}; } },
+ /* --- updates (Confirm; guard arms instead of blocking — user decides) --- */
+ sysupdate:{ guardArm:true, busy:'checking update set against the live-update guard …',
+ busy2:'pacman -Syu … (guard overridden)', ms:1400,
+ doneMsg:'system updated — 47 packages · reboot recommended',
+ err:'pacman: failed to commit transaction (conflicting files)',
+ run(){ S.upd.p=0; S.upd.c=0; S.reboot={v:'YES', st:'warn'}; } },
+ topgrade:{ guardArm:true, busy:'guard check, then topgrade (--disable git) …',
+ busy2:'topgrade full run … (guard overridden)', ms:1800,
+ doneMsg:'topgrade complete — system + ecosystems · reboot recommended',
+ err:'topgrade: system step failed — pacman lock held',
+ run(){ S.upd.p=0; S.upd.c=0; S.upd.a=0; S.tgage={v:0, st:'ok'}; S.reboot={v:'YES', st:'warn'}; } },
+};
+
+/* ================= widget builders ================= */
+function lever(act, label, cls){
+ const a = armed===act;
+ if(ERR[act]) return `<button class="key sm red" onclick="event.stopPropagation();fire('${act}')">RETRY</button>`;
+ return `<button class="key sm ${cls||''} ${a?'armed':''}" onclick="event.stopPropagation();fire('${act}')">${a?label+'?':label}</button>`;
+}
+function errline(act){ return act && ERR[act] ? `<div class="errline">✗ ${ERR[act]}</div>` : ''; }
+function lampFor(id, act){ return (act && ERR[act]) ? 'red' : lampCls(S[id].st); }
+function lrow(id, name, extra, valOverride, act){
+ return `<div class="lrow"><span class="lamp ${lampFor(id,act)}"></span><span class="who">${name}</span>
+ <span class="what">${extra||''}${(act&&ERR[act])?'✗ failed':(valOverride!==undefined?valOverride:S[id].v)}</span></div>`;
+}
+function barCls(st){ return st==='ok'?'okc':'warn'; }
+function cellBar(id,cap,unit,pct,o){ o=o||{}; const m=S[id];
+ return `<div class="cell">${o.lever?`<span class="lever">${o.lever}</span>`:''}
+ <div class="cap"><span class="lamp ${lampFor(id,o.act)}"></span> ${cap}</div>
+ <div class="val"><span class="readout">${m.v}<small> ${unit||''}</small></span></div>
+ <div class="bar ${barCls(m.st)}">${o.tick?`<span class="tk" style="left:${o.tick}%"></span>`:''}<span style="width:${Math.min(100,pct)}%"></span></div>
+ <div class="sub2">${o.sub||''}</div>${errline(o.act)}</div>`; }
+function cellCount(id,cap,o){ o=o||{}; const m=S[id];
+ const col = m.st==='fail'?'style="color:var(--fail)"':(m.st==='warn'?'style="color:var(--amber)"':'');
+ return `<div class="cell">${o.lever?`<span class="lever">${o.lever}</span>`:''}
+ <div class="cap"><span class="lamp ${lampFor(id,o.act)}"></span> ${cap}</div>
+ <div class="val"><span class="readout" ${col}>${m.v}${o.unit?`<small>${o.unit}</small>`:''}</span>${o.badge||''}</div>
+ <div class="sub2">${o.sub||''}</div>${errline(o.act)}</div>`; }
+function cellRing(id,cap,pct,small,o){ o=o||{}; const m=S[id];
+ const rc = (o.act&&ERR[o.act])?'var(--fail)':m.st==='ok'?'var(--pass)':m.st==='fail'?'var(--fail)':'var(--amber)';
+ return `<div class="cell">${o.lever?`<span class="lever">${o.lever}</span>`:''}
+ <div class="cap"><span class="lamp ${lampFor(id,o.act)}"></span> ${cap}</div>
+ <div class="center"><span class="ring" style="--p:${Math.min(100,pct)};--rc:${rc}"><b>${m.v}</b><small>${small}</small></span></div>
+ <div class="sub2" style="text-align:center">${o.sub||''}</div>${errline(o.act)}</div>`; }
+function cellBadge(id,cap,o){ o=o||{}; const m=S[id];
+ const cls = m.st==='ok'?'pass':m.st==='fail'?'red':m.st==='warn'?'':'ghost';
+ return `<div class="cell">${o.lever?`<span class="lever">${o.lever}</span>`:''}
+ <div class="cap"><span class="lamp ${lampFor(id,o.act)}"></span> ${cap}</div>
+ <div class="val"><span class="badge ${cls}">${String(m.v).toUpperCase()}</span></div>
+ <div class="sub2">${o.sub||''}</div>${errline(o.act)}</div>`; }
+
+/* dynamic rosters */
+function timerRoster(){
+ const st = S.timers.st;
+ const rows = [
+ ['paccache.timer','weekly', true],
+ ['btrfs-scrub@-.timer','monthly', st!=='fail'],
+ ['fstrim.timer','weekly', S.fstrim.st==='ok'],
+ ['reflector.timer','weekly', st!=='fail'],
+ ['snapper-cleanup.timer','daily', true],
+ ];
+ return rows.map(([n,c,ok])=>`<div class="lrow"><span class="lamp ${ok?'':'red'}"></span>
+ <span class="who">${n}</span><span class="what">${ok?c+' · ok':'disabled'}</span></div>`).join('');
+}
+function backupRoster(){
+ if(S.rsync.st==='ok') return `
+ <div class="lrow"><span class="lamp"></span><span class="who">hourly</span><span class="what">3h ago · ok</span></div>
+ <div class="lrow"><span class="lamp"></span><span class="who">daily 02:30</span><span class="what">11h ago · ok</span></div>`;
+ return `
+ <div class="lrow"><span class="lamp red"></span><span class="who">hourly</span><span class="what">52h ago · stale</span></div>
+ <div class="lrow"><span class="lamp red"></span><span class="who">daily 02:30</span><span class="what">rsync exit 23 · failed</span></div>`;
+}
+function peerRoster(){
+ const down = S.tsdown||[];
+ return ['truenas','velox','worker','cjennings'].map(p=>{
+ const d = down.includes(p);
+ return `<div class="lrow"><span class="lamp ${d?'red':''}"></span><span class="who">${p}</span>
+ <span class="what">${d?'offline':'online'}</span></div>`;}).join('');
+}
+
+/* ================= subpanel templates ================= */
+const SUB = {
+storage(){
+ if(zfsMode) return `
+ <div class="wgrid">
+ ${cellBar('zcap','zpool capacity','%',S.zcap.v,{lever:`<button class="key sm" onclick="event.stopPropagation();go('reclaim')">RECLAIM</button>`,sub:'CoW slows > 80 · best-fit > 90'})}
+ ${cellBar('cache','Pkg cache','GB',S.cache.v/10*100,{lever:lever('clean_cache','CLEAN'),act:'clean_cache',tick:100,sub:'Auto · thr ~10 GB'})}
+ ${cellRing('scrub','zpool scrub',S.scrub.v/60*100,'days',{lever:lever('scrub','SCRUB'),act:'scrub',sub:S.scrub.st==='ok'?'fresh':'warn 35d · crit 60d'})}
+ ${cellCount('ztrim','zpool autotrim',{lever:S.ztrim.st!=='ok'?lever('ztrim_fix','ENABLE',S.ztrim.st==='fail'?'red':''):'',act:'ztrim_fix',sub:'the fstrim counterpart on ZFS'})}
+ </div>
+ <div class="wgrid">
+ ${cellBadge('smart','SMART',{lever:lever('smarttest','SELF-TEST'),act:'smarttest',sub:'NVMe'})}
+ ${cellRing('wear','NVMe wear',Math.max(2,S.wear.v),'%',{sub:S.wear.st==='ok'?'0%':'high wear — plan replacement'})}
+ ${cellBadge('zhealth','Pool health',{sub:'zpool status -x'})}
+ ${cellCount('zerr','Pool errors',{sub:'read / write / cksum — no reset lever; investigate'})}
+ </div>
+ <div class="engrave striph">filesystem<span class="cnt">· 3</span></div>
+ <div class="strip">
+ ${lrow('zfrag','fragmentation', '', S.zfrag.v+'% — no defrag exists; keep < 80% full')}
+ ${lrow('integ','pacman file integrity', S.integ.st!=='ok'?lever('reinstall','REINSTALL')+' ':'', undefined, 'reinstall')}
+ ${lrow('deeptrim','deep-trim (keep 1)', lever('deeptrim','DEEP TRIM')+' ', undefined, 'deeptrim')}
+ </div>`;
+ return `
+ <div class="wgrid">
+ ${cellBar('disk','Disk usage','%',S.disk.v,{lever:`<button class="key sm" onclick="event.stopPropagation();go('reclaim')">RECLAIM</button>`,sub:'root · btrfs · thr 80/90'})}
+ ${cellBar('cache','Pkg cache','GB',S.cache.v/10*100,{lever:lever('clean_cache','CLEAN'),act:'clean_cache',tick:100,sub:'Auto · thr ~10 GB'})}
+ ${cellRing('scrub','btrfs scrub',S.scrub.v/55*100,'days',{lever:lever('scrub','SCRUB'),act:'scrub',sub:S.scrub.st==='ok'?'fresh':'past 30d cadence'})}
+ ${cellCount('fstrim','fstrim.timer',{lever:lever('fstrim','ENABLE',S.fstrim.st==='fail'?'red':''),act:'fstrim',sub:'Confirm · weekly TRIM'})}
+ </div>
+ <div class="wgrid">
+ ${cellBadge('smart','SMART',{lever:lever('smarttest','SELF-TEST'),act:'smarttest',sub:'last test: '+S.smartlast})}
+ ${cellRing('wear','NVMe wear',Math.max(2,S.wear.v),'%',{sub:S.wear.st==='ok'?'0% both drives':'high wear — plan replacement'})}
+ ${cellCount('deverr','Device errors',{sub:S.devrows.length?'per-device below · reset stays manual':'read / write · RAID1 · watch only'})}
+ ${cellCount('unalloc','btrfs unallocated',{lever:S.unalloc.st!=='ok'?lever('balance','BALANCE'):'',act:'balance',unit:' GiB',sub:'chunk headroom — raw space not yet carved into chunks'})}
+ </div>
+ ${S.devrows.length?`
+ <div class="engrave striph">device errors — which drive<span class="cnt">· cross-check SMART</span></div>
+ <div class="jlist">${S.devrows.map(r=>`<div class="lrow jrow"><span class="lamp red"></span>
+ <span class="who"><b>${r[0]}</b></span><span class="what">${r[1]}</span></div>`).join('')}</div>`:''}
+ <div class="engrave striph">disk — top consumers<span class="cnt">· evidence only</span></div>
+ <div class="jlist">${S.dtop.map(r=>`<div class="lrow jrow"><span class="who">${r[0]}</span>
+ <span class="what">${r[1]}</span></div>`).join('')}</div>
+ <div class="engrave striph">filesystem<span class="cnt">· 2</span></div>
+ <div class="strip two">
+ ${lrow('integ','pacman file integrity', S.integ.st!=='ok'?lever('reinstall','REINSTALL')+' ':'', undefined, 'reinstall')}
+ ${lrow('deeptrim','deep-trim (keep 1)', lever('deeptrim','DEEP TRIM')+' ', undefined, 'deeptrim')}
+ </div>`; },
+
+packages(){
+ const kept = OKEEP.map(k=>k.name);
+ const orows = S.orphlist.filter(r=>!kept.includes(r[0])).map(r=>{
+ const a = armed==='orem:'+r[0];
+ return `<div class="lrow jrow"><span class="lamp gold"></span>
+ <span class="who"><b>${r[0]}</b></span><span class="jmsg">${r[1]}</span>
+ <span class="what">
+ <button class="key sm" onclick="event.stopPropagation();orphKeep('${r[0]}')">KEEP</button>
+ <button class="key sm ${a?'armed':''}" onclick="event.stopPropagation();orphRemove('${r[0]}')">${a?'REMOVE?':'REMOVE'}</button>
+ </span></div>`;
+ }).join('');
+ const krows = OKEEP.map(k=>`<div class="lrow jrow known"><span class="lamp off"></span>
+ <span class="who">${k.name}</span><span class="what">kept ${k.date}
+ <button class="key sm" onclick="event.stopPropagation();orphUnkeep('${k.name}')">UNKEEP</button></span></div>`).join('');
+ const prows = S.pacnews.map(p=>`<div class="lrow jrow"><span class="lamp ${p.safe?'':'gold'}"></span>
+ <span class="who"><b>${p.f.split('/').pop()}</b></span><span class="jmsg">${p.f} · ${p.why}</span>
+ <span class="what"><button class="key sm" onclick="event.stopPropagation();pacnewAct('${p.f}')">${p.safe?'DELETE':'MERGE'}</button></span></div>`).join('');
+ const crows = S.upd.c ? S.cves.map(c=>`<div class="lrow jrow">
+ <span class="lamp ${c[2]==='high'?'red':c[2]==='medium'?'gold':''}"></span>
+ <span class="who"><b>${c[0]}</b></span><span class="jmsg">${c[1]}</span>
+ <span class="what" style="color:${c[2]==='high'?'var(--fail)':'var(--dim)'}">${c[2]} · fix: UPDATE</span></div>`).join('') : '';
+ return `
+ <div class="wgrid">
+ ${cellCount('orphans','Orphans',{lever:lever('orphans','REMOVE ALL'),act:'orphans',sub:'digest below · kept pkgs skipped'})}
+ ${cellCount('pacnew','.pacnew',{lever:lever('pacnew','REVIEW'),act:'pacnew',sub:'per-file below'})}
+ ${cellRing('keyring','Keyring age',S.keyring.v/30*100,'days',{lever:lever('keyring','REFRESH'),act:'keyring',sub:'refresh < 30d'})}
+ ${cellBadge('reboot','Reboot req.',{lever:S.reboot.v==='YES'?lever('reboot','REBOOT','red'):'',act:'reboot',sub:'kernel == modules'})}
+ </div>
+ <div class="wgrid">
+ ${cellRing('tgage','Topgrade age',S.tgage.v/14*100,'days',{lever:lever('topgrade','TOPGRADE'),act:'topgrade',sub:'ecosystems · guard-aware'})}
+ </div>
+ ${deck('packages',[
+ {id:'orphans', label:'ORPHANS', count:S.orphans.v, lamp:S.orphans.v?'gold':''},
+ {id:'pacnew', label:'PACNEW', count:S.pacnews.length, lamp:S.pacnews.length?'gold':''},
+ {id:'advis', label:'ADVISORIES', count:S.upd.c, lamp:S.upd.c?'red':''},
+ ])}
+ ${SUBSEC.packages==='orphans'?`
+ <div class="engrave striph">orphans — top by size<span class="cnt">· KEEP = intentional (the rust lesson)</span></div>
+ <div class="jlist">${orows || '<div class="rvnote">no unkept orphans.</div>'}</div>
+ ${OKEEP.length?`<div class="evh" style="margin-top:8px">kept · ${OKEEP.length}</div><div class="jlist">${krows}</div>`:''}`:''}
+ ${SUBSEC.packages==='pacnew'?`
+ <div class="engrave striph">pacnew files<span class="cnt">· safe-delete vs needs-merge</span></div>
+ <div class="jlist">${prows || '<div class="rvnote">none pending ✓</div>'}</div>`:''}
+ ${SUBSEC.packages==='advis'?`
+ <div class="engrave striph">security advisories<span class="cnt">· arch-audit</span></div>
+ <div class="jlist">${crows || '<div class="rvnote">clean ✓</div>'}</div>`:''}
+ <div class="engrave striph">update surface<span class="cnt">· informational</span></div>
+ <div class="strip">
+ <div class="lrow"><span class="lamp ${S.upd.c?'red':''}"></span><span class="who">arch-audit</span><span class="what">${S.upd.c?S.upd.c+' CVE — named above':'clean'}</span></div>
+ <div class="lrow"><span class="lamp gold"></span><span class="who">pending updates</span><span class="what">${S.upd.p}</span></div>
+ <div class="lrow"><span class="lamp ${S.upd.a>8?'gold':''}"></span><span class="who">AUR / foreign</span><span class="what">${S.upd.a?S.upd.a+' — '+S.aurnames:'fresh'}</span></div>
+ ${S.upd.f?`<div class="lrow"><span class="lamp gold"></span><span class="who">firmware</span><span class="what">${S.upd.f} — ${S.fwnames}</span></div>`:''}
+ </div>`; },
+
+systemd(){
+ const frows = S.funits.map(f=>`<div class="lrow jrow"><span class="lamp red"></span>
+ <span class="who"><b>${f.u}</b></span><span class="jmsg">since ${f.since} · ${f.info} · journalctl -u ${f.u}</span>
+ <span class="what">
+ <button class="key sm" onclick="event.stopPropagation();funitAct('${f.u}')">RESTART</button>
+ <button class="key sm" onclick="event.stopPropagation();funitAct('${f.u}',true)">RESET</button>
+ </span></div>`).join('');
+ return `
+ <div class="wgrid">
+ ${cellCount('failed','Failed units',{lever:S.failed.v>1?lever('unit_restart','RESTART ALL'):'',act:'unit_restart',sub:S.failed.v?'roster below · per-unit keys':'none failed'})}
+ ${cellCount('timers','Maint timers',{lever:lever('timers','ENABLE'),act:'timers',sub:'Confirm · enable missing'})}
+ ${cellBadge('sysrun','is-system-running',{sub:S.sysrun.st==='ok'?'whole-system verdict · watch':'degraded — '+S.failed.v+' failed unit'+(S.failed.v===1?'':'s')+' below'})}
+ ${cellCount('taint','Kernel taint',{sub:S.taint.st==='ok'?'0 = untainted · watch':'W = kernel warned once — clears on reboot'})}
+ </div>
+ ${S.funits.length?`<div class="engrave striph">failed units<span class="cnt">· ${S.funits.length}</span></div>
+ <div class="jlist">${frows}</div>`:''}
+ <div class="engrave striph">timer roster<span class="cnt">· 5</span></div>
+ <div class="strip">${timerRoster()}</div>`; },
+
+logs(){
+ const sig = jSignal(), kn = jKnown();
+ const sigRows = sig.slice(0,10).map(g=>{
+ const key = nkey(g);
+ const armedNow = armed==='mark:'+key;
+ return `<div class="lrow jrow"><span class="lamp gold"></span>
+ <span class="who"><b>${g.id}</b></span><span class="jmsg">${g.msg}</span>
+ <span class="what">×${g.n} · ${g.first}–${g.last}${g.unit?' · journalctl -u '+g.unit.replace('.service',''):''}
+ <button class="key sm ${armedNow?'armed':''}" onclick="event.stopPropagation();markKnown('${key}')">${armedNow?'MARK?':'MARK KNOWN'}</button></span></div>`;
+ }).join('');
+ const knRows = kn.map(g=>{
+ const key = nkey(g); const n = noiseFor(g);
+ return `<div class="lrow jrow known"><span class="lamp off"></span>
+ <span class="who">${g.id}</span><span class="jmsg">${g.msg}</span>
+ <span class="what">×${g.n} · ${n.shipped?'shipped default':'marked '+n.date}
+ <button class="key sm" onclick="event.stopPropagation();unmarkKnown('${key}')">UNMARK</button></span></div>`;
+ }).join('');
+ const cgrows = S.coregroups.map(g=>`<div class="lrow jrow"><span class="lamp gold"></span>
+ <span class="who"><b>${g[0]}</b></span><span class="jmsg">coredumpctl info ${g[0]}</span>
+ <span class="what">×${g[1]} · last ${g[2]}</span></div>`).join('');
+ const khrows = S.khwev.map(e=>`<div class="lrow jrow"><span class="who" style="color:var(--dim)">${e[0]}</span>
+ <span class="jmsg" style="color:var(--fail)">${e[1]}</span></div>`).join('');
+ return `
+ <div class="wgrid">
+ ${cellCount('cores','Coredumps 7d',{lever:lever('cores','CLEAR'),act:'cores',sub:S.coregroups.length?'by binary below · keep 3d':'Auto · keep 3d'})}
+ ${cellBar('journald','journald','GB',S.journald.v/2*100,{lever:lever('journald','VACUUM'),act:'journald',sub:'Auto · cap 2 GB'})}
+ ${cellCount('jerr','Journal errors',{sub:'this boot · noise-filtered'})}
+ ${cellBadge('khw','Kernel / HW',{sub:S.khw.st==='ok'?'MCE · USB · I/O · thermal — watch':'events below — hardware, watch only'})}
+ </div>
+ ${deck('logs',[
+ {id:'signal', label:'SIGNAL', count:S.jerr.v, lamp:S.jerr.st==='ok'?(S.jerr.v?'gold':''):'gold'},
+ {id:'noise', label:'KNOWN NOISE', count:kn.length, lamp:'off'},
+ {id:'cores', label:'COREDUMPS', count:S.cores.v, lamp:lampCls(S.cores.st)},
+ {id:'hw', label:'KERNEL/HW', count:S.khwev.length||'', lamp:lampCls(S.khw.st)},
+ ])}
+ ${SUBSEC.logs==='signal'?`
+ <div class="striph" style="display:flex;align-items:center;gap:10px">
+ <span class="engrave">journal — signal<span class="cnt">· ${sig.length} group${sig.length===1?'':'s'} · ${S.jerr.v} entries</span></span>
+ ${lever('openjournal','OPEN JOURNAL')}
+ </div>
+ <div class="jlist">${sigRows || '<div class="rvnote">no unexplained errors this boot ✓</div>'}</div>`:''}
+ ${SUBSEC.logs==='noise'?`
+ <div class="striph" style="display:flex;align-items:center;gap:10px">
+ <span class="engrave">known noise<span class="cnt">· ${kn.length} pattern${kn.length===1?'':'s'}</span></span>
+ <button class="key sm ${armed==='clearmarks'?'armed':''}" onclick="event.stopPropagation();clearMarks()">${armed==='clearmarks'?'CLEAR?':'CLEAR MARKS'}</button>
+ </div>
+ <div class="jlist">${knRows || '<div class="rvnote">no known-noise patterns match this boot.</div>'}</div>`:''}
+ ${SUBSEC.logs==='cores'?`
+ <div class="engrave striph">coredumps — by binary<span class="cnt">· ${S.cores.v} total · coredumpctl info</span></div>
+ <div class="jlist">${cgrows || '<div class="rvnote">none ✓</div>'}</div>
+ <div class="evh" style="margin-top:8px">rotation</div>
+ <div class="jlist">${lrow('applog','app-log cleanup', lever('applog','RUN')+' ', S.applog.v==='ok'?'7d rotation':'stale >7d', 'applog')}</div>`:''}
+ ${SUBSEC.logs==='hw'?`
+ <div class="engrave striph">kernel / hw events<span class="cnt">· hardware — watch only</span></div>
+ <div class="jlist">${khrows || '<div class="rvnote">clean — no MCE, I/O, USB, or thermal events ✓</div>'}</div>`:''}`; },
+
+power(){
+ const [tc,tg] = String(S.temps.v).split('/');
+ const trc = S.temps.st==='ok'?'var(--pass)':S.temps.st==='fail'?'var(--fail)':'var(--amber)';
+ const seg = ['perf','bal','power'].map(m=>
+ `<button class="${S.cpumode.v===m?'on':''}" onclick="event.stopPropagation();setMode('${m}')">${m.toUpperCase()}</button>`).join('');
+ const modeCell = `
+ <div class="cell"><div class="cap"><span class="lamp ${lampCls(S.cpumode.st)}"></span> CPU mode</div>
+ <div class="val"><span class="seg">${seg}</span></div>
+ <div class="sub2">EPP hint · ${zfsMode?'intel_pstate':'amd-pstate'} active</div></div>`;
+ const veloxCells = zfsMode ? `
+ ${cellRing('batt','Battery health',S.batt.v,'%',{sub:S.batt.st==='ok'?'of design · 187 cycles · hardware — watch only':'degraded · plan replacement — watch only'})}
+ ${cellCount('blimit','Charge limit',{lever:S.blimit.st!=='ok'?lever('bat_limit','SET 80%'):'',act:'bat_limit',sub:'longevity cap · sysfs threshold'})}` : '';
+ const evRows = (rows,kind) => rows.length
+ ? rows.map(r=>{
+ if(kind==='boot') return `<div class="lrow jrow"><span class="lamp ${r[1]==='clean'?'':'red'}"></span><span class="who">${r[0]}</span><span class="what">${r[1]}</span></div>`;
+ if(kind==='ev') return `<div class="lrow jrow"><span class="who" style="color:var(--dim)">${r[0]}</span><span class="jmsg" style="color:var(--fail)">${r[1]}</span></div>`;
+ // top-memory rows: KILL (arm-to-fire) — disabled on session-critical names
+ const prot = PROTECTED.includes(r[0]);
+ const armedNow = armed==='kill:'+r[0];
+ const key = prot
+ ? `<button class="key sm dis" title="session-critical — protected">KILL</button>`
+ : `<button class="key sm ${armedNow?'armed':''}" onclick="event.stopPropagation();killProc('${r[0]}')">${armedNow?'KILL?':'KILL'}</button>`;
+ return `<div class="lrow jrow"><span class="who">${r[0]}</span><span class="what">${r[1]} ${key}</span></div>`;
+ }).join('')
+ : `<div class="lrow jrow"><span class="what" style="margin-left:0">none this boot ✓</span></div>`;
+ return `
+ <div class="wgrid">
+ <div class="cell"><div class="cap"><span class="lamp ${lampCls(S.temps.st)}"></span> Temperatures</div>
+ <div class="gpair">
+ <div class="g"><span class="ring" style="--p:${tc};--rc:${trc}"><b>${tc}°</b></span><span class="lb">CPU</span></div>
+ <div class="g"><span class="ring" style="--p:${tg};--rc:${trc}"><b>${tg}°</b></span><span class="lb">NVME</span></div>
+ </div>
+ <div class="sub2" style="text-align:center">hardware — watch only</div></div>
+ ${cellBar('mem','Memory free','GB',S.mem.v/125*100,{sub:'of 125 GB · evidence below'})}
+ ${cellBar('unclean','Unclean boots','%',S.unclean.v,{sub:'evidence below — watch only'})}
+ ${cellCount('zram','Swap / zram',{lever:S.zram.st!=='ok'?lever('zram_fix','ENABLE'):'',act:'zram_fix',sub:S.zram.st==='ok'?'zstd · active':'no swap configured'})}
+ </div>
+ <div class="wgrid">
+ ${modeCell}
+ ${veloxCells}
+ ${!zfsMode?`<div class="cell"><div class="cap"><span class="lamp off"></span> Battery</div>
+ <div class="val"><span class="readout" style="color:var(--dim)">n/a</span></div>
+ <div class="sub2">desktop — no battery</div></div>`:''}
+ ${cellBadge('throttle','Thermal throttle',{sub:S.throttle.st==='ok'?'no events · hardware — watch only':'active — cooling issue · watch only'})}
+ </div>
+ <div class="strip striph" style="align-items:center">
+ <span class="engrave">top memory</span>
+ <span class="engrave" style="grid-column:span 2">evidence — watch only</span>
+ </div>
+ <div class="strip">
+ <div><div class="evh">kill: sigterm · arm-to-fire</div><div class="jlist">${evRows(S.memtop,'mem')}</div></div>
+ <div><div class="evh">recent boots</div><div class="jlist">${evRows(S.boots,'boot')}</div></div>
+ <div><div class="evh">throttle / oom</div><div class="jlist">${evRows(S.pevents,'ev')}</div></div>
+ </div>`; },
+
+services(){
+ const unexp = S.ctrs.filter(c=>!cexp(c.name));
+ const expd = S.ctrs.filter(c=>cexp(c.name));
+ const urows = unexp.map(c=>{
+ const aM = armed==='cexp:'+c.name;
+ return `<div class="lrow jrow"><span class="lamp gold"></span>
+ <span class="who"><b>${c.name}</b></span><span class="jmsg">exited ${c.when} · code ${c.code}${c.code?' · docker logs '+c.name:''}</span>
+ <span class="what">
+ <button class="key sm ${aM?'armed':''}" onclick="event.stopPropagation();ctrMark('${c.name}')">${aM?'MARK?':'MARK EXPECTED'}</button>
+ <button class="key sm" onclick="event.stopPropagation();ctrStartOne('${c.name}')">START</button>
+ </span></div>`;
+ }).join('');
+ const erows = expd.map(c=>{
+ const p = CEXPECT.find(x=>!x.disabled&&x.name===c.name)||{};
+ return `<div class="lrow jrow known"><span class="lamp off"></span>
+ <span class="who">${c.name}</span><span class="jmsg">exited ${c.when}</span>
+ <span class="what">${p.shipped?'shipped default':'marked '+(p.date||'')}
+ <button class="key sm" onclick="event.stopPropagation();ctrUnmark('${c.name}')">UNMARK</button></span></div>`;
+ }).join('');
+ const dfrows = S.ddf.map(r=>`<div class="lrow jrow"><span class="who">${r[0]}</span>
+ <span class="what">${r[1]} · reclaimable ${r[2]}</span></div>`).join('');
+ const cronRows = S.cron.st==='ok' ? `
+ <div class="lrow"><span class="lamp"></span><span class="who">log-cleanup (12:00)</span><span class="what">present</span></div>
+ <div class="lrow"><span class="lamp"></span><span class="who">rsyncshot hourly</span><span class="what">present</span></div>
+ <div class="lrow"><span class="lamp"></span><span class="who">rsyncshot daily 02:30</span><span class="what">present</span></div>` : `
+ <div class="lrow"><span class="lamp red"></span><span class="who">log-cleanup (12:00)</span><span class="what">not running — cronie down</span></div>
+ <div class="lrow"><span class="lamp red"></span><span class="who">rsyncshot hourly</span><span class="what">not running — cronie down</span></div>
+ <div class="lrow"><span class="lamp red"></span><span class="who">rsyncshot daily 02:30</span><span class="what">not running — cronie down</span></div>`;
+ return `
+ <div class="wgrid">
+ ${cellCount('docker','Docker reclaim',{lever:lever('docker','PRUNE'),act:'docker',unit:' GB',sub:'breakdown below · tiers 1+2'})}
+ ${cellCount('rsync','Last backup',{lever:lever('rsyncnow','RUN NOW'),act:'rsyncnow',unit:' ago',sub:'rsyncshot → truenas'})}
+ ${cellCount('dstopped','Stopped containers',{lever:unexp.length>1?lever('ctr_start','START ALL'):'',act:'ctr_start',sub:unexp.length?unexp.length+' unexpected · '+expd.length+' expected':'all expected'})}
+ ${cellBadge('cron','cron',{lever:S.cron.st!=='ok'?lever('cron_fix','START'):'',act:'cron_fix',sub:'entry roster below'})}
+ </div>
+ ${deck('services',[
+ {id:'containers', label:'CONTAINERS', count:unexp.length?unexp.length+'!':expd.length, lamp:unexp.length?'gold':''},
+ {id:'docker', label:'DOCKER DISK', count:S.docker.v+' GB', lamp:lampCls(S.docker.st)},
+ {id:'cron', label:'CRON & BACKUPS', count:'', lamp:(S.cron.st!=='ok'||S.rsync.st!=='ok')?'red':''},
+ ])}
+ ${SUBSEC.services==='containers'?`
+ ${unexp.length?`<div class="engrave striph">containers — signal<span class="cnt">· ${unexp.length} unexpected</span></div>
+ <div class="jlist">${urows}</div>`:''}
+ <div class="engrave striph">containers — expected<span class="cnt">· ${expd.length}</span></div>
+ <div class="jlist">${erows || '<div class="rvnote">no expected-stopped containers present.</div>'}</div>`:''}
+ ${SUBSEC.services==='docker'?`
+ <div class="engrave striph">docker disk<span class="cnt">· system df · reclaimable by type</span></div>
+ <div class="jlist">${dfrows}</div>
+ <div class="evh" style="margin-top:8px">virt</div>
+ <div class="jlist">${lrow('libvirt','libvirt VMs','', 'shut off · expected')}</div>`:''}
+ ${SUBSEC.services==='cron'?`
+ <div class="engrave striph">cron entries — expected<span class="cnt">· 3</span></div>
+ <div class="jlist">${cronRows}</div>
+ <div class="evh" style="margin-top:8px">backups</div>
+ <div class="jlist">${backupRoster()}</div>`:''}`; },
+
+snapshots(){
+ const t = S.snaptypes;
+ const staleN = Math.max(0, t.single-2);
+ const aS = armed==='snapstale';
+ return `
+ <div class="wgrid">
+ ${cellCount('snapper','Snapper count',{lever:lever('snapper','PRUNE'),act:'snapper',sub:S.snapper.st==='ok'?'by type below':'pile-up — see singles below'})}
+ ${cellBadge('stimer','Auto-timer',{lever:S.stimer.st!=='ok'?lever('stimer_fix','ENABLE'):'',act:'stimer_fix',sub:'timeline + cleanup'})}
+ ${cellCount('oldest','Oldest',{unit:' days',sub:S.oldest.st==='ok'?'within budget':'chain hoards space'})}
+ <div class="cell">${S.retention.st!=='ok'?`<span class="lever">${lever('retention','FIX')}</span>`:''}
+ <div class="cap"><span class="lamp ${lampFor('retention','retention')}"></span> Retention</div>
+ <div class="val" style="font-size:12px;color:${S.retention.st==='ok'?'var(--cream)':'var(--amber)'}">${S.retention.v}</div>
+ <div class="sub2">${S.retention.st==='ok'?'monthly=2 (the /home lesson)':'monthly=10 — silently hoards space'}</div>${errline('retention')}</div>
+ </div>
+ <div class="engrave striph">by type<span class="cnt">· singles escape timeline cleanup</span></div>
+ <div class="jlist">
+ <div class="lrow jrow"><span class="lamp"></span><span class="who">timeline</span><span class="what">${t.timeline} · auto-pruned</span></div>
+ <div class="lrow jrow"><span class="lamp ${staleN>3?'gold':''}"></span><span class="who"><b>single (manual)</b></span>
+ <span class="jmsg">oldest ${t.oldsingle}</span>
+ <span class="what">${t.single}${staleN>3?` <button class="key sm ${aS?'armed':''}" onclick="event.stopPropagation();snapDelStale()">${aS?'DELETE?':'DELETE STALE'}</button>`:''}</span></div>
+ <div class="lrow jrow"><span class="lamp"></span><span class="who">pre / post</span><span class="what">${t.prepost} · paired with pacman ops</span></div>
+ </div>`; },
+
+network(){
+ const n = parseInt(S.ts.v)||0;
+ const bars = [1,2,3,4].map(i=>`<i class="${i<=n?'on':''}"></i>`).join('');
+ const sig = lsnSignal();
+ const exp = LEXPECT.filter(p=>!p.disabled);
+ const pubN = sig.filter(r=>isPub(r.bind)).length;
+ const sigRows = sig.map(r=>{
+ const key = lkey(r);
+ const aFix = armed==='lsnfix:'+key, aMark = armed==='lexp:'+key;
+ const bindTag = isPub(r.bind)
+ ? `<span class="badge red" style="font-size:.55rem">PUBLIC</span>`
+ : `<span style="color:var(--dim);font-size:10px">loopback</span>`;
+ return `<div class="lrow jrow"><span class="lamp ${isPub(r.bind)?'red':'gold'}"></span>
+ <span class="who"><b>${r.proc}</b></span>
+ <span class="jmsg">:${r.port} · ${r.bind}${r.unit?' · '+r.unit+'.service':''}</span>
+ <span class="what">${bindTag}
+ <button class="key sm ${aMark?'armed':''}" onclick="event.stopPropagation();markExpected('${key}')">${aMark?'MARK?':'MARK EXPECTED'}</button>
+ <button class="key sm ${aFix?'armed':''}" onclick="event.stopPropagation();lsnFix('${key}')">${aFix?(r.unit?'STOP?':'KILL?'):(r.unit?'STOP':'KILL')}</button>
+ </span></div>`;
+ }).join('');
+ const expRows = exp.map(p=>{
+ const key = p.proc+'|'+p.port;
+ return `<div class="lrow jrow known"><span class="lamp off"></span>
+ <span class="who">${p.proc}</span><span class="jmsg">:${p.port}</span>
+ <span class="what">${p.shipped?'shipped default':'marked '+p.date}
+ <button class="key sm" onclick="event.stopPropagation();unmarkExpected('${key}')">UNMARK</button></span></div>`;
+ }).join('');
+ const fwNote = S.fw.st!=='ok' && pubN
+ ? ` <span style="color:var(--fail);letter-spacing:0;text-transform:none">· ufw down — ${pubN} public bind${pubN===1?'':'s'} exposed</span>` : '';
+ return `
+ <div class="wgrid">
+ ${cellBadge('fw','Firewall',{lever:S.fw.st!=='ok'?lever('fw_fix','ENABLE',S.fw.st==='fail'?'red':''):'',act:'fw_fix',sub:'ufw · mosh rule'})}
+ ${cellCount('listen','Unexpected listeners',{sub:S.listen.st==='ok'?'all matched · '+exp.length+' expected':S.listen.v+' unmatched · '+pubN+' public-bind'})}
+ <div class="cell">${S.ts.st!=='ok'?`<span class="lever">${lever('ts_fix','RESTART')}</span>`:''}
+ <div class="cap"><span class="lamp ${lampFor('ts','ts_fix')}"></span> Tailscale</div>
+ <div class="val"><span class="ladder ${S.ts.st==='fail'?'bad':''}">${bars}</span>
+ <span class="readout" style="font-size:15px">${S.ts.v}</span></div>
+ <div class="sub2">peers online</div>${errline('ts_fix')}</div>
+ ${cellCount('f2b','fail2ban',{lever:S.f2b.st!=='ok'?lever('f2b_fix','RESTART'):'',act:'f2b_fix',sub:S.f2b.st==='ok'?'sshd jail · '+S.f2b.v+' bans 7d':'service down'})}
+ </div>
+ <div class="striph" style="display:flex;align-items:center;gap:10px">
+ <span class="engrave">listeners — signal<span class="cnt">· ${sig.length} unexpected</span>${fwNote}</span>
+ </div>
+ <div class="jlist">${sigRows || '<div class="rvnote">every listener matches the expected list.</div>'}</div>
+ <div class="striph" style="display:flex;align-items:center;gap:10px">
+ <span class="engrave">expected<span class="cnt">· ${exp.length} pattern${exp.length===1?'':'s'}</span></span>
+ <button class="key sm ${armed==='clearlsn'?'armed':''}" onclick="event.stopPropagation();clearLsn()">${armed==='clearlsn'?'CLEAR?':'CLEAR MARKS'}</button>
+ </div>
+ <div class="jlist">${expRows}</div>
+ <div class="engrave striph">peers<span class="cnt">· 4</span></div>
+ <div class="strip">${peerRoster()}</div>
+ <div class="engrave striph">resolution<span class="cnt">· 2</span></div>
+ <div class="strip two">
+ ${lrow('ntp','NTP (chrony)', S.ntp.st!=='ok'?lever('ntp_fix','RESTART')+' ':'', S.ntp.st==='ok'?'synced · +2 ms':undefined, 'ntp_fix')}
+ ${lrow('dns','DNS / NetworkManager', S.dns.st!=='ok'?lever('netdoc','NET DOCTOR')+' ':'', undefined, 'netdoc')}
+ </div>`; },
+};
+
+/* ---- doctor: streams into the results wall; the metrics subpanel stays put ---- */
+let doctorRunning = false;
+function startDoctor(mode){
+ if(doctorRunning){ toast('doctor already running'); return; }
+ const title = mode==='reclaim' ? 'reclaim space' : 'clean up';
+ const steps = [
+ {name:'cache trim', act:'clean_cache',
+ res:()=> S.cache.st==='warn' ? (S.cache={v:0.4,st:'ok'}, 'reclaimed 8.4 GB') : 'nothing to reclaim'},
+ {name:'journal vacuum', act:'journald',
+ res:()=> S.journald.v>0.5 ? (S.journald={v:0.3,st:'ok'}, 'reclaimed 0.9 GB') : 'already tight'},
+ {name:'coredump clear', act:'cores',
+ res:()=> S.cores.v>0 ? (()=>{const n=S.cores.v; S.cores={v:0,st:'ok'}; return n+' cleared';})() : 'none to clear'},
+ {name:'app-log cleanup', act:'applog',
+ res:()=> (S.applog={v:'ok',st:'ok'}, 'nothing older than 7d')},
+ ];
+ if(mode==='reclaim'){
+ steps.push(
+ {name:'docker prune (tier 1)', act:'docker',
+ res:()=> S.docker.st==='warn' ? (S.docker={v:0.2,st:'ok'}, 'reclaimed 2.8 GB') : 'nothing to prune'},
+ {name:'snapper cleanup', act:'snapper',
+ res:()=> (S.snapper={v:Math.min(S.snapper.v,31),st:'ok'}, 'retention applied')},
+ {name:'disk re-read', act:'applog',
+ res:()=> { S.disk={v:Math.max(60,S.disk.v-11), st:S.disk.v-11>90?'fail':S.disk.v-11>80?'warn':'ok'}; return 'now '+S.disk.v+'%'; }},
+ );
+ }
+ doctorRunning = true;
+ resAdd('doctor · '+title, 'started · '+steps.length+' actions', 'done');
+ render();
+ let i=0, entry=null, failed=0;
+ function step(){
+ if(entry){
+ const p = steps[i-1];
+ if(failMode){ ERR[p.act]=ACTIONS[p.act].err; failed++; resDone(entry,'fail',ERR[p.act]); }
+ else { delete ERR[p.act]; resDone(entry,'done',p.res()); }
+ render();
+ }
+ if(i<steps.length){
+ entry = resAdd(steps[i].name, 'running…'); i++;
+ doctorTimer = setTimeout(step, 1000);
+ } else {
+ doctorRunning = false;
+ resAdd('doctor · '+title,
+ `${steps.length-failed} done${failed?' · '+failed+' failed':''}`,
+ failed?'fail':'done');
+ render();
+ }
+ }
+ step();
+}
+
+/* ---- review & fix ---- */
+const REVIEW = [
+ {act:'fstrim', who:'fstrim.timer', why:'weekly SSD TRIM disabled', id:'fstrim', ok:()=>zfsMode||S.fstrim.st==='ok'},
+ {act:'ztrim_fix', who:'zpool autotrim', why:'TRIM disabled on the pool', id:'ztrim', ok:()=>!zfsMode||S.ztrim.st==='ok'},
+ {act:'scrub', who:'scrub', why:'past the scrub cadence', id:'scrub', ok:()=>S.scrub.st==='ok'},
+ {act:'orphans', who:'orphans', why:'unowned packages present', id:'orphans', ok:()=>S.orphans.st==='ok'},
+ {act:'pacnew', who:'.pacnew files', why:'configs awaiting merge', id:'pacnew', ok:()=>S.pacnew.st==='ok'},
+ {act:'timers', who:'maint timers', why:'timers not firing', id:'timers', ok:()=>S.timers.st==='ok'},
+ {act:'keyring', who:'keyring', why:'refresh before next update', id:'keyring', ok:()=>S.keyring.st==='ok'},
+ {act:'docker', who:'docker reclaim', why:'space reclaimable (tiers 1+2)', id:'docker', ok:()=>S.docker.st==='ok'},
+ {act:'snapper', who:'snapper', why:'retention pass', id:'snapper', ok:()=>S.snapper.st==='ok'&&S.snapper.v<42},
+ {act:'deeptrim',who:'deep-trim', why:'keep 1 version — max reclaim, less downgrade headroom', id:'deeptrim', ok:()=>S.deeptrim.v==='keep 1'},
+ {act:'unit_restart', who:'failed units', why:'restart the failed unit(s)', id:'failed', ok:()=>S.failed.st==='ok'},
+ {act:'tgage', who:'topgrade', why:'ecosystem updates stale', id:'tgage', ok:()=>S.tgage.st==='ok', useAct:'topgrade'},
+ {act:'f2b_fix', who:'fail2ban', why:'service down', id:'f2b', ok:()=>S.f2b.st==='ok'},
+ {act:'cron_fix',who:'cronie', why:'service down', id:'cron', ok:()=>S.cron.st==='ok'},
+ {act:'ntp_fix', who:'NTP (chrony)', why:'clock unsynchronized', id:'ntp', ok:()=>S.ntp.st==='ok'},
+ {act:'ts_fix', who:'tailscaled', why:'peers unreachable', id:'ts', ok:()=>S.ts.st==='ok'},
+ {act:'stimer_fix', who:'snapshot timer', why:'auto-snapshots not firing', id:'stimer', ok:()=>S.stimer.st==='ok'},
+ {act:'zram_fix',who:'zram', why:'no swap configured', id:'zram', ok:()=>S.zram.st==='ok'},
+ {act:'balance', who:'btrfs balance', why:'unallocated headroom low', id:'unalloc', ok:()=>zfsMode||S.unalloc.st==='ok'},
+ {act:'reinstall', who:'file integrity', why:'modified package files', id:'integ', ok:()=>S.integ.st==='ok'},
+ {act:'retention', who:'snapshot retention', why:'monthly limit hoards space', id:'retention', ok:()=>S.retention.st==='ok'},
+ {act:'rsyncnow', who:'backup', why:'stale or failed — run now', id:'rsync', ok:()=>S.rsync.st==='ok'},
+ {act:'reboot', who:'reboot', why:'running kernel != installed', id:'reboot', ok:()=>S.reboot.v!=='YES'},
+ {act:'bat_limit', who:'charge limit', why:'no charge cap — battery longevity', id:'blimit', ok:()=>!zfsMode||S.blimit.st==='ok'},
+ {act:'ctr_start', who:'stopped containers', why:'unexpected exited containers', id:'dstopped', ok:()=>S.dstopped.st==='ok'},
+ {act:'fw_fix', who:'firewall', why:'ufw inactive — host exposed', id:'fw', ok:()=>S.fw.st==='ok'},
+];
+function renderReview(){
+ const items = REVIEW.filter(r=>!r.ok()||ERR[r.useAct||r.act]);
+ const rows = items.map(r=>{
+ const act = r.useAct||r.act;
+ const done = r.ok();
+ const failed = !!ERR[act];
+ return `<div class="rvrow">
+ <span class="lamp ${failed?'red':done?'':lampCls(S[r.id].st)}"></span>
+ <span class="who">${r.who}</span>
+ <span class="why ${failed?'errtx':''}">${failed?'✗ '+ERR[act]:r.why}</span>
+ <span class="keys">${done?'<span class="res">done ✓</span>':lever(act,'FIX')}</span></div>`;
+ }).join('');
+ return `
+ <div class="subhead"><span class="lamp gold"></span><span class="nm">Doctor · Review &amp; fix</span>
+ <span class="cnt">${items.length} Confirm items</span></div>
+ <div class="rvlist">${rows||'<div class="rvnote">nothing needs review — all Confirm items clear.</div>'}</div>
+ <div class="rvnote">Destructive fixes arm on first press, fire on the second. Metrics without a determinate
+ lever — failed units, unclean boots, updates, CVEs — stay on their subpanels as read-only telemetry.</div>
+ <div class="ofoot"><button class="key" onclick="go('cat:storage')">DONE</button></div>`;
+}
+
+/* ================= loading / hydration ================= */
+/* tiers mirror real probe cost: t0 = local file/daemon reads, t1 = process probes
+ (smartctl, docker df, snapper, du), t2 = package scans (cached in real build) */
+const TIERS = [
+ {ms:220, cats:['systemd','power','network','snapshots']},
+ {ms:650, cats:['storage','logs','services']},
+ {ms:1150, cats:['packages']},
+];
+function startLoad(){
+ loadTimers.forEach(clearTimeout); loadTimers=[];
+ loaded.clear();
+ $('lastscan').textContent = 'scanning…';
+ render();
+ TIERS.forEach(t=>{
+ loadTimers.push(setTimeout(()=>{
+ t.cats.forEach(c=>loaded.add(c));
+ if(loaded.size===CATS.length) $('lastscan').textContent='last scan just now · 44 checks · 1.2s';
+ render();
+ }, t.ms));
+ });
+}
+
+/* ================= render ================= */
+const ACT2CAT = { clean_cache:'storage', scrub:'storage', fstrim:'storage', deeptrim:'storage',
+ balance:'storage', reinstall:'storage', smarttest:'storage', ztrim_fix:'storage',
+ orphans:'packages', pacnew:'packages', keyring:'packages', reboot:'packages', topgrade:'packages',
+ timers:'systemd', unit_restart:'systemd',
+ cores:'logs', journald:'logs', applog:'logs', openjournal:'logs',
+ zram_fix:'power', bat_limit:'power',
+ docker:'services', cron_fix:'services', rsyncnow:'services', ctr_start:'services', fw_fix:'network',
+ snapper:'snapshots', stimer_fix:'snapshots', retention:'snapshots',
+ f2b_fix:'network', ntp_fix:'network', ts_fix:'network', netdoc:'network' };
+function catErr(cid){ return Object.keys(ERR).some(a=>ACT2CAT[a]===cid); }
+
+function renderSelector(){
+ $('selector').innerHTML = CATS.map(c=>{
+ const sel = view==='cat:'+c.id;
+ if(!loaded.has(c.id)){
+ return `<div class="selbtn loading ${sel?'sel':''}" onclick="go('cat:${c.id}')">
+ <span class="lamp busy"></span><span class="nm">${c.name}</span><span class="ct">···</span></div>`;
+ }
+ const k = counts(c);
+ const err = catErr(c.id);
+ const ct = err ? `<span class="a crit">✗ fail</span> · <span class="k">${k.ok}✓</span>`
+ : k.attn ? `<span class="a ${k.crit?'crit':''}">${k.attn}!</span> · <span class="k">${k.ok}✓</span>`
+ : `<span class="k">${k.ok} ✓</span>`;
+ return `<div class="selbtn ${(k.crit||err)?'crit':k.attn?'attn':'clear'} ${sel?'sel':''}" onclick="go('cat:${c.id}')">
+ <span class="lamp ${(k.crit||err)?'red':k.attn?'gold':''}"></span>
+ <span class="nm">${c.name}</span><span class="ct">${ct}</span></div>`;
+ }).join('');
+}
+function renderSub(){
+ const el = $('sub');
+ if(view==='review'){ el.innerHTML = renderReview(); return; }
+ const cid = view.slice(4);
+ if(!loaded.has(cid)){
+ const cat = CATS.find(c=>c.id===cid);
+ el.innerHTML = `<div class="subhead"><span class="lamp busy"></span><span class="nm">${cat.name}</span>
+ <span class="cnt">gathering…</span></div>
+ <div class="skel"><span class="lamp busy" style="width:14px;height:14px"></span>probing</div>`;
+ return;
+ }
+ const cat = CATS.find(c=>c.id===cid);
+ const k = counts(cat);
+ const [nfix,nwatch] = fixwatch(cid);
+ const cnt = (k.attn ? `<span class="att ${k.crit?'crit':''}">${k.attn} attention</span> · <span class="okc">${k.ok} ok</span>`
+ : `<span class="okc">all clear · ${k.ok} ✓</span>`)
+ + ` · ${nfix} fixable · ${nwatch} watch`;
+ el.innerHTML = `<div class="subhead">
+ <span class="lamp ${k.crit?'red':k.attn?'gold':''}"></span>
+ <span class="nm">${cat.name}</span><span class="cnt">${cnt}</span></div>` + SUB[cid]();
+}
+function render(){
+ recalcJerr(); recalcListen(); recalcCtrs();
+ renderSelector(); renderSub();
+ // faceplate verdict = worst diagnostic state
+ let worst='ok';
+ DIAG.forEach(d=>{ const st=S[d].st;
+ if(st==='fail') worst='fail'; else if(st==='warn'&&worst!=='fail') worst='warn'; });
+ const fl=$('facelamp');
+ fl.className = 'lamp ' + (loaded.size<CATS.length?'busy':worst==='fail'?'red':worst==='warn'?'gold':'');
+ // badges
+ let attn=0; CATS.forEach(c=>{ if(loaded.has(c.id)){ attn+=counts(c).attn; } });
+ $('attbadge').textContent = loaded.size<CATS.length ? 'SCANNING' : (attn ? attn+' ATTN' : 'ALL CLEAR');
+ const errN = Object.keys(ERR).length;
+ $('errbadge').style.display = errN ? '' : 'none';
+ $('errbadge').textContent = errN + ' FAILED';
+ // updates strip: green when nothing, amber for a few, red for CVEs / a lot
+ const u = S.upd;
+ const ucls = (u.c>0 || u.p>100) ? 'crit' : (u.p>0 || u.a>0 || u.f>0) ? 'warn' : 'ok';
+ $('updbar').className = 'updbar ' + ucls;
+ // CVE badge: red and visible only when there are CVEs to show
+ $('cvebadge').style.display = S.upd.c ? '' : 'none';
+ $('cvebadge').textContent = S.upd.c + ' CVE';
+ $('upd-p').textContent = S.upd.p; $('upd-c').textContent = S.upd.c + ' CVE';
+ $('upd-c').classList.toggle('cve', S.upd.c>0);
+ $('upd-c').style.color = S.upd.c ? '' : 'var(--dim)';
+ $('upd-a').textContent = S.upd.a; $('upd-f').textContent = S.upd.f;
+ $('k-clean').classList.toggle('on', doctorRunning);
+ $('k-review').classList.toggle('on', view==='review');
+ // guard-armed update keys: red + '?' while awaiting the override press
+ $('k-update').classList.toggle('armed', armed==='sysupdate');
+ $('k-update').textContent = armed==='sysupdate' ? 'UPDATE?' : 'UPDATE';
+ $('k-topgrade').classList.toggle('armed', armed==='topgrade');
+ $('k-topgrade').textContent = armed==='topgrade' ? 'TOPGRADE?' : 'TOPGRADE';
+ // reboot offer key appears on the updates strip once an update lands
+ $('upd-rb').innerHTML = S.reboot.v==='YES' ? lever('reboot','REBOOT','red') : '';
+}
+function go(v){
+ armed=null;
+ if(v==='doctor'||v==='reclaim'){ startDoctor(v==='reclaim'?'reclaim':undefined); return; }
+ view=v; render();
+}
+function closePanel(){ $('capsule').style.display='none'; $('closed').style.display='block'; }
+function openPanel(){ $('capsule').style.display='block'; $('closed').style.display='none'; startLoad(); }
+document.addEventListener('keydown', e=>{ if(e.key==='Escape') closePanel(); });
+startLoad();
+
+/* live refresh sim: the thermal/memory group re-reads every 3 s while its
+ subpanel is visible (panel-open gated, like the audio panel's meters) */
+setInterval(()=>{
+ if($('capsule').style.display==='none') return;
+ if(view!=='cat:power' || !loaded.has('power')) return;
+ const j = (v,lo,hi)=>Math.max(lo,Math.min(hi, v + (Math.random()<0.5?-1:1)));
+ let [tc,tg] = String(S.temps.v).split('/').map(Number);
+ if(badMode){ tc=j(tc,88,94); tg=j(tg,74,80); } else { tc=j(tc,57,65); tg=j(tg,50,58); }
+ S.temps.v = tc+'/'+tg;
+ S.mem.v = Math.round((S.mem.v + (Math.random()-0.5)*0.6)*10)/10;
+ render();
+}, 3000);
+</script>
+</body>
+</html>
diff --git a/todo.org b/todo.org
index bef8149..7022682 100644
--- a/todo.org
+++ b/todo.org
@@ -21,6 +21,19 @@ The vocabulary is open — topic tags are coined as needed — so these are conv
- *Effort / autonomy*: =:quick:= a spare-moment fix (minutes, not a sitting); =:solo:= Claude can carry it end to end — there's a build path, a test path, and no upfront decision needed (a leftover manual spot-check doesn't disqualify it).
- *Topic / area* (open): the subsystem a task touches — e.g. =:hyprland:= =:waybar:= =:mpd:= =:music:= =:network:= =:tooling:= =:llm:= =:eask:= =:pocketbook:= =:cmail:=. Coin a new one when it aids filtering.
* Archsetup Open Work
+** TODO [#B] Maintenance console — spec from design doc + prototype :feature:
+Run spec-create against [[file:docs/design/maintenance-console-design-ideas.org][maintenance-console-design-ideas.org]] (all decisions resolved 2026-07-06/07) and the interactive prototype ([[file:docs/prototypes/2026-07-07-maint-console-E5-selector-subpanel.html][E5]], the converged design). The design is complete: selector + dense subpanels, doctor (Clean up / Review & fix) streaming to the results wall, determinate remedies incl. guard-armed updates, evidence digests + curation lifecycles (known-noise, expected-listeners, expected-containers, kept-orphans), rotary band selectors on tall subpanels, refresh cadence, CLI-first architecture with the four-layer test strategy (unit fakes → read-only live → VM remedies via the archsetup test harness → AT-SPI GUI smoke on fixtures). Prototype's GOOD/BAD snapshots become test fixtures. Build lands as the =maint= package in dotfiles (fourth panel sibling); thresholds TOML owned by archsetup. Related: the proposed system-health-check workflow move from home (see doc) sequences at implementation time.
+
+** TODO [#C] Consistent keybinding family for the panel console :feature:hyprland:
+Consider putting every panel (net, bluetooth, audio, timer, and the coming maintenance console) on one consistent chord family — a shared modifier set (Super+Shift, Control+Alt, or similar) plus a mnemonic letter per panel (N/B/A/T/M). Today the panels open via waybar clicks only; a uniform chord family makes them keyboard-reachable and predictable. Watch for collisions with existing binds: Super+Shift+A is already PTT toggle, and the hold-to-talk grave bind is load-bearing. Decide the family, audit current hyprland binds for conflicts, wire via the dotfiles hyprland config, and document in the keybind reference. Both machines (velox can't QMK-remap, so chords must work on a plain laptop keyboard).
+
+** TODO [#C] Realtime lamp output for the net + bt doctors :feature:
+Retrofit the net doctor (=~/.dotfiles/net/src/net/doctor.py=) and bluetooth doctor (=~/.dotfiles/bluetooth/src/bt/doctor.py=) to stream results as a live output wall — one lamp per escalation step, amber while running, green on success, red on failure — instead of a final summary. Matches the maintenance-console doctor design (see [[file:docs/design/maintenance-console-design-ideas.org][maintenance-console-design-ideas.org]], "Doctor = live output wall"). Goal: every doctor in the system reads the same way. Both doctors already step through an escalation chain re-probing after each, so the steps are natural lamp boundaries.
+
+Scope note (Craig, 2026-07-07): realtime lamp *behavior* only. The maintenance console's wider results-wall layout (date+time stamp column, COPY, persistent history) does NOT backport — the net/bt panels are ~400px wide and lack the horizontal real estate. Their existing output wells keep their compact layout; this task just makes them stream live.
+
+Addendum (Craig, 2026-07-07): DO backport the 3.5-entry height convention — every panel's output well caps at 3.5 visible entries, the half-visible entry being the scroll cue, with the dark slate-on-black scrollbar. Layout stays compact per above; only the height cap + scroll affordance carries over.
+
** TODO [#B] Scrolling/Carousel layout: frame fit + wrap-around :hyprland:
:PROPERTIES:
:LAST_REVIEWED: 2026-07-06