fix(notifications): reload the inbox and its settings when the account changes #45

Merged
whitlocktech merged 1 commits from fix/inbox-session-scope into edge 2026-09-08 22:45:24 +00:00
Member

The defect Phase 14b found in MyEventsViewModel and flagged next door (#44). The notifications surface has the identical shape and leaks the same way — confirmed on the emulator before fixing it, not inferred.

What happens

A drawer route's view model outlives a sign-out. navigateTopLevel uses popUpTo(HOME) { saveState = true } with restoreState = true, so the NavBackStackEntry keeps its ViewModelStore and a view model that loaded only in init never runs again.

Signing out and back in as somebody else showed the second account the first account's inbox — titles and body text written for another person — with no request made at all. The tell was visible on screen: the badge above the list read the new account's real unread count, because the shell refreshes that on every session change, so the header and the list disagreed.

InboxCache was never the hole. It is keyed by (base URL, user id) and a snapshot has never crossed an account. The hole was the in-memory state, which nothing invalidated.

The fix

Both view models key on the signed-in account id, so a resume revalidation returning the same user does not refetch — the app re-validates its role on every resume, so gating on the session object itself would refetch constantly.

The inbox resets its state before loading rather than after: load() paints the cache only when there is no Success on screen, so otherwise the previous account's rows stay up for the whole round trip rather than being replaced at the end of it.

The settings screen behind the inbox's gear is fixed with it, and I widened to it deliberately rather than asking, because there a stale render is worse than disclosure: those controls are written from, so a screen still showing the previous account's preferences would send this account's sparse PUT built out of that account's rendered state. That is data corruption, not just a leak — and it is one tap from the screen already being fixed.

Verified

Walked on the emulator against a local website, before and after, with two accounts given deliberately different inboxes:

  • before — signed out of the admin, in as the player, opened the inbox: the player saw the admin's two rows, and the server logged no GET /auth/me/notifications at all;
  • after — the same sequence in one process: the server logs the fetch and each account sees its own rows (the admin's two; the player's six, including a row seeded to say so).

572 tests, 0 failures. Two new regression tests: the account switch, and that a same-user revalidation does not refetch.

One thing measured and left alone

Opening the settings screen fires two GET /auth/me/notifications/channels requests. I suspected my collector had doubled it, so I rebuilt this file from edge and measured: the baseline does it too. Pre-existing engagement Phase 8 behaviour, harmless, and not this PR's to change.

🤖 Generated with Claude Code

https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4

The defect Phase 14b found in `MyEventsViewModel` and flagged next door (#44). The notifications surface has the identical shape and leaks the same way — **confirmed on the emulator before fixing it**, not inferred. ## What happens **A drawer route's view model outlives a sign-out.** `navigateTopLevel` uses `popUpTo(HOME) { saveState = true }` with `restoreState = true`, so the `NavBackStackEntry` keeps its `ViewModelStore` and a view model that loaded only in `init` never runs again. Signing out and back in as somebody else showed the second account the **first account's inbox** — titles and body text written for another person — with **no request made at all**. The tell was visible on screen: the badge above the list read the new account's real unread count, because the shell refreshes *that* on every session change, so the header and the list disagreed. **`InboxCache` was never the hole.** It is keyed by `(base URL, user id)` and a snapshot has never crossed an account. The hole was the in-memory state, which nothing invalidated. ## The fix Both view models key on the **signed-in account id**, so a resume revalidation returning the same user does not refetch — the app re-validates its role on every resume, so gating on the session object itself would refetch constantly. The inbox resets its state **before** loading rather than after: `load()` paints the cache only when there is no `Success` on screen, so otherwise the previous account's rows stay up for the whole round trip rather than being replaced at the end of it. **The settings screen behind the inbox's gear is fixed with it**, and I widened to it deliberately rather than asking, because there a stale render is worse than disclosure: those controls are *written from*, so a screen still showing the previous account's preferences would send **this** account's sparse PUT built out of **that** account's rendered state. That is data corruption, not just a leak — and it is one tap from the screen already being fixed. ## Verified Walked on the emulator against a local website, **before and after**, with two accounts given deliberately different inboxes: - **before** — signed out of the admin, in as the player, opened the inbox: the player saw the admin's two rows, and the server logged **no** `GET /auth/me/notifications` at all; - **after** — the same sequence in one process: the server logs the fetch and each account sees its own rows (the admin's two; the player's six, including a row seeded to say so). **572 tests, 0 failures.** Two new regression tests: the account switch, and that a same-user revalidation does not refetch. ## One thing measured and left alone Opening the settings screen fires **two** `GET /auth/me/notifications/channels` requests. I suspected my collector had doubled it, so I rebuilt this file from `edge` and measured: **the baseline does it too.** Pre-existing engagement Phase 8 behaviour, harmless, and not this PR's to change. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
wtclaude added 1 commit 2026-09-08 22:14:25 +00:00
fix(notifications): reload the inbox and its settings when the account changes
All checks were successful
PR Checks / android-build (pull_request) Successful in 8m12s
aa055469a8
The defect Phase 14b found in `MyEventsViewModel` and flagged next door: the
notifications surface has the identical shape, and it leaks the same way.

A drawer route's view model outlives a sign-out. `navigateTopLevel` uses
`popUpTo(HOME) { saveState = true }` with `restoreState = true`, so the
`NavBackStackEntry` keeps its `ViewModelStore` and a view model that loaded only
in `init` never runs again. Signing out and back in as somebody else showed the
second account the FIRST account's inbox — titles and body text written for
another person — with no request made at all, while the badge above the list
showed the new account's real unread count, because the shell refreshes that on
every session change.

`InboxCache` was never the hole: it is keyed by (base URL, user id) and a snapshot
has never crossed an account. The hole was the in-memory state, which nothing
invalidated.

Both view models now key on the signed-in account id, so a resume revalidation
that returns the same user does not refetch. The inbox resets its state *before*
loading rather than after, because `load()` paints the cache only when there is no
`Success` on screen — otherwise the previous account's rows stay up for the whole
round trip.

The settings screen behind the inbox's gear is fixed with it, and there the stale
render is worse than disclosure: those controls are written from, so a screen
still showing the previous account's preferences would send this account's PUT
built out of them.

Walked on the emulator against a local website, before and after: two accounts
with deliberately different inboxes, signed out and in within one process. Before,
the second account saw the first's rows and the server logged no inbox fetch;
after, it logs the fetch and shows its own.

572 tests, 0 failures.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
whitlocktech merged commit ac2d75c3f9 into edge 2026-09-08 22:45:24 +00:00
whitlocktech deleted branch fix/inbox-session-scope 2026-09-08 22:45:26 +00:00
Sign in to join this conversation.
No description provided.