Skip to content

v0.19.0: LAN phone remote, reliability, data-integrity, accessibility, licence notices#23

Merged
JosephMaynard merged 10 commits into
masterfrom
feat/v0.19
Jul 21, 2026
Merged

v0.19.0: LAN phone remote, reliability, data-integrity, accessibility, licence notices#23
JosephMaynard merged 10 commits into
masterfrom
feat/v0.19

Conversation

@JosephMaynard

@JosephMaynard JosephMaynard commented Jul 21, 2026

Copy link
Copy Markdown
Owner

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.

  • Controls: add/remove goals per team, start/stop the clock, advance match phase, switch the on-air screen.
  • Pairing: the laptop shows a 6-digit PIN and a QR code of the LAN URL. The phone pairs once with the PIN.
  • Security: a sibling http+ws server bound to the LAN (the browser-source server stays bound to 127.0.0.1 and is untouched). Sockets start unpaired, the PIN is compared constant-time, and pairing is rate-limited (5 failed attempts arm a 30s cooldown) so a 6-digit PIN cannot be brute forced. Commands from unpaired sockets are dropped. Nothing is exposed to the internet.
  • Commands are intents routed through the same reverse-IPC path the keyboard shortcuts already use, so the operator's store stays the single source of truth. Live state is mirrored back to the phone, so two phones stay in sync. The mobile page auto-reconnects with backoff and a heartbeat watchdog.
  • Verified end to end: pair, goals, clock, phase, screen switch, live mirror, wrong-PIN rejection, and unpaired-command rejection.

Reliability

  • Crash recovery: the launch snapshot is now preserved until the operator resolves the Restore prompt. Previously the dashboard's blank startup state could overwrite the on-disk snapshot within the throttle window, so a second crash lost the match. Verified end to end.
  • Stream Deck: redraws now paint keys sequentially and await each write, and each redraw carries a generation token so a following redraw supersedes a stale one instead of interleaving writes and leaving a half-old, half-new panel.

Data integrity

  • Runtime zod schemas now validate Scores, Time, MatchState, CustomScreen, LiveMatch, and the saved-match array at the IPC intake and at storage load, using the existing per-field graceful-degradation style (a bad field degrades to a safe default rather than throwing away the whole object).
  • Uploads are capped at 10 MB with an extension allowlist and a magic-byte sniff (SVG checked without executing it).
  • Stale custom graphics are reconciled on load: entries whose backing file is gone are dropped, and a live/saved match referencing a now-missing custom screen falls back to a built-in screen instead of a broken image.
  • Browser-source and remote-control ports are bounded to a valid TCP range (1 to 65535).

Security and build hardening

  • Removed a stray experimentalFeatures flag; added navigation and popup denial to both windows.
  • The release workflow now gates on ts-check, lint, and test, and verifies the tag matches package.json before building.
  • The packaged app now ships LICENSE, TRADEMARKS.md, and a generated THIRD_PARTY_NOTICES.md (regenerated from the production dependency tree at package time).

Accessibility

  • The root html lang attribute now follows the active UI language.
  • Segmented controls announce selected state via aria-pressed; icon-only buttons have accessible names; the additional-time input has a real label; a prefers-reduced-motion rule neutralises animation for users who ask for it.

i18n

  • Finished the v0.18 extraction (the ColorPicker key-colour warning plus two logo-alt misses), translated into all 8 catalogues, along with the new accessibility and phone-remote strings.

Notes

  • Translations remain machine-generated and are disclosed as such; issue reports welcome.
  • The mobile remote page is English-only for its few labels (the rest is icons and numbers) and offers the built-in screens, not custom uploaded graphics, in this first version.

294 tests pass; ts-check, lint, and prettier are clean on changed files.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added LAN phone remote control with PIN pairing, QR codes, live match controls, and display selection.
    • Added expanded accessibility labels, reduced-motion support, and localized interface messaging.
  • Bug Fixes
    • Improved recovery of saved matches and custom screens when data or image files are invalid or missing.
    • Strengthened image upload validation and blocked in-app navigation and pop-up windows.
    • Prevented inconsistent Stream Deck redraws during rapid updates.
  • Documentation
    • Documented phone remote setup, controls, and security details.
    • Added packaged third-party license notices.

JosephMaynard and others added 8 commits July 21, 2026 11:22
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>
@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This 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.

Changes

Application feature and hardening

