PAF becomes saf/device (history kept), STATE.md dissolves into saf/state.md with the dated era archived, the substrate SAF moves up from souveraine, and every agreement points at saf/INDEX.md and nowhere else. one map, nothing to remember
7.4 KiB
TASK 19 — First-party Settings control center
Status: in progress (worktree, device acceptance blocked while phone is
locked). Repo: ~/Projects/souveraine.
Goal
Replace the removed ii settings application with one Souveraine-owned control center for phone and desktop. One page model, responsive chrome: stack navigation on phone, side rail on desktop. Every control is a view over the owning Config/service/daemon; no success-shaped switches.
Current surface
Owned pages: Device, Lock, Wallpaper, Dock, Navigation, Idle, Keyboard, Speech. Worktree adds Network & internet (Network/Cellular/Bluetooth), Display (real Brightness), and Sound & microphone (PipeWire defaults).
Still required: app permissions/health, storage, accounts, updates, battery/ power profile, and explicit unavailable/error states.
Launch/fullscreen failure found 2026-07-21
- Qt
Window.FullScreendid not match the pill's compositor-owned fullscreen path and could hide the navigation rail. Start windowed, then target the Settings PID with the same Hyprland Lua fullscreen dispatcher the pill uses. - The launcher allowed duplicate
qs -pinstances. Repeated taps coincided with repeated full-shell run IDs, notification/polkit contention, lost sessiond heartbeat, and fail-closed locks. Launcher must resolve the standalone QML realpath and useqs --no-duplicate -p <file>. - Until a controlled launch proves the log says
Launching config: .../ settings.qml, the phone desktop entry stays hidden. Never accept anothershell.qmlrun ID from the Settings button.
Polish sweep — folded in from TASK-22
Scoped by Casey 2026-07-22 as "smaller individual tweaks to the souveraine-settings app and fullscreen apps in general". It was never a separate work unit: its own text said the candidates come from this task's "still required" list, and its status line read "items to enumerate with Casey at session start", which is a placeholder rather than a task.
A. Settings paper cuts. The small items only — unavailable/error states,
battery/power profile stub, and whatever else Casey names. Not new pages.
The idle page's seconds-spinners were one of these and are done (2026-07-26:
presets, and the lock-screen timers now read live from sessiond). That work
turned up the sharper version of the same rule: reading the owning daemon is
only half of "no success-shaped switches". SetPolicy mutated memory and
persisted nothing, so a timer set in Settings was live until the next restart
and then silently back to the built-in 15 s. The page was honest; the daemon
forgot. Fixed 2026-07-26 — the policy is written to
$XDG_CONFIG_HOME/souveraine/device-state-policy.json and a failed save comes
back as ok:false instead of a control that looks like it took.
Worth sweeping for elsewhere: every other control this page owns should be checked against the same two questions — does it read the authority, and does the authority remember?
B. Fullscreen apps in general. Qt Window.FullScreen and the pill's
compositor-owned fullscreen path diverge; apps should start windowed and be
fullscreened through the same Hyprland Lua dispatcher the pill uses, per-PID.
Dock/ScreenCorners fullscreen detection needs the QML fallback for qs -p
windows — applied hot on both devices but stranded per-device until the tree
unification (unify-shell-trees-laptop-phone.md).
Ground rules: each tweak is its own small diff with its own device check, no batching. Anything touching the ii-base layer gets promoted to the laptop repo the same session (divergence doctrine).
The state machine needs a surface here (Casey, 2026-07-28)
"the souveraine-settings section needs to have all these state machine mechanics health checks built"
Everything the device state machine knows is currently legible only through
forensic.jsonl and qs ipc call sessiond device_state. No screen shows any of
it. Three separate gaps, already named elsewhere and converging here:
- TASK-08(f) —
sensors_degradedreaches no surface. §10 source health landed: a source that reported and then went silent for 90 s isDown, not quiet, and every forensic snapshot carries the flag. The trail knows; nothing else does. The crash reporter structurally cannot help — the SLPI outage exited status 0. Needs an ambient indicator saying which source and since when, anddevice_statealready shipssensor_healthover IPC, so the data is there. - TASK-08(g) — the confidence gates are computed, logged, and never branched on. A settings page over them would be honest only as a readout, not as controls, until (g)'s decision is made. Showing a slider for a threshold nothing consults is TASK-19's own "no success-shaped switches" rule broken.
- Grip is now a fourth evidence source with no readout (2026-07-28,
saf/device/edge-sense.md). A refused squeeze — the pocket veto — is recorded viarecord_decisionand visible nowhere. "Why didn't my squeeze work" has no answer on the device today.
Ordering follows TASK-19's existing rule: expose what actually actuates.
Health and evidence are observations and can be shown honestly right now —
state, panel, evidence, confidence, per-source health and last-seen, the
sensors_degraded flag, and the recent decision entries. Controls over the
confidence bands wait on (g).
That split matters: a health readout that is merely accurate is useful the day it ships, whereas a controls page over a machine that ignores its own thresholds would be lying.
Built 2026-07-31 (souveraine 1c90d7f) — the readout half. DeviceEvidence
was egress-only: every surface could report input to the machine and none could
read it back. It now has an ingress that polls device_state and
forensic_log, and the Device page renders state, lock phase, panel, dimmed,
display-active, idle, observed + confidence, wake-suppression, per-source health
with the sensors_degraded banner, and the last twelve decisions with their
reasons. Strictly a projection — it holds nothing the protocol owns and branches
on nothing, so it does not become §1's eighth actor; and it polls only while a
surface is watching, so an open settings page is not a request per second all
day. available: false renders as "sessiond is not answering on this device"
rather than healthy-looking zeroes, which is §10's rule that "no evidence" and
"evidence says nothing is happening" are different states.
Still owed here: last-seen per source (TASK-19 asked for it; device_state
ships health and freshness but no timestamp, so it is not shown rather than
faked), and grip as a fourth evidence source — it is absent from
device_state entirely, so "why didn't my squeeze work" still has no answer on
the device. Controls remain blocked on TASK-08(g).
Acceptance
- Hammer Settings icon ten times: exactly one standalone PID/window, zero new
shell.qmlrun IDs, one notification owner, one polkit agent, sessiond heartbeat continuous. - Settings log identifies
settings.qml; close exits the process. - Phone opens using the pill's fullscreen state; pill remains usable and its double tap returns the app to windowed.
- Every page parses and scrolls at 540×1080; controls change the real service and reflect failure/unavailability.
- Desktop uses the same page components in a normal resizable window.
Connects to
TASK-08 (power/update state), TASK-17 (keyboard/speech), TASK-07 (microphone), TASK-15 (idle), deploy maintenance lease in TASK-08.