# Users & Accounts redesign — identity, honestly Approved mock: `home-mocks/users.html` (scratchpad, :8642). Spec wins over mock on conflict. ## Goals 1. **Users**: profile hero (stock avatar gallery + file + remove), password change with strength meter, live-validating add-user form, keep-files deletion choice, per-user management (type / reset password / unlock), auto-login with the tradeoff stated. 2. **Fingerprint**: native guided enrollment (fprintd D-Bus), enrolled-finger list + removal, and the stuck state made reachable — the card renders when the authselect feature is on even with no reader, offering Turn off. 3. **Online Accounts**: "This desktop" cards (Home Assistant, phone) leading; GOA account cards with confirmed removal and busy states; native add for password providers (Nextcloud, IMAP); OAuth remains the single honest handoff. Page-level availability stops being gated on transient write errors. Non-goals: camera avatar capture, OAuth without GOA's dialog (impossible — no GIR), parental controls, group management beyond account type, username changes. ## Helper extensions (pinned — as built) **`scripts/panama-users`** (Python, unchanged language). Verbs: `snapshot` · `stock-avatars` · `set-real-name USER NAME` · `set-icon USER PATH [X Y SIZE]` · `set-account-type USER standard|administrator` · `set-automatic-login USER true|false` · `set-locked USER true|false` · `set-password USER` (stdin) · `reset-password USER` · `create-user USERNAME REALNAME TYPE` · `delete-user USERNAME keep|remove`. - `set-icon USER ""` clears the avatar (`SetIconFile("")`) — no separate verb. - `stock-avatars` prints its own shape: `{"avatars":[{"name","path"}],"error":""}`. Absolute paths, sorted by name; a missing `/usr/share/pixmaps/faces` yields `[]`. Fedora ships 15. - `delete-user` takes `keep|remove`; the older `keep-files|remove-files` spelling still works. - `reset-password USER` → `SetPasswordMode(1)`. No password material anywhere in the path. - `set-account-type` now carries the last-admin refusal **in the helper** (same sentence as `delete-user`), so a page that forgets the guard cannot demote the only administrator. - Every mutating verb still answers with the fresh snapshot plus `error`, exit 0. **`scripts/panama-fingerprint`** — **converted bash → Python** (a D-Bus signal loop is not a thing bash can do). `status` and `set-unlock` keep their exact CLI and JSON, and still shell out to `fprintd-list` / `authselect` / `panama-sudo --reason`, so the existing PATH-stub seam is untouched. Verbs: `status` · `set-unlock on|off` · `enroll FINGER` · `remove FINGER` · `remove-all`. - `status` → `{reader, readerName, enrolled, pamEnabled, unlockFeatureEnabled, error}`. `pamEnabled` and `unlockFeatureEnabled` are the same fact under the old and new name. **Behaviour change for C**: authselect is now parsed unconditionally, so a readerless machine reports `pamEnabled: true` when the feature is on. That used to be forced to `false`, which is what made the stuck state unreachable. (This machine is in it.) - `enroll FINGER` streams one JSON object per line: `{ok, stage, done, total, result, error}` with `stage ∈ claiming|scanning|done|failed`. fprintd `Claim` → `EnrollStart` → one line per `EnrollStatus` → `EnrollStop` → `Release`. Cancellation is SIGTERM (stopping the Process); the helper still releases the device. Exit 0 on `enroll-completed`, 1 otherwise. 90s idle watchdog. - `remove FINGER` / `remove-all` → `DeleteEnrolledFinger` / `DeleteEnrolledFingers2` under a Claim, and answer with the **`status` shape** so the service has one absorb path. - Finger vocabulary lives in `services/Fingerprint.qml` (`allFingers`, `fingerLabel`); the helper only shape-checks the name. - Fixture seam: `PANAMA_FINGERPRINT_FIXTURE` = JSON file `{"enrollStages":N,"results":["enroll-stage-passed",…],"error":""}`; every D-Bus call it stands in for is appended to `PANAMA_FINGERPRINT_LOG` (default `.log`) as `{"method","arguments"}`. No bus, no gi, no reader. **`scripts/panama-accounts`**: `list` **and `snapshot`** (alias; the service asks for `snapshot`) · `set` · `remove` · `add-nextcloud SERVER USERNAME` · `add-imap EMAIL IMAP_HOST SMTP_HOST USERNAME`. Passwords on stdin, never argv. Both adds answer with `{"accounts":[…],"error":""}`. - **GOA's add flow, verified**: `Goa.Manager.AddAccount(provider, identity, presentation_identity, credentials a{sv}, details a{ss}) → o` is fully daemon-side — goa-daemon writes `accounts.conf` and calls `goa_utils_store_credentials_for_id_sync` itself. The missing libgoa-backend GIR blocks only the *client-side sign-in dialog*, so password providers are genuinely addable and OAuth genuinely is not. `IsSupportedProvider` is checked first (`owncloud`, `imap_smtp` both true here; `nextcloud` is not a provider type — `owncloud` is). - Keys are GOA 3.58's own, taken from `goaowncloudprovider.c` / `goaimapsmtpprovider.c` and confirmed against this machine's `accounts.conf`. Nextcloud: credentials `{password}`, details `Uri` (`…/remote.php/webdav/`), `FilesEnabled`, `CalendarEnabled`, `CalDavUri` (`…/remote.php/dav/`), `ContactsEnabled`, `CardDavUri`, `AcceptSslErrors`. IMAP: credentials `{imap-password, smtp-password}`, details `Enabled`, `EmailAddress`, `Name`, `Imap{Host,UserName,UseSsl,UseTls,AcceptSslErrors}`, `Smtp{Host,UseAuth,UserName,AuthLogin,AuthPlain,UseSsl,UseTls,AcceptSslErrors}` — IMAPS + submission/STARTTLS defaults, one password for both servers. - The daemon validates nothing, so each add calls `EnsureCredentials` on the new object and **removes the account again** if the credentials are refused, reporting why. - Fixture seam: `PANAMA_ACCOUNTS_FIXTURE` = a JSON file in `list`'s shape. With it set nothing imports gi; validation still runs; each verb appends `{"verb","arguments","passwordBytes"}` to `PANAMA_ACCOUNTS_LOG` (default `.log`) — the password's **length**, never itself. ## Services (A — as built) - **UserAccounts**: `removeIcon()` (no argument — the hero card's own avatar), `stockAvatars` + `loadStockAvatars()` (cached; call it when the gallery opens), `deleteUser(user, keepFiles)` — **the bool inverted from the old `removeFiles`** — `resetPassword(user)`, `setAccountTypeFor(user, type)` (**renamed** from `setAccountType`), `setLocked(user, locked)`. `locked` and `loginTime` stay in the payload for the UI. - **Fingerprint**: `readerPresent`, `readerName`, `unlockFeatureEnabled`, `fingers` (**renamed** from `enrolled`), `allFingers`, `availableFingers`, `fingerLabel(f)`, `cardVisible` (= `readerPresent || unlockFeatureEnabled`), `busy`, `lastError`, `enrolling`, `enrollPhase`, `enrollFinger`, `enrollStage`, `enrollTotal`, `enrollResult`, `refresh()`, `setUnlockEnabled(on)`, `startEnroll(finger)`, `cancelEnroll()`, `removeFinger(f)`, `removeAll()`. - **OnlineAccounts**: `available` (GOA answered the last snapshot — persisted, never touched by a write), `snapshotError` / `writeError` and the derived `lastError`, `busy` (derived from the Processes), `attentionCount`, `refresh()` (cheap; call on every page open), `setService`, `remove`, `addNextcloud(server, user, password)`, `addImap(email, imapHost, smtpHost, user, password)` — password via Process stdin. ## UI (B) **UsersPage.qml** rebuilt per mock: hero card (avatar, Change picture… opens a popover with stock gallery + "Choose a file…" → existing AvatarPicker/Cropper flow, Remove button); Account card (Full name — live-committing field, Username honest row, Account type segment with the last-admin reason as detail, Password flow with a 4-segment strength meter — local heuristic, length + classes, no network; Automatic login with the mock's tradeoff copy); Fingerprint card per mock (stuck row with NO READER badge + Turn off when `unlockFeatureEnabled && !readerPresent`; full experience otherwise: toggle, enrolled list with per-finger Remove…, Add a fingerprint → inline enrollment panel driven by the enroll stream — touch icon, stage counter "N of M touches", cancel; no continuous animation); Other accounts card (expandable per-user rows: type segment, Reset password ("They set a new one at next sign-in"), Delete with a keep-files dropdown [Keep the files / Remove everything] + two-stage confirm; locked users show Unlock; add-user form with LIVE validation — use live TextFields, not blur-committing TextFieldRow, with the username rules + 31-char cap in the detail and mirrored in the enabled predicate). **OnlineAccountsPage.qml** rebuilt: gains `objectName: "accounts"`; "This desktop" card — Home Assistant row (state from the HomeAssistant service: connected/rooms, else "not set up", button → `openSettings("my-home")`) and Phone row (KdeConnect state, → `openSettings("phone")`); Accounts card — one expandable row per GOA account (per-service toggles disabled while `busy`, Remove with two-stage confirm), needs-attention rows keep "Sign in again"; Add card — Nextcloud + Mail native forms (server/user/password, password field cleared on success or close), and the OAuth row with the mock's honest copy → the existing GNOME dialog handoff (the ALLOWED exception stays for accounts). Errors render as a row, never as page-wide unavailability. ## Search & docs (C) New entries: Fingerprint, Enroll a fingerprint, Delete a user, Account type, Reset a password → users; Nextcloud, Google account, Remove an account, Add a mail account → accounts. Docs regen only if schema changes (none expected — verify). ## Contracts (C — write; hermetic stub runs only) - `user-accounts-contract`: extend — keep-files arg end to end, reset-password sends no password material (SetPasswordMode pinned), remove-icon empty-string call, stock list shape, live-validation predicate present in the page (the blur-commit bug regression pin: the add form's fields must not be TextFieldRow). - `fingerprint-contract`: major rework — enrollment is native now (streamed stages against a stubbed fprintd D-Bus layer or a stub helper — follow the hermetic pattern), the GNOME enrollment handoff needle INVERTS (page must NOT call openGnomePanel for users), stuck-state visibility pinned (`readerPresent || unlockFeatureEnabled`), finger vocabulary single-source kept, set-unlock reason kept. - `gnome-handoff-contract`: delete the `UsersPage.qml:system-users` ALLOWED exception; the `OnlineAccountsPage.qml:online-accounts` exception stays with its reason updated to name OAuth specifically. - NEW `online-accounts-contract`: hermetic — availability decoupled from write errors, passwords via stdin never argv, add flows validate inputs, remove requires the confirm state, snapshot exposes no secrets. - Backlog Phase 11; README count line (171 → 172 expected). ## Agent ownership (parallel) - **A**: `scripts/panama-users`, `scripts/panama-fingerprint`, `scripts/panama-accounts`, `services/UserAccounts.qml`, `services/Fingerprint.qml`, `services/OnlineAccounts.qml`. - **B**: `modules/settings/UsersPage.qml`, `OnlineAccountsPage.qml`, new components (+ qmldir); may read AvatarPicker/AvatarCropper and reuse unchanged. - **C**: `services/SettingsSearch.qml`, contracts above, backlog, README count line. Hard rules: NO live mutations — no user/password/icon/type changes, no GOA account writes, no authselect, no fprintd enrollment against real hardware (none exists here anyway), no polkit-triggering calls. Read-only probes and stubs only. Valid QML/Python at every save. B programs against the pinned APIs; A updates this spec before changing them.