Skip to content

Owner card in Settings, email confirmation landing, owner email off public pages - #45

Merged
max-tet merged 10 commits into
mainfrom
feature/owner-section
Oct 20, 2026
Merged

max-tet merged 10 commits into
mainfrom
feature/owner-section

Conversation

@ClaydeCode

@ClaydeCode ClaydeCode commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

Closes #13. Supersedes FreeshardBase/web-terminal#47 for the root UI: Sundial is replacing the web-terminal, so the owner email work lands here.

What is in it

Owner card, first in Settings, bound to the owner's user row (GET/PATCH /protected/users/me), not to the identity and not to the controller profile.

  • Name writes through via PATCH {display_name} (reuses fs-editable-text).
  • Email: PATCH {email} makes it the candidate (pending_email); the live value changes only on confirmation. States: no address (a prompt, not an error), pending (live address plus the candidate, Send again, Discard; a new address replaces the candidate), set. When GET /protected/settings reports email_enabled: false the address is set directly and there is no resend control. Note: the issue names oidc.email_verification as the signal; email_enabled from /protected/settings is what shard_core actually exposes.
  • A 502 from the PATCH is not a failed save. When it carries shard_core's JSON detail, the candidate is stored and only the mail did not go out: the card refetches the row, says exactly that and offers Send again. A successful resend clears the message. A bare 502 from the proxy (shard_core down, nothing stored) stays a failed save. 429 shows the server's detail; 403 says only the owner can change the address.
  • Emptying the field clears the address (PATCH {email: null}), as web-terminal#47 did.
  • Settings keeps one card instance across its full re-renders, so a WS update neither refetches the row nor throws away an edit in progress.
  • Pending/confirm pattern (the issue asks for one): pendingChange() in js/components/pending-change.js renders a candidate in a dashed sheet with a state label, a status line or warning, and equal-weight follow-up actions. The state derivation stays a pure, unit-tested function next to the feature (js/owner-email.js). Documented in agents.md.

Confirmation landing at /?confirm_email=<token>. main.js takes the token out of the URL (history.replaceState) before routing and hands it to the new confirm-email view. The anonymous-to-welcome redirect leaves that route alone, because the link is usually opened on a device that was never paired. It works at / and under /sundial/. The view confirms only on a click (mail scanners and prefetchers would otherwise spend the single-use token) and never logs the token.