Layer / File(s) Summary
LAN phone remote control
src/main-functions/*, src/main.ts, src/components/SystemSettingsMenu/*, src/types*, src/i18n/locales/*
Adds the LAN remote protocol, embedded phone UI, pairing, command routing, status IPC, QR display, and localized settings text.
Runtime validation and persistence reconciliation
src/zodSchemas.ts, src/main-functions/storage.ts, src/main-functions/customScreenReconciliation.ts, src/__tests__/*, src/main-functions/__tests__/*
Validates persisted settings, match data, custom screens, and saved matches; removes malformed or stale entries and protects restorable live-match state.
Upload and window security
src/main-functions/fileHandler.ts, src/main-functions/createAppWindow.ts, src/main-functions/__tests__/fileHandler.test.ts
Validates image size, extension, and content, while blocking in-page navigation and popup windows.
Accessibility, localization, and UI behavior
src/components/*, src/i18n/*, src/index.css, src/components/__tests__/*
Adds translated labels, language metadata synchronization, reduced-motion handling, notification dismissal callbacks, and accessibility-oriented test queries.
Device rendering and release support
src/stream-deck.ts, forge.config.ts, package.json, THIRD_PARTY_NOTICES.md, README.md, .github/workflows/release.yml
Serializes Stream Deck repainting, packages generated license notices, documents the phone remote, and verifies tagged releases before creating drafts.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 45.45% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the release-focused changes: LAN phone remote, reliability, data-integrity, accessibility, and licence notices.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/v0.19

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

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install timed out. The project may have too many dependencies for the sandbox.


Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (1)
src/main-functions/fileHandler.ts (1)

81-89: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Dead 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

📥 Commits

Reviewing files that changed from the base of the PR and between 54ec4f4 and f11ccfa.

⛔ Files ignored due to path filters (10)
  • dogfood-output/screenshots/dashboard-scoreboard-selected.png is excluded by !**/*.png
  • dogfood-output/screenshots/dashboard-scoreboard-wide.png is excluded by !**/*.png
  • dogfood-output/screenshots/dashboard.png is excluded by !**/*.png
  • dogfood-output/screenshots/initial.png is excluded by !**/*.png
  • dogfood-output/screenshots/minimum-window.png is excluded by !**/*.png
  • dogfood-output/screenshots/scoreboard-display-settled.png is excluded by !**/*.png
  • dogfood-output/screenshots/scoreboard-display-wide.png is excluded by !**/*.png
  • dogfood-output/screenshots/scoreboard-display.png is excluded by !**/*.png
  • dogfood-output/screenshots/system-settings-small.png is excluded by !**/*.png
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (59)
  • .github/workflows/release.yml
  • .prettierignore
  • README.md
  • THIRD_PARTY_NOTICES.md
  • dogfood-output/report.md
  • forge.config.ts
  • package.json
  • src/__tests__/zodSchemas.test.ts
  • src/components/AppNotification/AppNotification.tsx
  • src/components/ButtonGrid/Button.tsx
  • src/components/ColorPicker/ColorPicker.tsx
  • src/components/Dashboard/Dashboard.tsx
  • src/components/Dashboard/DashboardHeader.tsx
  • src/components/Dashboard/TimeControlPanel.tsx
  • src/components/Display/Display.tsx
  • src/components/Modal/ModalIcon.tsx
  • src/components/ScoresPanel/ScoreInput.tsx
  • src/components/SystemSettingsMenu/SystemSettingsMenu.tsx
  • src/components/TimeDisplay/TimeDisplay.tsx
  • src/components/__tests__/Dashboard.test.tsx
  • src/components/__tests__/ScoreInput.test.tsx
  • src/components/__tests__/SystemSettingsMenu.test.tsx
  • src/constants.ts
  • src/i18n/__tests__/index.test.ts
  • src/i18n/index.ts
  • src/i18n/locales/de/dashboard.json
  • src/i18n/locales/de/settings.json
  • src/i18n/locales/en/dashboard.json
  • src/i18n/locales/en/settings.json
  • src/i18n/locales/es-419/dashboard.json
  • src/i18n/locales/es-419/settings.json
  • src/i18n/locales/es-ES/dashboard.json
  • src/i18n/locales/es-ES/settings.json
  • src/i18n/locales/fr/dashboard.json
  • src/i18n/locales/fr/settings.json
  • src/i18n/locales/it/dashboard.json
  • src/i18n/locales/it/settings.json
  • src/i18n/locales/pt-BR/dashboard.json
  • src/i18n/locales/pt-BR/settings.json
  • src/i18n/locales/pt-PT/dashboard.json
  • src/i18n/locales/pt-PT/settings.json
  • src/index.css
  • src/main-functions/__tests__/customScreenReconciliation.test.ts
  • src/main-functions/__tests__/fileHandler.test.ts
  • src/main-functions/__tests__/remoteControlServer.test.ts
  • src/main-functions/__tests__/storage.test.ts
  • src/main-functions/createAppWindow.ts
  • src/main-functions/customScreenReconciliation.ts
  • src/main-functions/fileHandler.ts
  • src/main-functions/remoteControlPage.ts
  • src/main-functions/remoteControlServer.ts
  • src/main-functions/storage.ts
  • src/main.ts
  • src/preload.ts
  • src/stream-deck.ts
  • src/types.ts
  • src/types/types.d.ts
  • src/utils.ts
  • src/zodSchemas.ts

Comment thread dogfood-output/report.md Outdated
Comment on lines +3 to +4
Date: 2026-07-21
Build: v0.18.0 development build

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 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.

Comment thread forge.config.ts
Comment thread README.md Outdated
Comment thread src/components/__tests__/ScoreInput.test.tsx Outdated
Comment thread src/components/Dashboard/Dashboard.tsx
JosephMaynard and others added 2 commits July 21, 2026 14:02
- 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>
@JosephMaynard

Copy link
Copy Markdown
Owner Author

@coderabbitai review

1 similar comment
@JosephMaynard

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between f11ccfa and 70785c1.

📒 Files selected for processing (6)
  • .gitignore
  • README.md
  • forge.config.ts
  • src/components/Dashboard/Dashboard.tsx
  • src/components/__tests__/ScoreInput.test.tsx
  • src/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

Comment thread README.md

## 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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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.

@JosephMaynard
JosephMaynard merged commit 60d5a97 into master Jul 21, 2026
4 checks passed
@JosephMaynard
JosephMaynard deleted the feat/v0.19 branch July 21, 2026 14:08
JosephMaynard added a commit that referenced this pull request Jul 21, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant