Skip to content

Live Multiplayer and UI Integration

Sharp Ninja edited this page Oct 31, 2026 · 4 revisions

Restore end-to-end live multiplayer in the local runtime

Status: current release in progress, 31 October 2026. The original transport plan received historical 99/99 approval; the changed source and current player range still require fresh verification and review. The user explicitly rejected a lobby-polling-only fix. Actual authenticated private Realtime and server-driven progression remain required; faster polling is supplementary recovery.

Current release scope

Rules v2.8 and the approved completion plan require new games with every player count 2 through 10. The full matrix is 9 eight-round games, 54 player participations and 432 accepted UI solves, with at most two simultaneous answer UIs. The separate limit remains 25 observers, giving a maximum audience of 35 for the new release. Existing larger running matches and stored results survive; above-limit legacy waiting/countdown rooms keep Leave and a compatibility notice, block oversized new starts/admission and do not evict players merely to reduce the count.

Run the matrix after a frozen production build with pwsh -NoProfile -NonInteractive -File scripts/run-local-ui-integration.ps1 -PlayerCounts '2,3,4,5,6,7,8,9,10'. Run -StrictPush separately for the two-player/inviter-bound-observer journey with periodic UI reads suppressed. Preserve per-run reports and actual source identity. Public play is guest-only; installation is confined to the owner GitHub admin PWA in the release contract.

The isolated 15-player run in docs/evidence/hosting/guest-admin-recovery/round7-scale-15/ completed eight rounds/120 UI solves with seven recorded request failures and no uncertain retries or cleanup failures. It used a dirty build and does not approve the changed committed source or hosted release. The failed 20/25-player investigations remain historical. No current pass of all 9 counts or hosted rollout is claimed here.

Evidence and cause

The phone report showed a stale host roster and “Live updates unavailable.” The local tunnel runtime has no Supabase Realtime configuration (/api/realtime returns503). A real two-browser, two-guest code-join probe against the HTTPS game reproduced a host roster delay of12825ms without manual refresh; the host did not update within five seconds. This proves delayed recovery, not an indefinitely stuck page or all possible phone-specific behavior. Evidence: docs/evidence/room-sync/live-probe-before.json.

RoomSession in src/web/room-screen.tsx polls waiting rooms every15seconds, even when useRoomUpdates cannot connect. Countdown/holdout polling is already1second. The polling effect has no focus, visibility or online recovery handler. Browser background timer throttling can further delay recovery after switching apps. Current tests often click Refresh room and therefore do not prove automatic cross-client recovery.

Research provider: Perplexity unavailable — fallback process used. Relevant primary sources: React effect lifecycle, MDN page visibility and background timer throttling. Repository observations above are independently measured; mobile throttling is a supported explanation for additional delay, not a proven fact about this particular screenshot.

Change and contract

The primary fix is operationally enable the same real Supabase Realtime implementation already covered by tests/integration/transport.realtime.integration.ts, not invent a new SSE transport or depend on browser polling. Start the pinned self-hosted Realtime image and a separate disposable PostgreSQL instance for its internal tenant/auth schemas; preserve the existing Wordpairs database, rooms and sessions. Apply the existing private per-profile Realtime policy. Generate local-only random JWT/encryption/database secrets, anon and service-role tokens; store them in ignored private configuration and never print them. Use the existing Next runtime settings and publisher; publish only event IDs/type/aggregate/version invalidations.

