Skip to content

Conversation

@arii
Copy link
Owner

@arii arii commented Feb 5, 2026

Description

This change synchronizes the heart rate monitor TTL between the server and client to 30 seconds, eliminating "ghost tiles". It introduces a reactive useNow hook to ensure the UI updates even when no network messages are received, allowing for forceful removal of stale data based on the lastUpdated timestamp. It also adds a visual warning state after 10 seconds of inactivity and simplifies the WebSocket reducer.

The motivation behind this change is to improve the user experience by preventing "ghost tiles" – instances where heart rate monitor data persists in the UI long after it should have expired. By aligning the TTL and introducing a mechanism for client-side enforcement of data freshness, the UI will accurately reflect the real-time status of heart rate monitors.

No explicit dependencies are required for this change.

Fixes #6637

Change Type: 🐛 Bug fix (non-breaking change fixing an issue)

PR Scope Checklist

This checklist is mandatory for all PRs.

  • PR has a clear, single purpose: The title and description of the PR clearly state the purpose of the change.
  • All changes relate to the stated objective: The code changes should be directly related to the purpose of the PR.
  • No unrelated cleanup or refactoring: The PR should not contain any changes that are not directly related to the stated objective.
  • Title and description match the actual changes: The title and description should accurately reflect the changes in the PR.
  • Tests cover the specific change scope: The tests should be focused on the changes in the PR and should not include unrelated tests.

Impact Assessment

  • Changes are backward compatible (or breaking changes are documented)
  • Tests are added/updated for new functionality
  • Documentation is updated if needed
  • ADR is created/updated for significant architectural changes
Original PR Body

This change synchronizes the heart rate monitor TTL between the server and client to 30 seconds, eliminating "ghost tiles". It introduces a reactive useNow hook to ensure the UI updates even when no network messages are received, allowing for forceful removal of stale data based on the lastUpdated timestamp. It also adds a visual warning state after 10 seconds of inactivity and simplifies the WebSocket reducer.

Fixes #6637


PR created automatically by Jules for task 791622865149962011 started by @arii

@google-labs-jules
Copy link
Contributor

👋 Jules, reporting for duty! I'm here to lend a hand with this pull request.

When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down.

I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job!

For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with @jules. You can find this option in the Pull Request section of your global Jules UI settings. You can always switch back!

New to Jules? Learn more at jules.google/docs.


For security, I will only act on instructions from the user who triggered this task.

@github-actions github-actions bot changed the title Fix Stale HR Tile Persistence and Align TTL feat(medium): Fix Stale HR Tile Persistence and Align TTL Feb 5, 2026
@arii
Copy link
Owner Author

arii commented Feb 5, 2026

📋 Quality Gate Results

Check Status
Knip ✅ success
Lint ❌ failure
Build ✅ success
Infra Tests ❌ skipped
Unit Tests ❌ skipped
Component Tests ❌ skipped
Perf Tests ❌ skipped
Visual Tests ❌ skipped

❌ Lint Failure Details


> hrm@0.30.0 lint /home/runner/work/hrm/hrm
> eslint app/ components/ constants/ context/ hooks/ lib/ services/ tests/ types/ utils/ server.ts proxy.ts --cache


/home/runner/work/hrm/hrm/components/HrTileWrapper.tsx
  20:25  error  Error: Cannot call impure function during render

`Date.now` is an impure function. Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent).

/home/runner/work/hrm/hrm/components/HrTileWrapper.tsx:20:25
  18 |   // every second, ensuring the stale state is kept up-to-date.
  19 |   const isDataStale =
