Add planning poker: real-time estimation, on the landing page and the shared create flow - #40
Merged
Conversation
Add the OpenSpec change artifacts for a controller-run, real-time planning poker room: Fibonacci deck with ?/infinity/coffee cards, per-item waiting -> voting -> revealed phases with hidden votes and a synchronized reveal, an agree/close/spread agreement signal, and controller-recorded final estimates persisted as a room results log. Introduces the tool's first real-time layer (a Durable Object per room over WebSocket hibernation) while leaving the async event capabilities untouched. Includes proposal, design, delta spec (11 requirements / 28 scenarios), and tasks.
Use the modified Fibonacci deck (0 1 2 3 5 8 13 20 40 100) and make the controller's optional participation as an estimator explicit across the proposal, design, and spec (adds a controller-votes scenario).
Transport-agnostic slices, fully verified: - Agreement-signal logic + deck (src/lib/logic/poker.ts) with 10 unit tests: agree/close/spread by deck-index span, infinity forces spread, coffee raises the break hint, only agree pre-fills a suggestion. - Migration 0011: poker_rooms + poker_rounds (durable skeleton + final estimates; live phase/votes are not persisted). - Row types (PokerRoomRow, PokerRoundRow, RoomStatus). - E2e seed/read helpers for rooms + rounds (e2e-poker-* family). All 124 unit tests pass, svelte-check clean, lint clean, 0011 applies. The real-time transport (Durable Object vs D1-backed live state) is the remaining architectural decision and is deferred to the next step.
Adopt the D1-backed real-time transport (short-polled state endpoint) over a Durable Object, per the decision that adapter-cloudflare can't cleanly export a DO. Realign proposal/design/spec to match. - Migration 0011 extended: room phase + active_round_id + rev counter, poker_participants (heartbeat-presence roster), poker_votes (active item's transient votes). - Live-state row types (RoomPhase, ParticipantRole, PokerParticipantRow, PokerVoteRow) + card text encode/decode helpers. - pokerProvider (src/lib/data/poker.ts): self-contained D1 provider for room create, token resolution, roster/votes/results reads, and the vote + controller commands (open/reveal/revote/finalize/close), each bumping rev; vote lands only while phase='voting' (guarded in SQL). - buildSnapshot (src/lib/logic/poker-snapshot.ts): the single vote- privacy enforcement point, with tests proving no card leaks before reveal, own-vote echo, presence window, and the revealed signal. 128 unit tests pass, svelte-check + lint clean, 0011 applies fresh.
Full D1-backed real-time flow, verified end-to-end against the real Worker + local D1 (create -> open -> join -> vote -> reveal -> finalize -> close), including the privacy gate and control-auth: - Endpoints: GET /poker/api/[token]/state (viewer snapshot + heartbeat), POST /poker/api/[token]/command (join/vote/heartbeat/leave + controller open/reveal/revote/finalize/close). Token resolves to room+authority; control actions are controller-only (403 otherwise); unknown token 404s; closed room refuses mutations. - Pages: /poker/new (create), /poker/c/[token] (controller console), /poker/j/[token] (participant join + vote). noindex. - Client poll loop (RoomClient, ~1s) applying each command's fresh snapshot; graceful leave on pagehide. - Components: Card, Deck, Roster (presence + synchronized reveal flip), Signal (agree/close/spread strip + distribution + break hint). - Server helpers: token->room resolution, per-room pid cookie, snapshot assembly. Observer/estimator + phase enforced in SQL on castVote. - 49 poker message keys across all five locales (no em-dashes). svelte-check 0 errors/0 warnings, lint clean, 128 unit tests pass.
- Playwright spec (openspec/specs/planning-poker/planning-poker.spec.ts):
11 tests over two/three browser contexts against the real Worker +
local D1, polling the live state endpoint. Covers create->console,
live join/roster, refresh resumes seat, closed room, open voting live,
participant cannot drive phases, votes hidden until reveal + flip,
agree + record + persist, spread, infinity forces spread, close. All
11 pass; realtime flows get a 90s per-test timeout.
- DESIGN.md: the planning-poker rules (fixed blue accent, card deck,
face-down back, synchronized reveal flip, roster, agreement signal).
- Fixes found by the e2e run:
- wipeRoom NULLs active_round_id first (rooms<->rounds FK cycle).
- Drop the pagehide auto-leave: it fired on reload and dropped the
seat, breaking "refresh resumes the same seat"; presence window now
handles departure.
- Roster shows present seats only, so a departed seat is removed.
- tasks.md rewritten to the shipped D1-backed design; boxes checked.
svelte-check 0/0, lint clean, 128 unit tests, poker e2e 11/11.
- Create openspec/specs/planning-poker/spec.md (11 requirements / 29 scenarios), the source-of-truth capability spec, colocated with its Playwright suite. - Archive openspec/changes/add-planning-poker -> archive/2026-07-24-add-planning-poker.
- Landing page leads its body with a planning-poker section: new badge, one-tap reveal example, own call-to-action. - Room creation moves onto /create behind a "what are you making" choice; /poker/new removed. - Rooms reach parity with polls: organizer-picked highlighter, language fixed at creation, optional email on the create form (room link now, results summary on close). - Reveal is held until every present estimator has voted; the recorded estimate is limited to the range actually voted. - Enter submits the room's text fields; a closed room stops offering its join link. - Heartbeat write throttled to 5s against the 15s presence window.
malpou
force-pushed
the
claude/session-gel5nv
branch
from
July 25, 2026 08:46
6aa3d34 to
f1ffc9f
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Every capability the tool ships today is an asynchronous poll. Agile teams have a synchronous need: estimating a backlog together, live, one item at a time, with everyone revealing at once so nobody anchors on the loudest voice. No async poll type can express that.
This adds a planning-poker room — a controller-run, real-time estimation space — and then makes it a real part of the product: discoverable from the landing page, created from the same page as polls, and at feature parity with them.
What's in it
The capability
poll_type— planning poker shares no data with the events model, so it lives in its own tables and routes. A room has no options, invitees, timezone, answering mode, or choice toggles; the only field it shares with a poll is a title.waiting → voting → revealed. Votes stay hidden during voting (only who voted shows), then flip face-up together on reveal.0 1 2 3 5 8 13 20 40 100plus ? / ∞ / ☕.agree(all equal → pre-fills the suggestion) /close(one adjacent step) /spread(more than one step apart, any ∞, or no numeric votes). ∞ forces a spread; ☕ raises an advisory break hint.Discovery and creation
/createopens by asking what you're making. Picking a room reduces the form to a room name; picking a poll leaves the existing form untouched. Two form actions rather than one branching action, so neither validator knows about the other's fields./poker/newremoved. It was never released, linked, or indexed, so there is no link in the wild to preserve.noindexwith every other token page.Feature parity with polls
Fixes found by using it
Transport decision (documented in the change's design.md)
The original design proposed a Durable Object + WebSocket push. Mid-implementation this hit a concrete constraint:
@sveltejs/adapter-cloudflareexports only its own worker'sdefaultand overwritesmainon every build, so exporting a DO class fights the adapter and the single-worker e2e harness. Live session state stays in D1 (room phase, heartbeat-presence roster, per-participant votes) with clients short-polling a JSON state endpoint (~1s).Costing the loop afterwards: a six-person, one-hour room is ~21,600 requests and ~21,600 D1 writes, which sits inside the Workers paid plan's included allowances for several hundred sessions a month. A Durable Object would cut message volume ~100×, but the case for it is write throughput and push latency, not cost — D1 is a single SQLite writer shared with the whole poll product. As a cheap mitigation the heartbeat write is now throttled to once per 5s per seat against a 15s presence window, cutting poker's D1 writes ~5× with no behavior change. The data model already captures everything push would, so a DO upgrade needs no schema change.
Security
buildSnapshot, and unit-tested.castVoteis guarded in SQL (voting phase + registered estimator), so observers and non-joined callers cannot vote.Spec and design invariants
openspec/specs/planning-poker/spec.mdandopenspec/specs/landing-page/spec.mdare synced as the source of truth, each colocated with its Playwright suite.openspec/specs/DESIGN.mdupdated: rooms take an organizer-picked highlighter (was a fixed blue), plus the landing section's rules, badge, and reveal example.openspec/specs/PROJECT.mdpreviously had no mention of planning poker at all — it now documents the capability's purpose, token kinds, data model, routes, and the D1-versus-Durable-Object note.openspec/changes/archive/2026-07-24-add-planning-pokerand.../2026-07-25-unify-poker-creation.Verification
bun run check→ 0 errors / 0 warningsbun run lint→ cleanbun run test→ 141 unit tests (deck, agreement signal, estimate range, reveal gating, snapshot privacy, email composition)bun run test:e2e→ 235 passed, full suite against the real Worker + local D1Migration and deploy
Four additive D1 migrations, nothing to backfill:
0011_planning_poker.sql— the capability's tables0012_poker_room_email.sql— the controller's stored address0013_poker_room_locale.sql— the room's language0014_poker_room_accent.sql— the room's highlighterNo new binding, no new secret, no
wrangler.tomlchange.Important
Planning poker has never been released.
mainhas no poker files, so production currently serves 404 for every/pokerURL, and the remote database has never had any of these migrations applied. The landing page now advertises the tool, so the deploy order matters: merge →wrangler d1 migrations apply family-date-poll --remote→ deploy. Shipping the landing page without the migrations would advertise a tool that 404s.Notes for reviewers
playwright.config.tssetsreuseExistingServer: !CI, which will silently reuse a stale Worker left on :8787 by a killed run. Worth knowing before debugging a confusing local failure.