Files
Panama/docs/superpowers/specs/2026-08-24-users-accounts-redesign.md
T

12 KiB

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 USERSetPasswordMode(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-fingerprintconverted 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 ClaimEnrollStart → one line per EnrollStatusEnrollStopRelease. 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-allDeleteEnrolledFinger / 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 <fixture>.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 <fixture>.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 removeFilesresetPassword(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.