v0.19.0: LAN phone remote, reliability, data-integrity, accessibility, licence notices#23
Conversation
On launch the dashboard seeds the main process with its default (blank) scores, time, and match state, which the throttled writer would flush to disk within one window, overwriting the restorable snapshot. A second crash before the operator clicked Restore then lost the match. The main process now holds off writing over an unresolved launch snapshot with blank state: the first meaningful update (a goal, a running phase, a restore) unlocks persistence, and dismissing the restore prompt sends resolve-live-match so the snapshot is replaced rather than re-offered next launch. Verified end to end: seed a mid-match snapshot, launch without restoring, confirm it survives the throttle window and a simulated second crash, and that relaunch still offers it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Extracts the ColourPicker key-colour warning (and two hardcoded "PlayOverlay logo" alt strings missed in the v0.18 sweep) into the translation catalogues for all eight locales. Removes the unexplained experimentalFeatures webPreference and denies in-page navigation and window.open on every BrowserWindow, since the app never navigates and external links already go through shell.openExternal via IPC. Adds a verify job to the release workflow (ts-check, lint, test, and a tag/package.json version match check) that gates the build job, so a broken build or mistagged release can no longer reach users.
An optional, PIN-gated server that lets a phone on the same wifi control
the live match: add/remove goals, start/stop the clock, advance the match
phase, and switch the on-air screen. Off by default, bound to the local
network only, never exposed to the internet.
Server (src/main-functions/remoteControlServer.ts): a sibling to the
browser source server that binds to 0.0.0.0 so a phone can reach it,
mirroring its start/stop/{ok,error}/heartbeat/EADDRINUSE patterns. It
serves a single self-contained mobile page (remoteControlPage.ts) with no
external assets or filesystem access. WebSocket clients start unpaired and
must send the correct 6-digit PIN (constant-time compared) before any
command is honoured; pairing is rate-limited (5 failures per socket or
globally within a 30s window trips a 30s cooldown) to defeat brute forcing.
Commands are validated against an allowlist and are intents only, the app
computes the result. Pure helpers (PIN compare, command validation, LAN
IPv4 discovery) are Electron-free and unit-tested.
Main (src/main.ts): mints a fresh CSPRNG PIN on each enable, mirrors the
browser-source apply/queue lifecycle, routes commands to the control
window over reverse IPC (homeGoal/awayGoal/nextPhase reuse existing
channels; homeGoalRemove/awayGoalRemove/toggleClock/setScreen are new),
broadcasts a compact snapshot on every state change and on pairing, and
exposes get-remote-control-status plus a pushed status event for a live
connected-phone count.
Dashboard applies the new commands through the same store setters the
operator UI uses. Settings gains a Phone Remote section (toggle, port,
URL, PIN, QR code, connected count) in all eight locales. README documents
setup and the security model. Enabling the remote does not affect the
browser source or its 127.0.0.1-only binding.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…reconcile stale graphics Adds zod schemas (Scores, Time, MatchState, CustomScreen, LiveMatch, saved match settings list) for the boundaries that previously had no runtime validation, using the same per-field graceful-degradation style as matchSetingsSchema/appSettingsSchema: a bad field falls back to a safe default, and a bad entry in a list is dropped rather than discarding the whole object or list. Applied at the update-score/update-time/update-match- state IPC handlers (reject and log a wholly malformed payload, keep existing cache/broadcast behaviour for valid input) and at set-custom-screens/set- saved-match-settings (reject a non-array payload outright rather than persisting an empty list, drop only malformed entries otherwise). Storage load (getCustomScreens/getSavedMatchSettings/getLiveMatch) now validates whatever comes off disk the same way. browserSource/remoteControl ports are bounded to 1-65535 and degrade to their shipped defaults. fileHandler.saveImageFile now rejects uploads over 10 MB, uses an extension allowlist matching the browser-source server's supported image types, and sniffs the buffer's magic bytes (or, for SVG, checks it decodes as UTF-8 and contains an <svg root) before ever writing to disk. hasAllowedImageExtension and isValidImageBuffer are pure and unit tested; the existing null-rejection path in the upload UI already surfaces a translated failure message in all locales, so no UI/i18n changes were needed. Adds a pure, filesystem-injected reconciliation helper (customScreenReconciliation.ts): getCustomScreens drops custom screens whose backing file no longer exists (and persists the cleaned-up list), and getLiveMatch falls a restored matchState back to the scoreBug screen when its selected custom screen or an overlay no longer exists, instead of rendering a broken image. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ton labels, reduced motion Covers five gaps: document.documentElement.lang now tracks the active i18next language on both windows (set on init, kept current via a languageChanged listener); ButtonGrid's Button gets aria-pressed whenever it receives a selected prop, so every segmented control built on it (phase/screen/language/timer-mode pickers) announces its selected state; six previously nameless icon-only buttons (score +/-, the Adjust Time pencil, the additional-time clear icon, pause/resume, and the display window's fullscreen toggle) get an aria-label, reusing existing translation keys where the concept already exists and adding new ones (scoreInput.decreaseScoreAria/increaseScoreAria, timeControl.pause/ resume/additionalTimeLabel) across all 8 locales otherwise; the additional-time input gets a proper (visually hidden) label instead of relying on the modal title; and a prefers-reduced-motion media query in index.css neutralises animation/transition durations globally, covering Headless UI's Transition components and Tailwind's transition utilities in one place. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The deck is repainted whenever the phase, match settings, or button set changes, and those can fire in quick succession. Two problems made a redraw race the device: - the key paint loop was a forEach(async ...), so every fillKeyCanvas write fired concurrently and connectToStreamDeck resolved before any of them landed. It now paints sequentially and awaits each write. - a following redraw could start while a previous one was still writing, interleaving their writes and leaving a half-old, half-new panel. Each redraw now captures a generation token and bails the moment a newer redraw supersedes it (after acquiring the device, and between key writes), so only the latest redraw ever touches the panel. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Ship LICENSE, TRADEMARKS.md, and a generated THIRD_PARTY_NOTICES.md as packaged resources so an installed copy carries its own licensing, not just the source repo. Adds generate-license-file (dev) plus a generate:licenses script, and a forge generateAssets hook that regenerates the notices from the current production dependency tree before packaging so they never drift (falling back to the committed copy if the generator is unavailable). Bumps version to 0.19.0. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
📝 WalkthroughWalkthroughThis release adds a LAN phone remote with PIN pairing, WebSocket commands, QR setup, and live state updates. It also adds runtime validation and persistence reconciliation, upload and window hardening, accessibility and localization updates, Stream Deck repaint sequencing, license packaging, and release verification. ChangesApplication feature and hardening
Estimated code review effort: 4 (Complex) | ~60 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
ESLint install timed out. The project may have too many dependencies for the sandbox. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (1)
src/main-functions/fileHandler.ts (1)
81-89: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueDead
try/catch:Buffer.toString('utf8')never throws.Node's
Buffer.toString('utf8')replaces invalid byte sequences with U+FFFD rather than throwing, so this branch is unreachable. Harmless, but the comment/try implies a real failure mode that doesn't exist.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/main-functions/fileHandler.ts` around lines 81 - 89, Remove the unnecessary try/catch and temporary assignment in looksLikeSvg, converting the buffer directly to UTF-8 text before applying the existing SVG regex. Preserve the current detection behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@dogfood-output/report.md`:
- Around line 3-4: Regenerate dogfood-output/report.md using the v0.19.0 release
artifact and update its Date and Build metadata accordingly, replacing the
current v0.18.0 development-build entry.
In `@forge.config.ts`:
- Around line 29-40: Update the generateAssets hook to distinguish local
development from CI or release packaging: retain the committed
THIRD_PARTY_NOTICES.md fallback only locally, but rethrow the generation error
in CI/release contexts so packaging fails instead of shipping stale notices. Use
the existing generateAssets catch block and environment/configuration signals
already available in forge.config.ts.
In `@README.md`:
- Around line 119-134: Update both references to the phone and laptop being on
the “same wifi” in the Phone Remote instructions to say they must be on the same
local network, including the parenthetical connection requirement and the
pairing step.
In `@src/components/__tests__/ScoreInput.test.tsx`:
- Around line 45-54: Update the test sequence around the score increment in the
ScoreInput interaction test to rerender the component with score={2} after
asserting the increment callback, before clicking “Decrease Tigers score.” Keep
the decrement assertion and existing interaction flow unchanged so it verifies
decrementing from the updated controlled value.
In `@src/components/Dashboard/Dashboard.tsx`:
- Around line 350-366: Update decrementHomeTeamScore and decrementAwayTeamScore
to call setRestorableMatch(null) whenever the phone-remote score removal changes
the score, matching incrementHomeTeamScore, incrementAwayTeamScore, and
ScoresPanel manual edits so stale restore data cannot overwrite the correction.
---
Nitpick comments:
In `@src/main-functions/fileHandler.ts`:
- Around line 81-89: Remove the unnecessary try/catch and temporary assignment
in looksLikeSvg, converting the buffer directly to UTF-8 text before applying
the existing SVG regex. Preserve the current detection behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 1ac8b546-f959-4962-b662-4d0409808a22
⛔ Files ignored due to path filters (10)
dogfood-output/screenshots/dashboard-scoreboard-selected.pngis excluded by!**/*.pngdogfood-output/screenshots/dashboard-scoreboard-wide.pngis excluded by!**/*.pngdogfood-output/screenshots/dashboard.pngis excluded by!**/*.pngdogfood-output/screenshots/initial.pngis excluded by!**/*.pngdogfood-output/screenshots/minimum-window.pngis excluded by!**/*.pngdogfood-output/screenshots/scoreboard-display-settled.pngis excluded by!**/*.pngdogfood-output/screenshots/scoreboard-display-wide.pngis excluded by!**/*.pngdogfood-output/screenshots/scoreboard-display.pngis excluded by!**/*.pngdogfood-output/screenshots/system-settings-small.pngis excluded by!**/*.pngpackage-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (59)
.github/workflows/release.yml.prettierignoreREADME.mdTHIRD_PARTY_NOTICES.mddogfood-output/report.mdforge.config.tspackage.jsonsrc/__tests__/zodSchemas.test.tssrc/components/AppNotification/AppNotification.tsxsrc/components/ButtonGrid/Button.tsxsrc/components/ColorPicker/ColorPicker.tsxsrc/components/Dashboard/Dashboard.tsxsrc/components/Dashboard/DashboardHeader.tsxsrc/components/Dashboard/TimeControlPanel.tsxsrc/components/Display/Display.tsxsrc/components/Modal/ModalIcon.tsxsrc/components/ScoresPanel/ScoreInput.tsxsrc/components/SystemSettingsMenu/SystemSettingsMenu.tsxsrc/components/TimeDisplay/TimeDisplay.tsxsrc/components/__tests__/Dashboard.test.tsxsrc/components/__tests__/ScoreInput.test.tsxsrc/components/__tests__/SystemSettingsMenu.test.tsxsrc/constants.tssrc/i18n/__tests__/index.test.tssrc/i18n/index.tssrc/i18n/locales/de/dashboard.jsonsrc/i18n/locales/de/settings.jsonsrc/i18n/locales/en/dashboard.jsonsrc/i18n/locales/en/settings.jsonsrc/i18n/locales/es-419/dashboard.jsonsrc/i18n/locales/es-419/settings.jsonsrc/i18n/locales/es-ES/dashboard.jsonsrc/i18n/locales/es-ES/settings.jsonsrc/i18n/locales/fr/dashboard.jsonsrc/i18n/locales/fr/settings.jsonsrc/i18n/locales/it/dashboard.jsonsrc/i18n/locales/it/settings.jsonsrc/i18n/locales/pt-BR/dashboard.jsonsrc/i18n/locales/pt-BR/settings.jsonsrc/i18n/locales/pt-PT/dashboard.jsonsrc/i18n/locales/pt-PT/settings.jsonsrc/index.csssrc/main-functions/__tests__/customScreenReconciliation.test.tssrc/main-functions/__tests__/fileHandler.test.tssrc/main-functions/__tests__/remoteControlServer.test.tssrc/main-functions/__tests__/storage.test.tssrc/main-functions/createAppWindow.tssrc/main-functions/customScreenReconciliation.tssrc/main-functions/fileHandler.tssrc/main-functions/remoteControlPage.tssrc/main-functions/remoteControlServer.tssrc/main-functions/storage.tssrc/main.tssrc/preload.tssrc/stream-deck.tssrc/types.tssrc/types/types.d.tssrc/utils.tssrc/zodSchemas.ts
| Date: 2026-07-21 | ||
| Build: v0.18.0 development build |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Regenerate this report against v0.19.0.
This report identifies the tested build as v0.18.0, so it does not evidence validation of the v0.19.0 release artifact. Re-run dogfooding after the version bump or clearly label it as historical.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@dogfood-output/report.md` around lines 3 - 4, Regenerate
dogfood-output/report.md using the v0.19.0 release artifact and update its Date
and Build metadata accordingly, replacing the current v0.18.0 development-build
entry.
- Dashboard: the phone-remote score-removal handlers now clear a pending restore prompt, like the increment handlers and manual edits, so a stale restore snapshot cannot overwrite a correction made from the phone. - ScoreInput test: rerender with the applied increment before decrementing, so it proves the decrease works off the updated controlled value (1 to 2 to 1) rather than always off the original prop. - README: the phone remote now says "same local network (usually the same wifi)" instead of "same wifi", since the two devices only need to share a LAN (the laptop could be on ethernet). - forge: the notices-regeneration hook now rethrows in CI so a release build fails rather than silently shipping stale THIRD_PARTY_NOTICES; local packaging still falls back to the committed copy. - fileHandler: drop the dead try/catch in looksLikeSvg (Buffer.toString never throws). - Remove dogfood-output/, a local dogfooding artifact accidentally swept in by git add -A, and gitignore it so it cannot return. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
# Conflicts: # README.md
|
@coderabbitai review |
1 similar comment
|
@coderabbitai review |
✅ Action performedReview finished.
|
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@README.md`:
- Line 140: Update the language-availability sentence in the README to
explicitly scope the listed translations to the desktop interface and on-air
graphics, and state that the LAN phone remote supports English only. Preserve
the surrounding language-selection and user-entered text behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: ff7497f4-8dbb-415f-bb2a-ad8b1af2c467
📒 Files selected for processing (6)
.gitignoreREADME.mdforge.config.tssrc/components/Dashboard/Dashboard.tsxsrc/components/__tests__/ScoreInput.test.tsxsrc/main-functions/fileHandler.ts
🚧 Files skipped from review as they are similar to previous changes (4)
- forge.config.ts
- src/components/tests/ScoreInput.test.tsx
- src/main-functions/fileHandler.ts
- src/components/Dashboard/Dashboard.tsx
|
|
||
| ## Languages | ||
|
|
||
| The interface and the on-screen graphics are available in English, French, German, Italian, Spanish (Spain and Latin America), and Portuguese (Portugal and Brazil). On first launch PlayOverlay suggests a language based on your system and asks you to confirm it or pick another; if you dismiss the prompt without choosing, it stays unset and asks again next time. You can change the language whenever you like under **System Settings → Language**, and it applies to both the operator dashboard and the on-air graphics. Text you type yourself is never translated: team names, abbreviations, venues, and the titles of saved match settings and custom screens are always shown exactly as you enter them. |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Clarify that the phone remote is English-only.
This sentence can imply that the LAN phone remote is available in all listed languages, but the PR objective states that the mobile remote supports English only. Explicitly limit the language claim to the desktop interface and on-air graphics.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@README.md` at line 140, Update the language-availability sentence in the
README to explicitly scope the listed translations to the desktop interface and
on-air graphics, and state that the LAN phone remote supports English only.
Preserve the surrounding language-selection and user-entered text behavior.
…ht (#24) * Note the phone remote is English-only in the languages section The listed translations cover the desktop interface and on-air graphics; the LAN phone remote control page is English only for now. Addresses the remaining CodeRabbit comment from #23. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Add multi-step undo/redo for live match state (v0.20) Introduce an app-wide, PER-SLICE undo/redo history over the three stores that make up the on-air state (scores, time, matchState). Each action records and restores only the slice(s) it actually changed, which is a hard correctness requirement on air: the commonest undo, a score correction, must never touch the clock. If an operator misclicks a goal at 37:12 and presses Ctrl+Z at 42:00, the clock stays at 42:00. Restoring the time slice (and re-anchoring the running clock) happens only when the action moved the clock. Store (src/store/undo.ts): - captureUndo(label, slices) snapshots ONLY the given slices; the entry records which slices it holds. Pushes it, clears redo, caps at 50 (drops oldest). - undo()/redo() restore ONLY the entry's slices and run the clock re-sync ONLY when the time slice is part of the restore. - Calls the real store setters directly (never the wrapped action handlers), so a restore flows out to the display, browser source, and phone through the normal pipeline and never self-captures. - Snapshots are deep-cloned on capture and on restore so live mutations can never alias a stored snapshot; restores build full replacement objects so stale optional fields cannot survive the setters' merge. Slice assignment: - scores only: goals (add/remove, all sources), manual score edit, penalty scored/missed/reset (penalties live in scores). - time only: pause, resume, adjust time, additional time. - matchState only: screen switch. - time + matchState: next phase, and starting/stopping a phase (these set the clock AND the phase/previous-phase/auto-switched screen together, so undoing one correctly restores both). Clock re-sync: useMatchClock gains resyncToTime, registered with the undo store, which re-anchors the system-clock-driven timer to the restored value and keeps ticking (or stays paused) to match the restored state. Penalty reconciliation: penalty edits go through the shared stack and the shootout panel's "Undo Penalty" button delegates to the global undo, so it and Ctrl/Cmd+Z can never disagree. UI: undo/redo icon buttons in the desktop rail and mobile top bar, divided from the settings icons, genuinely disabled when their stack is empty, with accessible names naming the specific action. Shortcuts: renderer-level Ctrl/Cmd+Z and Ctrl/Cmd+Shift+Z, separate from the main-process accelerators, bailing when focus is in a text-entry element so native text undo is left alone. New undo i18n namespace added to all eight locale catalogues. Tests cover the per-slice stack logic (capture/undo/redo, multi-step, redo invalidation, empty no-op, deep snapshots, cap, per-slice isolation), the critical regression that a score-only undo leaves the clock untouched, that undoing a phase change restores both phase and clock, and the header button enable/disable/labelling. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Add persistent logging and a one-click diagnostics export Routes main-process console.error/warn through a new durable logger (src/main-functions/logger.ts): a size-capped, rotating text log under userData/logs/main.log plus in-memory ring buffers for the log tail, operator-action events, and recent failed operations. Every recorded undo/redo action (the captureUndo choke point) now mirrors to the main process over a new log-match-event channel, tagged with its source and timestamp, without changing what undo captures or how it restores. Adds an ipcMain export-diagnostics handler that assembles a privacy conscious support bundle (versions, OS info, sanitized app/match settings, server statuses, recent log/events/failures) as a single text file via a save dialog, explicitly excluding the phone-remote PIN and any token. System Settings gets a new "Export diagnostics" panel, translated into all 8 locales. * Add a "Go live check" preflight diagnostic before kickoff Gives the non-technical operator one button that reports whether they're ready to go live: second display present and separated from the control window, output window loaded, OBS browser source / phone remote actually running when enabled, missing team logos or custom screen/overlay files, keyboard shortcuts that failed to register, low disk space, and any unresolved crash-recovery snapshot. Read-only throughout, it only gathers and reports, it never moves a window, starts a server, or touches match state. The evaluation logic lives in a pure, Electron-free module (main-functions/preflight.ts) so it unit-tests without a running app; the signal gathering (which screen a window is on, service status, disk space via fs.statfs, missing files) lives in main.ts and degrades gracefully when a signal can't be determined. Custom screen/overlay reconciliation reuses the existing storage.ts logic; team logos get the same kind of check for the first time. New run-preflight IPC handler, preload/type additions, and a "Go live check" button on the dashboard next to the display controls that opens a modal listing every check with an ok/ warning/error indicator and an overall summary, re-run on every open. New preflight.json i18n namespace shipped in all eight locales. * Route the on-screen scored button through the goal handler The big "Team Scored" button called setScore(score + 1) directly, which took the generic score-edit undo label ("Undo score change") and read the possibly stale score prop rather than fresh store state. Adding a goal from the keyboard, Stream Deck, or phone already went through the goal handler. The button now does too, so it reads "Undo home goal" like the other paths, records a homeGoal/awayGoal match event, and can never clobber a concurrent update (e.g. a goal added from a phone) by acting on a stale prop. The pencil-edit modal stays a genuine score edit. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Bump version to 0.20.0 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Address CodeRabbit review of PR #24 Fixes five issues flagged in review: the penalty panel's undo button now reads "Undo Last Action" instead of "Undo Penalty" (it delegates to the shared global undo stack, so it can undo any last action); the durable log now redacts logged file paths to their base name and strips control characters before writing rejected uploads/deletions and dropped custom-screen/overlay paths, so a shared diagnostics export never leaks the operator's home directory or username; the "Go live check" preflight now uses a read-only custom-screen reconciliation instead of one that persists the cleaned-up list, so running the check can never mutate saved configuration; and an unrecognised (but present) match-event source is now recorded as an explicit 'unknown' instead of being silently misattributed to 'laptop'. Also applies four related nitpicks: extracts the duplicated MM:SS-to-seconds parsing in useMatchClock into a shared helper, adds focused resyncToTime tests with fake timers, has diagnostics.ts reuse logger.ts's formatLogLine instead of a duplicate local formatter, and has initLogger absorb the pre-init fallback logger's buffered entries so early log lines survive into the initialised logger's diagnostics export. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Refresh README screenshots for the current UI Recaptured the dashboard (now showing the undo/redo rail and go-live check), the score bug, and the spectator scoreboard against the v0.20 interface, replacing the stale pre-i18n captures. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Strengthen useMatchClock resync tests per CodeRabbit Seed the time store with the restored snapshot before resyncing so the active and paused cases assert the phase is preserved after ticking, and add a regression test that resyncing to a paused snapshot clears a previously running interval (the clock stays put and the store is not advanced). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
v0.19.0
This release adds the LAN phone remote and a batch of reliability, security, and accessibility hardening. It also carries the MIT relicence (merged from #22) so the release ships as MIT with separate trademark terms, which is what enables free Windows code signing.
Everything here is on this single branch to keep it to one review.
New feature: LAN phone remote (opt-in)
A phone on the same local network can control the live match. Off by default; enabled in System Settings.
Reliability
Data integrity
Security and build hardening
Accessibility
i18n
Notes
294 tests pass; ts-check, lint, and prettier are clean on changed files.
🤖 Generated with Claude Code
Summary by CodeRabbit