Skip to content

Comments

Multiple users logged in#128

Merged
sindrig merged 55 commits intomasterfrom
multiple-users-logged-in
Feb 18, 2026
Merged

Multiple users logged in#128
sindrig merged 55 commits intomasterfrom
multiple-users-logged-in

Conversation

@sindrig
Copy link
Owner

@sindrig sindrig commented Feb 10, 2026

Firebase State Hardening

Summary

This PR hardens the Firebase-only state architecture following the Redux → Context migration. It addresses race conditions, type safety issues, and adds comprehensive guards to prevent state corruption in multi-controller scenarios.

Problem

After migrating from Redux to React Context with Firebase as the single source of truth, several edge cases could still cause state corruption:

  1. Pre-hydration clobber: A component triggering an update before receiving the first Firebase snapshot could overwrite remote state with default values
  2. Stale-ref write races: Rapid sequential actions (e.g., clicking +1 goal twice quickly) could compute from stale refs and lose updates
  3. Empty listenPrefix writes: Writing to states//... creates invalid Firebase paths
  4. Unsafe snapshot parsing: 20+ lint errors from untyped Firebase data casting
  5. Multi-controller last-write-wins: Undocumented behavior when multiple controllers connect

Solution

Phase 1: Critical Guards

  • Added hydration tracking (isMatchHydrated, isControllerHydrated, isViewHydrated) to FirebaseStateContext.tsx
  • Reset hydration on listenPrefix change
  • Blocked all apply* functions when listenPrefix is empty
  • Blocked Firebase writes until hydration complete

Phase 2: Optimistic Updates

  • Made writes optimistic: update ref.current AND local state immediately before Firebase sync
  • Added .catch(console.error) to all Firebase sync calls for error visibility

Phase 3: Type Safety

  • Created firebaseParsers.ts with runtime-validated parsers for Match, Controller, View, and Locations data
  • Replaced JSON.parse(JSON.stringify(...)) with structuredClone in 3 places
  • Added error callbacks to all onValue Firebase subscriptions
  • Result: Reduced lint errors from 20 to 0

Phase 4: Performance

  • Wrapped context value in useMemo to prevent unnecessary re-renders

Phase 5: Testing

  • Created FirebaseStateContext.spec.tsx with 7 new unit tests covering:
    • Empty listenPrefix protection
    • Non-sync mode updates
    • Rapid sequential actions
    • Default state initialization

Phase 6: Documentation

  • Updated clock/AGENTS.md with:
    • Single source of truth explanation
    • Hydration guards documentation
    • Multi-controller behavior (last-write-wins semantics)

Files Changed

New Files

  • clock/src/contexts/firebaseParsers.ts - Type-safe Firebase snapshot parsers
  • clock/src/contexts/FirebaseStateContext.spec.tsx - Unit tests for state context

Modified Files

  • clock/src/contexts/FirebaseStateContext.tsx - Main implementation with guards, optimistic updates
  • clock/AGENTS.md - Updated architecture documentation

Supporting Files

  • .sisyphus/plans/firebase-hardening.md - Completed plan
  • .sisyphus/notepads/firebase-hardening/learnings.md - Learnings for future reference

Verification

All acceptance criteria met:

  • pnpm build passes
  • pnpm test passes (125 tests)
  • pnpm lint has 0 errors
  • ✅ Joining a running match does NOT reset the match state
  • ✅ Rapid button clicks (e.g., 2 goals quickly) are both recorded
  • ✅ Empty listenPrefix does not create invalid Firebase paths

Breaking Changes

None. All changes are backward compatible.

Testing Notes

For manual testing of multi-controller sync:

  1. Open the app in two browser windows
  2. Connect both to the same listenPrefix (e.g., "viken")
  3. Make changes in one window
  4. Verify changes appear in the other window

The hydration guards ensure that a newly-connected controller won't overwrite the existing match state.

