# Phase 3 Report - Real-Time State and Announcements

Date: 2026-07-15
Scope: Prompt 4 / Phase 3 only.

## Summary

Implemented the Phase 3 real-time foundation without adding a frontend framework or CSS framework. The implementation keeps PHP/MySQL as the intended durable authority and keeps the Node realtime process as a fan-out gateway only.

Added:

- Shared realtime client state machine with duplicate suppression, stale-event rejection, sequence-gap recovery, polling fallback, and bounded reconnect backoff.
- Shared event-state reducer and announcement renderer.
- Node realtime gateway core using the mature `ws` library, HTTP polling fallback, authenticated wedding-scoped subscriptions, internal-only publication, heartbeat cleanup, payload limits, backpressure handling, and a guard that only publishes envelopes marked as PHP/MySQL durable events.
- PHP durable realtime event service, state refresh endpoint, polling event endpoint, and admin event publication endpoint scaffold.
- MySQL reference migration for `realtime_events` and `realtime_outbox`.
- Read-only historical viewer remains separate and shared through UI modules.
- Dependency-free Phase 3 tests wired into `npm run ci:local`.

## Files Changed

Shared protocol/core/UI/platform:

- `packages/protocol/src/index.js`
- `packages/protocol/src/types.ts`
- `packages/core/src/realtime-client.js`
- `packages/core/src/event-state.js`
- `packages/core/src/index.js`
- `packages/ui/src/announcement-view.js`
- `packages/ui/src/index.js`

Realtime gateway:

- `realtime/src/gateway.mjs`
- `realtime/src/server.mjs`
- `realtime/package.json`
- `package-lock.json`

PHP backend:

- `server/src/Services/RealtimeEventService.php`
- `server/src/Controllers/RealtimeController.php`
- `server/src/Controllers/BootstrapController.php`
- `server/public/index.php`
- `server/database/migrations/003_phase3_realtime_events.sql`

Tests/tooling/docs:

- `tests/phase3/run-phase3-tests.mjs`
- `tests/phase0/ci-local.mjs`
- `package.json`
- `docs/PHASE3_REPORT.md`

## Event Envelope

Every realtime envelope uses the required fields:

- `eventId`
- `weddingId`
- `sequence`
- `stateVersion`
- `type`
- `occurredAt`
- `payload`

The gateway also requires `authoritativeSource: "php-mysql"` before distribution. Publication is available only through `/internal/publish` with `x-internal-realtime-token`; public `/publish` returns `PUBLISH_NOT_PUBLIC`. This is an internal guard proving that the gateway cannot mint authoritative state changes by itself.

## Phase 3 Event Types

Added protocol constants for:

- `PRESENCE_UPDATED`
- `WEDDING_PAUSED`
- `WEDDING_RESUMED`
- `REPLACEMENT_PENDING`
- `ANNOUNCEMENT_STARTED`
- `ANNOUNCEMENT_ENDED`
- `NORMAL_WIN_RECORDED`
- `MADE_IN_HEAVEN_RECORDED`
- `STATE_REFRESH_REQUIRED`

## Authority Model

- PHP assigns `stateVersion` and monotonic per-wedding `sequence` in `RealtimeEventService`.
- Durable events are stored in the local FileDatabase scaffold and mirrored by the MySQL reference migration.
- `realtime_outbox` is the intended durable-to-transient bridge for Redis/gateway publication.
- The Node gateway distributes events over `ws` and polling, but rejects envelopes not marked as PHP/MySQL durable events.
- The gateway does not decide wins, authorize uploads, replace occupants, or independently change wedding state. It also rejects malformed envelopes, unknown event types, wrong wedding scopes, stale sequences, oversized payloads, and non-PHP/MySQL authoritative sources.

## Client Recovery Behavior

The shared realtime state machine:

- applies ordered envelopes;
- ignores duplicate event IDs;
- rejects stale sequence/state-version events;
- stops dependent application on sequence gaps;
- calls canonical PHP state refresh on gaps;
- falls back to HTTPS polling when WebSocket is unavailable;
- uses bounded exponential backoff with jitter for reconnect timing;
- does not replay user commands on reconnect.

## Commands Run

