Make Applications a real app manager, and clean up storage without the racket

Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
This commit is contained in:
Gabriel Brown
2026-08-24 17:18:14 -04:00
parent b30bf40407
commit 5a0643357f
29 changed files with 5024 additions and 314 deletions
@@ -719,3 +719,182 @@ counted the same way `panama test` collects the suite.
`panama-settings-commands` to pick up. Verified by reading the diff — every
new setting here is system state (NetworkManager, firewalld, CUPS, gsettings),
not a Panama preference.
## Phase 10 (Applications, Storage & Snapshots) — append below
Spec: `2026-08-24-applications-storage-snapshots-redesign.md`. Applications
became an app manager (installed list with search, Flatpak uninstall, permission
summaries, per-app autostart, jump chips, the `panama apps` catalog rendered
natively, role dropdowns plus a single-type override). Storage grew a
proportional breakdown bar and an itemized "Clean up, honestly" card, and lost
the duplicate container-images row that opened a terminal. Snapshots made
retention editable and moved the file browser into its own card, which fixes
"browse from a collapsed volume card does nothing".
Three agents edited the tree concurrently. Everything below was reconciled
against the landed files at the end of the phase rather than against the spec's
pinned shapes.
### New contracts (1)
`quickshell/app-library-contract`. The README count line moves **170 → 171**;
`setup/readme-contract` was run and passes ("171 contracts, as documented").
### Run and passing
These four are hermetic or source-only, so they were run rather than deferred:
- **`quickshell/app-library-contract` — RUN END TO END, PASS.** Safe to run
because it never reaches the machine: `env -i`, a stub directory first on
`PATH`, recording stubs for flatpak/rpm/pkexec and screaming stubs for
dnf/yum/sudo/rpm-ostree/gio, `HOME` and every XDG directory inside the scratch
tree, and a fixture catalog through `PANAMA_EXTRAS_DIR`. It asserts, before
running the helper at all, that the helper names no binary by absolute path
and that all six commands resolve inside the stub directory. What it pins:
- **Catalog agreement.** `setup/lib/extras-catalog`'s bash functions and the
helper's Python parser are both run over the same fixture files and their
answers compared entry by entry — id, label, kind — rather than either being
read. The fixture exercises the format's corners: a labelled entry, an
unlabelled Flathub id (label derived from the last dotted component), a bare
dnf name, a `|`-labelled dnf name, comments, blank lines, and two indented
continuation lines that must fold into the entry above.
- **Ids are the catalog line verbatim.** `flatpak:org.example.Bravo`, not the
stripped ref: the bare ref is in the refusal list, because handing that to
`install` would be refused for every Flathub entry in the real catalog and
the failure would read as "installing is broken".
- **Nothing off-catalog installs**, and the empty command log is what proves
the refusal came first. Off-catalog ids, an unknown category, a path
escaping the catalog directory, a continuation line, and an entry that
exists in the *other* category are all refused with nothing run.
- **Extensions ride with their entry and nothing else does** (installing Echo
reaches for Echo and its two plugins, and no other `org.example.*`).
- **Removal is flatpak-only**, pinned three ways: an AST walk that fails if
any command list in the helper pairs dnf/yum/rpm-ostree with a removal verb
or invokes rpm for anything but a query; a second AST walk over `uninstall`
itself; and a runtime check that no argument — including a dnf package name
— produces a package-manager call.
- **Permission buckets from data**: `filesystems=host` → "Full file system
access" and `filesystems=home` → "Home folder" are checked against separate
fixtures so the two cannot be conflated; `devices=all` → Camera,
`sockets=pulseaudio` → Microphone, `shared=network` → Network; an
unrecognized key must still appear in the output; and a sandboxed
application must summarize strictly shorter than a permissive one, so the
summary cannot be a constant.
- **`quickshell/applications-settings-contract` — RUN END TO END, PASS**
(source-only). The bun-extracted `roles` / `matchesRole` half survives B's
rebuild verbatim and gained four fixtures, including the two shapes that once
made Archives match nothing: categories arriving as a QML list, and as a
comma-separated string.
- **`quickshell/default-apps-contract` — RUN END TO END, PASS.** Already
fixture-driven (its own XDG tree, stub `xdg-mime`/`xdg-settings`); the new
half adds a fixture `mime/globs2` and two desktop entries that declare
`MimeType`, then pins `search-types` shape, that candidates are only installed
applications, that a one-character query is not a search, that `set-type`
writes exactly one type, and that an unknown type, an unknown application, and
a type this system does not have are each refused with nothing reaching
xdg-mime.
- **`setup/readme-contract`, `quickshell/search-routing-contract`,
`quickshell/settings-ownership-contract` — RUN, PASS** after the nine new
search entries.
### Extended contracts, new halves run in isolation (3)
`disks-contract` and `snapshots-contract` each keep a first half that reads the
live machine (`panama-disks snapshot`, `panama-snapshots snapshot`), so neither
was run end to end. The **new** halves were extracted into scratch scripts and
run on their own, and both pass:
- **`quickshell/disks-contract`** (new half run, PASS). It is hermetic: `env -i`,
`HOME` and `XDG_CACHE_HOME` inside the scratch tree, the block tree from
`PANAMA_DISKS_LSBLK`, and recording stubs for gio/flatpak/pkexec/dnf/podman.
- **Two proofs run before anything is cleaned**: `breakdown.path` must be the
fixture home, and the cache segment must be the 12 × 111111 bytes this
contract wrote a moment earlier. Only then is `clean cache` allowed to run —
which is what makes it safe to assert that the symlink planted inside the
fixture cache was unlinked rather than followed, and that the file it points
at outside the cache survived.
- **The arithmetic**: home + applications + caches + system == `usedBytes`
exactly, `free` == `freeBytes`, every segment non-negative, and
`exceedsUsed` false on the fixture. The two ways the measurement can be
wrong (`complete === false`, `exceedsUsed === true`) are pinned as things
the page says out loud.
- **The anti-racket stance**, pinned as an absence in the cleanup card's own
string literals: no "running out", "recommended", "act now", "junk", "safe
to remove", "you should", no exclamation marks, and no single button that
clears everything. The extractor takes the *tightest* card whose own title
is the cleanup one, and scans only double-quoted literals — QML is full of
`!` and none of it is shouting.
- **Each cleanable does its own one thing**: `gio trash --empty` (never rm),
`flatpak uninstall --unused --noninteractive`, and
`pkexec dnf clean packages` — never `clean all`, never a removal. An AST
walk pins that this is the *only* package-manager command in the whole
helper.
- **The unused-runtime size is borrowed, not recomputed**: with
`PANAMA_APPLICATIONS_HELPER` pointed at a recording stub, the row must
report what that stub said (780000) and the stub must have been asked. Two
ideas of "unused" would show one number and free another.
- `backing_device` reading findmnt's SOURCE rather than `st_dev` is pinned
with the reason: btrfs gives every subvolume its own device number, and the
first version of the breakdown lost the system-wide flatpak installation to
exactly that.
- **`quickshell/snapshots-contract`** (new half run, PASS). Retention is
exercised against a recording `snapper` stub under `env -i`, after asserting
snapper resolves inside the stub directory — the only way to run a write verb
without changing how this machine keeps its snapshots.
- Refusals here are read from the JSON `error` field, not an exit code: this
helper answers a refusal with fresh state and an `error` in it. Whether a
refusal happened *first* is read from the absence of a `set-config` in the
log, because the helper reads the configuration list on its way back out
either way.
- Ceiling pinned at **50 in both places** — the helper's `RETENTION_LIMIT` and
the service's `retentionMax` — because a dropdown offering a value the
helper refuses fails after the user has already chosen.
- The browser-card fix is pinned structurally: a brace-aware scan (strings and
comments skipped) walks the QML ancestry of `Snapshots.browseEntries` and
fails if the enclosing card is a delegate of the per-volume Repeater. This
was checked against the OLD page first, where it correctly fails, so it is
not a check that passes on anything.
### Docs updated in the same wave
- `services/SettingsSearch.qml` — nine entries added, all routing to leaves that
exist in `SettingsRoutes`: **Installed applications**, **Uninstall an
application**, **Application permissions**, **Install applications**,
**Autostart**, **File associations** → `applications`; **Clean up storage**,
**Application caches** → `storage`; **Snapshot retention** → `snapshots`.
Checked by evaluating the array: 154 entries, no duplicate labels, every
`page` a real leaf, and none of `settings-search-contract`'s 21 ranked queries
or 9 leaf-routing queries matches a new entry, so no pinned top result moves.
(The first draft of the Snapshot retention detail said "each volume keeps",
which put it in the result set for the pinned `volume` query; reworded.)
- No settings docs regenerated: this phase adds no schema keys. Every new
setting is system state (flatpak, dnf, snapper, xdg-mime), not a Panama
preference — verified by reading the diff.
### Still open before the run
- **`disks-contract` and `snapshots-contract` have not been run end to end.**
Their first halves read the live machine, so they want the same quiet moment
the other system contracts do. Nothing in the new halves depends on that
order.
- **`settings-search-contract` has not been run**: it starts a Quickshell
harness. The nine new entries were checked statically as described above, but
the schema-label sweep and the ranked queries need the live harness.
- **`settings-pages-contract`, `settings-docs-contract`, `settings-jump-contract`
and `settings-buttons-contract` were not run against the three rebuilt pages.**
ApplicationsPage grew four new components (InstalledAppRow, FileTypePicker,
SettingsChip, StorageBreakdownBar); the qmldir registration of the first three
is pinned by `applications-settings-contract`, but nothing here has loaded the
QML.
- **`app-library-contract` pins the fixture seam `PANAMA_EXTRAS_DIR` and the
permission bucket wording** ("Camera", "Microphone", "Full file system
access", "Home folder", "Network", "Devices"). Both match the landed helper;
changing either is meant to be a deliberate act that updates this contract,
and will read as a surprise the first time somebody tries.
- Run order for this phase: the hermetic and source-only ones first
(`app-library-contract`, `applications-settings-contract`,
`default-apps-contract`, `search-routing-contract`,
`settings-ownership-contract`, `setup/readme-contract`), then the read-only
system ones (`disks-contract`, `snapshots-contract`, `containers-contract`),
then the harness ones (`settings-search-contract`, `health-ui-contract`), and
`settings-pages-contract` last, as before.
@@ -0,0 +1,141 @@
# Applications, Storage & Snapshots redesign
Approved mock: `home-mocks/applications.html` (scratchpad, :8642). Spec wins over mock on
conflict. Scope: the Applications *tab* (Gaming and Screen Intelligence untouched) plus the
System category's Storage and Snapshots tabs, folded into this phase by decision.
## Goals
1. **Applications becomes an app manager**: searchable installed list (DesktopEntries +
flatpak metadata), flatpak uninstall, honest system-package rows, permission summaries,
autostart toggle per app, jump chips to the app's rules elsewhere.
2. **The panama apps catalog goes native**: category chips → entries with installed state,
installing through the same dnf/flatpak paths (polkit prompts). Same files as the CLI.
3. **Defaults modernized**: role dropdowns replace the hand-rolled accordion; a single-file-type
override ("One file type") for when a role's family is too broad.
4. **Storage**: proportional breakdown bar with honest captions; folder bars share ONE scale;
"Clean up, honestly" — itemized, sized, click-each, nothing pre-selected, nothing nags
(the anti-racket rules are contract-pinned); the duplicate container affordances merge.
5. **Snapshots**: retention becomes editable (wire the caller-less `Snapshots.setRetention`);
the file browser becomes its own card that opens regardless of source-card state (fixes the
collapsed-card bug); rollback stays absent (contract).
Non-goals: dnf package removal from Settings (honest refusal row instead), full flatpak
permission *editing* (summary + Open Flatseal when installed), per-app storage classification
beyond the flatpak sizes, portal-grant editing, snapshot space measurement (quotas honesty
stays).
## New helper: `scripts/panama-applications` (pinned verbs)
House discipline (validated inputs, JSON out, mutations return fresh state, bounded timeouts):
- `flatpaks``[{ id, name, size, origin }]` (from `flatpak list` machine-readable columns).
- `permissions <app-id>``{ summary: [human strings], raw: {...} }` from `flatpak info
--show-permissions` — curated buckets: Camera, Microphone, Full file system access, Home
folder, Network, Devices; unknown keys summarized honestly, never dropped silently.
- `uninstall <app-id>` → `flatpak uninstall --noninteractive` (app only; a second verb
`unused-runtimes` lists what `--unused` would remove, and `clean-unused` removes them —
the Storage cleanup row uses these).
- `catalog` → categories + entries from `setup/packages/extras/*` (same parser rules as
`setup/lib/extras-catalog`: `flatpak:<id> | Label`, bare dnf names, indented continuations),
each entry `{ id, label, kind: flatpak|dnf, installed }` (installed via flatpak info / rpm -q).
- `install <category> <entry-id>` → flatpak: `flatpak install --noninteractive flathub <id>`;
dnf: `pkexec dnf install -y <pkg>`. Refuses ids not present in the catalog files (no
arbitrary package installation through this surface — contract-pinned).
## Extended helpers
- `scripts/panama-default-apps`: `search-types <query>` → matching MIME types with their
current handler + candidate apps; `set-type <mime> <desktop-id>` → `xdg-mime default`
(validated against installed desktop entries). Role behavior untouched.
- `scripts/panama-disks`: `breakdown` → segments `{ home, applications, caches, system, free }`
in bytes — home = the scan targets under ~ minus caches, applications = flatpak app+runtime
sizes (user-readable sums), caches = ~/.cache, system = used the others ("System &
everything else" caption — honest arithmetic remainder), free from the fs. `cleanables` →
`[{ id, label, detail, bytes, privileged }]` for: cache (~/.cache), trash, flatpak-unused,
dnf-cache (privileged). `clean <id>` executes exactly one, refusing unknown ids; trash via
`gio trash --empty`; cache via a guarded rm of ~/.cache/* (never follows symlinks out).
- `scripts/panama-snapshots`: retention already supported (`set-retention`); add
`set-retention` argument validation for the three horizons if not already split
(hourly/daily/weekly numbers 0-50).
## Services (A)
- **`services/AppLibrary.qml`** (new): merges `DesktopEntries.applications` with the flatpak
metadata (id-matched via the entry's flatpak export), exposes `apps: [{ entryId, name, icon,
kind: flatpak|system, flatpakId, size }]`, `permissionsFor(id)` (cached),
`uninstall(flatpakId)`, catalog state (`categories`, `entriesFor(cat)`, `install(...)`),
`busy/lastError`, seam `PANAMA_APPLICATIONS_HELPER`.
- **`services/DefaultApps.qml`**: `searchTypes(query)`, `setType(mime, desktopId)` wrappers.
- **`services/Disks.qml`**: `breakdown`, `cleanables`, `clean(id)` wrappers; cleanables
refresh after any clean.
- **`services/Snapshots.qml`**: nothing new needed beyond confirming `setRetention`'s shape
matches three-horizon editing; adjust if it takes a single string.
## UI (B)
**ApplicationsPage.qml** rebuilt: Installed applications card (search field filters name +
id; rows: letter/icon tile, name + source badge, subtitle id·size or "Installed by the system
package manager"; expanded: Permissions row (+ Open Flatseal when installed), Start with the
session (autostart toggle — creates/toggles the autostart entry for that app), "Elsewhere in
Settings" jump chips (shown only where a rule exists: Notifs.appRule customized/present →
notifications; app in AudioDevices applications → sound; PrivacyPage relevance is static —
link when the app id appears in its rules if cheaply knowable, else omit), Uninstall (flatpak,
two-stage danger) or the honest dnf refusal row with the exact command). Browse-the-catalog
card (category chips, entry rows, Install with polkit caption for dnf entries, INSTALLED
badge). Default applications card: role dropdowns via OptionPickerRow-style (candidates from
the existing role matching; keep all 10 roles + family-count details), then the "One file
type" row expanding to a search field over `searchTypes` with per-type app pickers. Autostart
card: existing rows restyled with the standardized confirm; compositor autostart row kept
read-only. The Search card stays but the hardcoded "Super+Space" is replaced by a live
`Keybinds` lookup (launcher description match, literal fallback).
**StoragePage.qml**: breakdown stack bar + legend at top (honest captions; "System &
everything else" for the remainder); Folders card bars share one scale (the disk, or the
largest — pick the DISK so it's comparable to the free-space bar, with a note); "Clean up,
honestly" card from `cleanables` (row per item: label, honest cost detail, size, Clear… with
two-stage confirm; privileged rows carry the password caption; zero-byte rows render inert
with "nothing to do"); the "Unused container images" kitty-terminal row is REMOVED (the
Containers card below already routes properly — one affordance, not two). Drive/Filesystems/
Removable/Swap cards kept.
**SnapshotsPage.qml**: per-config Keep row becomes three dropdowns (hourly/daily/weekly →
setRetention); the browser moves OUT of the volume card into its own card rendered whenever
browsing state is set (opening from a collapsed card now works — this is the bug fix);
timeline/preview/expander kept; Space card kept with its honesty.
## Search & docs (C)
New entries: Installed applications, Uninstall an application, Application permissions,
Install applications (catalog), Autostart, File associations → applications; Clean up storage,
Application caches → storage; Snapshot retention → snapshots. Docs regen only if schema
changes (none expected — verify).
## Contracts (C — write, never run)
- `applications-settings-contract`: reconcile with the rebuilt page (role labels, card
titles, AutostartAppPicker, the bun matcher extraction).
- NEW `app-library-contract`: fixture-driven (stub flatpak/rpm): catalog parser agreement
with `setup/lib/extras-catalog` rules, install refuses off-catalog ids, uninstall argv
shape, permissions summary buckets, dnf never invoked for removal anywhere in the helper.
- `disks-contract`: extend — breakdown arithmetic honesty (segments sum ≤ used, remainder
labeled), cleanup rules: every cleanable itemized with bytes, nothing auto-selected, no
cleanable executes without its id being explicitly passed, cache rm is guarded, trash via
gio. Pin the anti-racket copy stance (no urgency language).
- `snapshots-contract`: extend — setRetention wired from the page, browser renders
independent of the volume card's open state, rollback still absent.
- `default-apps-contract` family rules untouched; new needles for set-type validation.
- Backlog: Phase 10. README count line if count changes.
## Agent ownership (parallel)
- **A**: `scripts/panama-applications` (new), `scripts/panama-default-apps`,
`scripts/panama-disks`, `scripts/panama-snapshots` (only if retention args need splitting),
`services/AppLibrary.qml` (new), `services/DefaultApps.qml`, `services/Disks.qml`,
`services/Snapshots.qml`.
- **B**: `modules/settings/ApplicationsPage.qml`, `StoragePage.qml`, `SnapshotsPage.qml`,
new components + qmldir.
- **C**: `services/SettingsSearch.qml`, contracts above, backlog, README count line.
Hard rules for everyone: no live mutations (no flatpak install/uninstall, no dnf, no rm, no
gio trash, no snapper writes, no xdg-mime writes) — read-only probes and stub fixtures only;
no test runs; valid QML/Python at every save. B programs against the pinned APIs.