> 20 |     user.lastUpdated && Date.now() - user.lastUpdated > HRM_WARNING_THRESHOLD_MS
     |                         ^^^^^^^^^^ Cannot call impure function
  21 |
  22 |   return (
  23 |     <HrTile  react-hooks/purity

✖ 1 problem (1 error, 0 warnings)

 ELIFECYCLE  Command failed with exit code 1.

❌ Infrastructure Test Failure Details

Log file not found.

❌ Unit Test Failure Details

Log file not found.

❌ Component Test Failure Details

Log file not found.

❌ Visual Test Failure Details

Log file not found.

❌ Performance Test Failure Details

Log file not found.

⚠️ Some checks failed. Full logs available in workflow artifacts.


Report generated for commit: 36ef33da9f304e83eac029c4ace69723a17c49ff

@arii
Copy link
Owner Author

arii commented Feb 5, 2026

Reviewed commit: 8f08b83ca1408916fcd5cf26b8e29b8cee74c534

This PR introduces crucial improvements for handling stale HR tile data, enhancing both the accuracy of the displayed information and the overall client-side state management. The addition of the useNow hook, coupled with HRM_STALE_THRESHOLD_MS and HRM_WARNING_THRESHOLD_MS, provides a robust mechanism for visually alerting users to stale data and eventually removing unresponsive tiles from the UI. The simplification of the webSocketReducer to directly map incoming server HRM_UPDATE payloads reinforces the 'Single Source of Truth Principle' (.github/copilot-instructions.md), which is excellent.

Strengths

  • Reactive Timestamp Hook: The new useNow hook (hooks/useNow.ts) is a clean and effective way to introduce reactive time-based updates across components. It follows React best practices for state and effects.
  • Centralized Constants: Defining HRM_STALE_THRESHOLD_MS and HRM_WARNING_THRESHOLD_MS in utils/constants.ts centralizes configurable values, improving maintainability.
  • Simplified Reducer Logic: The HRM_UPDATE case in context/webSocketReducer.ts is significantly simplified. By directly mapping the server's payload, it ensures the client-side hrmData accurately reflects the server's authoritative state, removing stale or disconnected clients automatically.
  • Comprehensive Testing: The new Playwright test (verification/verify_stale.py) is a strong addition. It effectively uses the window.TEST_CONTROLS pattern (as described in docs/TYPESCRIPT_PATTERNS.md - 'Testing Client-Side Hooks and State') to mock time and verify the visual states of tiles (active, warning, removed), providing excellent coverage for this feature.
  • Alignment with Architecture: The changes align well with the project's 'Stateful Server Architecture' and 'Single Source of Truth Principle' by ensuring the client UI correctly reflects the server's state regarding active HRM devices.

Issues

  • Blocking: HrmData Import Path in HrTileWrapper.tsx
    • File: components/HrTileWrapper.tsx
    • Line: 5
    • Description: The import import { HrmData } from '@/context/webSocketReducer' is incorrect. The provided diff for context/webSocketReducer.ts does not export a type named HrmData. This will lead to a build error. The HrmData type used in HrTileWrapper.tsx needs to include isConnected and lastUpdated (which are added in the reducer) as well as isAlerting and alertMessage (which are passed from HrmConnectionPanel).
    • Recommendation: Please define and export a comprehensive client-side HRM data type from context/webSocketReducer.ts (e.g., ClientHrmData) that includes all properties used in HrTileWrapperProps. Alternatively, ensure HrmData is correctly defined and exported from types/websocket.ts if it represents the full client-side state, and then re-import from there. This ensures strict type safety and avoids potential runtime issues related to type mismatches, per .github/copilot-instructions.md (Strict Type Safety).

Test Coverage

Test coverage for the new stale tile logic is excellent with the addition of verify_stale.py, demonstrating proper behavior across different time thresholds.

Verdict

Request Changes

Reviewed at commit: 8f08b83ca1408916fcd5cf26b8e29b8cee74c534

@arii
Copy link
Owner Author

arii commented Feb 5, 2026

📋 Quality Gate Results

Check Status
Knip ✅ success
Lint ✅ success
Build ✅ success
Infra Tests ✅ success
Unit Tests ❌ failure
Component Tests ✅ success
Perf Tests ✅ success
Visual Tests ✅ success

❌ Unit Test Failure Details

  tabataTimer.ts             |   94.73 |       75 |     100 |   94.73 | 54                                                        
 services/timer              |   93.57 |    84.37 |     100 |   97.08 |                                                           
  timerCommands.ts           |    93.2 |    83.87 |     100 |    96.9 | 221-222,258                                               
  timerQueries.ts            |     100 |      100 |     100 |     100 |                                                           
  timerState.ts              |     100 |      100 |     100 |     100 |                                                           
 utils                       |   88.06 |    73.07 |     100 |   87.86 |                                                           
  socketManager.ts           |   88.06 |    73.07 |     100 |   87.86 | 60-61,80,129-130,207-208,290-315,368-380                  
-----------------------------|---------|----------|---------|---------|-----------------------------------------------------------

Summary of all failing tests
 FAIL  tests/unit/context/webSocketReducer.test.ts
  ● webSocketReducer › HRM_UPDATE action › should add a new user with a lastUpdated timestamp

    expect(received).toBeDefined()

    Received: undefined

      101 |         expect.objectContaining({ ...baseUser, isConnected: true })
      102 |       )
    > 103 |       expect(state.hrmData[0].lastUpdated).toBeDefined()
          |                                            ^
      104 |     })
      105 |
      106 |     it('should update an existing user and their lastUpdated timestamp', () => {

      at Object.<anonymous> (tests/unit/context/webSocketReducer.test.ts:103:44)

  ● webSocketReducer › HRM_UPDATE action › should update an existing user and their lastUpdated timestamp

    expect(received).not.toBe(expected) // Object.is equality

    Expected: not 12345

      119 |       expect(state.hrmData[0].zone).toBe('aerobic')
      120 |       expect(state.hrmData[0].isConnected).toBe(true)
    > 121 |       expect(state.hrmData[0].lastUpdated).not.toBe(12345)
          |                                                ^
      122 |     })
      123 |
      124 |     it('should remove users that are not in the payload immediately', () => {

      at Object.<anonymous> (tests/unit/context/webSocketReducer.test.ts:121:48)


Test Suites: 1 failed, 95 passed, 96 total
Tests:       2 failed, 571 passed, 573 total
Snapshots:   5 passed, 5 total
Time:        26.473 s
Ran all test suites matching tests/unit.
 ELIFECYCLE  Command failed with exit code 1.

⚠️ Some checks failed. Full logs available in workflow artifacts.


Report generated for commit: f23e0ec9b8f56440737439fa8588c4974332f497

@google-labs-jules
Copy link
Contributor

📋 Quality Gate Results

Check Status
Knip ✅ success
Lint ❌ failure
Build ✅ success
Infra Tests ❌ skipped
Unit Tests ❌ skipped
Component Tests ❌ skipped
Perf Tests ❌ skipped
Visual Tests ❌ skipped

❌ Lint Failure Details


> hrm@0.30.0 lint /home/runner/work/hrm/hrm
> eslint app/ components/ constants/ context/ hooks/ lib/ services/ tests/ types/ utils/ server.ts proxy.ts --cache


/home/runner/work/hrm/hrm/components/HrTileWrapper.tsx
  20:25  error  Error: Cannot call impure function during render

`Date.now` is an impure function. Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent).

/home/runner/work/hrm/hrm/components/HrTileWrapper.tsx:20:25
  18 |   // every second, ensuring the stale state is kept up-to-date.
  19 |   const isDataStale =
> 20 |     user.lastUpdated && Date.now() - user.lastUpdated > HRM_WARNING_THRESHOLD_MS
     |                         ^^^^^^^^^^ Cannot call impure function
  21 |
  22 |   return (
  23 |     <HrTile  react-hooks/purity

✖ 1 problem (1 error, 0 warnings)

 ELIFECYCLE  Command failed with exit code 1.

❌ Infrastructure Test Failure Details

Log file not found.

❌ Unit Test Failure Details

Log file not found.

❌ Component Test Failure Details

Log file not found.

❌ Visual Test Failure Details

Log file not found.

❌ Performance Test Failure Details

Log file not found.

⚠️ Some checks failed. Full logs available in workflow artifacts.


Report generated for commit: 36ef33da9f304e83eac029c4ace69723a17c49ff

Fixed. The lint error was caused by calling Date.now() during render. I've replaced it with the useNow() hook in HrTileWrapper.tsx and ensured the webSocketReducer.ts remains pure by using the server-provided updatedAt timestamp.

@google-labs-jules
Copy link
Contributor

Reviewed commit: 8f08b83ca1408916fcd5cf26b8e29b8cee74c534

This PR introduces crucial improvements for handling stale HR tile data, enhancing both the accuracy of the displayed information and the overall client-side state management. The addition of the useNow hook, coupled with HRM_STALE_THRESHOLD_MS and HRM_WARNING_THRESHOLD_MS, provides a robust mechanism for visually alerting users to stale data and eventually removing unresponsive tiles from the UI. The simplification of the webSocketReducer to directly map incoming server HRM_UPDATE payloads reinforces the 'Single Source of Truth Principle' (.github/copilot-instructions.md), which is excellent.

Strengths

  • Reactive Timestamp Hook: The new useNow hook (hooks/useNow.ts) is a clean and effective way to introduce reactive time-based updates across components. It follows React best practices for state and effects.
  • Centralized Constants: Defining HRM_STALE_THRESHOLD_MS and HRM_WARNING_THRESHOLD_MS in utils/constants.ts centralizes configurable values, improving maintainability.
  • Simplified Reducer Logic: The HRM_UPDATE case in context/webSocketReducer.ts is significantly simplified. By directly mapping the server's payload, it ensures the client-side hrmData accurately reflects the server's authoritative state, removing stale or disconnected clients automatically.
  • Comprehensive Testing: The new Playwright test (verification/verify_stale.py) is a strong addition. It effectively uses the window.TEST_CONTROLS pattern (as described in docs/TYPESCRIPT_PATTERNS.md - 'Testing Client-Side Hooks and State') to mock time and verify the visual states of tiles (active, warning, removed), providing excellent coverage for this feature.
  • Alignment with Architecture: The changes align well with the project's 'Stateful Server Architecture' and 'Single Source of Truth Principle' by ensuring the client UI correctly reflects the server's state regarding active HRM devices.

Issues

  • Blocking: HrmData Import Path in HrTileWrapper.tsx
    • File: components/HrTileWrapper.tsx
    • Line: 5
    • Description: The import import { HrmData } from '@/context/webSocketReducer' is incorrect. The provided diff for context/webSocketReducer.ts does not export a type named HrmData. This will lead to a build error. The HrmData type used in HrTileWrapper.tsx needs to include isConnected and lastUpdated (which are added in the reducer) as well as isAlerting and alertMessage (which are passed from HrmConnectionPanel).
    • Recommendation: Please define and export a comprehensive client-side HRM data type from context/webSocketReducer.ts (e.g., ClientHrmData) that includes all properties used in HrTileWrapperProps. Alternatively, ensure HrmData is correctly defined and exported from types/websocket.ts if it represents the full client-side state, and then re-import from there. This ensures strict type safety and avoids potential runtime issues related to type mismatches, per .github/copilot-instructions.md (Strict Type Safety).

Test Coverage

Test coverage for the new stale tile logic is excellent with the addition of verify_stale.py, demonstrating proper behavior across different time thresholds.

Verdict

Request Changes

Reviewed at commit: 8f08b83ca1408916fcd5cf26b8e29b8cee74c534

Fixed. I've defined and exported ClientHrmData from context/webSocketReducer.ts, which includes isConnected, lastUpdated, isAlerting, and alertMessage. HrTileWrapper.tsx now uses this comprehensive type for its props, ensuring strict type safety.

@google-labs-jules
Copy link
Contributor

📋 Quality Gate Results

Check Status
Knip ✅ success
Lint ✅ success
Build ✅ success
Infra Tests ✅ success
Unit Tests ❌ failure
Component Tests ✅ success
Perf Tests ✅ success
Visual Tests ✅ success

❌ Unit Test Failure Details

  tabataTimer.ts             |   94.73 |       75 |     100 |   94.73 | 54                                                        
 services/timer              |   93.57 |    84.37 |     100 |   97.08 |                                                           
  timerCommands.ts           |    93.2 |    83.87 |     100 |    96.9 | 221-222,258                                               
  timerQueries.ts            |     100 |      100 |     100 |     100 |                                                           
  timerState.ts              |     100 |      100 |     100 |     100 |                                                           
 utils                       |   88.06 |    73.07 |     100 |   87.86 |                                                           
  socketManager.ts           |   88.06 |    73.07 |     100 |   87.86 | 60-61,80,129-130,207-208,290-315,368-380                  
-----------------------------|---------|----------|---------|---------|-----------------------------------------------------------

Summary of all failing tests
 FAIL  tests/unit/context/webSocketReducer.test.ts
  ● webSocketReducer › HRM_UPDATE action › should add a new user with a lastUpdated timestamp

    expect(received).toBeDefined()

    Received: undefined

      101 |         expect.objectContaining({ ...baseUser, isConnected: true })
      102 |       )
    > 103 |       expect(state.hrmData[0].lastUpdated).toBeDefined()
          |                                            ^
      104 |     })
      105 |
      106 |     it('should update an existing user and their lastUpdated timestamp', () => {

      at Object.<anonymous> (tests/unit/context/webSocketReducer.test.ts:103:44)

  ● webSocketReducer › HRM_UPDATE action › should update an existing user and their lastUpdated timestamp

    expect(received).not.toBe(expected) // Object.is equality

    Expected: not 12345

      119 |       expect(state.hrmData[0].zone).toBe('aerobic')
      120 |       expect(state.hrmData[0].isConnected).toBe(true)
    > 121 |       expect(state.hrmData[0].lastUpdated).not.toBe(12345)
          |                                                ^
      122 |     })
      123 |
      124 |     it('should remove users that are not in the payload immediately', () => {

      at Object.<anonymous> (tests/unit/context/webSocketReducer.test.ts:121:48)


Test Suites: 1 failed, 95 passed, 96 total
Tests:       2 failed, 571 passed, 573 total
Snapshots:   5 passed, 5 total
Time:        26.473 s
Ran all test suites matching tests/unit.
 ELIFECYCLE  Command failed with exit code 1.

⚠️ Some checks failed. Full logs available in workflow artifacts.


Report generated for commit: f23e0ec9b8f56440737439fa8588c4974332f497

Fixed. I've updated the unit tests in tests/unit/context/webSocketReducer.test.ts to include the updatedAt timestamp in mock payloads, aligning them with the new data-driven expiry logic in the reducer.

@arii
Copy link
Owner Author

arii commented Feb 5, 2026

Reviewed commit: f8e2bf113269e88372f1273ae425f08ce2516dc6

This PR effectively addresses the persistence and staleness of HR tiles by implementing a robust time-based mechanism. The introduction of the useNow hook is a clean way to manage client-side reactive timestamps, and the integration of updatedAt from the server as the lastUpdated timestamp in the client-side state correctly establishes the server as the single source of truth for data freshness. The simplification of the HRM_UPDATE reducer logic to directly mirror the server's payload is a significant improvement in code clarity and maintainability, aligning well with the "Single Source of Truth Principle" from .github/copilot-instructions.md. The new unit tests for the reducer and the Playwright E2E test (verify_stale.py) provide excellent coverage for this new temporal logic, demonstrating the feature's correctness across different staleness thresholds. The E2E test's use of window.__TEST_CONTROLS__ for mocking Date.now() and dispatching messages is a good example of following the project's _test_ property pattern for testing client-side state.

Strengths

  • Robust Staleness Logic: The implementation correctly uses updatedAt from the server, HRM_STALE_THRESHOLD_MS, and HRM_WARNING_THRESHOLD_MS to manage tile visibility and warnings.
  • Clean Client-side Time Management: The useNow hook is well-designed and reusable for other time-sensitive UI elements.
  • Improved Reducer Logic: The HRM_UPDATE reducer has been simplified and clarified, directly reflecting the server's state, which is a significant improvement in maintainability and adherence to the "Single Source of Truth" principle.
  • Comprehensive Testing: New unit tests for the reducer and a thorough Playwright E2E test specifically verify the staleness behavior.
  • Type Safety: The introduction of ClientHrmData enhances type clarity in components.
  • Adherence to Guidelines: The PR demonstrates a strong understanding and application of project guidelines, including type safety and the preferred testing patterns (e.g., window.TEST_CONTROLS).

Reviewed at commit: f8e2bf113269e88372f1273ae425f08ce2516dc6

@google-labs-jules
Copy link
Contributor

Reviewed commit: f8e2bf113269e88372f1273ae425f08ce2516dc6

This PR effectively addresses the persistence and staleness of HR tiles by implementing a robust time-based mechanism. The introduction of the useNow hook is a clean way to manage client-side reactive timestamps, and the integration of updatedAt from the server as the lastUpdated timestamp in the client-side state correctly establishes the server as the single source of truth for data freshness. The simplification of the HRM_UPDATE reducer logic to directly mirror the server's payload is a significant improvement in code clarity and maintainability, aligning well with the "Single Source of Truth Principle" from .github/copilot-instructions.md. The new unit tests for the reducer and the Playwright E2E test (verify_stale.py) provide excellent coverage for this new temporal logic, demonstrating the feature's correctness across different staleness thresholds. The E2E test's use of window.__TEST_CONTROLS__ for mocking Date.now() and dispatching messages is a good example of following the project's _test_ property pattern for testing client-side state.

Strengths

  • Robust Staleness Logic: The implementation correctly uses updatedAt from the server, HRM_STALE_THRESHOLD_MS, and HRM_WARNING_THRESHOLD_MS to manage tile visibility and warnings.
  • Clean Client-side Time Management: The useNow hook is well-designed and reusable for other time-sensitive UI elements.
  • Improved Reducer Logic: The HRM_UPDATE reducer has been simplified and clarified, directly reflecting the server's state, which is a significant improvement in maintainability and adherence to the "Single Source of Truth" principle.
  • Comprehensive Testing: New unit tests for the reducer and a thorough Playwright E2E test specifically verify the staleness behavior.
  • Type Safety: The introduction of ClientHrmData enhances type clarity in components.
  • Adherence to Guidelines: The PR demonstrates a strong understanding and application of project guidelines, including type safety and the preferred testing patterns (e.g., window.TEST_CONTROLS).

Reviewed at commit: f8e2bf113269e88372f1273ae425f08ce2516dc6

Acknowledged. Thank you for the positive review! I'm glad the changes align well with the project's principles and architectural guidelines.

@arii
Copy link
Owner Author

arii commented Feb 5, 2026

Reviewed commit: c22474bd29db6ce98cb2cc2015f5412ad01a7f7f

Summary

This PR introduces a robust solution for handling stale HR tile persistence and aligning the time-to-live (TTL) logic across the application. The changes are well-implemented, significantly improve the client's ability to reflect the server's single source of truth, and include comprehensive testing.

Strengths

  • Architectural Alignment: The refactoring of webSocketReducer.ts for the HRM_UPDATE case to directly map the server's payload perfectly aligns with the "Single Source of Truth" principle outlined in .github/copilot-instructions.md. This greatly simplifies client-side state management for HRM data.
  • Improved Type Safety: The introduction of ClientHrmData in webSocketReducer.ts and its usage in HrTileWrapper.tsx enhances type clarity and safety, making components' expected props more explicit.
  • Clean Staleness Detection: The new useNow hook provides an elegant and reusable mechanism for reactive time-based updates, which is crucial for dynamic staleness detection in the UI. This hook is well-implemented with lazy useState initialization and proper useEffect cleanup.
  • Consistent TTL Management: The introduction of HRM_STALE_THRESHOLD_MS and HRM_WARNING_THRESHOLD_MS constants, along with their consistent application in client-side components (HrTileWrapper.tsx, HrmConnectionPanel.tsx, HrmTiles.tsx) and the server's socketManager.ts, ensures a unified approach to managing HR data freshness.
  • Enhanced Server-Side Timestamps: Adding updatedAt: Date.now() in socketManager.ts when HRM data is saved (for new clients and updates) provides the necessary server-side timestamp for accurate client-side staleness calculations.

Issues

No blocking issues were identified. The changes are well-executed and adhere to project standards.

Test Coverage

  • Unit Tests: The unit tests for webSocketReducer.test.ts were appropriately updated to reflect the new updatedAt field and the simplified HRM_UPDATE logic, verifying correct state transitions.
  • End-to-End Verification: The new verification/verify_stale.py Playwright test is an excellent addition. It provides robust, end-to-end verification of the stale tile logic, including mocking Date.now() to simulate time progression and taking screenshots to confirm visual changes. This is a high-quality test that thoroughly validates the feature.

Verdict

✅ Verified that the PR correctly implements stale HR tile persistence and aligns TTL. The code quality is high, architectural principles are followed, and testing is thorough. Ready for approval.

Reviewed at commit: c22474bd29db6ce98cb2cc2015f5412ad01a7f7f

@arii arii added the refactor label Feb 5, 2026
@google-labs-jules
Copy link
Contributor

Reviewed commit: c22474bd29db6ce98cb2cc2015f5412ad01a7f7f

Summary

This PR introduces a robust solution for handling stale HR tile persistence and aligning the time-to-live (TTL) logic across the application. The changes are well-implemented, significantly improve the client's ability to reflect the server's single source of truth, and include comprehensive testing.

Strengths

  • Architectural Alignment: The refactoring of webSocketReducer.ts for the HRM_UPDATE case to directly map the server's payload perfectly aligns with the "Single Source of Truth" principle outlined in .github/copilot-instructions.md. This greatly simplifies client-side state management for HRM data.
  • Improved Type Safety: The introduction of ClientHrmData in webSocketReducer.ts and its usage in HrTileWrapper.tsx enhances type clarity and safety, making components' expected props more explicit.
  • Clean Staleness Detection: The new useNow hook provides an elegant and reusable mechanism for reactive time-based updates, which is crucial for dynamic staleness detection in the UI. This hook is well-implemented with lazy useState initialization and proper useEffect cleanup.
  • Consistent TTL Management: The introduction of HRM_STALE_THRESHOLD_MS and HRM_WARNING_THRESHOLD_MS constants, along with their consistent application in client-side components (HrTileWrapper.tsx, HrmConnectionPanel.tsx, HrmTiles.tsx) and the server's socketManager.ts, ensures a unified approach to managing HR data freshness.
  • Enhanced Server-Side Timestamps: Adding updatedAt: Date.now() in socketManager.ts when HRM data is saved (for new clients and updates) provides the necessary server-side timestamp for accurate client-side staleness calculations.

Issues

No blocking issues were identified. The changes are well-executed and adhere to project standards.

Test Coverage

  • Unit Tests: The unit tests for webSocketReducer.test.ts were appropriately updated to reflect the new updatedAt field and the simplified HRM_UPDATE logic, verifying correct state transitions.
  • End-to-End Verification: The new verification/verify_stale.py Playwright test is an excellent addition. It provides robust, end-to-end verification of the stale tile logic, including mocking Date.now() to simulate time progression and taking screenshots to confirm visual changes. This is a high-quality test that thoroughly validates the feature.

Verdict

✅ Verified that the PR correctly implements stale HR tile persistence and aligns TTL. The code quality is high, architectural principles are followed, and testing is thorough. Ready for approval.

Reviewed at commit: c22474bd29db6ce98cb2cc2015f5412ad01a7f7f

Acknowledged. Thank you for the detailed and positive review! I am glad that the architectural alignment, type safety improvements, and testing strategy met the project standards. This task is now complete.

@arii
Copy link
Owner Author

arii commented Feb 5, 2026

Reviewed commit: 282d5a047fa32dae622d9c75753fdac65811cf80

Summary

This PR effectively addresses the issue of stale HR tiles persisting and aligns the Time-To-Live (TTL) logic across both the server and client. The introduction of the useNow hook and corresponding constants, combined with refined reducer logic and comprehensive UI updates, provides a robust solution. The new Playwright verification test is a significant enhancement, ensuring the feature behaves as expected.

Strengths

  • useNow Hook: The hooks/useNow.ts file introduces a well-patterned, reactive timestamp hook that updates every second. This is a clean and reusable solution for time-dependent UI updates and adheres to React best practices for state management and effect cleanup.
  • Aligned TTLs: The server-side socketManager.ts and client-side logic (reducer, components) now consistently use HRM_STALE_THRESHOLD_MS and HRM_WARNING_THRESHOLD_MS from utils/constants.ts. This ensures a unified approach to identifying and handling stale HR data.
  • Simplified Reducer Logic: The HRM_UPDATE case in webSocketReducer.ts has been significantly simplified. By mapping directly from the incoming payload, the client-side state for hrmData now perfectly mirrors the authoritative server state. This strongly adheres to the "Single Source of Truth Principle" from .github/copilot-instructions.md.
  • Comprehensive Client-Side Updates: Components like HrTileWrapper, HrmConnectionPanel, and HrmTiles correctly integrate the useNow hook and new staleness constants to visually warn and remove stale tiles. The useMemo dependencies were correctly updated.
  • Robust Test Coverage: The addition of verification/verify_stale.py is an excellent E2E test that uses Playwright with a mocked Date.now() to thoroughly verify the stale tile behavior at different time thresholds. This demonstrates a high level of confidence in the implemented solution. Unit tests for the reducer were also updated to reflect the new updatedAt field.

Issues

  • Minor Type Alias Usage: In components/HrTileWrapper.tsx, the HrTileWrapperProps interface defines user: HrmData & { isAlerting: boolean; alertMessage?: string }. While functionally correct, it's equivalent to the newly introduced ClientHrmData type in context/webSocketReducer.ts. For consistency and clarity, it would be slightly cleaner to directly use user: ClientHrmData here. This is a very minor stylistic suggestion and not blocking.

Test Coverage

Test coverage for the new stale tile logic is excellent, particularly with the new Playwright E2E test (verification/verify_stale.py) which simulates time progression to verify the warning and removal states. The unit tests for the reducer were also appropriately updated. Tests were updated successfully.

Verdict

approve

Reviewed at commit: 282d5a047fa32dae622d9c75753fdac65811cf80

@arii arii added the medium label Feb 5, 2026
@google-labs-jules
Copy link
Contributor

Reviewed commit: 282d5a047fa32dae622d9c75753fdac65811cf80

Summary

This PR effectively addresses the issue of stale HR tiles persisting and aligns the Time-To-Live (TTL) logic across both the server and client. The introduction of the useNow hook and corresponding constants, combined with refined reducer logic and comprehensive UI updates, provides a robust solution. The new Playwright verification test is a significant enhancement, ensuring the feature behaves as expected.

Strengths

  • useNow Hook: The hooks/useNow.ts file introduces a well-patterned, reactive timestamp hook that updates every second. This is a clean and reusable solution for time-dependent UI updates and adheres to React best practices for state management and effect cleanup.
  • Aligned TTLs: The server-side socketManager.ts and client-side logic (reducer, components) now consistently use HRM_STALE_THRESHOLD_MS and HRM_WARNING_THRESHOLD_MS from utils/constants.ts. This ensures a unified approach to identifying and handling stale HR data.
  • Simplified Reducer Logic: The HRM_UPDATE case in webSocketReducer.ts has been significantly simplified. By mapping directly from the incoming payload, the client-side state for hrmData now perfectly mirrors the authoritative server state. This strongly adheres to the "Single Source of Truth Principle" from .github/copilot-instructions.md.
  • Comprehensive Client-Side Updates: Components like HrTileWrapper, HrmConnectionPanel, and HrmTiles correctly integrate the useNow hook and new staleness constants to visually warn and remove stale tiles. The useMemo dependencies were correctly updated.
  • Robust Test Coverage: The addition of verification/verify_stale.py is an excellent E2E test that uses Playwright with a mocked Date.now() to thoroughly verify the stale tile behavior at different time thresholds. This demonstrates a high level of confidence in the implemented solution. Unit tests for the reducer were also updated to reflect the new updatedAt field.

Issues

  • Minor Type Alias Usage: In components/HrTileWrapper.tsx, the HrTileWrapperProps interface defines user: HrmData & { isAlerting: boolean; alertMessage?: string }. While functionally correct, it's equivalent to the newly introduced ClientHrmData type in context/webSocketReducer.ts. For consistency and clarity, it would be slightly cleaner to directly use user: ClientHrmData here. This is a very minor stylistic suggestion and not blocking.

Test Coverage

Test coverage for the new stale tile logic is excellent, particularly with the new Playwright E2E test (verification/verify_stale.py) which simulates time progression to verify the warning and removal states. The unit tests for the reducer were also appropriately updated. Tests were updated successfully.

Verdict

approve

Reviewed at commit: 282d5a047fa32dae622d9c75753fdac65811cf80

Acknowledged. Thank you for the approval! I've already updated HrTileWrapper.tsx to use the ClientHrmData type in the latest commit for better consistency. Glad the verification test and overall logic meet the project standards.

@arii
Copy link
Owner Author

arii commented Feb 5, 2026

Reviewed commit: a0730757a6bdc4336355eefde3fd2a430d82cf4a

This PR delivers a comprehensive solution for managing stale HR tiles, significantly improving the client-side state's accuracy and resilience. The approach of using a lastUpdated timestamp, alongside dedicated constants for warning and stale thresholds, is robust and well-implemented.

Strengths

  • Improved Reducer Logic: The simplification of the HRM_UPDATE case in webSocketReducer.ts is a significant win. By treating the incoming payload as the single source of truth and mapping it directly, the reducer becomes much cleaner, more reliable, and less prone to state inconsistencies. This directly aligns with the Single Source of Truth Principle from .github/copilot-instructions.md and Reducing Lines of Code from docs/CODE_REVIEW_GUIDELINES.md.
  • Reactive Time Management: The introduction of the useNow hook provides a clean, reusable, and performant way to trigger time-based re-renders for stale data detection across components.
  • Type Safety: The new ClientHrmData type in webSocketReducer.ts improves type safety and consolidates related properties, making HrTileWrapperProps much clearer.
  • Consistent TTL Application: The HRM_STALE_THRESHOLD_MS and HRM_WARNING_THRESHOLD_MS constants are well-defined and consistently applied across the client (for filtering and visual warnings) and server (for cleaning up stale sessions).
  • Comprehensive Testing: The addition of verification/verify_stale.py is an excellent end-to-end verification. Mocking Date.now() and taking screenshots provides strong confidence that the feature works as intended visually and functionally. The unit tests for webSocketReducer.ts were also appropriately updated to reflect the new updatedAt logic.
  • Architectural Adherence: The changes align well with the project's stateful server architecture and strict type safety guidelines. No any types were introduced, and client-side logic correctly reacts to server-pushed state.

Issues

No significant issues found. The changes are well-implemented and adhere to project standards.

Verdict

This PR is ready for approval. The logic is sound, the code is cleaner, and the testing provides solid verification.

Reviewed at commit: a0730757a6bdc4336355eefde3fd2a430d82cf4a

@google-labs-jules
Copy link
Contributor

Reviewed commit: a0730757a6bdc4336355eefde3fd2a430d82cf4a

This PR delivers a comprehensive solution for managing stale HR tiles, significantly improving the client-side state's accuracy and resilience. The approach of using a lastUpdated timestamp, alongside dedicated constants for warning and stale thresholds, is robust and well-implemented.

Strengths

  • Improved Reducer Logic: The simplification of the HRM_UPDATE case in webSocketReducer.ts is a significant win. By treating the incoming payload as the single source of truth and mapping it directly, the reducer becomes much cleaner, more reliable, and less prone to state inconsistencies. This directly aligns with the Single Source of Truth Principle from .github/copilot-instructions.md and Reducing Lines of Code from docs/CODE_REVIEW_GUIDELINES.md.
  • Reactive Time Management: The introduction of the useNow hook provides a clean, reusable, and performant way to trigger time-based re-renders for stale data detection across components.
  • Type Safety: The new ClientHrmData type in webSocketReducer.ts improves type safety and consolidates related properties, making HrTileWrapperProps much clearer.
  • Consistent TTL Application: The HRM_STALE_THRESHOLD_MS and HRM_WARNING_THRESHOLD_MS constants are well-defined and consistently applied across the client (for filtering and visual warnings) and server (for cleaning up stale sessions).
  • Comprehensive Testing: The addition of verification/verify_stale.py is an excellent end-to-end verification. Mocking Date.now() and taking screenshots provides strong confidence that the feature works as intended visually and functionally. The unit tests for webSocketReducer.ts were also appropriately updated to reflect the new updatedAt logic.
  • Architectural Adherence: The changes align well with the project's stateful server architecture and strict type safety guidelines. No any types were introduced, and client-side logic correctly reacts to server-pushed state.

Issues

No significant issues found. The changes are well-implemented and adhere to project standards.

Verdict

This PR is ready for approval. The logic is sound, the code is cleaner, and the testing provides solid verification.

Reviewed at commit: a0730757a6bdc4336355eefde3fd2a430d82cf4a

Acknowledged. Thank you for the positive review and the approval! I'm glad the improvements to the reducer logic, type safety, and testing coverage were well-received.

@arii
Copy link
Owner Author

arii commented Feb 5, 2026

Reviewed commit: 7969ff5a5f0690317bc92d5d08a7c4c4e82a2b8d

This PR delivers a comprehensive and well-implemented solution for handling stale HR tile persistence and aligning Time-To-Live (TTL) values across the application. The changes are consistent, enhance reliability, and adhere to the project's architectural principles.

Strengths

  • Reducer Simplification: The refactoring of the HRM_UPDATE case in webSocketReducer.ts is a significant improvement. By directly mapping the incoming server payload, the logic is much cleaner, more concise, and robustly aligns with the "Single Source of Truth" principle. This is a prime example of reducing complexity and lines of code, aligning with docs/CODE_REVIEW_GUIDELINES.md.
  • Consistent Stale Data Handling: The introduction of HRM_STALE_THRESHOLD_MS and HRM_WARNING_THRESHOLD_MS constants, along with their consistent application on both the client (for visual warnings and removal) and server (for janitor cleanup), ensures a coherent approach to data freshness.
  • useNow Hook: The new useNow hook provides a centralized, reusable, and efficient mechanism for components to react to time progression. This prevents boilerplate and promotes code reusability, which is excellent.
  • Type Safety: The introduction of ClientHrmData enhances type safety for components consuming HRM data merged with alert states.
  • Robust Testing: The unit test updates for webSocketReducer.test.ts correctly reflect the new lastUpdated behavior. Crucially, the new Playwright E2E test (verification/verify_stale.py) effectively validates the entire stale tile lifecycle, including visual warnings and eventual removal, by cleverly mocking Date.now(). This demonstrates thoroughness in verifying the new feature.
  • Server-Side updatedAt: The server-side changes in socketManager.ts to consistently set updatedAt on client connection and data updates are fundamental for accurate staleness detection on the client, ensuring the lastUpdated values are reliable.

Verdict

This PR is well-executed and directly addresses the described feature. The code quality, architectural alignment, and test coverage are all excellent. No further changes are requested.

✅ Verified: Stale tile persistence logic is correctly implemented on both client and server, reducer simplified, useNow hook is effective, and comprehensive tests validate the behavior. No regressions found. Ready for approval.

Reviewed at commit: 7969ff5a5f0690317bc92d5d08a7c4c4e82a2b8d

@google-labs-jules
Copy link
Contributor

Reviewed commit: 7969ff5a5f0690317bc92d5d08a7c4c4e82a2b8d

This PR delivers a comprehensive and well-implemented solution for handling stale HR tile persistence and aligning Time-To-Live (TTL) values across the application. The changes are consistent, enhance reliability, and adhere to the project's architectural principles.

Strengths

  • Reducer Simplification: The refactoring of the HRM_UPDATE case in webSocketReducer.ts is a significant improvement. By directly mapping the incoming server payload, the logic is much cleaner, more concise, and robustly aligns with the "Single Source of Truth" principle. This is a prime example of reducing complexity and lines of code, aligning with docs/CODE_REVIEW_GUIDELINES.md.
  • Consistent Stale Data Handling: The introduction of HRM_STALE_THRESHOLD_MS and HRM_WARNING_THRESHOLD_MS constants, along with their consistent application on both the client (for visual warnings and removal) and server (for janitor cleanup), ensures a coherent approach to data freshness.
  • useNow Hook: The new useNow hook provides a centralized, reusable, and efficient mechanism for components to react to time progression. This prevents boilerplate and promotes code reusability, which is excellent.
  • Type Safety: The introduction of ClientHrmData enhances type safety for components consuming HRM data merged with alert states.
  • Robust Testing: The unit test updates for webSocketReducer.test.ts correctly reflect the new lastUpdated behavior. Crucially, the new Playwright E2E test (verification/verify_stale.py) effectively validates the entire stale tile lifecycle, including visual warnings and eventual removal, by cleverly mocking Date.now(). This demonstrates thoroughness in verifying the new feature.
  • Server-Side updatedAt: The server-side changes in socketManager.ts to consistently set updatedAt on client connection and data updates are fundamental for accurate staleness detection on the client, ensuring the lastUpdated values are reliable.

Verdict

This PR is well-executed and directly addresses the described feature. The code quality, architectural alignment, and test coverage are all excellent. No further changes are requested.

✅ Verified: Stale tile persistence logic is correctly implemented on both client and server, reducer simplified, useNow hook is effective, and comprehensive tests validate the behavior. No regressions found. Ready for approval.

Reviewed at commit: 7969ff5a5f0690317bc92d5d08a7c4c4e82a2b8d

Acknowledged. Thank you for the positive review and the final approval! I'm glad the architectural alignment and testing strategy were successful. This task is now complete.

@arii
Copy link
Owner Author

arii commented Feb 6, 2026

Reviewed commit: 52143b713b40f10df62c93c6710786d19c0bf537

This PR introduces significant improvements to how HR tile data persistence and staleness are handled, both on the client and server. The changes are well-structured, adhere to project guidelines, and are thoroughly tested.

Strengths

  • Centralized Logic: The introduction of utils/hrm.ts with the getActiveHrmData function is an excellent refactoring. It centralizes the complex logic for filtering HRM data (stale, placeholder names, zero values) and merging active alerts, significantly improving readability and maintainability in HrmConnectionPanel.tsx and HrmTiles.tsx.
  • Robust Stale Data Detection: The useNow hook, combined with the lastUpdated timestamp in the webSocketReducer and the new HRM_STALE_THRESHOLD_MS/HRM_WARNING_THRESHOLD_MS constants, provides a robust and reactive mechanism for detecting and visually indicating stale HR data.
  • Performance Optimization: Memoizing HrTileWrapper with a custom arePropsEqual function is a great touch, preventing unnecessary re-renders and improving client-side performance.
  • Server-Client Alignment: The webSocketReducer's simplified HRM_UPDATE logic, which directly maps the incoming payload, ensures the client-side state perfectly mirrors the server's HrmDataStore. The addition of updatedAt: Date.now() on the server-side hrmDataStore.save calls ensures the server's timestamp is always current, which is crucial for client-side staleness detection.
  • Comprehensive Testing: The addition of new unit tests for HrTileWrapper and getActiveHrmData, along with a Playwright E2E verification script (verification/verify_stale.py), demonstrates a high commitment to quality and ensures the new functionality works as expected across different scenarios.
  • Type Safety: The introduction of ClientHrmData enhances type clarity for component props.

Issues

No blocking issues were found. The PR is of high quality.

Test Coverage

Test coverage is excellent. New unit tests cover the HrTileWrapper component and the getActiveHrmData utility. The Playwright E2E test specifically verifies the stale tile behavior by mocking time, which is a very effective way to test time-sensitive logic.

Verdict

✅ Verified that the PR correctly implements stale HR tile persistence and aligns TTL. The code is clean, efficient, and well-tested. No regressions or new issues were found. Ready for approval.

Reviewed at commit: 52143b713b40f10df62c93c6710786d19c0bf537

@arii
Copy link
Owner Author

arii commented Feb 6, 2026

🤖 AI Technical Audit

Code Review: PR #6657 - Fix Stale HR Tile Persistence

This PR effectively addresses the "ghost tiles" issue by implementing client-side TTL synchronization and a reactive time hook. The logic for filtering stale data is sound, and the architectural decision to align server/client TTL is correct. However, the implementation of HrTileWrapper introduces significant maintenance debt through brittle memoization logic that should be refactored.

🚫 ANTI-AI-SLOP DIRECTIVES

  1. OVERLY VERBOSE COMMENTS:
    • File: context/webSocketReducer.ts
    • Issue: The comments inside the HRM_UPDATE case (lines 87-90) are narrative history ("THE FIX: ...", "Simplify: ..."). Code comments should explain why the code does what it does now, not the history of the fix (that belongs in git commit messages).
  2. OVER-ENGINEERING / CODE RATIO:
    • File: components/HrTileWrapper.tsx
    • Issue: The arePropsEqual function (lines 30-43) manually compares 8 distinct properties of the user object. This is a maintenance hazard; adding a new field to ClientHrmData requires remembering to update this function.
    • Fix: Decompose the user object in the parent component and pass primitives to HrTileWrapper. React.memo handles primitive comparison automatically, eliminating 15 lines of brittle code.
  3. STALE FEATURES:
    • Confirmed deletion of ad-hoc filtering logic in HrmConnectionPanel.tsx (lines 16-36) and HrmTiles.tsx (lines 17-48) in favor of the new getActiveHrmData utility. This is excellent cleanup.

File-by-File Analysis

components/HrTileWrapper.tsx

Problem: As noted above, passing the entire user object forces you to write a custom comparator because getActiveHrmData returns new object references every second (via useNow).

Recommendation: Remove arePropsEqual entirely. Update HrTileWrapper to accept individual primitives or use a stable ID/Value check. However, the cleanest React pattern is passing primitives.

Implementation Sample (Refactor):

// components/HrTileWrapper.tsx
// Remove arePropsEqual and user object prop
interface HrTileWrapperProps {
  name: string
  value: number
  maxHr: number
  calories: number
  isConnected: boolean
  isDataStale: boolean
  isAlerting: boolean
  alertMessage?: string
}

const HrTileWrapper = memo(({ value, maxHr, ...props }: HrTileWrapperProps) => {
  const hrZoneProps = useHrZone(value, maxHr)
  return <HrTile bpm={value} percentMax={hrZoneProps.percentage} {...props} />
})

Note: This requires updating the usage in HrmTiles.tsx to spread the user properties.

context/webSocketReducer.ts

Observation: The simplification of HRM_UPDATE to trust the server payload as the source of truth is much more robust than the previous merging strategy. It correctly handles the removal of disconnected clients implicitly.

Best Practice: Ensure lastUpdated is consistently relying on Date.now() (Client Time) rather than updatedAt (Server Time) to avoid clock skew issues, which this PR correctly does.

hooks/useNow.ts

Observation: Standard interval hook. Good use of lazy initialization for useState.

utils/hrm.ts

Observation: getActiveHrmData is well-tested and consolidates logic previously scattered across components. This is a strong architectural improvement.


Architectural Impact

  1. Performance: The useNow hook triggers a re-render of the list components every second. Because getActiveHrmData returns new object references, pure components receiving the user object will re-render unnecessarily unless memoized. The current PR fixes this with arePropsEqual, but the refactor suggested above is cleaner.
  2. Reliability: The client-side TTL check (30s) acts as a reliable fail-safe for network interruptions where DEVICE_OFFLINE messages might be missed.

GitHub Checks

All checks passed. The new tests in HrTileWrapper.test.tsx and hrm.test.ts provide good coverage for the new staleness logic.

Review automatically published via RepoAuditor.

@arii
Copy link
Owner Author

arii commented Feb 7, 2026

🤖 AI Technical Audit

Principal Engineer Review: PR #6657 - Fix Stale HR Tile Persistence

This PR effectively addresses the "ghost tiles" issue by enforcing a strict server-authoritative state model in the Redux reducer and implementing a reactive client-side staleness check. The architectural shift to treat the server payload as the "Single Source of Truth" (SSoT) is the correct approach for synchronization issues.

🛡️ Anti-AI-Slop & Quality Directives

  1. Over-Engineering / Maintenance Risk: The manual memoization in components/HrTileWrapper.tsx via arePropsEqual is fragile. It manually compares 8 distinct properties. If ClientHrmData evolves (e.g., adding a zoneLabel or batteryLevel), this function will silently fail to trigger updates, leading to subtle bugs. Recommendation: See "Code Ratio" below for a cleaner solution using destructuring.
  2. Code Ratio: We can delete ~15 lines of code in HrTileWrapper.tsx while maintaining performance. Instead of a custom arePropsEqual, the parent component (HrmTiles) should destruct updatedAt and lastUpdated out of the passed object. By passing only the visual data as props (via spread), React.memo's default shallow comparison handles the optimization automatically without the maintenance burden.
  3. Stale Features: The logic in webSocketReducer.ts correctly obsoletes the old "merge and keep" strategy. The implementation properly replaces it.
  4. Performance: The introduction of useNow (1Hz tick) in HrmTiles causes the getActiveHrmData calculation and the main grid render to occur every second. The optimization in HrTileWrapper is therefore critical. The current arePropsEqual works but is verbose.

📁 File-by-File Analysis

context/webSocketReducer.ts

Logic Analysis:
The change to HRM_UPDATE is a breaking change regarding state persistence, but a positive one. By mapping payload directly to the state (while preserving local flags like isConnected), you ensure that any device stopped by the server is immediately removed from the client.

  • Assumption Check: This relies on the server broadcasting the full list of active sessions on every tick. If the server sends differential updates (deltas), this logic will flicker or remove active users. Given socketManager.ts (implied) seems to use broadcastState(), this is likely safe, but verify the server behavior.

components/HrTileWrapper.tsx

Problem: Fragile Memoization
The arePropsEqual function hardcodes property checks.

Proposed Refactor (Code Removal):
Instead of the custom comparator, strictly pass visual props in the parent.

utils/hrm.ts

Best Practice:
The separation of filtering logic into getActiveHrmData is excellent. It makes the staleness logic unit-testable (as evidenced by the new tests).

Nitpick: isDataStale calculation duplicates the math slightly but is acceptable for readability.

hooks/useNow.ts

Best Practice:
Standard implementation. Lazy initialization of useState is a nice touch for performance.

🏗️ Architectural Impact

The system moves from a "Client accumulates state" model to a "Server dictates state + Client filters stale data" model. This is much more robust for real-time monitoring. The client-side useNow acts as a fail-safe for network freezes, which is a necessary resiliency pattern for WebSocket applications.

Review automatically published via RepoAuditor.

@arii
Copy link
Owner Author

arii commented Feb 8, 2026

Reviewed commit: 64cdfac7a7d12d77f736ebd25ce083cdf1cd9973

This PR effectively addresses the issue of stale HR tile persistence and aligns TTLs by introducing a robust client-side data management strategy. The changes are well-structured, improve code readability, and enhance performance.

Strengths

  • Architectural Improvement: The introduction of the useNow hook and the getActiveHrmData utility centralizes complex data filtering, staleness detection, and alert merging logic. This significantly improves modularity and adheres to the DRY principle.
  • Reducer Simplification: The webSocketReducer's HRM_UPDATE logic has been streamlined to directly reflect the server's state, correctly handling the removal of offline devices and ensuring the client-side state is a true reflection of the server's single source of truth.
  • Performance Optimization: The use of memo with a custom arePropsEqual function in HrTileWrapper is a good practice for preventing unnecessary re-renders, especially in a real-time data display component.
  • Enhanced Type Safety: The new ClientHrmData interface provides a clear and comprehensive type for HRM data on the client, including alert states.
  • Comprehensive Testing: New unit tests for HrTileWrapper and getActiveHrmData, along with updates to webSocketReducer tests, demonstrate thorough coverage of the new functionality and logic.

Issues

No blocking issues were found. The code is clean and functional.

Test Coverage

The new features are well-covered by unit tests, and existing tests were updated to reflect the changes in the reducer logic.

Verdict

✅ Verified that the PR correctly implements stale HR tile persistence and TTL alignment. The architectural improvements, code simplification, and test coverage are excellent. No regressions or new issues were found. Ready for approval.

Reviewed at commit: 64cdfac7a7d12d77f736ebd25ce083cdf1cd9973

@arii
Copy link
Owner Author

arii commented Feb 8, 2026

@conflict-resolve

1 similar comment
@arii
Copy link
Owner Author

arii commented Feb 8, 2026

@conflict-resolve

@github-actions
Copy link
Contributor

github-actions bot commented Feb 8, 2026

⏳ Attempting to automatically resolve conflicts for branch fix/stale-hr-tile-persistence-791622865149962011...

@github-actions
Copy link
Contributor

github-actions bot commented Feb 8, 2026

Error: Unable to automatically resolve all conflicts.
The following files have unresolved conflicts:


Please resolve these conflicts manually.

@arii
Copy link
Owner Author

arii commented Feb 8, 2026

Reviewed commit: 0b8305d127e01d6e7b625b5b8e96dbe08d721016

This PR effectively addresses the issue of stale HR tile persistence and aligns TTL by introducing a reactive useNow hook, centralizing HRM data processing with the getActiveHrmData utility, and refining the webSocketReducer logic. The changes significantly improve data consistency, performance, and maintainability.

Strengths

  • Improved Data Consistency: The webSocketReducer now more accurately reflects the server's single source of truth by directly mapping the incoming HRM_UPDATE payload, automatically removing offline devices. This aligns well with the "Single Source of Truth Principle" from .github/copilot-instructions.md.
  • Centralized Data Processing (DRY): The new getActiveHrmData utility consolidates complex filtering, staleness detection, and alert mapping logic. This eliminates redundant code in HrmConnectionPanel and HrmTiles, making these components cleaner and easier to maintain.
  • Performance Optimization: Wrapping HrTileWrapper with memo and providing a custom arePropsEqual function, along with memoizing HrmTiles, helps prevent unnecessary re-renders, improving client-side performance.
  • Clear Staleness Detection: The useNow hook provides a robust mechanism for time-based re-renders, enabling accurate and reactive staleness detection for HR tiles based on defined thresholds in utils/constants.ts.
  • Enhanced Type Safety: The introduction of ClientHrmData and the refined reducer logic improve the overall type safety of the HRM data handling on the client side.
  • Comprehensive Test Coverage: New unit tests for HrTileWrapper, getActiveHrmData, and updated tests for webSocketReducer ensure the new logic is well-covered and prevent regressions.

Issues

  • None blocking.

Test Coverage

Excellent. The new useNow hook, getActiveHrmData utility, and HrTileWrapper component are all covered by dedicated unit tests. Existing tests for webSocketReducer have been appropriately updated to reflect the new logic. This addresses the "TEST COVERAGE ALERT" effectively.

Verdict

This PR is well-executed and significantly improves the robustness and maintainability of the HRM data display. The changes align with project guidelines and best practices.

Reviewed at commit: 0b8305d127e01d6e7b625b5b8e96dbe08d721016

@arii
Copy link
Owner Author

arii commented Feb 8, 2026

@pr-squash-rebase

@arii
Copy link
Owner Author

arii commented Feb 8, 2026

@pr-squash

## Description

This change synchronizes the heart rate monitor TTL between the server and client to 30 seconds, eliminating "ghost tiles". It introduces a reactive `useNow` hook to ensure the UI updates even when no network messages are received, allowing for forceful removal of stale data based on the `lastUpdated` timestamp. It also adds a visual warning state after 10 seconds of inactivity and simplifies the WebSocket reducer.

The motivation behind this change is to improve the user experience by preventing "ghost tiles" – instances where heart rate monitor data persists in the UI long after it should have expired. By aligning the TTL and introducing a mechanism for client-side enforcement of data freshness, the UI will accurately reflect the real-time status of heart rate monitors.

No explicit dependencies are required for this change.

Fixes #6637

## Change Type: 🐛 Bug fix (non-breaking change fixing an issue)

## PR Scope Checklist

_This checklist is mandatory for all PRs._

- [x] **PR has a clear, single purpose:** The title and description of the PR clearly state the purpose of the change.
- [x] **All changes relate to the stated objective:** The code changes should be directly related to the purpose of the PR.
- [x] **No unrelated cleanup or refactoring:** The PR should not contain any changes that are not directly related to the stated objective.
- [x] **Title and description match the actual changes:** The title and description should accurately reflect the changes in the PR.
- [x] **Tests cover the specific change scope:** The tests should be focused on the changes in the PR and should not include unrelated tests.

## Impact Assessment

- [x] Changes are **backward compatible** (or breaking changes are documented)
- [x] **Tests** are added/updated for new functionality
- [x] **Documentation** is updated if needed
- [ ] **ADR** is created/updated for significant architectural changes

<details>
<summary>Original PR Body</summary>

This change synchronizes the heart rate monitor TTL between the server and client to 30 seconds, eliminating "ghost tiles". It introduces a reactive `useNow` hook to ensure the UI updates even when no network messages are received, allowing for forceful removal of stale data based on the `lastUpdated` timestamp. It also adds a visual warning state after 10 seconds of inactivity and simplifies the WebSocket reducer.

Fixes #6637

---
*PR created automatically by Jules for task [791622865149962011](https://jules.google.com/task/791622865149962011) started by @arii*
</details>
@github-actions
Copy link
Contributor

github-actions bot commented Feb 8, 2026

✅ Commits squashed successfully!

@github-actions github-actions bot force-pushed the fix/stale-hr-tile-persistence-791622865149962011 branch from 0b8305d to d028540 Compare February 8, 2026 21:24
@arii
Copy link
Owner Author

arii commented Feb 10, 2026

📋 Quality Gate Results

Check Status
Knip ✅ success
Lint ❌ failure
Slop ✅ success
Build ❌ failure
Infra Tests ❌ skipped
Unit Tests ❌ skipped
Component Tests ❌ skipped
Perf Tests ❌ skipped
Visual Tests ❌ skipped

❌ Lint Failure Details


> hrm@0.30.0 lint /home/runner/work/hrm/hrm
> eslint app/ components/ constants/ context/ hooks/ lib/ services/ tests/ types/ utils/ server.ts proxy.ts --cache


/home/runner/work/hrm/hrm/components/HrTileWrapper.tsx
  56:0  error  Parsing error: '}' expected

/home/runner/work/hrm/hrm/components/HrmTiles.tsx
  83:28  error  Parsing error: ',' expected

✖ 2 problems (2 errors, 0 warnings)

 ELIFECYCLE  Command failed with exit code 1.

❌ Build Failure Details

Log file not found.

❌ Infrastructure Test Failure Details

Log file not found.

❌ Unit Test Failure Details

Log file not found.

❌ Component Test Failure Details

Log file not found.

❌ Visual Test Failure Details

Log file not found.

❌ Performance Test Failure Details

Log file not found.

⚠️ Some checks failed. Full logs available in workflow artifacts.


Report generated for commit: 6ecc9b5107f0432d59b09d1edea749b723a2ab16

@arii
Copy link
Owner Author

arii commented Feb 10, 2026

🤖 AI Technical Audit

Deep Code Review: PR #6657

This PR addresses the "ghost tile" issue by enforcing a strict TTL and introducing a reactive time hook. While the architectural approach is sound (centralizing logic in utils/hrm.ts), there are critical implementation errors in HrTileWrapper.tsx that will prevent compilation, along with logic assumptions in the reducer that need verification.

⚠️ Critical Issues (Blocking)

1. Broken Implementation in HrTileWrapper.tsx (Compilation Error)

The diff for components/HrTileWrapper.tsx indicates a malformed edit. You are attempting to use useHrZone without importing it, and it appears you have duplicated the component definition rather than replacing it.

Problem:

  1. useHrZone is used but not imported.
  2. The component function HrTileWrapper appears twice in the file (based on the context of the diff), or the logic is mixed between the old manual calculation and the new hook.
  3. The variable hrZoneProps is defined but never used in the JSX return statement.

Implementation Sample (Fix):

import { memo } from 'react'
import HrTile from '@/components/HrTile'
import { ClientHrmData } from '@/context/webSocketReducer'
import { useHrZone } from '@/hooks/useHrZone' // ⬅️ MISSING IMPORT

interface HrTileWrapperProps {
  user: ClientHrmData
  isDataStale: boolean
}

const HrTileWrapper = ({ user, isDataStale }: HrTileWrapperProps) => {
  // Use the hook instead of manual calculation
  const { percentage, zone, color } = useHrZone(user.value, user.maxHr)

  return (
    <HrTile
      name={user.name || ''}
      bpm={user.value}
      percentMax={percentage} // ⬅️ Update usage
      zone={zone}
      calories={user.calories || 0}
      isConnected={user.isConnected}
      isDataStale={isDataStale}
      isAlerting={user.isAlerting}
      {...(user.alertMessage && { alertMessage: user.alertMessage })}
    />
  )
}

2. Potential Data Loss in webSocketReducer.ts

In the HRM_UPDATE case, you changed the logic from merging updates into the existing state to mapping the payload as the new state.

Problem:

// Current PR Logic
const mergedHrmData = payload.map((newUser) => { ... })
return { ...state, hrmData: mergedHrmData }

If the server sends a delta (only users who changed) rather than a full snapshot of all active users, this logic will delete all users not present in the current update packet. Verify if HRM_UPDATE is guaranteed to be a full snapshot. If it's a delta, you must keep the previous activeHrmData filtering logic.

⚙️ Architecture & Best Practices

  • Performance (arePropsEqual): The custom comparator in HrTileWrapper is fragile. It manually compares user.value, user.calories, etc. If you add a new property to ClientHrmData (e.g., restingHr), this comparator will ignore it, potentially leading to stale UI. Given that getActiveHrmData returns new object references every second (via useNow -> map), React.memo is useful, but consider comparing the specific primitives passed to HrTile or using a deep equality check on the user object if the shape is stable.

  • Global Re-renders: HrmTiles uses useNow which updates every 1 second. This forces the entire grid to reconcile every second. While React is fast, ensure HrTile (the child) is strictly memoized (which you did with HrTileWrapper) to avoid layout thrashing.

🚫 Anti-AI-Slop Directives

  1. OVERLY VERBOSE COMMENTS: In context/webSocketReducer.ts, the comment // Simplify: The HRM_UPDATE payload from the server is the single source of truth... is unnecessarily wordy. The code speaks for itself. Remove it.
  2. DUPLICATE HOOKS/TYPES: You introduced ClientHrmData in the reducer file. Check if this overlaps with HrmData in types/websocket.ts. If they are identical (merging alerts), consolidate them to avoid type drift.
  3. CODE RATIO: HrTileWrapper.tsx contains a massive arePropsEqual function (approx 15 lines). This is maintenance debt.
    Deletion Candidate:
    // Instead of manual prop comparison, rely on primitive props in the child or Lodash isEqual if strictly necessary.
    // Or simply:
    export default memo(HrTileWrapper, (prev, next) => {
      return prev.isDataStale === next.isDataStale && 
             prev.user.updatedAt === next.user.updatedAt // Assuming updatedAt changes on value update
    })
  4. STALE FEATURES: You correctly removed the logic from HrmConnectionPanel and moved it to utils/hrm.ts. This is a good cleanup.

✅ Automated Checks

Status: ⚠️ FAILING (Predicted)

  • Build: Will fail in components/HrTileWrapper.tsx due to missing useHrZone import and potential syntax errors from the botched diff application.
  • Tests: tests/unit/components/HrTileWrapper.test.tsx will pass only because it mocks useHrZone, hiding the runtime import error.

Review automatically published via RepoAuditor.

@arii
Copy link
Owner Author

arii commented Feb 10, 2026

🤖 AI Technical Audit

Code Review: Fix Stale HR Tile Persistence

This PR addresses the "ghost tile" issue by aligning server/client TTLs and introducing client-side staleness checks. While the architectural approach is generally sound, there is a critical logic flaw regarding which timestamp is used for staleness detection, and some unnecessary noise in the codebase.

⚠️ Critical Logic Flaw: Timestamp Usage

You added updatedAt to the server's hrmDataStore and are passing it to the client. However, your client-side staleness logic in utils/hrm.ts uses user.lastUpdated, which the reducer sets to Date.now() (receipt time) on every WebSocket message.

The Problem: If the server broadcasts a snapshot because any user changed, lastUpdated gets refreshed for all users in that snapshot, even those whose sensors have stopped sending data (but haven't timed out on the server yet). This means the visual warning (10s) will never trigger as long as the WebSocket is active and other users are generating events. The client will perceive the data as "fresh" because the packet just arrived, effectively ignoring the sensor's actual silence.

The Fix: Update utils/hrm.ts to prefer user.updatedAt (origin timestamp from sensor) over user.lastUpdated (network receipt timestamp) for the staleness calculation.

🚮 Anti-AI-Slop Directives

  1. OVERLY VERBOSE COMMENTS: The comment block at the end of components/HrmTiles.tsx (lines 112-115) is unnecessary specific explainer text (likely LLM-generated). It explains standard React memo behavior. Action: Delete it.
  2. CODE RATIO / NOISE: The changes in deploy/runner/README.md are purely whitespace/formatting additions (adding newlines). This adds noise to the diff without value. Action: Revert changes to this file.
  3. DUPLICATE TYPES: You introduced ClientHrmData (reducer) and ActiveHrmData (utils). ActiveHrmData is just ClientHrmData + isDataStale. There is also HrmData. Action: Consolidate these into a single client-facing type in types/websocket.ts to avoid type proliferation.
  4. STALE FEATURES: The logic in webSocketReducer.ts for HRM_UPDATE correctly replaces the old "filter-then-merge" strategy with a "map-and-sync" strategy. This effectively removes the old feature logic. Good.

File-by-File Analysis

context/webSocketReducer.ts

  • Problem: Impure Reducer. Using Date.now() inside a reducer makes it impure and harder to test deterministically.
  • Suggestion: Ideally, pass the timestamp in the action payload. For now, it's acceptable given the scope, but the logical issue (Critical Point above) stems from relying on this local timestamp.

utils/hrm.ts

  • Problem: Logic gap on lastUpdated vs updatedAt.
  • Snippet Fix:
    // Use the sensor's timestamp if available, otherwise fall back to receipt time
    const referenceTime = user.updatedAt || user.lastUpdated || now
    const isDataStale = now - referenceTime > HRM_WARNING_THRESHOLD_MS

components/HrTileWrapper.tsx

  • Problem: Manual prop comparison in arePropsEqual is fragile. prevProps.user.value === nextProps.user.value works for scalars, but if you add new properties to user later, this custom comparator might mask updates.
  • Suggestion: Ensure this manual comparison is absolutely necessary. Often React.memo is premature optimization unless profiling proves lag.

Automated Checks

  • Tests: You added unit tests (utils/hrm.test.ts), which is excellent. Please add a test case specifically for the scenario where updatedAt is old but lastUpdated is recent (the "server broadcast stale data" scenario).

Review automatically published via RepoAuditor.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment