# Planning Poker

A small, calm planning-poker tool for backlog refinement. Built for an
internal team (~6 people, up to 15) spread across several countries.

- **No accounts, no installs.** The facilitator starts a session and shares a
  link; everyone joins with just a display name.
- **Ephemeral by design.** There is **no database**. Rooms live entirely in a
  Supabase Realtime channel; when the last person leaves, nothing remains
  anywhere. Copy the results before you close the tab.
- **Non-intimidating.** Short Fibonacci deck (1 · 2 · 3 · 5 · 8 · 13 · ? · ☕),
  votes hidden until the facilitator reveals, gentle consensus nudges,
  no leaderboards, no shaming.
- **Zero build step.** Plain HTML/CSS/JS. The Supabase client is vendored and
  pinned at `vendor/supabase.js` — at runtime the page talks to **no external
  host except your own Supabase project**.

## Setup (about 5 minutes, once)

1. Create a free project at [supabase.com](https://supabase.com) — pick a
   region near the team's middle ground (e.g. `eu-central-1`).
   You will **not** create any tables, auth, or storage.
2. In the dashboard, open **Project Settings → API** and copy the
   **Project URL** and the **anon public** key.
3. Paste both into [`config.js`](config.js).
4. Deploy (below) or run locally. Done.

### "Is the anon key safe to commit?"

Yes — the anon key is public by design; it ships to every browser that opens
the page. What it can do is defined by your project's contents and Row Level
Security. This project has **no tables, no storage and no auth**, so the key
grants nothing beyond joining realtime channels. If someone had the key they
could open a websocket to your project — the same thing the app itself does.

### Realtime settings

Nothing to configure: channels (broadcast/presence) work out of the box on
every plan. This app uses public channels — access control is the
unguessable room id in the link (12 random hex-ish chars, ~48 bits).

## Deploy

The `planning-poker/` folder is fully self-contained (design tokens and the
Supabase client are vendored inside it), so any static host can serve just
this folder — there is nothing to build.

**Cloudflare Pages (recommended):**

1. dash.cloudflare.com → *Workers & Pages → Create → Pages → Connect to
   Git* → pick this repo.
2. Build settings:
   - Framework preset: **None**
   - Build command: **leave empty** (if the form insists, use `echo ok`)
   - Build output directory: **`planning-poker`**
3. The tool lands at `https://<project>.pages.dev/` and every push to
   `main` redeploys it. Free tier, unlimited static bandwidth, private
   repos fine.

Also works on Netlify (publish directory `planning-poker`, or drag the
folder onto app.netlify.com/drop) or GitHub Pages (public repos on the
free plan; the tool is then under `/<repo>/planning-poker/`).

Hash-based routing (`#/room/…`) means no redirect/rewrite rules are needed
anywhere.

Note on `web-tokens.css`: it's a committed copy of
`../palettes/web-tokens.css` so the folder deploys standalone. If the
palette generator ever changes the tokens, re-copy the file.

## Running / testing locally

```sh
python3 -m http.server 8000
# then open http://localhost:8000/planning-poker/
```

To simulate a team, open the room link in a second browser or an incognito
window (each has its own identity). One normal + one incognito + one other
browser gives you three participants.

## How a session works

1. **Start a session** → you're the facilitator. Share the invite link
   (works across countries — it's just a URL).
2. People join with a name and an optional role
   (QA / QA Offshore / iOS / Android / Web / MDW / Scrum Master /
   Observer — Scrum Masters and observers don't vote).
3. The room shows a session title ("Backlog Refinement Session" by
   default — settable when creating the room and renameable anytime via
   the pencil icon) with today's date. There is no Jira integration, on
   purpose: the expected rhythm is *read the story in Jira → switch to
   this tab → Start voting → reveal → type the points into Jira →
   Start new voting*. Votings are labelled "User Story 1", "User
   Story 2", …; an "Add story title" button lets the facilitator
   optionally paste a ticket title at any point without resetting votes.
4. Everyone picks a card. Until the reveal, others only see a ✓ next to
   your name — never your number.
5. Facilitator reveals: named votes, average, median, and a gentle nudge
   ("Consensus — nice." / "Pretty close…" / "A range of views…").
   `?` and `☕` are never counted in the numbers.
6. **Copy results** puts a plain-text summary on the clipboard, ready to
   paste into a Jira comment. Then re-vote or move to the next story.

If the facilitator disconnects, a banner appears after a few seconds and
anyone can take over — the current story and phase are preserved.

### Scrum Master view (for screen sharing)

Joining as **Scrum Master** switches the room to a presentation board made
to be shared on a Teams call: a large story title, one big card per voter
(face-down with a ✓ while voting, flipping to the value on reveal), a large
"4 of 6 have voted" status, and big average/median numbers. The Scrum
Master doesn't vote — their screen is the table everyone looks at, while
each team member votes quietly from their own browser. Anyone can switch
the board on or off with the **Board view** button in the header.

## Honesty notes (for the security-review conversation)

- **Votes are hidden by the UI, not by cryptography.** Vote values travel in
  presence payloads before the reveal, so a determined person could open
  browser devtools and inspect websocket frames to peek early. For an
  internal, low-stakes estimation tool this trade-off buys a reveal that is
  instant and can never get stuck waiting on a flaky client. If this ever
  matters, a commit-reveal scheme is the upgrade path.
- **The facilitator role is honor-system.** There is no server to verify it;
  anyone technical could self-promote via the console. Same reasoning as
  above.
- **Room capacity (15) is enforced client-side.** A perfectly timed
  simultaneous join could briefly exceed it; harmless for a team of 6.
- **Nothing is ever stored server-side.** No tables exist; Supabase relays
  websocket messages and forgets them. There is nothing to retain, leak,
  export, or delete.

## Architecture (for maintainers)

Three files, no framework, no build:

- `index.html` — one page, three screens (landing / join / room).
- `styles.css` — layout only; every color comes from the repo's design
  tokens (`../palettes/web-tokens.css`, theme `coast`, dark mode via
  `data-mode="dark"` following `prefers-color-scheme` + a manual toggle).
- `app.js` — routing, identity, realtime protocol, rendering, stats.

**Protocol:** one Supabase Realtime channel per room
(`poker-<roomId>`); presence is the entire state store — there are no
broadcast events and no server state.

- Every participant `track()`s
  `{ v, name, role, facilitator, joinedAt, vote, voteRound }`.
- The facilitator's payload additionally carries
  `room: { title, story, phase: idle|voting|revealed, roundId, roundNum, seq }`.
  `seq` increases monotonically; clients adopt the highest seq they see, so
  refreshes/races converge. New round ⇒ new `roundId`; votes tagged with an
  old `voteRound` are ignored by every renderer, which makes round resets
  race-free.
- Late joiners need no catch-up dance: the first presence sync already
  contains the facilitator's room state and everyone's vote status.
- Facilitator identity survives refresh via a localStorage key + a cached
  copy of the room state. If no facilitator is present for ~5 s, anyone can
  take over; if two end up facilitating, the earliest joiner wins and the
  other self-demotes.

**Motion:** the reveal is a 3D card flip with a small stagger, votes pop in,
results slide up with a count-up, and joins/theme switches ease in. Two rules
keep this safe: state-critical motion is **transition-based** (a throttled or
occluded tab — exactly a screen-shared one — freezes keyframe animations on
their first frame, while transitions always land on the final state), and
keyframe entrances start legible (opacity ≥ 0.4) so a frozen first frame never
blanks content. Everything collapses under `prefers-reduced-motion`.

`vendor/supabase.js` is `@supabase/supabase-js` v2.110.2 (UMD build from
npm, unmodified). To upgrade: download the new
`package/dist/umd/supabase.js` from the npm tarball, keep the version header
comment, commit.
