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:
@@ -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.
|
||||
Reference in New Issue
Block a user