Skip to content

Hosted account page: refetch after every mutation, wait out approvals - #1266

Open
nedtwigg wants to merge 5 commits into
mainfrom
account-mutation-refresh
Open

nedtwigg wants to merge 5 commits into
mainfrom
account-mutation-refresh

Conversation

@nedtwigg

@nedtwigg nedtwigg commented Oct 21, 2026 •

Copy link
Copy Markdown
Member

After approving a Natural Voice or remote-control request, the account page no longer shows the old state. It says it is waiting for Dormouse on the computer, re-reads every 5 s (the desktop's poll interval), and switches to "Natural Voice is on." or "Your computer is enrolled." once the desktop's poll redeems the approval. If nothing redeems it within a minute, it says the computer has not finished yet and that the page updates when you come back to it. It never shows "on" before the desktop holds its token.

Audit

Mutation Stale before? Now
Approve voice enrollment Yes: on stays false until the desktop's next poll mints the token, after the page's one refresh Waiting notice, re-read until on, or a "not finished yet" notice after 60 s
Approve remote enrollment Yes: the Burrow row is created at redemption, so Computers stays stale Waiting notice, re-read until a new computer is listed, or "not finished yet"
Approve when the change cannot show (voice already on for the account, or no computers list was read) No: nothing on the page changes Unchanged message, one refetch
Turn off Natural Voice No (patched locally) Refetched from the server
Remove computer No (patched locally, passkeys un-attributed by hand) Refetched from the server
Remove passkey No (patched locally) Refetched from the server
Email sign-in, sign out No (already refreshed) Same, through the shared helper
Accept terms No: it shows nothing; recorded inside refresh Unchanged
Connect provider / provider sign-in No: the page leaves and reloads Unchanged (not a mutation the helper wraps)
Outside the page (desktop turns voice off, phone adds a passkey, another tab) Shown on the next window focus, though mobile Safari fires no focus when you switch back from another app Refreshed on window focus and when the tab becomes visible; nothing runs while the tab is idle

Mechanism

  • mutate() in hosted/src/App.tsx wraps every account mutation: it runs the action, then refetches the account through the existing refresh() instead of hand-patching local state. If that refetch fails, the mutation is still reported as a success: reread() handles the failure, and the page shows "Your change is saved, but this page could not reload to show it." with Retry.
  • An action may return a Pending (what shows its completion, plus the waiting, done, and late copy). Only approvals return one. mutate() then re-reads in the background using settle() (hosted/src/settle.ts). The page stays usable while it waits. Any later action or unmount cancels the wait.
  • A message written from an old read never outlives a newer read that contradicts it. Errors record whether a failed read caused them (ReadFailed), and the next successful refresh() clears only those; an action's own failure stays until the next action. After the 60 s timeout an approval stays outstanding, and the first later refresh that shows it replaces the late notice with the done notice.
  • The pre-launch "Coming soon" redirect follows the same rule. It shows only after the latest account lookup succeeded and found no login (preLaunchNotice). Before, it was keyed on "no error on screen": a failed lookup whose error a new /enroll link, Don't approve, or Change email then cleared sent a signed-in page to nedshed.dev. main has the same bug. Those steps now keep a failed read's error (keepRead).
  • approvalShows() (hosted/src/settle.ts) turns the pre-approval state into the predicate: voice turned on, or a burrowId not listed before.
  • ENROLLMENT_POLL_INTERVAL_S moved from relay-api.ts to policy-constants.ts, since the frontend's import graph may reach only that module.

Rejected: counting an approved but unredeemed voice approval as on on the server. That is dishonest when the desktop never redeems, and it does nothing for remote enrollment. Also rejected: a new approval-status route. Re-reading the existing routes is enough, and no wire contract changes, so docs/specs/hosted.md is unchanged.

Test plan

  • hosted/server/tests/settle.test.ts: the approval predicates, the bounded re-read loop, reread, the read-error rules, and preLaunchNotice. Each guarded line was mutation-checked: 29 mutations, all red.
  • Mutation-checked the relocated ENROLLMENT_POLL_INTERVAL_S assertion in relay.test.ts.
  • pnpm exec tsc --noEmit and pnpm run test in hosted: 13 files, 197 tests.
  • pnpm run lint:specs, pnpm run lint:md
  • Local pnpm dev:hosted driven with agent-browser, with redemption simulated by inserting the token or Burrow row:
    • Voice: the waiting notice, then "Natural Voice is on." within one interval.
    • Remote: the waiting notice, then "Your computer is enrolled." with the computer listed.
    • Remove computer and turn off voice still update the page.
    • An unredeemed approval shows the "not finished yet" notice after about 60 s.
    • With get-session aborted, removing a computer shows the saved-but-could-not-reload message, not a failure. Retry then shows the list without it.
    • A visibilitychange event alone refreshes the page and shows a computer added in the database.
    • The saved-but-could-not-reload alert clears on the next successful read, while an action's error (a 409 approval) survives one.
    • After the late voice notice, a token inserted plus a visibilitychange event switch the notice to "Natural Voice is on."
    • With get-session failing, changing the /enroll link keeps the page on the sign-in prompt with its connection error, never "Coming soon". It recovers on the next good read.
    • An approval refused 403 (account not entitled) or 409 keeps the page with its error.
  • Production: turn on Natural Voice from the desktop, approve, and stay on the page. It should show waiting, then on, with no manual refresh.
  • Production: enroll remote control the same way. The computer should appear without a refresh.

🤖 Generated with Claude Code

nedtwigg and others added 2 commits October 20, 2026 22:35
The account page will re-read an approval's result at the Burrow's poll
pace, and its import graph may reach only policy-constants, never
relay-api.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every account-page mutation now goes through one helper, mutate(), which
shows the account as the server has it afterwards instead of patching
local state. An approval returns a Pending: the desktop's next poll
redeems it, so the page says it is waiting and re-reads at the poll pace
until Natural Voice turns on or a new computer is listed, for up to a
minute, then says the computer has not finished and leaves the rest to
the focus refresh.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Deploying mouseterm with  Cloudflare Pages  Cloudflare Pages

Latest commit: 42cff4a
Status: ✅  Deploy successful!
Preview URL: https://76741923.mouseterm.pages.dev
Branch Preview URL: https://account-mutation-refresh.mouseterm.pages.dev

View logs

@dormouse-bot dormouse-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove computer, remove passkey, and turn off Natural Voice now report a failed post-mutation refresh as if the mutation failed. Inline below.

Comment thread hosted/src/App.tsx Outdated
mutate() re-read the account with a bare refresh(), so a failed reload
surfaced through act() as the mutation's own failure: a removed computer
stayed listed under "Could not connect", and Turn off Natural Voice
showed its "is off" notice beside an error. The re-read now goes
through reread(), which reports the failure itself: the page says the
change is saved but could not reload, and offers Retry. An approval
still waits on its computer through follow().

The page also refreshes when the tab becomes visible, since mobile
Safari fires no focus returning from another app.

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

@dormouse-bot dormouse-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new "saved but could not reload" message outlives a reload that later succeeds. Details inline.

Comment thread hosted/src/App.tsx Outdated
@nedtwigg
nedtwigg marked this pull request as ready for review October 21, 2026 06:52

@dormouse-bot dormouse-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The "late" approval notice stays on screen after a later refresh shows the change, so it contradicts the page beneath it. Details inline.

Comment thread hosted/src/App.tsx
Comment thread hosted/src/App.tsx
Two messages outlived the reads that contradicted them. The "change is
saved, but this page could not reload" alert stayed up after a focus,
visibilitychange, or approval re-read succeeded; and an approval's late
notice ("has not turned on Natural Voice yet") stayed after a later
refresh showed it on.

Errors now carry whether a failed read caused them: refresh() throws
ReadFailed, and a successful refresh clears only such an error, so an
action's own failure stays until the next action. A late approval stays
outstanding after its timeout; the first refresh whose account shows it
replaces the late notice with the done one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
comingSoon was `!session && !error`, so "no error on screen" stood in
for "the account lookup succeeded". When a lookup failed (a 429 from the
auth limiter under repeated reads, an outage), session stayed null and
any later step that cleared the error (a new /enroll link's hashchange,
Don't approve, Change email) turned a signed-in page into "Coming soon"
and redirected it to nedshed.dev. main has the same path.

The page now records what the latest lookup found (pending, failed,
signed in, signed out), and preLaunchNotice() shows the notice only
after a lookup that succeeded and found no login. The steps that read
nothing keep a failed read's error (keepRead) instead of clearing it,
so the page keeps saying it could not connect until a read succeeds.

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

This branch is waiting to be deployed

1 waiting (outdated) deployment
hosted-preview — 197bf456 Waiting Oct 21, 2026 by nedtwigg via deploy #1721
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.

2 participants