Repository navigation
Owner card in Settings, email confirmation landing, owner email off public pages - #45
Merged
Merged
Conversation
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
force-pushed
the
feature/owner-section
branch
from
October 20, 2026 10:27
2c387d0 to
a82ec35
Compare
…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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.PATCH {display_name}(reusesfs-editable-text).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. WhenGET /protected/settingsreportsemail_enabled: falsethe address is set directly and there is no resend control. Note: the issue namesoidc.email_verificationas the signal;email_enabledfrom/protected/settingsis what shard_core actually exposes.PATCH {email: null}), as web-terminal#47 did.pendingChange()injs/components/pending-change.jsrenders 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.jstakes the token out of the URL (history.replaceState) before routing and hands it to the newconfirm-emailview. 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-tokenconfirms the mock candidate;--no-emailmimics a self-hosted shard.Difference from the issue: 204 either way
POST /public/users/confirm-emailanswers 204 whether or not the token matched (anything else would let a caller probe for pending addresses; checked inshard_core/web/public/users.pyon 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.jsboot (both imports kept,PUBLIC_ROUTESplus #44'soidcReturnQuery()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.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.invalid. Also took the cheap advisories (naming, why-comments, test names stated as behaviour,agents.mdwording)./?confirm_email=T&oidc_rd=...left for the authorize URL and dropped the token unspent (also flagged by the adversarial reviewer).Open advisories, not done here:
t()escapes, the toast setstextContent), soa&b@x.orgwould show asa&b@x.org. Repo-wide, the same applies to existing toasts such astoast.installFailed.<body>after Send again, Discard and Confirm (the buttons are rebuilt).[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.jstracks its load lifecycle in three fields (#loaded,#loadFailed,#user === null); one state field would read better.pairing.pymakes pairing codes withrandom.choicesinstead ofsecrets.https://<shard>/?confirm_email=, so it only reaches Sundial on shards where Sundial serves/.Recommended reading order
js/owner-email.js: token parsing, the three states, the 502 discriminatorjs/components/pending-change.js,js/components/owner-card.js: the pattern and the cardjs/views/settings.js: wiring, About removaljs/main.js,js/views/confirm-email.js,js/components/dock.js: the landingjs/views/welcome.js,js/views/public.js,js/store.js: removalsjs/i18n/*.json,css/ui.css,css/views.csstools/dev_server.py, thentests/unit/owner-email.test.js,tests/e2e/owner.spec.js,tests/e2e/confirm-email.spec.js,tests/e2e/owner-email-public.spec.jsagents.mdVerification
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).confirm-emailfrom 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.shard_coreon 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