Finish the wonderland: System told truthfully, in eight tabs instead of ten

Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
This commit is contained in:
Gabriel Brown
2026-08-24 23:31:52 -04:00
parent 9ffaf45a4d
commit be0e55214b
57 changed files with 5040 additions and 925 deletions
+1 -1
View File
@@ -87,7 +87,7 @@ Found on **Shell Control Center**.
## datetime
Found on **System Date & Time**.
Found on **System Date, Time & Region**.
| Setting | Default | What it does |
|---|---|---|
@@ -1672,3 +1672,235 @@ settings generator writes 38 of them).
`search-routing-contract`, `ipc-targets-contract`, `settings-jump-contract`,
`settings-docs-contract`, `readme-contract`, all eight of `tests/hypr/`),
then the sandboxed-harness ones, then the live settings window last.
## Phase 15 (System) — the last one
Spec: `2026-08-24-system-redesign.md`. System went from ten tabs to eight:
Region & Language merged into **Date, Time & Region** — a date format and the
clock that shows it are one subject, and splitting them meant changing how a
date is written on a page that never showed one — and the Manual became a
routable leaf with no tab, opened from About's Manual card, a deep link, or the
search box rather than found by scanning a strip. With that, the settings
redesign is complete: fourteen categories, thirty-eight leaves, every one of
them rebuilt.
The bug list this phase carried was longer than the layout change. The
hardcoded Quickshell version, the auto-download switch nested inside the
Firmware card, `CHECK_TITLES` missing `panama.updates`, two Health actions with
no target, RegionPage's stale "Open appearance" handoff, `panama-updates`'
stale dnf-automatic comment, the backup colour gap, Restore without
confirmation, and four pieces of dead code.
Three agents edited the tree concurrently; everything below was reconciled
against the landed files rather than against the spec's pinned shapes, and
every contract named here was **run**.
### No new contracts (175 → 175)
Every pin fitted a file that already existed, so the suite did not grow. That
was the judgement call this phase offered and it went the honest way: a
`system-pages-contract` would have been a new file holding assertions that
belong beside the ones they are variations of — the nav counts beside the nav
taxonomy, the repair command beside the repair, the single-check verb beside
the scan. `setup/readme-contract` — RUN, PASS ("175 contracts, as documented").
### What each contract gained, and why
- **`quickshell/settings-nav-contract` — RUN, PASS** (14 categories, 38 leaves
of which 1 hidden, 3 retired ids). It learned the third leaf shape:
`SettingsRoutes.hiddenLeaves`, which is how the manual stays addressable
without a tab. A hidden leaf that is also a tab, or also a category, now
fails — that overlap is the one the existing checks could not see, because
the hidden half draws no row anywhere. Then: the System strip is pinned at
exactly eight tabs in order (the number IS the point — it is the horizontal
space one row has, and an eleventh subject needs a decision, not another
entry), `region` is pinned to resolve specifically to `datetime` rather than
merely to *a* leaf, `RegionPage.qml` must be gone, and the manual must be a
leaf and must not be a tab.
It also grew the check that would have caught the retired search entries:
every page id in `SettingsSearch` — extra entries and group routes alike —
must be a live leaf, with a distinct failure message when it is a retired id,
since that is the case that still opens a window and lands somewhere else.
Plus a table of the fifteen subjects the consolidation moved, each asserted
to be findable by its own name on the tab that now owns it.
- **`quickshell/settings-pages-contract` — RUN, PASS** (production PIDs
preserved). `Updates`, `DateTime`, `Containers` and `Manual` joined the
page-scaffold sweep; Containers is the one that was waiting for it, since its
root was a bare `Item` and every convention the scaffold carries was
hand-rolled there. The live routing list gained `updates`, `datetime` and
`containers`, and the retired `region` id is now driven through the real IPC
and asserted to land on `datetime`.
- **`quickshell/updates-contract` — RUN, PASS** (2 dnf, 3 flatpak, 0 firmware).
Three additions. **Download sizes add up or are not offered**: a total is
present only when every pending item was priced, because a partial figure
shown as the download understates it, and understating it is the direction
that costs somebody money on a metered connection. **A changelog is a read**,
proved by construction rather than by inspection: `dnf5`, `flatpak`,
`fwupdmgr`, `pkexec` and `systemctl` are replaced with stubs that record
their argv and answer nothing, and the recorded argv is then checked for
mutating tokens — which also exercises the honest-absence path, the common
case on a machine with third-party repositories. Names outside
`NAME_PATTERN` must be refused before anything is launched. **One
application, by name**: the per-app argv is asserted exactly
(`flatpak update -y --noninteractive <id>`) against a stub, and an ID the
last scan did not list must be refused without running flatpak at all.
- **`quickshell/panama-doctor-contract` — RUN, PASS.** The `CHECK_TITLES`
KeyError is pinned twice: as the invariant (every id in `CHECK_ORDER` has a
title, asserted by importing the module) and as the path (a probe forced to
raise must come back as a titled warning row rather than taking the report
with it). Worth both — the bug needed the updates probe to fail, which it
almost never does, and the containment that was supposed to catch it *was*
the KeyError, raised inside the `except` handling the original failure.
Then: the updates action's `target: "updates"`, driven by a fixture cache
with a security count; `repairCommand` asserted against `REPAIR_COMMANDS`
itself rather than restated, so an argv change cannot leave the row
describing the old one; and the `check CHECK_ID` verb — full snapshot
envelope, exactly one check, summary arithmetic covering only that check,
refusal of unauthored ids with no report and no filesystem mutation, and no
second output mode.
The fixture also gained `XDG_CACHE_HOME`, which was leaking: `check_updates`
reads the Updates cache from the environment rather than through
`DoctorConfig`, so the contract had been reading the real machine's pending
updates.
- **`quickshell/health-service-contract` — RUN, PASS.** The both-sides pin is
derived from both files: every `open`/`instructions` target the doctor emits
must appear in Health's `settingsTargets`/`instructionTargets`. This is the
one where half a change does not degrade a row, it blanks the page —
`validAction` false makes `validCheck` false makes `consumeSnapshot` reject
the whole report. Live fixtures either side of it: a check targeting
`updates` is accepted intact, and one targeting `storage` — a real page the
list deliberately omits — is rejected, which is what makes the allow-list an
allow-list. Also `repairCommand` surviving the projection (a new known key is
dropped silently unless it is added, and the failure is invisible: the repair
still works, only the sentence saying what it runs is gone), a check without
one still accepted, `refreshCheck` asking for `check <id>` and never `--json`
while leaving the other rows and recomputing the headline counts, an unknown
id starting no process, and `saveReport` writing the same redacted projection
to a named file.
- **`quickshell/health-ui-contract` — RUN, PASS.** The Colour profiles handoff
assertion is inverted rather than deleted (Displays owns colour profile, bit
depth, SDR brightness and saturation per output, which is more than GNOME's
panel can say in a session it does not manage); Digital wellbeing stays. The
repair command must be shown, and must not be gated on `lastRepair`,
`repairingId` or a working state — after the fact is not the same answer,
because by then the decision is made. A per-row re-check and a save-report
row must exist. The fixture grew a `panama.updates` check whose action is
followed for real through `requestHealthAction`, landing on `updates`: the
third piece of the chain the doctor and the service each pin one end of.
- **`quickshell/settings-backup-contract` — RUN, PASS.** Named snapshots
(fifteen rows reading `2026-08-24 11:03:07` are fifteen rows nobody can
choose between), with the name reaching a filename — which is the oldest way
a helper gets talked into writing outside its own directory. Seven hostile
names, each of which must either fail or produce a file inside the backup
directory. Delete gets the same confinement as restore in the other
direction, and a worse failure mode: restore reading the wrong file
overwrites settings, delete resolving the wrong name destroys something that
is not a backup. Traversal, absolute paths, an escaping symlink, an
already-deleted name and an empty name all refused. `bytes` per row, checked
against `stat`.
- **`quickshell/settings-backup-live-contract` — RUN, PASS.** The colour gap,
both halves. `layoutFromStoredDisplays` must carry `vrrMode`,
`colorProfile`, `bitdepth`, `sdrBrightness`, `sdrSaturation` and `mirrorOf`
through, and must still accept a record written before they existed.
`layoutsEqual` must detect a difference in each of them — **this is the
assertion the shipped behaviour fails**, and the reason the restore silently
did nothing: the seven-field comparison called a snapshot with different
colour settings equal, so the restore took the early return and reported
success while leaving HDR off. Plus the inverse for the two float fields: a
value that came back one ulp different is the same value, or every restore
reapplies the layout it already has.
Two harness seams were added for this (`layoutFor`, `layoutsMatch`), because
the colour half is invisible from `handleRestoreOutput` — geometry restores
perfectly and success is reported either way.
- **`quickshell/settings-sync-contract` — RUN, PASS.** The preview's `changes`
must be `{key, from, to}` with both sides already text, a `changeCount`
agreeing with what would be applied, a rendered list no longer than that
count, and no side over 200 characters — with a bundle carrying a 4000-
character `weatherLocation` proving the cap is real rather than incidental.
Stringifying in the helper rather than in QML is what makes the cap
enforceable at all. Observed on this machine: 115 changes, 40 rendered,
absent values rendered as "not set".
- **`quickshell/manual-contract` — RUN, PASS** (5 chapters). Titles now come
from the files: the chapter list moved to `ManualChapters.qml`, which both
the reader and About's card instantiate, and the contract fails on any
`label:` beside a filename — the drift it is guarding against had already
happened, under a comment claiming it could not. It also fails if either page
names chapter files itself. In-app links: the handler must route
`panama://settings/<page>` through `ShellState.openSettings` while still
sending everything else out of the desktop, asserted by following the call
(inline block or named function, brace-matched) rather than by reading one
line, and every such link written in a chapter must name a real leaf. Plus
the taxonomy check inverted: the manual must be a hidden leaf and must NOT be
a System tab.
- **`quickshell/gnome-handoff-contract` — RUN, PASS** (6 handoffs checked
against 37 pages). Two panels joined `OWNED`: `system region`, because Date,
Time & Region now offers a language picker, four per-category format
dropdowns backed by the installed locales, and a live preview of what each
renders; and `color`, because Displays has offered a colour profile per
output for some time. Both are derived rather than hand-asserted, so the door
cannot come back under a name nobody thought to list. Plus two inverses:
`RegionPage.qml` must stay gone, and `DateTimePage` must not open a GNOME
panel for something it now does itself.
- **`quickshell/search-routing-contract` — RUN, PASS** (146 routed settings).
Unchanged; run because `SettingsSearch` moved.
### Search entries
Two moved: Language and Regional formats, from `region` to `datetime`. Fifteen
added: the seven About facts nobody could search for (Hostname, Kernel version,
Device model, Installed memory, Uptime, Serial number, BIOS version — none of
them the label of a preference, all of them things people arrive looking for by
name, and "Installed memory" rather than "Memory" because that one IS already a
schema label on Bar); the two Sync verbs (Export settings, Import settings —
"Carry settings to another machine" was the only entry, and nobody searches for
a sentence); and six format subjects on the merged tab (Date format, Number
format, Currency, Measurement units, Paper size, First day of the week) plus
"Set the clock by hand".
### Schema docs and launcher commands regenerated
`quickshell/scripts/panama-settings-docs` and `panama-settings-commands` both
run without `--check` and their output committed. `docs/settings.md` stays at
**174 settings across 36 groups** — no preference was added this phase — but
the `datetime` group is now "Found on **System Date, Time & Region**", which
is the whole point of a generated document: the tab was renamed in one place.
The launcher went 38 generated commands to **37**: `settings-region` was
removed, and `settings-manual` survives because the generator learned about
`hiddenLeaves` — a leaf with no tab is the one leaf that cannot be found by
scanning, so it is the one that most needs a launcher entry. Keywords picked up
the new search entries on their own: `settings-about` gained hostname, kernel
version, device model, installed memory, uptime, serial number and BIOS
version; `settings-datetime` gained language, regional formats and the six
format subjects; `settings-sync` gained export and import.
`quickshell/settings-docs-contract` — RUN, PASS (174 settings documented).
`quickshell/panama-commands-contract` — RUN, PASS (76 commands), after teaching
its own leaf derivation about `hiddenLeaves` too: it counted 75 and found 76,
which is the generator and the contract disagreeing about what a leaf is
rather than a stale command. `quickshell/qmldir-registration-contract` — RUN,
PASS (189 components), covering the two new ones, `ManualChapters` and
`FieldActionRow`.
### Deferred, and why
- **`quickshell/settings-search-contract` — NOT RUN.** It daemonizes a
Quickshell instance against the user's real XDG directories rather than a
scratch set, and constructing `SettingsSearch` calls
`DesktopStyle.ensureStarted()`, which replays application preferences to the
live GTK configuration. Every case it pins was checked statically instead:
none of the fifteen new labels equals or prefixes any of the twenty-one
pinned queries, so none can outrank one, and every schema label is still
reachable because the schema half of the index did not change. The new static
page-id sweep in `settings-nav-contract` covers the failure this phase could
actually have introduced.
- **Nobody has typed a locale into the four format dropdowns.** The category
round-trip is A's, through `panama-locale`; no `localectl` write was made
from any contract here, and none should be.
- **The manual's in-app links have not been clicked.** The handler is pinned
structurally and every link a chapter writes is checked against the taxonomy,
but no chapter currently writes one, so the routing has been proven correct
and never exercised.
- **No backup has been restored against the real store**, and no update has
been applied. Both are pinned through stubs and scratch directories, which is
the sanctioned path and the only one that should ever run here.
@@ -0,0 +1,143 @@
# System redesign — the finale, 10 tabs become 8
Approved mock: `home-mocks/system.html` (scratchpad, :8642). Spec wins over mock on conflict.
Storage, Snapshots, Containers keep their shipped designs (Containers gets the root-type fix
only). TEST GRANT ACTIVE: agents may RUN contracts related to their work as they build; the
full suite stays the orchestrator's call.
## Consolidation (pinned)
System tabs become: About · Software Update · System Health (`services`) · Storage ·
Snapshots · Containers · **Date, Time & Region** (`datetime`) · Sync & Backup. The `region`
tab retires (retired-map → `datetime`). The `manual` tab retires as a TAB but `manual` stays
a routable leaf (opened from About's Manual card and existing deep links) — B determines the
mechanism (hidden-leaf list or equivalent) and C reconciles `settings-nav-contract`'s
category/leaf counts.
## Bug kills (all in scope)
`SystemSettings.quickshellVersion` hardcoded "0.3.0" → read `qs --version`; the auto-download
switch nested in the Firmware card; Health's target-less "Open Software Update" actions (add
`target: "updates"` AND extend `Health.settingsTargets` in the same change — adding one
without the other invalidates whole snapshots); `CHECK_TITLES` missing `panama.updates`
(KeyError on timeout); the `SettingsShell.healthDiagnostics` TypeError (guard the binding on
`typeof item.uiDiagnostics === "function"`); RegionPage's stale "Open appearance" handoff;
`panama-updates`' stale dnf-automatic comment; the backup **color gap**
(`layoutFromStoredDisplays` + `layoutsEqual` extended to `vrrMode`, `colorProfile`,
`bitdepth`, `sdrBrightness`, `sdrSaturation`, `mirrorOf` — optional/back-compat like
`Displays.isPersistedLayoutEntry`); Restore without confirmation; stray `settings.json.*`
temp files (clean once, and if the writer can leak them, fix the leak); dead code
(`Containers.wouldExpose`, `SettingsSync.left`, `SettingsBackup.lastAction`,
`HealthPage.statusLabel`; `DateTime.localTime/universalTime/rtcTime` become USED by the
manual-set flow rather than deleted).
## A — services & scripts (pinned APIs)
- `scripts/panama-about`: rows gain `Firmware` (DMI `bios_version` + `bios_date`, bootctl
only as fallback — it reports "n/a" here) and `Secure Boot` (mokutil, absent-tolerant); a
`Panama` row (`git describe --tags --always --dirty` + `%cr`, absent-tolerant outside a
checkout), placed right after `Operating system`. `SystemSettings.quickshellVersion` now
parsed from `qs --version`, kept as a one-shot Process started from `refresh()` and
guarded by `quickshellVersionRead` — so a plain binding on `quickshellVersion` is enough
and B needs no extra call. The literal stays as the fallback only.
- `scripts/panama-updates`: `changelog <source> <name>``{source, name, kind, text, error}`
where `kind` is `advisory` | `changelog` | `none` (dnf: `dnf5 advisory info --json
--updates --contains-pkgs=` first, else `dnf5 changelog --upgrades` with dnf's two header
lines stripped; flatpak: `remote-info --cached --log` History, which Flathub does not
populate, so in practice the honest "publishes no release notes"; firmware: the fwupd
release notes cached by `check`). Names constrained by `NAME_PATTERN` before argv.
`check` gains per-item `bytes` and per-source `downloadBytes`, present only when EVERY
item in that source was priced (dnf from `dnf5 repoquery --queryformat %{downloadsize}`,
flatpak parsed from its rendered `download-size` column — its `--json` omits the column).
`apply flatpak <app-id>` appends one ID, checked against the last scan. Stale
dnf-automatic comments fixed; `DNF_TIMER` named. `services/Updates.qml`:
`changelogFor(source, name)` → record or `null`, cached by `source/name`, serial queue,
reactive via `changelogRevision` (read it in the binding, DesktopPreferences.get pattern);
`applyFlatpakApp(id)`; `downloadBytes` + `downloadSize` (string) + `sourceDownloadSize(s)`
+ `formatBytes(n)`; `loadingChangelog`.
- `scripts/panama-doctor`: `CHECK_TITLES["panama.updates"]`; updates actions get
`target: "updates"`; snapshot gains `repairCommand` (a joined argv string) on the five ids
in REPAIR_COMMANDS only — the three in-process repairs have no command line and claim
none; new `panama-doctor check <id>` verb printing a FULL snapshot envelope holding that
one check (same schema, same summary arithmetic, so callers reuse `validSnapshot`).
`services/Health.qml`: `settingsTargets` gains "updates"; `refreshCheck(id)` (splices one
validated row in, recomputes the summary, drops a reply the newer full scan superseded)
with `refreshingId` / `refreshingCheck`; `saveReport(path)` via `tee` (default
`defaultReportPath` = `~/panama-health-report.txt`) reporting through `lastSaveResult`,
and `reportText()` for the plain-text body; `safeCheck`/`validCheck` carry `repairCommand`.
- `services/DateTime.qml`: `setTime(iso)` via `timedatectl set-time` (validated against
`timePattern` = `YYYY-MM-DD HH:MM[:SS]` AND round-tripped through Date, refused while NTP
is on AND while `statusRead` is still false — "not looked yet" is not "off"); `localTime`
/ `universalTime` / `rtcTime` wired from `TimeUSec`/`RTCTimeUSec`, advanced locally once a
second while `DateTime.tracking` is true (B sets it while the Clock card is on screen; no
process per tick).
- `scripts/panama-locale` + `services/SystemLocale.qml`: `get [cat]` / `set <cat> <locale|"">`
/ `categories` / `overrides` (one JSON object for all five) — `LC_TIME`, `LC_NUMERIC`,
`LC_MONETARY`, `LC_MEASUREMENT`, `LC_PAPER`. "Match language" is `""` and is implemented by
re-issuing `localectl set-locale` with every OTHER assignment, since localectl replaces
locale.conf with exactly what it is given. Service exposes `categories`,
`categoryValue(cat)` (`""` = match language), `categoryLabel(cat)`, `setCategory(cat,
locale)`, `categoryRevision` (read it in bindings), `busy`, and the pendingRestart
discipline.
- `scripts/panama-settings-backup`: `create [name] [homeState]` (label sanitized by
`LABEL_RE`, stored INSIDE the envelope — the filename stays the timestamp SNAPSHOT_RE
pins, since that is what ordering, pruning and restore confinement rely on), `delete
<name>` (through `snapshot_source`, the same confinement gate restore uses), list gains
`bytes` and `label`. Stale-temp sweep extended to Quickshell's QSaveFile leftovers
(`settings.json.XXXXXX`, no leading dot) with a one-hour age guard so an in-flight write
is never destroyed. `services/SettingsBackup.qml`: `create(name)`, `deleteBackup(name)`,
dead `lastAction` removed, the color-gap fix per above (`storedDisplayFields`,
`validStoredField`, both used by `layoutFromStoredDisplays` and `layoutsEqual`).
- `services/SettingsSync.qml` + helper: preview output gains `changes: [{key, from, to}]`
with both values rendered to text by the helper (`render()`, `VALUE_LIMIT` 120), capped at
`CHANGE_LIMIT` 40 with a `changeCount` for what is not shown; the import still applies
everything. Service gains `changeCount`; dead `left` removed.
- `modules/settings/SettingsShell.qml`: the healthDiagnostics guard (A owns this single
edit — B is told hands-off that binding).
## B — UI
`AboutPage.qml` showpiece per mock (hero, truthful versions block, Hardware card with
GraphicsDevices joined, Device-name → Sharing row, Manual card listing chapters with titles
READ FROM THE FILES (first heading), Read → `openSettings("manual")` + section); Design
principles card dropped (the manual carries the philosophy). `ManualPage.qml`: chapter titles
from file first-headings (make the old comment true); `onLinkActivated` routes
`panama://settings/<page>` (or equivalent scheme) links through `ShellState.openSettings`,
everything else external; keep the tabs/reader shape. `UpdatesPage.qml`: restructure per
mock (headline; System packages with changelog expanders + download size; Automatic card
with BOTH switches; Applications & firmware rows with per-app flatpak updates when present;
history kept). `DateTimePage.qml` becomes the merged tab (Clock card with manual-set
revealed when NTP off; Timezone; Language & formats card — language picker, four category
dropdowns [Match language + installed locales], first-day-of-week fact row, live preview
grid from `Qt.locale()` for the *chosen* format locales); `RegionPage.qml` DELETED (retired
route). `HealthPage.qml`: repair rows show `repairCommand` in the detail before running,
per-check Re-check button (`refreshCheck`), Save-report row, Color-profiles handoff row
REMOVED (Digital wellbeing stays), the updates action now navigating. `SyncPage.qml`:
preview diff list from `changes` (mono-ish tabular rows, del/add tones), Restore + Reset
two-stage confirms (the house danger pattern), backup rows with name field on create +
size + Delete…, reset subtitle mentions themes. `ContainersPage.qml`: root becomes
`SettingsPage` per convention (logs drill-in preserved). `SettingsRoutes.qml`: the 8-tab
strip + region retired + manual leaf mechanism (KEEP the categories array literal-shaped).
## C — periphery
Search: hostname/kernel/model/memory/uptime/serial → about; export settings/import
settings → sync; currency/formats/measurement/paper/first day → datetime; region retired
routing verified; manual entries keep working (leaf). Contracts: `settings-nav-contract`
(counts + retired map), `settings-pages-contract` (routing list: add updates/containers/
datetime, drop region), `updates-contract` (changelog verb read-only pins; per-app flatpak
apply argv), health contracts (target "updates" validity BOTH sides, repairCommand shown
before run, single-check verb full-shape, KeyError fixture), `settings-backup-contract` +
`-live` (extended fields round-trip — a stored record with color fields restores them and
`layoutsEqual` detects color-only drift; delete confinement; name sanitation),
`settings-sync-contract` (changes shape, no secrets in stringified values), `manual-contract`
(titles-from-files, in-app link routing), `gnome-handoff-contract` (region handoffs gone —
consider OWNED), NEW `system-pages-contract` if the pins don't fit existing files (C's
judgment; keep count growth honest). Backlog Phase 15. README count line. Docs/commands
regen at the end. RUN the contracts you write/touch as you go (grant active); report results.
Hard rules still: no live mutations of system state (no dnf/flatpak installs, no
timedatectl/localectl writes, no backup restores against the real store — the
scratch-daemonizing harness pattern and hermetic stubs are the sanctioned test paths, and
the grant means you may RUN those freely). Valid QML at every save. B programs against A's
pinned APIs; A updates this spec before changing them.