feat(call-to-play): retain terminal outcomes
Keep complete running and cancelled histories visible for fifteen minutes so peers retain the roster, chat, and outcome long enough to understand what happened. Compact them to terminal tombstones afterward without charging settled calls against the active-history limit. Model running and cancelled as durable read-only frontend states, exclude them from active badges, prune retired raw events, and document the lifecycle. Add peer scenario S49 to prove a late joiner reconstructs a terminal call with its roster and chat intact. Test Plan: - just fmt - just clippy - just test - just frontend-test - just build - just peer-cli-tests S48 S49 - python3 -m py_compile crates/lanspread-peer-cli/scripts/run_extended_scenarios.py - git diff --cached --check
This commit is contained in:
+29
-17
@@ -98,7 +98,7 @@ The default screen. A grid of game cards over a dark, gradient-tinted background
|
||||
- **Right zone (col 3, flex space-between with two sub-groups):**
|
||||
- **Sort menu** (pinned left, hugging search) — 36 px button, same surface style as search. Label `Sort: <bold value>` plus 13 px sort-bars icon and 11 px chevron. Click reveals dropdown menu below. Options: `Name (A–Z)`, `Size (largest)`, `Recently Played`, `Status`. This is the only thing on the *left* side of the right zone — it's part of the search cluster, so it hugs the search.
|
||||
- **Kebab menu** (`⋮`, pinned far-right) — 36×36 button with same surface as search. Menu items: `Settings` (opens Settings dialog), `Refresh library`, separator, `Unpack logs`, `About SoftLAN`. This is the only "app-level" control left in the top bar; the game-folder picker has moved into Settings.
|
||||
- **Call to Play button** (`.ctp-btn`, sits just left of the kebab in the far-right sub-group) — 36 px pill, flag icon + `Call to Play` label. Carries an accent-filled **badge** with the count of active (non-started) calls. Opens the Call to Play overlay. See the **"Call to Play"** section for the full feature. In variant B it lives in row 1's right group, between the storage meter and the kebab.
|
||||
- **Call to Play button** (`.ctp-btn`, sits just left of the kebab in the far-right sub-group) — 36 px pill, flag icon + `Call to Play` label. Carries an accent-filled **badge** with the count of active, non-terminal calls. Opens the Call to Play overlay. See the **"Call to Play"** section for the full feature. In variant B it lives in row 1's right group, between the storage meter and the kebab.
|
||||
|
||||
**Narrow-window fallback** (container width < 1100 px): the grid is replaced by a single `display: flex; flex-wrap: nowrap; gap: 16px` row. All items align left-to-right in source order (brand → filter → search → sort → kebab). The search field becomes `flex: 1 1 auto` so it absorbs remaining slack. The geometric centering is abandoned at narrow widths because there isn't enough horizontal slack for it to read cleanly. Implement via container query (`@container launcher (max-width: 1100px)`) on the launcher root; a viewport media query is an acceptable fallback if you're not using container queries yet.
|
||||
|
||||
@@ -466,7 +466,7 @@ the call and its history expire as a unit.
|
||||
### Three surfaces
|
||||
|
||||
1. **Top-bar button** (`CallToPlayButton` / `.ctp-btn`) — flag icon + label +
|
||||
an accent badge counting active (non-`started`) calls. Opens the overlay.
|
||||
an accent badge counting active, non-terminal calls. Opens the overlay.
|
||||
2. **Quick bars** (`CallToPlayTicker` / `.ctp-ticker-stack`) — a persistent
|
||||
stack rendered at the top of the grid area, **one row per active call**.
|
||||
Sorted **ready → starting-soon → the rest**, ties broken by whichever
|
||||
@@ -474,7 +474,8 @@ the call and its history expire as a unit.
|
||||
then by `deadline`). Clicking a row opens the overlay focused on that call.
|
||||
3. **Overlay** (`CallToPlayOverlay`) — a modal (same scrim/panel treatment as
|
||||
the other dialogs) with a header, a **Call a new match** button, the create
|
||||
form, and a list of **nomination cards** (started calls sink to the bottom).
|
||||
form, and a list of **nomination cards** (Running and Cancelled calls sink
|
||||
to the bottom).
|
||||
|
||||
### Status model
|
||||
|
||||
@@ -482,14 +483,16 @@ Two derived values drive everything (`calltoplay.jsx`):
|
||||
|
||||
- `phaseOf(call)` → `'now'` (no `scheduledFor`) · `'scheduled'` (>15 min out) ·
|
||||
`'checkin'` (within the 15-min lead).
|
||||
- `statusOf(call)` (quick-bar/label status) → `'started'` · `'expired'`
|
||||
(deadline elapsed) · `'ready'` (`readyCount >= maxPlayers`, or state `done`) ·
|
||||
`'soon'` (`deadline - now ≤ 15 min`) · `'scheduled'` (has a clock time) ·
|
||||
`'call'` (a plain play-now call).
|
||||
- `statusOf(call)` (quick-bar/label status) → `'running'` · `'cancelled'` ·
|
||||
`'expired'` (deadline elapsed) · `'ready'` (`readyCount >= maxPlayers`, or
|
||||
state `done`) · `'soon'` (`deadline - now ≤ 15 min`) · `'scheduled'` (has a
|
||||
clock time) · `'call'` (a plain play-now call).
|
||||
|
||||
Quick-bar labels + LED colors: **SCHEDULED**, **CALL TO PLAY**, **STARTING
|
||||
SOON**, **READY**, **TIME'S UP** (`TICKER_LABEL`), each with its own dot color via
|
||||
`.ctp-ticker-dot[data-status]`.
|
||||
SOON**, **READY**, **TIME'S UP**, **RUNNING**, **CANCELLED** (`TICKER_LABEL`),
|
||||
each with its own dot color via `.ctp-ticker-dot[data-status]`. Running and
|
||||
Cancelled receipts remain for 15 minutes, sort after actionable calls, and do
|
||||
not contribute to the top-bar badge.
|
||||
|
||||
**Per-participant ready state:** `ready` (explicitly readied, or their `readyAt`
|
||||
countdown has elapsed) · `in` (RSVP'd to a scheduled call but not checked in
|
||||
@@ -502,7 +505,7 @@ as larger `AvatarChip`s in the nomination card roster.
|
||||
|
||||
Top to bottom:
|
||||
|
||||
1. **Header** — square game cover + title + a sub-line: `Called by <creator> · N/M peers have it installed` (or `Scheduled by <creator> · starts at HH:MM · …`), and a **timer** on the right: a live `M:SS` countdown for play-now/check-in, the **clock time + "in N min"** for a scheduled call, `Ready`, `Time's up`, or `Launching…`. If catalog data is temporarily unavailable, the card still renders the caller, game ID, roster, chat, and coordination actions with a clear `Game unavailable here` label. Countdown urgency (`data-urgency` high/mid/low) tints it as time runs low.
|
||||
1. **Header** — square game cover + title + a sub-line: `Called by <creator> · N/M peers have it installed` (or `Scheduled by <creator> · starts at HH:MM · …`), and a **timer** on the right: a live `M:SS` countdown for play-now/check-in, the **clock time + "in N min"** for a scheduled call, `Ready`, `Time's up`, `Running`, or `Cancelled`. If catalog data is temporarily unavailable, the card still renders the caller, game ID, roster, chat, and coordination actions with a clear `Game unavailable here` label. Countdown urgency (`data-urgency` high/mid/low) tints it as time runs low.
|
||||
2. **Check-in note** — only in the `checkin` phase: a clock icon + "Starting soon — check-in is open" (or a personalized nudge if you RSVP'd).
|
||||
3. **Progress bar** — time remaining as a fill (accent, → green when done); hidden while a call is still in the far-out `scheduled` phase.
|
||||
4. **Roster** — `readyCount/maxPlayers ready` (scheduled shows `N in · up to M players`; check-in adds `· K not checked in yet`), then avatar chips for each participant plus empty slots up to `maxPlayers`.
|
||||
@@ -510,6 +513,10 @@ Top to bottom:
|
||||
6. **Chat** — the collapsible per-call chat panel.
|
||||
7. **Cancel** — creators get a `Cancel this call` link with an inline confirm.
|
||||
|
||||
Running and Cancelled cards are read-only: the complete roster and chat remain
|
||||
visible, but participant controls, creator controls, chat composition, and
|
||||
cancel actions are disabled.
|
||||
|
||||
### Create form (`CreateNominationForm`)
|
||||
|
||||
Game search (typeahead over the catalog) → on pick, max-players defaults to the
|
||||
@@ -546,8 +553,8 @@ type Nomination = {
|
||||
readyAt?: number; // ms epoch a 'pending' buffer elapses
|
||||
}>;
|
||||
messages: { id: string; fromId: string; from: string; text: string; at: number }[];
|
||||
state: 'open' | 'done' | 'started';
|
||||
startedAt?: number;
|
||||
state: 'open' | 'done' | 'running' | 'cancelled';
|
||||
terminalAt: number | null;
|
||||
};
|
||||
```
|
||||
|
||||
@@ -570,11 +577,16 @@ create, response, RSVP, chat, leave, cancel, start, or deadline-extension action
|
||||
is an immutable, uniquely identified event. Connected peers receive new events
|
||||
immediately, while `Hello` / `HelloAck` exchange the bounded, deduplicated event
|
||||
history so a late joiner reconstructs every event and chat message for active
|
||||
calls. Started and cancelled calls compact to terminal tombstones; expired
|
||||
calls are removed after the five-minute grace period. The frontend reducer
|
||||
turns that event history into the `Nomination` state above and derives
|
||||
time-based phase changes locally. Stable peer IDs identify actors and enforce
|
||||
creator controls; `settings.username` is only the display name.
|
||||
calls. Running and Cancelled calls retain their complete history for 15 minutes
|
||||
so late joiners can see the outcome, roster, and chat, then compact to a Start
|
||||
or Cancel tombstone for the rest of the peer session. Unresolved calls are
|
||||
removed after the five-minute post-deadline recovery period. The frontend
|
||||
reducer turns that event history into the `Nomination` state above, derives
|
||||
time-based phase changes locally, and prunes retired raw events. Stable peer IDs
|
||||
identify actors and enforce creator controls; `settings.username` is only the
|
||||
display name. These deadlines use event wall-clock timestamps, so LAN clocks
|
||||
are assumed to be reasonably close; no clock-synchronization protocol is
|
||||
attempted.
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user