← Back
Changelog
All versions
v2.0.5 (current)
- Security: mask pre-reveal vote values in usersUpdate; raw votes now travel only through revealVotes and private selfState sync
- Fix: duplicate usernames no longer corrupt outlier highlighting or round reveal audit votes — outliers use clientId and audit stores vote records
- Fix: rejected votes roll optimistic card selection back to the last server-confirmed vote state
- Fix: host voting participation can no longer change mid-round after votes, countdown, or reveal state is active
- Fix: join-as-spectator can no longer fail mid-round — isSpectator sent in join payload instead of redundant post-join emit
- Security: upgrade Starlette to 0.49.x for CVE-2025-54121 and CVE-2025-62727 fixes
- Security: upgrade vendored Socket.IO client to 4.8.3 to avoid the vulnerable ws dependency chain
- Packages: upgrade FastAPI, Pydantic, pydantic-core, Uvicorn, python-socketio, python-engineio, idna, click, ESLint, Prettier, Ruff, and Black pins
- Tooling: move no-control-regex disable directly above CONTROL_CHARS_RE so npm run lint passes
- Tooling: format README.md with Prettier so npm run format passes
- Tooling: add requirements-dev.txt and document Ruff/Python check commands in README
v2.0.4
- Accessibility: #countdown marked aria-live=assertive and aria-atomic=true — screen readers announce the countdown
- Accessibility: #modalBackdrop dialog gets aria-labelledby=modalMessage — screen readers name the dialog
- UX: deckSelector re-enabled after vote reveal — host can change deck for next round without waiting for usersUpdate
- UX: confetti canvas deduplicated — rapid consecutive consensus rounds no longer stack canvases and double RAF cost
- UX: isSpectator persisted to sessionStorage+localStorage — spectator preference restored on re-login after tab close
- UX: toast when another user renames — usersUpdate handler compares previous vs new username
- Fix: vote rate-limit now emits actionFailed toast instead of silently dropping the vote
v2.0.3
- Fix: showModal clones confirm/cancel buttons on entry — stacked modals no longer accumulate duplicate click handlers
- Fix: setVotingEnabled only emitted on creator flow — reconnecting host no longer spuriously resets revealed flag
- Security: POST /create CSRF check uses scheme+netloc equality instead of startswith — prefix-bypass attack closed
- Security: style-src 'unsafe-inline' removed from CSP — runtime JS uses element.style.setProperty, not inline style blocks
- Security: socket rate-limit eviction threshold raised to 2 h — covers the 3600 s requestNewRound window
v2.0.2
- Fix: votes cast after reveal are now silently rejected — post-reveal vote no longer corrupts vote map for next round
- Fix: countdown clears countdownActive after emitting revealVotes, not before — prevents requestNewRound from racing in and resetting votes mid-reveal
- Fix: reconnect token stored only after session existence confirmed — joins to expired sessions no longer accumulate stale token entries
v2.0.1
- Fix: join modal spectator toggle aligned with username input — width constrained to match text field
- Fix: socket disconnected on joinFailed — prevents sleeping browser tabs from retrying indefinitely
v2.0.0
- Refactor: server.py split into app/ package (config, logging_setup, state, rate_limit, core, routes, sockets)
- Refactor: index.js split into 8 ES modules (state, connection, cards, ui, host, modal, toast, utils)
- Refactor: buildJoinPayload() helper in state.js centralises join payload — eliminates 5 duplicated call sites across connection.js, host.js, index.js
- Refactor: socket_rate_limits warnings added for all rate-limited events (vote, changeDeck, hostVotingDecision, setSpectator, setVotingEnabled)
- Refactor: _OUTLIER_STEP_THRESHOLD moved to module level; variable renames for clarity (cancelled_session, sorted_indices, existing_user, voting_participants, deck_val, vote_val)
- Refactor: IP captured before socket_ip_map.pop in disconnect; ip field added to user_disconnected audit and join rate-limit log
- Refactor: dead theme_config/theme_config_mtime removed from state.py; routes.py maintains its own private cache
- Refactor: _RID_RE renamed _REQUEST_ID_RE to match SESSION_ID_RE/CLIENT_ID_RE convention; exc_info=True on all exception handlers in routes.py
- Refactor: _quote_val parameters renamed value/text/char; CONTROL_CHARS_RE in utils.js extended to match server-side invisible Unicode ranges
- Refactor: userList alias removed from ui.js; changelog tooltip v/c renamed versionKey/changelogItem; realVotes duplicate removed from index.js
- Refactor: welcome.js version tooltip variable names renamed — v→versionKey, c→changelogItem
- Security: reconnect tokens — server issues token_urlsafe(32) on first join; rejoin requires matching token, preventing clientId impersonation
- Security: POST /create checks Origin/Referer against CORS_ORIGINS when not wildcard — prevents CSRF session creation
- Security: socket rate limits keyed by IP instead of SID — reconnection no longer resets the window
- Security: TRUSTED_PROXY_IPS env var — XFF only trusted when TCP peer is in the configured allowlist
- Security: socket_rate_limits uses LRU ordering — attackers can no longer flood new keys to evict legitimate entries
- Security: _CONTROL_CHARS_RE extended to strip zero-width joiners, soft hyphens, bidi marks, and invisible Unicode — prevents homograph usernames
- Security: modal innerHTML replaced with textContent by default; allowHtml=true required for trusted static strings
- Security: X-Request-ID header sanitised — non-alphanumeric characters stripped, capped at 32 chars to prevent log injection
- Security: audit() text-mode values quoted when containing spaces, equals, or backslash — prevents log field injection
- Security: /health and /metrics require Authorization: Bearer <METRICS_TOKEN> when METRICS_TOKEN env var is set
- Security: /healthz public liveness probe added — safe for load-balancer checks without exposing sensitive data
- Security: max_http_buffer_size lowered from 1 MB to 64 KB — reduces memory amplification from oversized Socket.IO frames
- Security: Socket.IO AsyncServer now receives cors_credentials=ALLOW_CREDENTIALS explicitly
- Security: TRUSTED_PROXY_IPS peer check also applied in Socket.IO connect handler
- Security: app JS files served with Cache-Control: no-cache — prevents stale cached JS after updates
- Security: CORS allow_headers tightened from wildcard to Content-Type + X-Request-ID
- Fix: usersUpdate and revealVotes payloads emit list of user objects instead of SID-keyed dict — socket IDs no longer leak to all clients
- Fix: duplicate userLeft/hostLeft events on rapid disconnect-reconnect-disconnect cycle eliminated via _pending_leave_tasks dedup
- Fix: countdown_active gauge stuck at 1 when requestNewRound cancels countdown task — try/except CancelledError + finally
- Fix: float votes normalised to int before storage; bool inputs and non-finite floats rejected at socket boundary
- Fix: changeDeck and join store list() copy of deck preset instead of shared reference
- Fix: voteChanged flag cleared on requestNewRound so first vote of new round is not flagged as a change
- Fix: userJoined no longer sends clientId to room; skip_sid prevents self-toast without clientId comparison
- Fix: theme decorations (crown, santa hat) missing on cached reload — applyTheme now defers to DOMContentLoaded so logo element is present
- Fix: CORS_ORIGINS missing from routes.py import — NameError on POST /create
- Fix: spectator status in join payload locked during active rounds — prevents mid-round un-spectate via reconnect
- UX: spectate toggle in join-name modal (default off) — users choose before entering session
- UX: first visit to any session always shows join modal, even with cached username; reconnect token gates auto-rejoin
- UX: toast when another user switches to spectating or rejoins voting
- Feature: /decks endpoint — deck presets served from server, fetched on load (config in one place)
- Feature: THEME_TZ env var — timezone-aware theme date matching via zoneinfo
- Feature: gameplay metrics counters — pokering_votes_total, pokering_reveals_total, pokering_countdown_active in /metrics
- Storage: sessionStorage/localStorage keys migrated from jiraPoker* to pokering* (one-time on load)
- DX: index.html switched to ES module script tag; eslint updated to sourceType: module
v1.5.3
- UX: pencil icon on your own user-list row — click to rename yourself mid-session
- UX: rename blocked while you have voted or during countdown
- UX: toast confirms the new name after rename
- UX: removed redundant 'Welcome, <name>' heading — name visible in user list
- UX: deck selector now visible to all users (disabled for guests) so everyone sees which deck is active
- UX: session creators see one combined modal (username + host settings) instead of two separate steps
v1.5.2
- Security: welcome-page version tooltip now HTML-escapes changelog items (XSS fix)
- Security: modal username prefill no longer interpolated into HTML attribute
- Correctness: client username regex synced to server (unicode letters/digits/spaces/-'_, max 30)
- Audit: setVotingEnabled + hostVotingDecision events now include IP
- Audit: audit() helper prefixes LogRecord-reserved keys with 'x_' so JSON mode preserves them
- Cleanup: dropped unused /version/full endpoint and unread cleanup counters
- Cleanup: /metrics switched from HTMLResponse to PlainTextResponse
- Cleanup: logging.warning fallbacks routed through configured logger
- Cleanup: inline HTML styles replaced with utility classes (.hidden, .controls-row, .emph-warning)
- Cleanup: innerText replaced with textContent (avoids reflow)
- Cleanup: cached myUser lookup — no more per-event Object.values().find() scans
v1.5.1
- Vote: click a different card before reveal to change your vote (one change per round)
- Vote: unselected cards dimmed after first pick; yellow dashed outline on hover signals swap available
- Vote: after vote change, all cards lock — second change rejected by server with actionFailed toast
- Vote: ↻ indicator next to username in user list flags users who changed their vote this round
- Vote: toast broadcasts to every user when someone changes their vote
- State: voteChanged flag preserved across reconnects + reset on new round (server-authoritative)
- UX: number-key shortcuts now trigger swap on already-voted card (blocked after change used)
v1.5.0
- Spectator: any user can toggle spectate on/off via always-visible button (was host-only)
- Spectator: toggle gated identically to deck change — disabled mid-round, during countdown/reveal
- Spectator: setSpectator socket event with rate-limit, server-side gate, actionFailed on reject
- Spectator: isSpectator preserved across reconnects so F5 doesn't silently opt back into voting
- Spectator: spectators excluded from voter count and auto-reveal trigger (unified with host opt-out)
- Audit: user_spectator_toggled event logs username, clientId, previous + new state
v1.4.8
- UX: browser tab title reflects live vote progress (`N/M voted — Pokering Points`)
- UX: tab title switches to `Votes revealed` on reveal, restores default on round reset
- UX: number keys 1-9 select corresponding card during active voting
- UX: Enter triggers new round for host after votes revealed
- UX: keyboard shortcuts ignored while modals open or form fields focused
v1.4.7
- Accessibility: `#status` marked `aria-live=polite` — screen readers announce vote progress
- Accessibility: focus trapped inside modals (username, host settings, confirms); Escape closes cancellable modals
- Accessibility: vote cards rendered as native `<button>` with `aria-label`, keyboard-focusable, Enter/Space activate
- Accessibility: koningsdag `--text-secondary` darkened to #222 (was #FFF5E6) — WCAG AA on orange primary-bg
- Performance: diff-based `userVoted` event replaces full `usersUpdate` on each vote — patch-only on clients
- Performance: confetti rewritten with canvas + requestAnimationFrame (was 80 DOM nodes + CSS animation)
- Performance: `/theme` response cached in localStorage for 1 hour — skips network on repeat page loads
- Performance: vendored `socket.io.min.js` served with `Cache-Control: public, max-age=31536000, immutable`
v1.4.6
- Audit: structured `audit()` event helper + LOG_FORMAT=json for SIEM/aggregator-friendly output
- Audit: vote_cast / vote_changed log every vote with username, clientId, value, prior value, IP
- Audit: round_revealed logs vote map, average, median, outliers, and new consensus flag
- Audit: countdown_started / countdown_cancelled, round_started (was 'new round') with round counter
- Audit: user_joined with role (host/user/spectator), user_reconnected distinct from user_joined
- Audit: user_disconnected (immediate) and user_left (after 2s grace) as distinct events
- Audit: session_ended on cleanup with reason, duration, round_count, total_votes
- Audit: host actions logged — deck_changed (with IP), host_voting_decision, voting_locked/unlocked
- State: per-session roundCount + totalVotes counters for end-of-session summary
- State: isSpectator flag tracked server-side (enforcement lands with Phase 8 Spectator Mode UI)
v1.4.5
- CI: GitHub Actions workflow runs ruff + black (Python) and prettier + eslint (JS) on every push/PR to main
- Tooling: pyproject.toml with ruff (isort, bugbear, pyupgrade) + black (line-length 100)
- Tooling: package.json with eslint flat config + prettier (dev-only, no build step)
- Formatting: normalized all JS/CSS/HTML/MD via prettier (2-space indent, single quotes, trailing commas)
- Formatting: normalized Python via black + ruff --fix (import sort, trailing newlines)
v1.4.4
- Correctness: late joiners at reveal state now receive revealVotes + user chips; cards disabled
- Reliability: graceful shutdown broadcasts serverShutdown event before closing sockets
- Reliability: /health now reports per-background-task liveness (session/rate-limit/log cleanup)
- Observability: X-Request-ID middleware with contextvars-backed trace ID in every log line
- UI: global error boundary surfaces uncaught errors + unhandled rejections via toast
- Build: /changelog.html now server-rendered from version.py (single source of truth)
- Dependencies: requirements.txt tilde-pinned (~=X.Y.Z) for reproducible installs
- Tooling: .editorconfig for consistent indentation across editors
- Documentation: README badges, architecture mermaid diagram, polished language
v1.4.3
- Correctness: actionFailed event for non-join rejections (host-only, rate-limit) — no redirect loop
- Correctness: countdown state synced to mid-countdown joiners (rejoin shows remaining time)
- Correctness: countdownTask stored + cancelled defensively on new round
- Correctness: revealed flag reset defensively in changeDeck / setVotingEnabled
- Correctness: last_create_time prune cutoff 30min → 1min (matches 3s cooldown)
- Correctness: upper-median documented for even vote counts (ordinal-safe, no interpolation)
- Correctness: requestNewRound rate-limit comment fixed (was '3/hr', actual 30/hr)
- Cleanup: removed unused DECK_VALUE_MIN/MAX and DECK_SIZE_MIN/MAX constants
v1.4.2
- Security: self-hosted socket.io client, dropped jsdelivr from CSP
- Security: connect-src CSP narrowed in production (no localhost origins)
- Security: fail-loud on CORS_ORIGINS=* with credentials in production
- Security: removed deprecated X-XSS-Protection header
- Security: session ID now uses full token_urlsafe(12) length (dropped redundant slice)
- Security: username regex broadened to unicode letters/digits/spaces with control-char stripping
- Security: Socket.IO cors_allowed_origins now locked to CORS_ORIGINS
- Security: /create switched from GET to POST (303 redirect) to avoid prefetch state change
- Security: join handler no longer reads client-supplied asgi.scope for IP — uses server-stored map
- PROXY_DEPTH env var for multi-proxy X-Forwarded-For parsing (default last-hop)
- Rate-limit tracking dicts bounded via MAX_RATE_LIMIT_ENTRIES with oldest-first eviction
- LOG_RETENTION_DAYS env var (default 30) with background cleanup of rotated log files
- Version badge styles moved from inline <style> block to public/css/version-badge.css
v1.4.1
- Clickable version badge opens full changelog page in new tab
- Full version history: changelogs for all versions from 1.0.0 to current
- Rate limit IP whitelist via RATE_LIMIT_WHITELIST environment variable
- Socket reconnect: auto-rejoin with preserved vote state, connection status indicator
- Toast notifications: join, leave, deck change, connection restored, link copied
- Deck change toast shows friendly deck name
- User list revamp: voted/pending/spectator states, vote count, live status row
- Playing-card redesign: corner values, gradient depth, hover/select lift
- Revealed vote cards: bigger, corner values, outlier glow, names wrap below card
- Stat tiles: Average, Median, Votes count, Consensus celebration or Outlier count
- Card flip animation on reveal; confetti on unanimous consensus
- User leave grace period (2s) prevents false leaves on reconnect
v1.4.0
- Host session settings modal: sliders for join voting and start voting enabled
- Host can lock/unlock voting from the game UI (only when no votes cast)
- Locked voting: cards disabled with lock indicator for all users
- Post-reveal lock scheduling: host can schedule lock change for next round
- All host controls disabled during countdown to prevent race conditions
- New Session button redesigned from link to button with disabled state
- In-place re-vote: Start New Round resets votes without redirect or new session
- Host vote decision now scoped per-session to prevent modal skip across sessions
v1.3.2
- Reliability: reduce create session cooldown from 10s to 3s
- Reliability: increase new round limit from 3/hour to 30/hour
- Dependencies: update all packages to latest, remove unused shortuuid
v1.3.1
- Version tooltip now shows last 2 versions' changelogs
- Fixed changelog not appearing on game page
v1.3.0
- Koningsdag theme: crown decoration, falling Dutch flags, orange color scheme
- Audit logging with file rotation (5MB, 3 backups)
- Username persistence via localStorage with modal prefill
- Deck presets: Fibonacci, Hours, T-shirt sizes with host-only switching
- Host-left notification: overlay when host disconnects
- Version tooltip: hover version badge to see changelog
- Security: crypto-secure client IDs, removed CSP unsafe-inline
- Security: proxy-aware IP detection, timezone-aware rate limiting
- Correctness: session ID message, noscript text, outlier colors, default deck
v1.2.0
- Christmas theme with configurable decorations and snow particles
- Monitoring endpoints and Socket.IO rate limiting
- Performance and accessibility improvements
v1.1.0
- Fibonacci sequence as default voting deck
- Outlier detection: flag votes by Fibonacci step-distance from median
- Reuse client deck selection across rounds
- 16-character cryptographically secure session IDs
- Per-IP rate limiting and input validation
- Resource limits: max sessions, users per session, username length
v1.0.0
- Initial release: real-time planning poker with FastAPI + Socket.IO
- Host controls: exclude self from voting, request new round
- Question mark vote option for abstaining
- Vote average and outlier highlighting after reveal
- User list with live presence tracking
- Session timeouts with automatic cleanup
- Basic per-IP rate limiting for session creation
- Username limited to 20 characters