```sh
node --check packages/core/src/realtime-client.js
node --check realtime/src/gateway.mjs
node --check realtime/src/server.mjs
node --check realtime/src/gateway.mjs
node tests/phase3/run-phase3-tests.mjs
npm run ci:local
npm ls ws
npm audit --omit=dev
node -e "JSON.parse(...)"
php -v
composer --version
docker --version
mysql --version
redis-server --version
```

## Test Results

`node tests/phase3/run-phase3-tests.mjs` passed:

```text
ok - ordered delivery applies monotonic envelopes
ok - duplicate event IDs are suppressed
ok - sequence gap triggers PHP canonical state recovery
ok - stale events are rejected without rewinding state
ok - polling fallback applies HTTPS events when WebSocket is unavailable
ok - bounded reconnect backoff includes jitter and cap
ok - gateway fans durable PHP events to 500 connected in-memory clients
ok - gateway rejects malformed, unknown, wrong-scope, stale, oversized, and non-authoritative envelopes
ok - backpressure closes slow clients and skips delivery
ok - real ws connection requires auth and receives only subscribed wedding events
ok - public publish is rejected and internal publish requires service credential
ok - HTTP polling is authenticated and wedding scoped
metrics - ws500 delivered=500 p50=26.26ms p95=34.99ms rssDelta=16453632
ok - 500 real ws clients receive event with latency and cleanup metrics
ok - ordinary desktop guest can receive events but cannot mutate images
ok - Phase 3 PHP routes and durable-event migration exist
ok - ws library is used instead of hand-written framing
ok - all required Phase 3 event names are present in protocol constants
phase 3 tests passed
```

`npm run ci:local` passed Phase 0, Phase 1, Phase 2, and Phase 3 checks.

JSON manifest/package parsing passed.

## Measured Results

- In-memory simulated connected realtime clients: `500`
- Real `ws` connected clients: `500`
- Fan-out delivery count: `500`
- Duplicate suppression: duplicate event ID ignored without advancing sequence
- Gap recovery: sequence gap triggered canonical state refresh and updated local `sequence`/`stateVersion`
- Polling fallback: activated when `WebSocket` implementation is unavailable
- Authenticated polling: bad token rejected; correct token receives only its wedding scope
- Internal publication: public `/publish` rejected; `/internal/publish` requires service credential
- 500-client real WebSocket delivery: p50 `26.26ms`, p95 `34.99ms`, RSS delta `16453632` bytes
- Disconnect cleanup: gateway client count returned to `0`
- Reconnect backoff sample with deterministic jitter: `105ms`, `210ms`, capped at `1050ms` for the test configuration
- `ws` dependency: `ws@8.21.1`
- `npm audit --omit=dev`: `found 0 vulnerabilities`

## Failures / Blocking Validation

PHP runtime validation remains blocked in this Codex environment:

```text
php -v
zsh:1: command not found: php

composer --version
zsh:1: command not found: composer

docker --version
zsh:1: command not found: docker

mysql --version
zsh:1: command not found: mysql

redis-server --version
zsh:1: command not found: redis-server
```

Therefore, Phase 3 runtime verification is not complete by the user-defined completion standard. JavaScript and real `ws` integration tests pass, but PHP 8.3 syntax checks, Composer validation/install, PHPUnit, MySQL migrations, Redis/outbox execution, Apache/PHP HTTP endpoint exercise, and PHP desktop rejection runtime tests could not run in this environment. Before moving to Phase 4, run those checks under Docker/CI or on a host with PHP 8.3, Composer, MySQL 8, and Redis installed.

## Deferred Work

- Redis pub/sub or streams connection is represented by the durable outbox shape but not executed locally.
- MySQL persistence remains represented by reference migrations and the FileDatabase scaffold until repository-backed persistence is implemented.
- Camera capture, image upload, occupant replacement, final history exports, and AirBridge behavior were intentionally not implemented in Phase 3.

## Architecture Constraints Preserved

- HTML5/CSS3/vanilla JavaScript ES modules only.
- No React, Vue, Lit, Svelte, JSX, Bootstrap, Tailwind, jQuery, or frontend framework added.
- PWA, extension, admin, and history remain thin entry points over shared packages.
- Desktop historical viewer remains read-only.
- Ordinary desktop guests can receive/display realtime events but still cannot mutate image state.
- Authenticated administrator desktop exceptions remain server-side permission decisions.