- Migrated MatchController and TeamController from Redux to useMatch/useController hooks
- Updated TeamController to generate UUIDs for penalties locally
- Fixed type mismatches in Clock and HalfStops components
- Included pending Context migrations for AssetController and other components found in working tree to ensure build consistency
@codecov
Copy link

codecov bot commented Feb 11, 2026

Codecov Report

❌ Patch coverage is 89.66757% with 115 lines in your changes missing coverage. Please review.
✅ Project coverage is 86.86%. Comparing base (1102631) to head (630ed80).
⚠️ Report is 1 commits behind head on master.

Files with missing lines Patch % Lines
clock/src/contexts/FirebaseStateContext.tsx 89.04% 46 Missing ⚠️
clock/src/contexts/LocalStateContext.tsx 68.75% 15 Missing ⚠️
clock/src/controller/Controller.tsx 74.41% 11 Missing ⚠️
clock/src/controller/asset/team/MatchesOnPitch.tsx 79.06% 9 Missing ⚠️
clock/src/match/Clock.tsx 61.90% 8 Missing ⚠️
clock/src/match/Team.tsx 53.33% 7 Missing ⚠️
clock/src/firebase.ts 53.84% 6 Missing ⚠️
clock/src/controller/TeamSelector.tsx 69.23% 4 Missing ⚠️
clock/src/match-controller/TeamController.tsx 50.00% 3 Missing ⚠️
clock/src/controller/HalfStops.tsx 60.00% 2 Missing ⚠️
... and 4 more
Additional details and impacted files

Impacted file tree graph

@@             Coverage Diff             @@
##           master     #128       +/-   ##
===========================================
+ Coverage   52.15%   86.86%   +34.71%     
===========================================
  Files          69       33       -36     
  Lines        2161     1599      -562     
  Branches      563      467       -96     
