Skip to content

fix(mobile): stop warning about updates that were never coming, and stop blocking starts on an unknown outcome - #515

Merged
krishagel merged 2 commits into
mainfrom
fix/no-screen-noise
Sep 16, 2026
Merged

krishagel merged 2 commits into
mainfrom
fix/no-screen-noise

Conversation

@krishagel

Copy link
Copy Markdown
Member

Two screens told operators something untrue and took actions away from them. Both are removed rather than reworded.

1. The false banner on a closed event

Reported in the field: a closed event showing "New updates are not arriving. What is shown may be out of date. PSD EOC keeps retrying." with a Refresh now button.

The banner isn't about closure — it fires on any timeline poll error while entries are on screen. But a closed event's timeline is final: the only transition out of closed is reopen-as-correction, which by contract creates a distinct event (event.ts:1296). No further entry can ever arrive.

So on a closed event the message is false by construction, and the Refresh control cannot produce anything.

Server logs for that sighting: every capability succeeded, zero 5xx, four scattered 4xx (normal auth refresh). The trigger was a dropped request on the device — ordinary, and not worth alarming anyone about.

Fix: stop polling once an event is closed; never show that banner there. The timeline stays fully readable and the screen's own "This event is closed. Its timeline is still here." is the whole truth. On an active event the banner is unchanged — there it's true and useful.

2. The unknown-outcome gate stops blocking

An unknown start/join outcome took the whole screen and refused every later start until acknowledged. In an emergency tool that is the most expensive failure available — and it bought nothing.

The duplicate it guarded against is already prevented on live data: the confirmation screen lists the events active at the site (start/index.tsx:1111) and the start sends activeEventIdsSeen, which the server records (events.ts:274). That runs on fresh data at the moment of starting. The fence was stale state from an earlier request that could prove nothing — the same reason the automatic version was deleted in 0196d411.

Fix: the record is now a dismissible notice on the home and start screens, with the same copy that claims nothing about what the server did. submit accepts from unresolved and supersedes it. Nothing is blocked.

The read-only active-event list, its refresh-error copy, and the check action existed only to serve the blocking screen and are removed with it.

Coverage

  • A closed event schedules no poll and reports no error even when a forced refresh fails (fails without the guard).
  • Hydration still never replays an abandoned request and still exposes no event identity across sessions — but the other session can now start.
  • The screens render the notice and no longer gate on an unknown outcome.

Gate

Prettier, ESLint, typecheck clean repo-wide. 6/6 shards green; 61 mobile native tests pass.

Platform parity

Client-only logic and shared React Native UI. Needs verifying on iOS and Android.

…top blocking starts on an unknown outcome

Two screens were telling operators something untrue and taking actions away
from them. Both are removed rather than reworded.

A closed event's timeline is final: the only transition out of `closed` is
`reopen-as-correction`, which by contract creates a distinct event, so no
further entry can ever arrive. The room polled it anyway, and any ordinary
network blip then rendered "New updates are not arriving. What is shown may be
out of date." over a Refresh control that could not produce anything, because
nothing was coming. Server logs for one such sighting show every capability
succeeding and zero 5xx, so the trigger was a dropped request on the device --
normal, and not something to alarm anyone about. Polling now stops once the
event is closed and that banner is never shown there. The screen's own "This
event is closed. Its timeline is still here." is the whole truth. On an active
event the banner is unchanged, because there it is both true and useful.

An unknown start or join outcome no longer blocks anything. It was a gate that
took the entire screen and refused every later start until acknowledged, which
in an emergency tool is the most expensive failure available, and it bought
nothing: the duplicate it guarded against is already prevented on live data by
the confirmation screen, which lists the events active at the site and sends
`activeEventIdsSeen` with the start. That check runs on fresh data at the
moment of starting; the fence was stale state from an earlier request that
could prove nothing, which is the same reason the automatic version of it was
deleted in `0196d411`. The record is now disclosed as a dismissible notice on
the home and start screens, with the same copy that claims nothing about what
the server did, and `submit` accepts from `unresolved`, superseding it.

The read-only active-event list, its refresh-error copy, and the check action
existed only to serve the blocking screen and are removed with it.

Coverage: a closed event schedules no poll and reports no error even when a
forced refresh fails; hydration still never replays an abandoned request and
still exposes no event identity across sessions, but the other session can now
start; the screens render the notice and no longer gate on an unknown outcome.

Gate: Prettier, ESLint and typecheck clean across the repository; 6/6 test
shards green and 61 mobile native tests pass.
Copilot AI lite review requested due to automatic review settings September 16, 2026 18:03

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@claude

claude Bot commented Sep 16, 2026

Copy link
Copy Markdown

Review — PSD engineering standards

Scope: client-only mobile change (packages/mobile/src/**). No contracts, server capabilities, migrations, or logging paths touched, so no FERPA/PII or CI-weakening concerns apply here.

IMPORTANT: none.

I traced the two behavioral changes against their tests:

  • StartMutationCoordinator.submit() now also admits from phase === 'unresolved' (start-mutation-coordinator.ts:729-734), not just idle. Verified against start-mutation-coordinator.test.ts:548-583: a second session's submit() genuinely invokes run() (call count 1→2, next.accepted === true) even though the first session's request is still abandoned/unresolved — the test isn't just checking a snapshot field, it proves the new request actually executes. The write in submit() (start-mutation-coordinator.ts:743-762) does overwrite the still-unresolved durable record before the new op even starts, which is called out and justified in the code comment (server-side dedup via activeEventIdsSeen on fresh data, per the PR description) — intentional, not accidental data loss.
  • EventRoomSyncController.isFinalized() gating (sync-controller.ts:202-215, 294-299) correctly suppresses both polling and the stale-timeline error only once model.event.status === 'closed'. Checked the consumer (event-room-screen.tsx:2244-2247) — it only renders the sync-error banner when sync.error !== null, so setting error: null on a finalized event actually suppresses the banner as claimed, it isn't a cosmetic no-op.

NITs (2):

  1. components/start/start-mutation-attention.tsx:656-733OtherSessionStartMutationAttention's status="unresolved" branch (plus onAcknowledgeUnresolved prop and its copy) is now dead in the app: both call sites (app/index.tsx:262, app/start/index.tsx:550) invoke it with default props only, since the unresolved case moved to the new UnresolvedOutcomeNotice. The branch and its dedicated tests in start-mutation-attention.test.tsx still exist but nothing in the app can reach them anymore — worth deleting the unresolved-specific path along with this change rather than leaving unreferenced UI code behind.
  2. The new UnresolvedOutcomeNotice component (start-mutation-attention.tsx:476-528) has no direct render/interaction test (e.g. onDismiss firing on press, disabled state when online=false). Coverage for the underlying state transition is solid at the coordinator level (start-mutation-coordinator.test.ts), but root-provider-boundary.test.ts:275-297 only asserts on source-string containment (consistent with that file's pre-existing style), not on actual rendered behavior of the new component.

Diff is otherwise clean — good test-to-behavior correspondence on the two substantive logic changes.

The page() helper typed its event parameter as the active EVENT fixture, so a
lifecycle test could not hand it a closed event at all. It now takes the
contract's event type, which is what the schema parses anyway.
@krishagel
krishagel merged commit e28789c into main Sep 16, 2026
11 checks passed
@krishagel
krishagel deleted the fix/no-screen-noise branch September 17, 2026 02:37
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