A loopback-only Caddy gateway routes the existing HTTPS ngrok origin's /realtime/v1/websocket to Realtime's /socket/websocket and only /realtime/v1/api/broadcast to the authenticated broadcast API, with the tenant Host. Other /realtime/* paths return404; tenant-management APIs stay private. Other app requests go to Next. Gateway administration is disabled; database and Realtime ports bind to loopback only. Public clients receive only the anon key and their own short-lived profile token. Preserve private-channel RLS; never use a public-channel fallback. Keep the current tunnel URL and the active lease expiry; adapt its owned cleanup to the gateway upstream and temporary new services. After the interrupted session expired, the resumed run renewed an explicitly temporary one-hour lease ending27 October2026 at20:42:31UTC.

Configure a generated server-only scheduler secret and a serialized one-second local worker calling the existing protected deadline/outbox route over loopback. This makes countdowns, hints, reveal and notifications advance even if clients do not poll. Keep foreground deliverOutbox bounded to one event; allow the worker to drain a separate bounded batch of25 (the SQL maximum), and repeatedly process the initial durable backlog before declaring live delivery healthy. A deadline failure must not prevent an independent publication attempt. The protected worker endpoint must return separate deadline and publication outcomes and report either failure truthfully, including unconfigured Realtime. Preserve unpublished events after failures and retain backlog/oldest-pending-age evidence. No cloud/provider credentials or paid services are required. The local self-host mode is documented separately from future hosted Supabase/Vercel provisioning.

Provide a reproducible local setup/lifecycle script in scripts/, with private runtime/config and owned container identities kept outside tracked files. New services use the already-pinned test images. Do not restart unrelated containers or change the game DB schema. Back up the old runtime privately; stop/remove only resources this setup created. Cleanup must not let restored configuration silently point at stopped Realtime: retain a factual stopped/unconfigured state or restore the previous four settings. Preserve the original expiry and existing localhost server when adapting the temporary runtime.

The browser's existing subscribed notifications trigger actor-authorized room/match/reaction reads. Prove those reads are caused by actual private WebSocket messages, not safety polling. Add/repair minimal client scheduling only where tests expose a real gap, retaining the recovery design below. On account change/unmount, clear channels and pending reads; tokens stay scoped to the actual session.

Focus, visible and online events refresh authorized state immediately. Hidden visibility transitions do not refresh. RoomSession, GamePanel and SocialPanel coalesce a single follow-up while a read is pending; older notifications must not be lost behind an older snapshot. Channel setup rechecks cancellation after awaited authentication. Online recovery refreshes the short-lived transport identity and accurately restores connected status.

Faster disconnected polling is deferred. Waiting-room safety polling remains15seconds and countdown/game polling remains1second. SYNC-01 and SYNC-03 from the superseded fallback proposal are not implemented or accepted completion criteria. Actual private push, autonomous deadlines and immediate resume recovery are the delivered scope.

Acceptance and test plan

Canonical extension: TR-ROOM-SYNCRECOVERY-038 and TEST-ROOMSYNC-139, linked to FR-LOBBYLIFECYCLE-003 alongside its existing mappings.

The narrower SYNC-01..03 are recovery contracts; add these primary live-delivery checks before considering the user's issue fixed:

  • LIVE-01: actual private WebSocket subscriptions report SUBSCRIBED for two players and an inviter-bound observer, with cross-profile channel access rejected. API authorization remains authoritative and notification payloads contain only invalidation metadata.
  • LIVE-02: with room/game safety-polling intervals suppressed in the browser test, a second code-joining player appears on the host automatically; readiness/countdown reaches category selection, category/solve/hint/score/reveal changes propagate to the other player and observer. Assert actual WebSocket notifications and subsequent authorized refetches, not manually refreshed snapshots.
  • LIVE-03: a local serialized scheduler advances real database deadlines and drains durable outbox batches independently of client reads; catch up the backlog, preserve canonical command retry/duplicate semantics and do not drop unpublished events.
  • LIVE-04: the existing HTTPS tunnel serves both app and authenticated Realtime paths, private credentials remain server-only, temporary services have owned cleanup, and all existing rooms/sessions survive rollout. No physical phone validation is inferred from browser checks.

The user additionally requires repeatable UI integration tests, not service-only tests. Add tests/browser/live-multiplayer.integration.spec.ts, a dedicated Playwright config and npm run test:live-ui requiring an explicit target origin. It uses real browser UI, session cookies, database-backed room/game endpoints and private WebSockets, without HTTP route fixtures. Each run creates its own profiles/room and leaves its room in cleanup. The suite runs two players plus an inviter-bound observer and checks code entry, join, readiness, countdown, category/answer/hint/score/reveal/reactions, token renewal beyond120seconds and disconnect/reconnect. Suppress periodic room/game/social reads, record actual incoming changed frames before remote assertions and retain event-driven authorized reads. Add a no-browser-read scheduler probe and private subscription denial for anonymous/cross-profile clients plus client-publish denial checks. Document setup and a repeatable command; live ngrok tests are a separate operational tier from the default deterministic suite.

LIVE-05 covers coalesced room/game reads and cancellation after deferred authentication. LIVE-06 covers renewal beyond the initial token expiry and actual socket closure followed by a new socket and another push-driven transition. LIVE-07 now requires repeatable browser play at every configured count 2 through 10 under the 31 October amendment; the former 2 through 25 scope is historical. Test status and scope must match retained execution evidence.

Write deterministic regression tests before code changes; run the existing actual Realtime integration against disposable services as the baseline transport/privacy contract. Add a real HTTPS two-player/observer browser run and server scheduler check for the current operational runtime. Run focused and affected regression suites, type/lint/build, and independent hostile validation. The earlier fallback-only review is superseded; do not present its approval as approving this expanded architecture or a working game.

Review, rollout and rollback

Independent hostile plan approval99/99 precedes implementation; final review follows execution evidence. Keep the user’s current tunnel URL and database intact. Replace the production app behind it with the corrected build after tests, preserving the configured HTTPS origin and original one-hour tunnel expiry. Sessions and rooms are persisted in PostgreSQL; a server restart does not reset them. Rollback restores the prior runtime/gateway configuration and a compatible application build. No commit/push, new paid service, new human gate or extension of the public tunnel lifetime is implied.

Historical implementation and review findings

Hostile review found four defects before final approval: foreground calls joined a worker publication batch; GamePanel dropped refresh requests during an in-flight GET; channel setup could subscribe after unmount while authentication was pending; and an older cleanup watcher could operate on replacement state. Focused regressions first reproduced the source races. Foreground requests now return without joining an active publication batch; game reads coalesce a follow-up; channel setup checks cancellation; cleanup captures the lease generation, exact container IDs/labels and process start tokens. Linux process identity uses /proc/<pid>/stat start ticks because repeated .NET start-time conversions varied by fractions of a millisecond.

The later deadline-worker review found a separate in-flight batch: foreground requests joined deadline dispatch even though the publication path had been fixed. The local candidate returns without joining an existing deadline batch; the protected worker waits for its actual outcome, and an idle foreground request still starts and awaits catch-up. Worker failures reach worker callers and a later invocation can retry. The independent correct and wrong fixture receipts are under docs/evidence/hosting/guest-admin-recovery/deadline-worker-mock-v2/ and deadline-route-mock/. This source change is uncommitted and is not proof that the 25-player UI case or hosted rollout works.

The later local candidate separates game and reaction refresh keys: a match notification refreshes the game without automatically rereading reactions; room and reaction notifications still refresh the room and both views. The social view also refetches when an active round becomes a summary, including for observers. Independent fixture controls precede this change in docs/evidence/hosting/guest-admin-recovery/room-social-routing-mock-v2/, game-social-mock/ and social-transition-mock/. Twenty-two focused tests passed against the implementation. The 25-player case remains unverified; this is an uncommitted candidate, not a hosted result. The subsequent local strict-push test passed for two players and an observer through eight rounds, including reactions, token renewal and reconnect. Its receipt is docs/evidence/hosting/guest-admin-recovery/round7-strict-social-push/; it does not establish 25-player or hosted performance.

The gateway initially returned empty200 responses for the public Host because its site was bound to the localhost hostname. It now accepts Host routing on its configured port while explicitly binding the listener to127.0.0.1. Gateway configuration is validated before launch. The public play page and denied tenant-management path are checked separately.

The strict browser run suppresses1second/15second safety polls and the20second UI presence/read loop. An independent test presence loop uses actual authenticated mutation requests and SQL version reads without periodic UI reads; it pauses during SQL-only scheduler proofs. Expected match/room notification versions must precede corresponding authorized response versions. Reactions use the existing moderation aggregate notification. The seeded Opposites catalog must remain unchanged for answer lookup. Every run creates its own profiles/room; cleanup leaves only that room and never touches the reported user room.

Current evidence remains in progress until the final execution and review receipts below are written. Earlier failures are retained under docs/evidence/room-sync/; they are not passing evidence.

Verified core execution,27 October2026

The strict real UI game passed in3.9minutes: two players and a bound observer completed all eight rounds with periodic UI reads suppressed. It recorded182 incoming metadata frames and authorized versioned reads, autonomous countdown and hint progression with all browsers offline, hint privacy, reactions, equal final rankings, renewal beyond120seconds, a closed/reconnected socket followed by push, and anonymous/cross-profile/client-publication denials. No system-error frame occurred. Evidence: docs/evidence/room-sync/live-ui-final.log and live-ui-observations.json. This is a real browser/API/database/WebSocket integration, not an HTTP fixture.

The installed SDK cause was independently reproduced without network: its default Supabase Auth callback replaced a manually supplied game JWT with the anonymous key during internal refresh. The hook now provides the supported accessToken callback with the current verified game token, updates it on renewal and replaces closed channels. The regression failed before that fix. All530 unit/acceptance tests and the production build pass. The strict phone-path game also passed again in3.3minutes after the connected safety-read cadence adjustment. A bounded-concurrency suite run and expanded240-second source-archive budget preserve every assertion after the busy shared host exceeded the previous60-second archive timeout. Cleanup injection checks pass both obsolete-generation protection and process disappearance during cleanup.

The UI player-count matrix is still in progress. Earlier2/3-player eight-round cases passed; a4-player concurrent-submission run returned503 and later request aborts produced no HTTP response. Those failure logs remain retained. The matrix now limits simultaneous answer controls to two, waits for every submission task to settle, and exercises the actual pending-request retry control with identical original command ID and payload. It does not claim25 simultaneous answer submissions. Hosted/provider capacity is not inferred from these local results.

The temporary runtime received one bounded renewal after review so authorized validation and phone verification can finish, preserving the same URL and owned resources. Its hard expiry is27 October2026 at21:27:48UTC (4:27PM America/Chicago). The lease generation was replaced atomically and scheduler lifetime renewed. This changes the earlier assistant-chosen expiry; the user did not specify a one-hour cutoff. No further renewal is implied.

Connected safety-read cadence adjustment

The strict no-poll game passes, but subsequent real UI matrix runs with default one-second game/social reads hit transient server failures and browser request aborts at small player counts. This is evidence of a load-sensitive failure, not proof of a PostgreSQL capacity defect: the dedicated database permits100 connections, observed app connections were3or fewer, and PostgreSQL logs show no corresponding query/deadlock/capacity error. Do not raise database limits speculatively.

Pass the actual live subscription status from RoomSession to GamePanel and SocialPanel. Connected clients retain event-driven authorized reads and poll only every15seconds as a safety check; disconnected clients retain one-second game/social recovery. Keep display/reaction expiry clocks independent. Split timer cleanup from request-lifetime invalidation so a cadence transition never overlaps or drops an in-flight GET or its coalesced follow-up. Verify that lifecycle with a deferred React regression before implementation, run affected suites/build, then rerun strict and player-count UI integration. This is a hypothesis about redundant-read pressure; classify it as resolved only after execution evidence improves. Waiting-room disconnected cadence remains15seconds as explicitly deferred above.

Historical private UI harness plan (shared cluster, later superseded)

Use a private loopback HTTPS harness for the full2..25 UI matrix, alongside the separately passing real HTTPS/ngrok phone-path test. The matrix currently depends on remote tunnel latency and aborted requests even at small counts. A private harness removes that environmental dependency without mocking browser HTTP, database transactions or Realtime. It does not prove25-player public-tunnel/provider capacity; keep those qualifications explicit and preserve the failures.

Add a PowerShell runner requiring the explicit existing local TLS game runtime JSON and a loopback database host. Create a new ignored private directory, create a uniquely named disposable game database in that existing local TLS PostgreSQL cluster, migrate it with the repository migrations, replace only the database name in the copied connection URL, and start a separate owned pinned Realtime/internal-Postgres/gateway set on loopback ports3004/3005. Preserve all original public-runtime services, game DB schema, user rooms and sessions. Use the current production build without rebuilding a checkout used by running servers. Generate a one-day loopback-only self-signed certificate/key with OpenSSL; keep the key/config mode0600. A small Node built-in HTTPS/WebSocket proxy binds127.0.0.1:3443 and forwards to the owned HTTP gateway. No new public endpoint, package dependency, paid resource or system trust-store change.

The app and scheduler use the new HTTPS127.0.0.1 origin and generated private runtime. Node trusts that generated public certificate only through NODE_EXTRA_CA_CERTS in the child environment. Browser/API certificate exceptions are restricted to the explicit loopback harness; production/remote browser targets retain normal HTTPS verification, and PostgreSQL TLS verification remains enabled. Both the browser and server publisher still authenticate through the actual private Realtime policy.

Run the same UI integration spec against that origin with2workers, real API setup and actual UI readiness/categories/every answer, canonical retries and per-count evidence. A bounded runner lifetime and finally cleanup stop only captured child processes (PID/start-token guards) and the exact label-owned service IDs from its unique private directory. The harness never shares the original global outbox: its application, worker and broker operate only on the uniquely named game database. Stop all owned publishers before terminating connections to and dropping only that recorded disposable database. Never reset the original database or operator rooms. Use a random database name matching the runner-owned prefix and a private ownership receipt; cleanup validates both. Preserve sanitized failure/result logs; delete only the generated owned secret/scratch directory after cleanup. The existing public lease keeps its hard expiry21:27:48UTC and needs no further renewal.

The isolated plan received hostile approval99/99. Implementation review then required truthful Docker cleanup (including daemon/inspect/removal/owner failures), preserving unresolved startup receipts, upstream response-reset handling, strict-TLS verification of the exact application URL, and execution-based receipt flags. Six injected cleanup scenarios, unresolved-state restart rejection and the proxy reset-after-headers regression now pass. A real2/3-player smoke run completed all8rounds without failed requests or uncertain retries and removed its owned database/containers/processes. The full2..25 run remains pending until its final receipt is written.

The first private full matrix completed counts2..11, then Chromium crashed a page during round5 at12players. Its database and services cleaned up successfully; the failure log/receipt are retained. The runner had used shared tmpfs /tmp despite this host's earlier scratch failures. At inspection that filesystem had3.5GiB free and system memory about6.7GiB available; no core dump or kernel OOM entry was found, so the crash cause is not confirmed. Browser scratch now uses a fresh short workspace directory, and the remaining counts run independently. Do not report the aborted matrix as a complete pass.

Historical storage-isolated UI execution plan

The monitored14-player rerun passed countdown and progressed into gameplay. It also captured the same COMMIT in IO/WalSync over7.075seconds, with query age reaching9.259seconds and two aggregate-lock requests queued behind it (monitor lines94..99, then release at100). Query age is not a direct wait-duration counter. Independent host I/O pressure was substantial. This proves a multi-second flush bottleneck in that run, not a deadlock or a definitive cause for every earlier timeout. Do not increase the pool, weaken fsync/synchronous_commit, or relax game assertions.

Replace the harness's shared-cluster game database with a separately owned pinned PostgreSQL container. Give only disposable harness game and Realtime internal PostgreSQL data bounded tmpfs mounts (512MiB each, soft WAL checkpoint/recycling targets128/32MiB); preserve fsync and synchronous_commit. This is volatile test storage and cannot prove production power-loss durability or host/provider capacity. The normal disk-backed PostgreSQL integration evidence remains separate. The existing operator database is never contacted, migrated or reset by the new harness.

Generate the one-day localhost/IP certificate and private role passwords in a newly created mode0700 private directory. Initialize the fresh database container on an ephemeral loopback port, copy the certificate/key into its owned data directory, chown to its postgres user and chmod0600, enable/reload PostgreSQL TLS, create the fresh cluster's test roles and apply migrations. Before any application publisher starts, use the exact generated service-role connection URL and CA to assert current_database and server port with verified TLS. The existing private runtime input may supply optional server settings; all database, origin, broker and scheduler settings are replaced with owned test configuration.

Retain actual UI/API/private WebSockets, all8round/solve/ranking/canonical retry assertions and20-second chooser budget. Retain complete failure counts, explicit100-record caps and failure UI diagnostics. Cleanup stops owned publishers, confirms exact label-owned broker and game containers absent, and only deletes successfully created private/scratch directories. Partial startup, Docker inspection/removal failures or identity mismatch retain recovery receipts. Update injected startup/collision tests for the fully owned game container; exercise the new runtime and rerun all counts2..25 after storage isolation. No public endpoint or system trust-store change.

Storage-isolated implementation received scoped hostile approval99/99, including an independent rerun of eight startup/failure/collision contracts. The monitored14-player disk-backed run eventually passed all112UI solves, with155 mixed failed requests and5canonical uncertain retries. Its complete failure total and explicit100-detail truncation flag remain retained. No change to database pool size, fsync, synchronous_commit,20-second chooser assertion or solve/ranking criteria was made. The new storage-isolated matrix remains pending; its volatile storage is not production durability evidence. WAL sizing is a soft target (PostgreSQL17 documentation); usage/error artifacts capture the hard tmpfs capacity separately.

Historical heartbeat response-body repair plan

An independent real-Chromium loopback diagnostic confirmed a client lifecycle defect: a completed23-byte200 JSON response left unread emitted requestfailed ERR_ABORTED at the AbortSignal deadline in3/3trials, while consuming the body emitted requestfinished with no failures in3/3controls. RoomSession's presence path currently leaves its response body unread. This explains that mechanism, not every prior failure. The first storage-isolated25-player game failed round1 after18solves; that gap remains open and its249failed-request total/100-detail cap are retained.

Consume the presence response body before clearing its canonical retry record and starting the authorized refresh. Preserve the same pending command if reading the body fails. Keep interval timing, authority, deadline behavior and every game assertion unchanged. First write a deferred-body React regression through the actual RoomSession to prove body consumption and retained retry identity. Add an actual-browser integration against the real runtime: unread session-response negative control, consumed control, and the production online-triggered presence request must finish before its10-second abort deadline, without a test-side response-body read or HTTP fixtures. Add a focused harness selector for this regression. Build/verify after the red test, then rerun heartbeat, strict live game and all2..25UI cases. No source pool/durability or timeout relaxations.

Clone this wiki locally