===========================================
+ Hits         1127     1389      +262     
+ Misses       1034      210      -824     
Flag Coverage Δ
unittests 86.86% <89.66%> (+34.71%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
clock/src/constants.ts 100.00% <100.00%> (ø)
clock/src/contexts/firebaseParsers.ts 100.00% <100.00%> (ø)
clock/src/controller/MatchActionSettings.tsx 100.00% <100.00%> (+36.84%) ⬆️
clock/src/controller/asset/PlayerCard.tsx 100.00% <100.00%> (+85.00%) ⬆️
clock/src/controller/asset/team/Team.tsx 100.00% <100.00%> (+96.55%) ⬆️
clock/src/hooks/useGlobalShortcuts.ts 100.00% <100.00%> (ø)
clock/src/match-controller/MatchController.tsx 100.00% <100.00%> (ø)
clock/src/match/TimeoutClock.tsx 100.00% <100.00%> (+22.72%) ⬆️
clock/src/match/TwoMinClock.tsx 100.00% <100.00%> (+32.35%) ⬆️
clock/src/screens/ScoreBoard.tsx 65.21% <ø> (-30.34%) ⬇️
... and 15 more

... and 26 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

sindrig and others added 19 commits February 11, 2026 07:47
- Remove sync prop and hydration guards - Firebase is now single source of truth
- All state updates flow through Firebase onValue subscriptions
- Add Firebase Emulator support (VITE_USE_EMULATOR=true)
- Update CI to run e2e tests against Firebase Emulator
- Add docker-compose.yml for local emulator setup
- Update AGENTS.md with new architecture documentation
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
- TeamAssetController.spec.tsx: 49 tests covering player lineup management, axios API calls, window.confirm dialogs
- Team.spec.tsx: 28 tests covering player CRUD operations with forms and axios persistence
- MatchesOnPitch.spec.tsx: 10 tests covering match fetching with window.prompt and axios
- PlayerCard.spec.tsx: 19 tests covering canvas text measurement and player rendering

All tests use proper TypeScript typing (no eslint-disable, no 'as any').
Established patterns for axios mocks, window API spies, and canvas mocking.
@github-actions
Copy link

Deployed to staging: https://staging.irdn.is

The button was incorrectly placed next to the post-login Stjórnandi
selector in LoginPage. It belongs next to the pre-login Skjár selector
in Controller, replacing the auto-navigate onChange behavior. LoginPage
reverted to its original onChange auto-select.

Co-authored-by: Sisyphus <sisyphus@opencode.ai>
@github-actions
Copy link

Deployed to staging: https://staging.irdn.is

sindrig and others added 2 commits February 16, 2026 15:02
Display a RingLoader spinner when a screen is selected or user is
authenticated but Firebase data hasn't loaded yet. The ready state
tracks when all three onValue callbacks (match, controller, view)
have fired at least once, preventing stale default state from
flashing on page refresh.

Co-authored-by: Sisyphus <sisyphus@opencode.ai>
Test that spinner shows when Firebase state is not ready or auth
is not loaded, and that it does not show in pre-login state or
when everything is loaded.

Co-authored-by: Sisyphus <sisyphus@opencode.ai>
@github-actions
Copy link

Deployed to staging: https://staging.irdn.is

sindrig and others added 2 commits February 16, 2026 15:07
The previous commit moved RefreshHandler to App.tsx as a top-level
sibling, causing the orange button to overlap match controls and
block pointer events on the Byrja/Pása buttons, breaking e2e tests.

Co-authored-by: Sisyphus <sisyphus@opencode.ai>
@github-actions
Copy link

Deployed to staging: https://staging.irdn.is

…offset sync

- Remove compensation block that was replacing Firebase timestamps with Date.now() - 150
- Add Firebase .info/serverTimeOffset subscription to measure client-server clock skew
- Implement getServerTime() utility function using serverTimeOffsetRef
- Update all timestamp write operations (startMatch, pauseMatch, matchTimeout, buzz) to use getServerTime()
- Export getServerTime() via useMatch() hook for consumers
- Update Clock.tsx to use getServerTime() instead of Date.now()
- Update TwoMinClock.tsx to use getServerTime() for elapsed time calculation
- Update TimeoutClock.tsx to use getServerTime() for timeout duration
- Update ScoreBoard.tsx to use getServerTime() for buzzer timing
- Add getServerTime to all component dependency arrays
- Add server time offset test suite in FirebaseStateContext.spec.tsx
- Test getServerTime availability and default behavior
- Test Firebase started timestamp preservation (no Date.now() mutation)
- Test write operations use getServerTime for timestamps
- Update TimeoutClock.spec.tsx and TwoMinClock.spec.tsx for getServerTime compatibility
…imestamp

Compute countdown duration from local time (what the user/UI sees) then
place the started timestamp in the getServerTime() coordinate system so
Clock.tsx elapsed calculation (getServerTime() - started) gives the
correct negative countdown value.

Previously countdown() used moment() (Date.now) for the absolute started
timestamp while Clock.tsx read getServerTime() (Date.now + offset),
causing the countdown to instantly expire when there was any server time
offset.
@github-actions
Copy link

Deployed to staging: https://staging.irdn.is

@sindrig sindrig requested a review from Copilot February 17, 2026 09:46
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR completes the migration from Redux to a Firebase-backed React Context architecture, adding stronger guards against multi-client race conditions and improving type safety, emulator support, and test coverage.

Changes:

  • Removed Redux reducers/actions and rewired UI components to FirebaseStateContext + LocalStateContext.
  • Added runtime-validated Firebase snapshot parsers and emulator configuration for unit/e2e stability.
  • Updated Playwright + CI workflow to run against Firebase emulators and adjusted e2e helpers/tests accordingly.

Reviewed changes

Copilot reviewed 81 out of 100 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
clock/src/reducers/listeners.ts Removed Redux listeners slice in favor of context-driven Firebase state.
clock/src/reducers/listeners.spec.ts Removed unit tests for deleted Redux reducer.
clock/src/reducers/controller.ts Removed Redux controller reducer in favor of context actions/state.
clock/src/reducers/auth.ts Removed Redux auth reducer in favor of LocalStateContext.
clock/src/match/TwoMinClock.tsx Migrated penalty clock from Redux to context and server-time.
clock/src/match/TimeoutClock.tsx Migrated timeout clock from Redux to context and server-time.
clock/src/match/Team.tsx Removed Redux connector; reads match type from context.
clock/src/match/Clock.tsx Migrated main match clock to context and server-time.
clock/src/match/Clock.spec.tsx Reworked tests to render with context providers (no Redux store).
clock/src/match-controller/TeamController.tsx Removed Redux dispatch; uses context match actions.
clock/src/match-controller/MatchController.tsx Removed Redux connector; uses match/controller contexts.
clock/src/match-controller/MatchController.spec.tsx Reworked tests to use providers; no longer asserts dispatched Redux actions.
clock/src/index.tsx Removed Redux Provider/persist gate; added local+firebase context providers.
clock/src/hooks/useGlobalShortcuts.ts Added hook-based global shortcuts wired to context actions.
clock/src/hooks/useFirebaseSync.ts Removed Redux-based Firebase sync hook (now handled by context).
clock/src/firebaseDatabase.ts Switched state writes to update() and added partial sync helper.
clock/src/firebase.ts Added emulator support and Vite env-based config selection.
clock/src/controller/media/MediaManager.tsx Removed Redux connector; reads auth/prefix from local context.
clock/src/controller/media/ImageList.tsx Removed Redux dispatch; uses controller context actions.
clock/src/controller/asset/team/Team.tsx Migrated team asset editing to controller/match contexts.
clock/src/controller/asset/team/MatchesOnPitch.tsx Migrated to contexts and added local state; removed Redux wiring.
clock/src/controller/asset/team/MatchesOnPitch.spec.tsx Added unit tests with mocked context hooks and axios.
clock/src/controller/asset/team/MatchSelector.tsx Removed Redux connector; uses controller context for match selection.
clock/src/controller/asset/Substitution.tsx Removed Redux connector; reads view state from context.
clock/src/controller/asset/RemoveAssetDropzone.tsx Removed Redux dispatch; uses controller context remove action.
clock/src/controller/asset/PlayerCard.tsx Converted to functional component using view context + memoized font sizing.
clock/src/controller/asset/PlayerCard.spec.tsx Added unit tests for PlayerCard with mocked view context.
clock/src/controller/asset/AssetQueue.tsx Removed Redux connector; uses controller context for queue updates.
clock/src/controller/asset/AssetController.tsx Removed Redux wiring and legacy shortcut component usage.
clock/src/controller/asset/Asset.tsx Removed Redux wiring; uses controller/view/local auth contexts.
clock/src/controller/TeamSelector.tsx Removed Redux wiring; uses match context update.
clock/src/controller/RefreshHandler.tsx Removed Redux wiring; uses controller+local auth contexts.
clock/src/controller/RedCardManipulation.tsx Removed Redux connector; uses match context for red card updates.
clock/src/controller/PenaltiesManipulationBox.tsx Removed Redux connector; uses match context for penalties.
clock/src/controller/MatchActions.tsx Removed Redux connector; uses match/controller contexts + local prefix.
clock/src/controller/MatchActionSettings.tsx Removed Redux connector; uses match/controller/view contexts.
clock/src/controller/MatchActionSettings.spec.tsx Added unit tests with mocked context hooks.
clock/src/controller/LoginPage.tsx Simplified authenticated UI to prefix selector + logout only (no login form).
clock/src/controller/HalfStops.tsx Removed Redux wiring; uses match context setters.
clock/src/controller/Controller.tsx Refactored controller to 3-state UX using local+firebase contexts.
clock/src/controller/Controller.spec.tsx Added unit tests covering the 3 controller states via mocked hooks.
clock/src/contexts/firebaseParsers.ts Added runtime parsing/validation for Firebase snapshot data.
clock/src/contexts/LocalStateContext.tsx Added local context for auth/listenPrefix + authData subscription.
clock/src/constants.ts Centralized controller/view constants and backgrounds.
clock/src/actions/view.ts Removed Redux view actions.
clock/src/actions/remote.ts Removed Redux remote actions.
clock/src/actions/match.ts Removed Redux match actions/thunks.
clock/src/actions/global.ts Removed Redux global actions.
clock/src/actions/controller.ts Removed Redux controller actions.
clock/src/StateListener.tsx Updated connection indicator to rely on local auth context.
clock/src/GlobalShortcut.ts Removed legacy shortcut component (replaced by hook).
clock/src/GlobalShortcut.spec.tsx Removed tests for deleted shortcut component.
clock/src/App.tsx Reworked app boot flow into 3 states and added disconnect/spinner behavior.
clock/src/App.spec.tsx Added unit tests for the 3 App states and spinner behavior with mocked hooks.
clock/src/App.spec.jsx Removed old Redux-based App tests.
clock/playwright.config.ts Adjusted CI worker/webServer behavior for emulator stability.
clock/package.json Removed Redux dependencies/types, aligning deps with context architecture.
clock/e2e/penalties.spec.ts Updated e2e tests to use emulator helpers and deterministic fake clock.
clock/e2e/match-flow.spec.ts Updated e2e flow tests to use emulator helpers and deterministic fake clock.
clock/e2e/image-upload.spec.ts Updated e2e tests to use emulator helpers (removed env credential parsing).
clock/e2e/fixtures/test-helpers.ts Added emulator user/bootstrap, DB cleanup, and fake clock utilities.
clock/e2e/basic-navigation.spec.ts Updated navigation e2e to emulator helpers and deterministic fake clock.
clock/e2e/assets.spec.ts Updated assets e2e to emulator helpers and added team-view describe block.
clock/AGENTS.md Updated architectural docs for Firebase-only context model + emulator/dev notes.
FIX_CI_EMULATOR.md Added investigation notes and mitigation ideas for emulator CI flakiness.
AGENTS.md Updated repo-level docs to reflect new clock architecture/testing approach.
.github/workflows/build.yml Added emulator startup + Vite startup in CI before Playwright e2e run.
.firebaserc Added default Firebase project for emulator/CLI usage.
Files not reviewed (1)
  • clock/pnpm-lock.yaml: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

… code

Delete old unused syncState function (duplicate of syncPartialState) and rename syncPartialState to syncState. Both functions had identical update() semantics, making the old syncState dead code. The new name aligns with documentation which already referred to firebaseDatabase.syncState().

- Remove duplicate syncState function (lines 38-43)
- Rename syncPartialState → syncState in firebaseDatabase.ts
- Update 4 production call sites in FirebaseStateContext.tsx
- Update 87 test references in FirebaseStateContext.spec.tsx
- Simplify test mock to single syncState function

Diff mechanism in applyMatchUpdate depends on update() semantics (not set()).
All 540 tests pass.
…e writes

Only reconstruct the penalty array that contains the key being modified.
Previously both home2min and away2min were always filtered/mapped, creating
new references and triggering unnecessary Firebase writes via the diff mechanism.

Uses .some() check + conditional spread to only include changed arrays in the
update object, reducing penalty operation Firebase writes by ~50%.
…e writes

Add useRef guard to ensure buzz() and removeTimeout() fire exactly once when
timeout reaches 0. Previously these fired on every 100ms interval tick once the
condition was met. Latch resets when timeout changes (new timeout starts).
Add useRef guards for half-stop and countdown-end conditions to ensure
pauseMatch() and buzz() fire exactly once when their conditions are met.
Previously these fired on every 100ms interval tick once the condition was
true. Latches reset when match state changes (started/countdown toggles).

Also includes formatting fixes from prettier for previously committed files.
@github-actions
Copy link

Deployed to staging: https://staging.irdn.is

@sindrig sindrig merged commit f70fe01 into master Feb 18, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant