docs: design LAN presentation synchronization
This commit is contained in:
@@ -492,9 +492,11 @@ Implementation:
|
|||||||
[`presentation lifecycle story expansion plan`](historical/superpowers/plans/2026-07-09-presentation-lifecycle-story-expansion.md).
|
[`presentation lifecycle story expansion plan`](historical/superpowers/plans/2026-07-09-presentation-lifecycle-story-expansion.md).
|
||||||
- Evidence assets and rehearsal timing: prepare fallback screenshots/recordings,
|
- Evidence assets and rehearsal timing: prepare fallback screenshots/recordings,
|
||||||
expected run states, and a timed walkthrough checklist for a 15-minute defense.
|
expected run states, and a timed walkthrough checklist for a 15-minute defense.
|
||||||
- Presenter companion feasibility: decide whether phone/laptop control is local
|
- Active design: LAN presentation synchronization pairs `/present` and
|
||||||
only, same-network, or out-of-network; defer implementation until the core
|
`/presenter` through an ephemeral bidirectional room with uniform code/QR/URL
|
||||||
presentation is stable.
|
pairing, revision-ordered location updates, reconnect behavior, and explicit
|
||||||
|
presenter termination. Public-internet TLS is not part of the first slice:
|
||||||
|
[`LAN presentation synchronization`](superpowers/specs/2026-07-14-lan-presentation-sync-design.md).
|
||||||
|
|
||||||
Boundaries: this is not a production admin panel, generic visual workflow
|
Boundaries: this is not a production admin panel, generic visual workflow
|
||||||
editor, scheduler, external Google Drive/mail integration, or benchmark evidence
|
editor, scheduler, external Google Drive/mail integration, or benchmark evidence
|
||||||
|
|||||||
@@ -0,0 +1,250 @@
|
|||||||
|
# LAN Presentation Synchronization Design
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Approved for implementation planning on 2026-07-14.
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
The audience deck at `/present` and the presenter reader at `/presenter` can
|
||||||
|
currently navigate the same storyboard, but they do so independently. During a
|
||||||
|
defense, the presenter reader should run on a phone and control the audience
|
||||||
|
deck on a laptop. Navigation performed directly on the laptop must also update
|
||||||
|
the phone.
|
||||||
|
|
||||||
|
This design adds an ephemeral, bidirectional synchronization channel through
|
||||||
|
`@lda/web-server`. The first version is for a trusted local network. It does not
|
||||||
|
implement TLS or claim secure public-internet operation.
|
||||||
|
|
||||||
|
## Existing Controls
|
||||||
|
|
||||||
|
The audience route currently supports:
|
||||||
|
|
||||||
|
- `Space` or `ArrowRight` for the next beat;
|
||||||
|
- `ArrowLeft` for the previous beat;
|
||||||
|
- `Escape` for the top overlay;
|
||||||
|
- direct hash navigation, discussion branches, and figure focus paths; and
|
||||||
|
- local mouse interaction with figures, evidence, forms, and questions.
|
||||||
|
|
||||||
|
The presenter route currently supports Previous and Next links, arrow keys,
|
||||||
|
mobile swipe gestures, sidebar navigation, Q&A navigation, and direct hashes.
|
||||||
|
|
||||||
|
Synchronization must reuse these controls and the existing canonical hashes.
|
||||||
|
It must not create a second storyboard or navigation implementation.
|
||||||
|
|
||||||
|
## Goals
|
||||||
|
|
||||||
|
- Pair `/present` and `/presenter` from either route.
|
||||||
|
- Allow either client to change the shared presentation location.
|
||||||
|
- Reflect laptop navigation on the phone and phone navigation on the laptop.
|
||||||
|
- Preserve standalone behavior before pairing and after disconnection.
|
||||||
|
- Support reconnection to a short-lived in-memory session.
|
||||||
|
- Expose an explicit session termination action from `/presenter`.
|
||||||
|
- Keep the protocol small enough to test as a state machine.
|
||||||
|
|
||||||
|
## Non-Goals
|
||||||
|
|
||||||
|
- Public-internet deployment, TLS termination, or production authentication.
|
||||||
|
- Remote workflow execution, approval submission, evidence controls, or notes
|
||||||
|
editing.
|
||||||
|
- Persisting sessions across server restarts.
|
||||||
|
- Synchronizing scroll position, open disclosure widgets, timers, or local form
|
||||||
|
drafts.
|
||||||
|
- Replacing hashes with server-owned slide identifiers.
|
||||||
|
|
||||||
|
## Pairing Experience
|
||||||
|
|
||||||
|
Both routes use the same compact **Pair presentation** panel.
|
||||||
|
|
||||||
|
### Start A Session
|
||||||
|
|
||||||
|
Pressing **Start session** creates an in-memory room initialized from the
|
||||||
|
creator's current canonical hash. The panel displays:
|
||||||
|
|
||||||
|
- a short, case-insensitive join code;
|
||||||
|
- a QR code;
|
||||||
|
- a copyable join URL; and
|
||||||
|
- a waiting or connected status.
|
||||||
|
|
||||||
|
If the creator is on `/present`, the generated link opens `/presenter`. If the
|
||||||
|
creator is on `/presenter`, it opens `/present`. This makes pairing symmetric
|
||||||
|
without requiring the phone to be the permanent transport host.
|
||||||
|
|
||||||
|
### Join A Session
|
||||||
|
|
||||||
|
Either route can enter the short code in the same panel. A QR or URL carries
|
||||||
|
the same code and skips manual entry. After joining, the new client immediately
|
||||||
|
receives the latest accepted location and revision.
|
||||||
|
|
||||||
|
The expanded pairing panel then collapses into a small status surface showing
|
||||||
|
the connection state and peer presence. `/presenter` additionally exposes an
|
||||||
|
**End presentation** action behind a confirmation step. Any connected presenter
|
||||||
|
client may end the trusted-LAN session. Closing one browser only disconnects
|
||||||
|
that client and does not immediately terminate the room.
|
||||||
|
|
||||||
|
## Authority And Data Flow
|
||||||
|
|
||||||
|
The server is authoritative only for the latest accepted synchronization
|
||||||
|
snapshot. The clients remain authoritative for navigation semantics.
|
||||||
|
|
||||||
|
1. A local control computes its next location using the existing route logic.
|
||||||
|
2. The client updates its local route and publishes the canonical hash with its
|
||||||
|
current base revision.
|
||||||
|
3. The server accepts one update for that revision, increments the revision,
|
||||||
|
and broadcasts the resulting snapshot.
|
||||||
|
4. Peers apply the snapshot through their existing hash/reducer path.
|
||||||
|
5. Applying a remote snapshot does not republish the same change.
|
||||||
|
|
||||||
|
The synchronized value is the complete canonical hash, including scene, beat,
|
||||||
|
discussion branch, and optional figure focus path. This lets laptop-only
|
||||||
|
interactions update the phone without adding dedicated protocol messages for
|
||||||
|
each interaction.
|
||||||
|
|
||||||
|
When two clients publish from the same base revision, the first accepted update
|
||||||
|
wins. The stale client receives the current snapshot and converges instead of
|
||||||
|
overwriting newer state.
|
||||||
|
|
||||||
|
## Server Module
|
||||||
|
|
||||||
|
Add a focused `presentation-sync` module to `@lda/web-server`. Its public
|
||||||
|
boundary owns:
|
||||||
|
|
||||||
|
- room creation and code lookup;
|
||||||
|
- join-token validation;
|
||||||
|
- WebSocket membership;
|
||||||
|
- the current hash and monotonic revision;
|
||||||
|
- presenter and audience presence;
|
||||||
|
- explicit termination; and
|
||||||
|
- inactivity cleanup.
|
||||||
|
|
||||||
|
The room store is in memory. A room remains reconnectable for ten minutes after
|
||||||
|
all clients disconnect and expires after two hours without activity. A server
|
||||||
|
restart ends every room.
|
||||||
|
|
||||||
|
The module must not import console storyboard data. It validates message shape,
|
||||||
|
hash length, allowed hash prefixes, revision ordering, and payload bounds only.
|
||||||
|
|
||||||
|
## Protocol
|
||||||
|
|
||||||
|
Session setup uses ordinary JSON HTTP endpoints. Live synchronization uses one
|
||||||
|
WebSocket per client.
|
||||||
|
|
||||||
|
Client messages:
|
||||||
|
|
||||||
|
- `location.publish`: canonical hash, base revision, and client message ID;
|
||||||
|
- `session.end`: presenter-requested termination; and
|
||||||
|
- `ping`: bounded liveness signal when needed by the runtime.
|
||||||
|
|
||||||
|
Server messages:
|
||||||
|
|
||||||
|
- `location.snapshot`: accepted hash, revision, and originating message ID;
|
||||||
|
- `presence.snapshot`: connected presenter and audience counts;
|
||||||
|
- `location.rejected`: stale revision plus the current snapshot;
|
||||||
|
- `session.ended`: explicit or expired termination; and
|
||||||
|
- `protocol.error`: bounded, user-safe error information.
|
||||||
|
|
||||||
|
Every message is schema-decoded before use. Unknown message variants and
|
||||||
|
oversized payloads close or reject the offending connection without affecting
|
||||||
|
the room.
|
||||||
|
|
||||||
|
## Client Integration
|
||||||
|
|
||||||
|
A source-owned presentation synchronization controller is shared by `/present`
|
||||||
|
and `/presenter`. It exposes a small state model:
|
||||||
|
|
||||||
|
- standalone;
|
||||||
|
- creating;
|
||||||
|
- waiting;
|
||||||
|
- joining;
|
||||||
|
- connected;
|
||||||
|
- reconnecting;
|
||||||
|
- failed; and
|
||||||
|
- ended.
|
||||||
|
|
||||||
|
The controller owns HTTP setup, WebSocket lifecycle, revision tracking,
|
||||||
|
remote-application suppression, and presence. Route components provide the
|
||||||
|
current canonical hash and a callback that applies a remote hash. Pairing UI
|
||||||
|
does not know storyboard semantics.
|
||||||
|
|
||||||
|
Local navigation remains usable while disconnected. On reconnection, the
|
||||||
|
server snapshot wins. The UI must state this clearly rather than silently
|
||||||
|
merging divergent histories.
|
||||||
|
|
||||||
|
## LAN Operation
|
||||||
|
|
||||||
|
For rehearsal, build the console and expose the Hono server on the laptop's LAN
|
||||||
|
interface:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
pnpm --dir web build
|
||||||
|
$env:WEB_HOST = "0.0.0.0"
|
||||||
|
pnpm --dir web start
|
||||||
|
```
|
||||||
|
|
||||||
|
Both devices then use `http://<laptop-lan-ip>:8787`. The existing workflow RPC
|
||||||
|
server may remain loopback-only because browser workflow calls continue through
|
||||||
|
the Hono proxy running on the laptop.
|
||||||
|
|
||||||
|
The join code is protection against accidental room crossover, not
|
||||||
|
authentication against hostile network users. Internet exposure is deferred to
|
||||||
|
infrastructure such as a trusted tunnel or reverse proxy and is not part of
|
||||||
|
this implementation.
|
||||||
|
|
||||||
|
## Failure Behavior
|
||||||
|
|
||||||
|
- An invalid or expired code leaves the client standalone and shows a compact
|
||||||
|
retryable error.
|
||||||
|
- A dropped WebSocket enters reconnecting state with bounded backoff.
|
||||||
|
- The room survives a temporary client disconnect.
|
||||||
|
- A stale location publish is rejected and replaced by the latest snapshot.
|
||||||
|
- Explicit termination moves every client to ended state and stops automatic
|
||||||
|
reconnection.
|
||||||
|
- Server restart or room expiry requires a new pairing session.
|
||||||
|
- Local navigation never depends on synchronization availability.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
### Pure State Tests
|
||||||
|
|
||||||
|
- room creation, code uniqueness, and initial location;
|
||||||
|
- monotonic revision acceptance and stale-update rejection;
|
||||||
|
- presence accounting, disconnect grace, expiry, and termination; and
|
||||||
|
- bounded protocol decoding.
|
||||||
|
|
||||||
|
### Server Tests
|
||||||
|
|
||||||
|
- create and join endpoint success and failure;
|
||||||
|
- two WebSocket clients receiving the initial snapshot;
|
||||||
|
- presenter-to-audience and audience-to-presenter location propagation;
|
||||||
|
- stale publish convergence;
|
||||||
|
- reconnect snapshot recovery; and
|
||||||
|
- presenter termination broadcasting to all clients.
|
||||||
|
|
||||||
|
### Console Tests
|
||||||
|
|
||||||
|
- uniform pairing panel on both routes;
|
||||||
|
- code, QR target, URL, waiting, connected, reconnecting, and ended states;
|
||||||
|
- local hash changes publishing once;
|
||||||
|
- remote hashes applying without feedback loops;
|
||||||
|
- presenter controls updating the audience route;
|
||||||
|
- audience keyboard/hash changes updating presenter notes; and
|
||||||
|
- standalone navigation when the server is unavailable.
|
||||||
|
|
||||||
|
### Browser Smoke
|
||||||
|
|
||||||
|
Use two browser contexts against the LAN-style server:
|
||||||
|
|
||||||
|
1. Create on `/presenter`, join `/present`, and navigate in both directions.
|
||||||
|
2. Create on `/present`, join `/presenter`, and repeat the same checks.
|
||||||
|
3. Reload one client and verify it receives the latest snapshot.
|
||||||
|
4. End from `/presenter` and verify both clients leave synchronized mode.
|
||||||
|
|
||||||
|
## Success Criteria
|
||||||
|
|
||||||
|
- A phone running `/presenter` can move the audience deck backward and forward.
|
||||||
|
- Laptop navigation updates the phone to the same scene and beat.
|
||||||
|
- Pairing can begin from either route using code, QR, or URL.
|
||||||
|
- Both clients converge after reconnecting or racing one update.
|
||||||
|
- Ending from `/presenter` terminates the shared room.
|
||||||
|
- Losing synchronization never breaks local presentation navigation.
|
||||||
|
|
||||||
Reference in New Issue
Block a user