Give identity its due: native enrollment, honest deletion, and sign-in that stays home
Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
This commit is contained in:
@@ -898,3 +898,218 @@ run on their own, and both pass:
|
||||
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.
|
||||
|
||||
## Phase 11 (Users & Accounts) — append below
|
||||
|
||||
Spec: `2026-08-24-users-accounts-redesign.md`. Users grew a profile hero with a
|
||||
stock-avatar gallery and a Remove, a password flow with a strength meter, a
|
||||
live-validating add-user form, a keep-files deletion choice, and per-user
|
||||
management (type / reset password / unlock). Fingerprint enrollment stopped
|
||||
handing people to GNOME's Users panel and became native — fprintd's
|
||||
`EnrollStart` driven from `panama-fingerprint`, streaming one JSON line per
|
||||
touch — and the card learned to appear in the state that needs acting on rather
|
||||
than only when a reader is attached. Online Accounts gained native Nextcloud and
|
||||
IMAP forms, confirmed removal, and an availability that no longer collapses on a
|
||||
failed write.
|
||||
|
||||
Three agents edited the tree concurrently. Everything below was reconciled
|
||||
against the landed files at the end of the phase, not against the spec's pinned
|
||||
shapes: three of the needles moved during the phase (the card-visibility
|
||||
expression went into the service as `cardVisible`, the add-user field became
|
||||
`LiveFieldRow`, and the fingerprint helper became Python) and each is pinned
|
||||
where it actually landed.
|
||||
|
||||
### New contracts (1)
|
||||
|
||||
`quickshell/online-accounts-contract`. The README count line moves
|
||||
**171 → 172**; `setup/readme-contract` was run and passes ("172 contracts, as
|
||||
documented").
|
||||
|
||||
### Run and passing
|
||||
|
||||
- **`quickshell/fingerprint-contract` — RUN END TO END, PASS.** The whole file
|
||||
was reworked, because two of its needles inverted. Hermetic: `env -i`, a stub
|
||||
directory first on `PATH` (asserted, before the helper runs, that
|
||||
fprintd-list, authselect, sudo and pkexec all resolve inside it), both bus
|
||||
addresses pointed at sockets that do not exist, `PANAMA_PATH` pointed at a
|
||||
recording `panama-sudo` that runs **nothing**, and enrollment replayed from
|
||||
`PANAMA_FINGERPRINT_FIXTURE` with the calls it stood in for landing in
|
||||
`PANAMA_FINGERPRINT_LOG`. What it pins:
|
||||
- **The inverted needle.** `openGnomePanel("system", "users")` is now a
|
||||
failure rather than a requirement, checked across every settings page.
|
||||
`gnome-handoff-contract` lost its `UsersPage.qml:system-users` exception at
|
||||
the same time; this is the other half of that, said from the page's side.
|
||||
- **The stuck state, from the data.** With the stub fprintd reporting no
|
||||
devices and the stub authselect reporting `with-fingerprint`, `status` must
|
||||
still answer `unlockFeatureEnabled: true` — the combination that used to be
|
||||
unrepresentable, because the feature was only read when fprintd answered.
|
||||
This machine is in that state, which is how it was found.
|
||||
- **Card visibility followed one level of indirection.** The expression moved
|
||||
into `services/Fingerprint.qml` as `cardVisible: readerPresent ||
|
||||
unlockFeatureEnabled`; the contract accepts either the page spelling it out
|
||||
or the page binding to a service property that does, and resolves the second
|
||||
rather than forbidding it.
|
||||
- **The enrollment stream as a stream.** A five-stage fixture with a retry in
|
||||
the middle produces nine lines; every one must be a JSON object on its own,
|
||||
the counter monotonic and inside `0..total`, `total` constant, and the
|
||||
retried touch must **not** have advanced the counter — a panel that counts a
|
||||
failed touch promises a finish that never arrives. The terminal line says
|
||||
which way it ended.
|
||||
- **Claim and Release bracket everything**, read from the call log: first call
|
||||
`Claim`, last call `Release`, `EnrollStart` after the claim — including on a
|
||||
failed enrollment, because a reader left claimed refuses the next attempt
|
||||
with "busy with something else" forever.
|
||||
- **A terminal failure is reported in words**, not in fprintd's vocabulary
|
||||
(`enroll-duplicate` → "That finger is already enrolled").
|
||||
- **Nonsense is refused before the reader is touched**: four bad finger names
|
||||
against both `enroll` and `remove`, each of which must leave the call log
|
||||
empty.
|
||||
- **`remove` deletes the finger it was asked about**, and `remove-all` uses
|
||||
`DeleteEnrolledFingers2` with no arguments — its predecessor took a user
|
||||
name, which is how a typo deletes somebody else's prints.
|
||||
- **With no fixture and no bus it still answers in its own shape**: the page
|
||||
reads this stream line by line, and a traceback leaves an enrollment panel
|
||||
open forever.
|
||||
- **The stated reason survived the rewrite to Python**: the escalation must go
|
||||
through `panama-sudo --reason …`, and a bare `sudo`/`pkexec` in the argv log
|
||||
is a failure.
|
||||
- **Finger vocabulary**: a single id elsewhere is a default choice and is
|
||||
allowed; two in one file is a copy of the list, and the copy is what falls
|
||||
behind.
|
||||
- **`quickshell/user-accounts-contract` — RUN END TO END, PASS.** Its snapshot
|
||||
half still reads the live account service, which is read-only; every verb that
|
||||
would change something is pinned from the source and never invoked
|
||||
(`set-icon gib ""` really does clear the avatar). New halves:
|
||||
- **`reset-password` carries no password material**, by AST: it must call
|
||||
`SetPasswordMode`, must resolve its mode argument to **1** (through the
|
||||
named constant, so the meaning survives), must not call `SetPassword`, and
|
||||
its body — docstring excluded, because the docstring is allowed to say the
|
||||
word — must not mention stdin, openssl, crypt or passwd.
|
||||
- **keep-files end to end, as three separate links**, because any one of them
|
||||
can invert on its own: the helper derives `DeleteUser`'s boolean from its
|
||||
own argument (a literal there is the bug), the service's ternary is checked
|
||||
against the *polarity of its own parameter name* (Panama flipped this once
|
||||
already, `removeFiles` → `keepFiles`), and the page passes a variable rather
|
||||
than a constant. Both vocabularies are exercised — `keep`/`remove` and the
|
||||
legacy `keep-files`/`remove-files` — by reading which refusal comes back,
|
||||
so a vocabulary that stopped being understood shows up as the wrong message
|
||||
rather than as no message.
|
||||
- **`removeIcon` sends an empty path**, and `set_icon` recognizes it rather
|
||||
than handing "" to GdkPixbuf, which would fail and leave the avatar.
|
||||
- **The live-validation regression, two ways.** Structurally: the property the
|
||||
form's user-name test reads must be assigned from a per-keystroke handler,
|
||||
and the component that assigns it must not be `TextFieldRow`. Bluntly: the
|
||||
page carries no `TextFieldRow` at all. The page's regex is compared
|
||||
character for character against the helper's `USERNAME`, and the 31-cap must
|
||||
appear in the page's own text — a form that accepts a name accountsservice
|
||||
then refuses has no way to say why.
|
||||
- **The last administrator is refused helper-side too**, not only by the page
|
||||
that offers the control.
|
||||
- **Stock avatars**: shape read from a real run (`{name, path}`, absolute
|
||||
paths, empty list legitimate).
|
||||
- **`quickshell/gnome-handoff-contract` — RUN, PASS** (9 handoffs, 39 pages).
|
||||
The `UsersPage.qml:system-users` exception is deleted; the
|
||||
`OnlineAccountsPage.qml:online-accounts` one stays with its reason rewritten
|
||||
to name OAuth specifically — libgoa-backend ships without a GIR binding, so
|
||||
the provider's own dialog is the only way to obtain a token, and Nextcloud and
|
||||
IMAP are added on the page itself.
|
||||
- **`setup/readme-contract` — RUN, PASS** after the count line moved.
|
||||
|
||||
### Run with one half relaxed (1)
|
||||
|
||||
- **`quickshell/online-accounts-contract` — everything passes except six
|
||||
input-validation cases**, which are pinned as the spec asks and which the
|
||||
landed `panama-accounts` does not yet satisfy. See "Still open" below. Run
|
||||
with those six removed, the contract passes end to end. It is hermetic: `env
|
||||
-i`, a stub directory first on `PATH` (gdbus/busctl/dbus-send/
|
||||
gnome-control-center all resolve there, asserted before running), a `gi`
|
||||
stand-in on `PYTHONPATH` whose `require_version` always raises — so if the
|
||||
fixture seam were ever removed the contract would stop working rather than
|
||||
quietly start editing the session's accounts — and both bus addresses pointed
|
||||
at sockets that do not exist. Accounts come from `PANAMA_ACCOUNTS_FIXTURE`;
|
||||
what each verb would have done lands in `PANAMA_ACCOUNTS_LOG`. What it pins:
|
||||
- **Availability is not an error**, at the definition and at every assignment:
|
||||
no line that decides `available` may mention a write error, and the service
|
||||
must keep at least two error strings, or "GOA did not answer" and "that one
|
||||
change did not happen" are the same fact. This is the regression where one
|
||||
refused toggle replaced four working accounts with "not available".
|
||||
- **The same thing from the data**: a refused add must come back with the
|
||||
error *and* the full account list, because the page draws its list from the
|
||||
same answer that carries the message.
|
||||
- **The password on stdin, proved rather than assumed.** The log records the
|
||||
password's **length**, never the thing itself, so the sentinel can be
|
||||
checked for everywhere it might have gone (stdout, stderr, the log, the
|
||||
scratch home) while `passwordBytes` proves it was read at all rather than
|
||||
dropped.
|
||||
- **The listing cannot carry a credential**, read from `describe`'s own dict
|
||||
keys and from the GOA properties it reads, so a password property added
|
||||
later is caught at the source rather than after it reaches the page.
|
||||
- **An added account is proved, not assumed**: `call_add_account_sync` must be
|
||||
followed by `call_ensure_credentials_sync`, and `verify` must be able to
|
||||
`call_remove_sync` — GOA does not check what it is handed, so without that
|
||||
step a mistyped password produces an account that exists, looks correct, and
|
||||
never syncs, which is the exact failure this page exists to explain.
|
||||
- **Provider types**: `owncloud` (the fork kept the id) and `imap_smtp`. A
|
||||
provider type GOA does not know is not an error — AddAccount is simply never
|
||||
offered it.
|
||||
- **Both names for the listing** (`list` and `snapshot`) answer the same
|
||||
thing, and reading the list changes nothing.
|
||||
- **Removal carries the path intact** — removing the wrong account is
|
||||
unrecoverable and looks like a success — and is confirmation-gated on the
|
||||
page, read from the object block around the call rather than from a
|
||||
`confirming` property declared anywhere in the file.
|
||||
|
||||
### Docs updated in the same wave
|
||||
|
||||
- `services/SettingsSearch.qml` — nine entries added, all routing to leaves that
|
||||
exist in `SettingsRoutes`: **Fingerprint**, **Enroll a fingerprint**, **Delete
|
||||
a user**, **Account type**, **Reset a password** → `users`; **Nextcloud**,
|
||||
**Google account**, **Add a mail account**, **Remove an account** →
|
||||
`accounts`. Checked by evaluating the array: 163 entries, no duplicate labels,
|
||||
every `page` a real leaf, and none of `settings-search-contract`'s 21 ranked
|
||||
queries or 9 leaf-routing queries changes its top result — the only new entry
|
||||
any of them touches at all is Fingerprint, which matches `lock` through
|
||||
"Unlock" and sits far below the existing first hit.
|
||||
- No settings docs regenerated: this phase adds no schema keys. Every new
|
||||
setting is system state (accountsservice, fprintd, authselect, GOA), not a
|
||||
Panama preference — verified by reading the diff.
|
||||
|
||||
### Still open before the run
|
||||
|
||||
- **`online-accounts-contract` fails six validation cases against the landed
|
||||
helper**, and they are left pinned rather than relaxed, because the spec asks
|
||||
for them and each produces an account that exists, looks right in the list,
|
||||
and never syncs. `nextcloud_uris` accepts anything with a host once `https://`
|
||||
is prepended, and `imap_account` accepts any non-empty string as a server:
|
||||
- `add-nextcloud` accepts `not a url`, `https://cloud.example.org; reboot`,
|
||||
and `https://cloud example org`;
|
||||
- `add-imap` accepts `imap example com`, `imap.example.com; reboot`, and
|
||||
`-imap.example.com` as either server.
|
||||
|
||||
A hostname check in `nextcloud_uris` and one applied to both hosts in
|
||||
`imap_account` closes all six. `''`, `ftp://…` and `https://` (no host) are
|
||||
already refused, and every other assertion in the file passes.
|
||||
- **`settings-search-contract` has not been run**: it starts a Quickshell
|
||||
harness. The nine new entries were checked statically as described above.
|
||||
- **`settings-pages-contract`, `settings-docs-contract`, `settings-jump-contract`
|
||||
and `settings-buttons-contract` were not run against the two rebuilt pages.**
|
||||
Six new components landed (LiveFieldRow, SecretFieldRow, PasswordStrengthRow,
|
||||
StockAvatarPicker, FingerprintEnrollPanel, OnlineAccountRow) and are
|
||||
registered in `qmldir`, but nothing here has loaded the QML.
|
||||
- **The enrollment cancel path is pinned only from the source.** The canned
|
||||
fprintd has no main loop, so `SIGTERM` releasing the device is exercised by
|
||||
neither half; what is pinned is that the release happens in a `finally` and
|
||||
that a *failed* enrollment still releases.
|
||||
- **`user-accounts-contract`'s first half reads the live account service**
|
||||
(`panama-users snapshot`, `stock-avatars`), so it wants the same quiet moment
|
||||
the other system contracts do, though nothing it does writes.
|
||||
- **Seams pinned by name**: `PANAMA_FINGERPRINT_FIXTURE`/`_LOG`,
|
||||
`PANAMA_ACCOUNTS_FIXTURE`/`_LOG`, and the log shapes
|
||||
(`{method, arguments}` and `{verb, arguments, passwordBytes}`). Changing
|
||||
either is meant to be a deliberate act that updates these contracts.
|
||||
- Run order for this phase: the hermetic ones first (`fingerprint-contract`,
|
||||
`online-accounts-contract`), then the source-only ones
|
||||
(`gnome-handoff-contract`, `setup/readme-contract`,
|
||||
`settings-ownership-contract`, `search-routing-contract`), then the read-only
|
||||
system one (`user-accounts-contract`), then the harness ones
|
||||
(`settings-search-contract`), and `settings-pages-contract` last, as before.
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user