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

172 lines
12 KiB
Markdown

# 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 `<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 `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.