The owner email leaves the public and duplicate places: no mailto on Welcome, no email field on Public, no Owner / Owner email in the Settings About list (that was the controller's copy of the same values). The unused i18n keys are gone.

Mock dev server: users/me (GET, PATCH), resend, discard, settings, confirm-email. Under just serve, /?confirm_email=dev-token confirms the mock candidate; --no-email mimics a self-hosted shard.

Difference from the issue: 204 either way

POST /public/users/confirm-email answers 204 whether or not the token matched (anything else would let a caller probe for pending addresses; checked in shard_core/web/public/users.py on freeshard main), 422 on a malformed body and 429 on a flood of misses. So the landing cannot report success or failure. Its copy is hedged: "If the link was still valid, your address is now confirmed…", it names the one-hour expiry, and it points to Send again in Settings. 429 and network failures get their own messages.

Review panel

Rebased onto main after #44 merged; the only conflict was js/main.js boot (both imports kept, PUBLIC_ROUTES plus #44's oidcReturnQuery() on the welcome redirect). Then five fresh reviewers ran one at a time against the diff and the issue, without the implementer's reasoning, each fix committed before the next reviewer started.

Reviewer Blocking Resolution
Adversarial (correctness) none Fixed three confirmed advisories in be7fd5c: focus left the email input on every 30 s disk-usage re-render of Settings; a save, resend or discard rebuilt both fields and threw away an edit in the other; confirming an unchanged address PATCHed it again, which re-sent the confirmation mail and spent the send budget.
Test adversary 3 All fixed in 3910239: the token URL being pushStated instead of replaced, a delayed self-firing confirm (the 100 ms sleep is now a fake clock run for a minute), and a failed discard showing success were each invisible to the suite. Also added tests for confirm and resend being disabled while in flight, 422 on confirm, 429 on resend, an unreadable /settings, markup in a candidate address, and token-free Referers on boot requests. Each new test was checked against a mutation of the code under it.
DevEx / readability 1 Fixed in 1a90589: the confirm view's state comment omitted invalid. Also took the cheap advisories (naming, why-comments, test names stated as behaviour, agents.md wording).
Security none Fixed in a49af31: a paired browser opening /?confirm_email=T&oidc_rd=... left for the authorize URL and dropped the token unspent (also flagged by the adversarial reviewer).
UX (Daylight Instrument, rendered) 3 All fixed in 0a8fa17: the landing asked to confirm "this address" with no address on the page; the after-click hint pointed to Settings on a device that may never have been paired; the pending block never said mail keeps going to the live address until confirmation. German copy reworked where flagged.

Open advisories, not done here:

  • Toasts double-escape interpolated values (t() escapes, the toast sets textContent), so a&b@x.org would show as a&amp;b@x.org. Repo-wide, the same applies to existing toasts such as toast.installFailed.
  • Focus falls to <body> after Send again, Discard and Confirm (the buttons are rebuilt).
  • Small layout points: [empty] directly above "No address yet.", two stacked "cannot send email" sentences when mail is off and a candidate exists, the pending note at body size next to small hints, the candidate in mono next to the live address in sans.
  • owner-card.js tracks its load lifecycle in three fields (#loaded, #loadFailed, #user === null); one state field would read better.
  • freeshard-core, not this PR: the resend POST is reachable from a form on an app subdomain (domain-scoped cookie, no CSRF check); pairing.py makes pairing codes with random.choices instead of secrets.
  • Rollout: the mail link is https://<shard>/?confirm_email=, so it only reaches Sundial on shards where Sundial serves /.

Recommended reading order

  1. js/owner-email.js: token parsing, the three states, the 502 discriminator
  2. js/components/pending-change.js, js/components/owner-card.js: the pattern and the card
  3. js/views/settings.js: wiring, About removal
  4. js/main.js, js/views/confirm-email.js, js/components/dock.js: the landing
  5. js/views/welcome.js, js/views/public.js, js/store.js: removals
  6. js/i18n/*.json, css/ui.css, css/views.css
  7. tools/dev_server.py, then tests/unit/owner-email.test.js, tests/e2e/owner.spec.js, tests/e2e/confirm-email.spec.js, tests/e2e/owner-email-public.spec.js
  8. agents.md

Verification

  • just check: all modules parse. python3 tools/check_i18n.py: 0 problems.
  • npm run test:unit: 129/129 pass. E2e: 94/94 pass (after the rebase onto Return to the app login after pairing (oidc_rd) #44 and the review fixes below).
  • Mutation check: each new test was run against a deliberate mutation of the code under it, 24 mutations in total, and every one made its test fail. Among them: dropping the refetch after the 502 (the first read has no candidate and later reads have the normalised one, so only the refetch can show it), keeping the delivery message after a resend, treating any 502 as delivered, confirming one tick after mount (the test drains a tick before asserting), dropping confirm-email from the anonymous allow-list, keeping the token in the URL, logging the token, offering resend with mail disabled, re-adding the Welcome mailto and the Public email field, re-rendering or refetching the card on re-attach. Two control edits (comment-only) left the tests green, which confirms the harness detects a change in result.
  • Visual check of the card's pending state and the landing in headless Chromium.
  • Not tested against a real shard_core: the API shape was taken from shard_core on freeshard main (web/protected/users.py, web/public/users.py, web/protected/settings.py, service/owner_email.py, data_model/user.py).

🤖 Generated with Claude Code

ClaydeCode and others added 5 commits October 20, 2026 10:24
freeshard#220 moves the owner's address onto the owner's user row and drops
identities.email. Welcome built a mailto link from it on the shard's public
landing page, and Public offered it as an editable identity field; both
go (web-terminal#46 did the same in the Vue UI). The address is edited in
the Settings Owner card from now on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Owner card sits first in Settings and is bound to the owner's user row
(GET/PATCH /protected/users/me), not to the identity or the controller
profile. The name writes through; an address becomes pending_email and only
goes live when the mailed link is opened. It renders none (a prompt),
pending (live address plus the candidate, send again, discard) and set, and
with GET /protected/settings reporting email_enabled: false it sets the
address directly and offers no resend.

A 502 from the PATCH that carries shard_core's JSON detail means the
candidate was stored and only the mail did not go out, so the card refetches
the row, says exactly that and offers the resend; a bare proxy 502 stays a
failed save. A successful resend clears the message. Settings keeps one card
instance across its full re-renders, so a WS update neither refetches nor
throws away an edit in progress. The About list loses owner and owner email,
the controller's copy of the same two values.

The candidate rendering is pendingChange(), a small markup helper with a
dashed sheet, meant for later pending/confirm flows too.

The mail link is /?confirm_email=<token>. main.js takes the token out of the
URL before routing and hands it to the new confirm-email view, which the
anonymous-to-welcome redirect leaves alone: the link is usually opened on a
device that was never paired. The view confirms only on a click, since mail
scanners and prefetchers would otherwise spend the single-use token, and its
copy is hedged because POST /public/users/confirm-email answers 204 whether
or not the token matched.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GET/PATCH /protected/users/me, resend, discard, GET /protected/settings and
POST /public/users/confirm-email, so the Owner card and the landing work under
`just serve`. A new address goes pending and /?confirm_email=dev-token confirms
it; --no-email mimics a self-hosted shard. The identity no longer carries an
email, matching freeshard#220.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Unit tests for token parsing, state derivation and the 502 discriminator.
E2e for every card state, the 502 recovery (the first read has no candidate,
later reads have the normalised one, so only the refetch can show it), resend
clearing the delivery message, a proxy 502, 429, 403, discard, clearing,
verification disabled, an edit surviving a Settings re-render, and the landing
(no confirm on its own after draining a tick, anonymous, /sundial/, no token
in the URL or the console). All state transitions are stubbed per test with
page.route; the shared mock state is never written.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ange

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ClaydeCode
ClaydeCode force-pushed the feature/owner-section branch from 2c387d0 to a82ec35 Compare October 20, 2026 10:27
ClaydeCode and others added 5 commits October 20, 2026 10:44
…TCH for an unchanged address

The Settings view re-renders on every disk_usage push (every 30 s), and the
owner card rebuilt both editable fields after each save, resend and discard.
Focus dropped from the email input on the former, a half-typed value was lost
on the latter. Confirming an unchanged address PATCHed it again, which made
the live address its own candidate and spent the shard's mail send budget.

Found by the adversarial review panel.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lure paths

The token URL is replaced, not pushed; boot requests carry no token in their
Referer; a fake clock runs a minute past landing so a delayed auto-confirm
fails the test; confirm and resend are disabled in flight; 422 on confirm,
a failed discard, a 429 on resend, an unreadable /settings and markup in a
candidate address each get a test. Every new test was checked against a
mutation of the code under it.

Found by the test adversary of the review panel.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
List the 'invalid' confirm state, name what watch([]) relies on, say why
Settings refocuses, rename emailHints to emailStatus, keep the token param
module-private, list the confirm view with the other views, and state test
names as behaviour. agents.md: pendingChange has one caller so far.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A paired browser opening /?confirm_email=T&oidc_rd=... left for the
authorize URL before the confirm view rendered, dropping the token unspent.

Found by the review panel (adversarial and security).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…meanwhile

The confirm landing asked to confirm "this address" without showing one;
it now refers to the address the link was sent to. The sent hint names a
paired device for checking. The pending block says mail still goes to the
live address (or nowhere) until confirmation. German: quoted button and
section names, no comma before "oder".

Found by the UX reviewer of the review panel.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@max-tet
max-tet merged commit 872e7d6 into main Oct 20, 2026
3 checks passed
@max-tet
max-tet deleted the feature/owner-section branch October 20, 2026 11:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Owner section in settings, with the email's verification states

2 participants