Skip to content

fix: MeshCore low battery and inactive node notifications never fire#3672

Merged
Yeraze merged 3 commits into
mainfrom
claude/great-dijkstra-tj3eoj
Jun 23, 2026
Merged

fix: MeshCore low battery and inactive node notifications never fire#3672
Yeraze merged 3 commits into
mainfrom
claude/great-dijkstra-tj3eoj

Conversation

@Yeraze

@Yeraze Yeraze commented Jun 23, 2026

Copy link
Copy Markdown
Owner

Summary

Fixes #3671 — MeshCore "Notify on Low Battery" never fires regardless of threshold configured.

Root cause: Both lowBatteryNotificationService and inactiveNodeNotificationService build their list of sources to scan by calling sourceManagerRegistry.getAllManagers(). However, MeshCore managers are registered exclusively in meshcoreManagerRegistry and are never added to sourceManagerRegistry. This is even noted in a comment in meshcoreManager.ts:

"even though MeshCoreManager isn't registered in sourceManagerRegistry"

As a result:

  • The voltage-threshold battery check never ran for any MeshCore source
  • Inactive-node notifications also never fired for MeshCore sources (same bug, same fix)
  • The threshold setting itself saves correctly — the service just never reaches MeshCore

Changes

  • lowBatteryNotificationService.ts — combine both registries when building the manager list; map MeshCoreManager instances to the minimal { sourceId, sourceType: 'meshcore' } shape the loop needs
  • inactiveNodeNotificationService.ts — same fix
  • Both test files — add meshcoreManagerRegistry mock; update the MeshCore-specific test cases to set managers via meshcoreManagerRegistry.list() (the actual production path) rather than sourceManagerRegistry.getAllManagers()

Test plan

  • All 27 tests in lowBatteryNotificationService.test.ts pass
  • All tests in inactiveNodeNotificationService.test.ts pass
  • Full Vitest suite passes (0 failures)
  • MeshCore tests now go through meshcoreManagerRegistry.list() → adapter, matching production code path

🤖 Generated with Claude Code

https://claude.ai/code/session_01Lr9Jh3yPne1NVhnw4JmUNY


Generated by Claude Code

Both lowBatteryNotificationService and inactiveNodeNotificationService
iterate sourceManagerRegistry.getAllManagers() to find sources to check,
but MeshCore managers are registered exclusively in meshcoreManagerRegistry
and are never added to sourceManagerRegistry. As a result, no MeshCore
source was ever included in the notification scan — the voltage threshold
check and inactivity check simply never ran.

Fix: combine both registries when building the manager list in each
service, mapping MeshCoreManager instances to the minimal
{ sourceId, sourceType: 'meshcore' } shape the services need.

Update test fixtures to mock meshcoreManagerRegistry.list() for the
MeshCore test cases, reflecting the actual production code path.

Fixes: #3671

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lr9Jh3yPne1NVhnw4JmUNY
@github-actions

github-actions Bot commented Jun 23, 2026

Copy link
Copy Markdown

Claude finished @Yeraze's task —— View job


PR Review

Tasks:

  • Gather context on PR
  • Read changed files
  • Analyze code and provide review feedback

Summary

This PR correctly identifies and fixes a real bug: lowBatteryNotificationService and inactiveNodeNotificationService only iterated over sourceManagerRegistry, which never contains MeshCore managers. The fix — combining both registries at scan time — is the right approach given the architectural split between the two registries.

The fix is clean, minimal, and well-tested. A few observations below.


Code Quality

Duplicate pattern in two services (lowBatteryNotificationService.ts:112-116, inactiveNodeNotificationService.ts:110-114)

Both services now contain identical 5-line blocks to build the combined manager list:

type MinimalManager = { sourceId: string; sourceType: string };
const managers: MinimalManager[] = [
  ...sourceManagerRegistry.getAllManagers(),
  ...meshcoreManagerRegistry.list().map(m => ({ sourceId: m.sourceId, sourceType: 'meshcore' as const })),
];

This is fine for two callsites, but if a third notification service appears (e.g., a future "node offline" alert), the same block would need to be copied again. Consider extracting a shared helper (e.g., getAllSourceManagers() in a shared utilities module or in meshcoreRegistry.ts itself) to centralise this pattern. Not a blocker for merging — just worth noting.

Type alias is re-declared in both files

type MinimalManager = { sourceId: string; sourceType: string } is declared inside each private method. Moving it to module scope in each file, or into a shared types file, would be cleaner.


Potential Issues

sourceManagerRegistry.getAllManagers() already includes Meshtastic-only sources — duplicate sourceId risk is low but worth confirming

The combined array could theoretically contain the same sourceId twice if a manager somehow appeared in both registries. Based on the current architecture this can't happen (MeshCore managers are only in meshcoreManagerRegistry), but a quick comment or assert would make that invariant explicit.

nodeNum: 0 for MeshCore alerts (lowBatteryNotificationService.ts:264, inactiveNodeNotificationService.ts:261)

MeshCore nodes don't have a Meshtastic-style nodeNum, so 0 is used as a placeholder. This is consistent with the existing MeshCore data model, but 0 is also a technically valid (if unusual) node number. A sentinel like -1 or null would be unambiguous. Again, not a new issue introduced by this PR, but worth documenting.


Test Coverage

Test coverage is good and the structural change is important: MeshCore tests now set up managers via mockMeshcoreList (the production path) rather than mockGetAllManagers (which would never be hit in production). This correctly validates the real code path.

A few minor observations:

  • The beforeEach in lowBatteryNotificationService.test.ts:77-78 sets mockGetAllManagers to a single Meshtastic source and mockMeshcoreList to empty ([]). The MeshCore describe block (line 223-228) overrides both for its tests. This is clear and intentional — good.

  • inactiveNodeNotificationService.test.ts has a leftover mockGetInactiveMonitoredNodesAsync mock alias (line 29) that duplicates mockGetInactiveMonitoredNodesAsync (re-aliased to nodes.getInactiveMonitoredNodes on line 30-31). The top-level const on line 16 is declared but the binding on line 29 (getInactiveMonitoredNodesAsync) refers to a different path (databaseService.getInactiveMonitoredNodesAsync) which is the old pre-4.x direct-method path. This is likely dead code from a prior test refactor. It doesn't affect test correctness but could be cleaned up.


Security / Permissions

The permission check (databaseService.checkPermissionAsync) is correctly preserved for MeshCore sources — users who don't have nodes:read on a MeshCore source are skipped before any alert query runs. Good.


Overall Assessment

Approve. The root cause is correctly identified, the fix is minimal and targeted, tests cover the production code path, and no regressions are introduced. The suggestions above are non-blocking improvements.

@github-actions

github-actions Bot commented Jun 23, 2026

Copy link
Copy Markdown

Claude finished @Yeraze's task —— View job


PR Review

Tasks:

  • Gather context on PR
  • Read changed files
  • Analyze code and provide review feedback

Summary

This PR correctly identifies and fixes a real bug: lowBatteryNotificationService and inactiveNodeNotificationService only iterated over sourceManagerRegistry, which never contains MeshCore managers. The fix — combining both registries at scan time — is the right approach given the architectural split between the two registries.

The fix is clean, minimal, and well-tested. A few observations below.


Code Quality

Duplicate pattern in two services (lowBatteryNotificationService.ts:112-116, inactiveNodeNotificationService.ts:110-114)

Both services now contain identical 5-line blocks to build the combined manager list:

type MinimalManager = { sourceId: string; sourceType: string };
const managers: MinimalManager[] = [
  ...sourceManagerRegistry.getAllManagers(),
  ...meshcoreManagerRegistry.list().map(m => ({ sourceId: m.sourceId, sourceType: 'meshcore' as const })),
];

This is fine for two callsites, but if a third notification service appears (e.g., a future "node offline" alert), the same block would need to be copied again. Consider extracting a shared helper (e.g., getAllSourceManagers() in a shared utilities module or in meshcoreRegistry.ts itself) to centralise this pattern. Not a blocker for merging — just worth noting.

Type alias is re-declared in both files

type MinimalManager = { sourceId: string; sourceType: string } is declared inside each private method. Moving it to module scope in each file, or into a shared types file, would be cleaner.


Potential Issues

sourceManagerRegistry.getAllManagers() already includes Meshtastic-only sources — duplicate sourceId risk is low but worth confirming

The combined array could theoretically contain the same sourceId twice if a manager somehow appeared in both registries. Based on the current architecture this can't happen (MeshCore managers are only in meshcoreManagerRegistry), but a quick comment or assert would make that invariant explicit.

nodeNum: 0 for MeshCore alerts (lowBatteryNotificationService.ts:264, inactiveNodeNotificationService.ts:261)

MeshCore nodes don't have a Meshtastic-style nodeNum, so 0 is used as a placeholder. This is consistent with the existing MeshCore data model, but 0 is also a technically valid (if unusual) node number. A sentinel like -1 or null would be unambiguous. Again, not a new issue introduced by this PR, but worth documenting.


Test Coverage

Test coverage is good and the structural change is important: MeshCore tests now set up managers via mockMeshcoreList (the production path) rather than mockGetAllManagers (which would never be hit in production). This correctly validates the real code path.

A few minor observations:

  • The beforeEach in lowBatteryNotificationService.test.ts:77-78 sets mockGetAllManagers to a single Meshtastic source and mockMeshcoreList to empty ([]). The MeshCore describe block (line 223-228) overrides both for its tests. This is clear and intentional — good.

  • inactiveNodeNotificationService.test.ts has a leftover mock alias issue on line 28-30: getInactiveMonitoredNodesAsync is declared as a const on line 15 and then also bound to databaseService.getInactiveMonitoredNodesAsync on line 28 and to databaseService.nodes.getInactiveMonitoredNodes on line 30. Both the top-level getInactiveMonitoredNodesAsync path (line 28) and the nodes.getInactiveMonitoredNodes path (line 30) point to the same mock function, but line 28 exposes a now-dead legacy path (databaseService.getInactiveMonitoredNodesAsync) that the service no longer calls. It doesn't affect test correctness but could confuse future readers.


Security / Permissions

The permission check (databaseService.checkPermissionAsync) is correctly preserved for MeshCore sources — users who don't have nodes:read on a MeshCore source are skipped before any alert query runs. Good.


Overall Assessment

Approve. The root cause is correctly identified, the fix is minimal and targeted, tests cover the production code path, and no regressions are introduced. The suggestions above are non-blocking improvements.

The service uses databaseService.nodes.getInactiveMonitoredNodes (the
current 4.x path); the top-level getInactiveMonitoredNodesAsync key in
the mock was the old pre-4.x direct-method path and is never called.

Addressed review feedback from PR #3672.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lr9Jh3yPne1NVhnw4JmUNY
@github-actions

github-actions Bot commented Jun 23, 2026

Copy link
Copy Markdown

Claude finished @Yeraze's task —— View job


PR Review

Tasks:

  • Gather context on PR
  • Read changed files
  • Analyze code and provide review feedback

Summary

This PR correctly identifies and fixes a real bug: lowBatteryNotificationService and inactiveNodeNotificationService only iterated over sourceManagerRegistry, which never contains MeshCore managers. The fix — combining both registries when building the manager list — is the right approach given the architectural split.

The fix is clean, minimal, and well-tested. A few observations below.


Code Quality

Duplicate pattern across two services (lowBatteryNotificationService.ts:112-116, inactiveNodeNotificationService.ts:110-114)

Both services now contain identical 5-line blocks:

type MinimalManager = { sourceId: string; sourceType: string };
const managers: MinimalManager[] = [
  ...sourceManagerRegistry.getAllManagers(),
  ...meshcoreManagerRegistry.list().map(m => ({ sourceId: m.sourceId, sourceType: 'meshcore' as const })),
];

This is fine for two callsites, but if a third notification service is added, the pattern needs to be copied again. Consider extracting a shared helper (e.g., getAllSourceManagers() in a shared utilities module or in meshcoreRegistry.ts itself). Not a blocker — just worth tracking.

type MinimalManager is function-scoped in both files

It's declared inside each private method. Moving it to module scope, or into a shared types file, would be cleaner but is not blocking.


Potential Issues

nodeNum: 0 placeholder for MeshCore alerts (lowBatteryNotificationService.ts:264, inactiveNodeNotificationService.ts:260)

MeshCore nodes don't have a Meshtastic-style nodeNum, so 0 is used as a placeholder. 0 is technically a valid (if unusual) node number. A sentinel like -1 or null would be unambiguous. This is pre-existing behaviour, not introduced by this PR, but worth documenting.

Duplicate sourceId risk

The combined array could theoretically contain the same sourceId twice if a manager somehow appeared in both registries. Based on the current architecture this cannot happen (MeshCore managers are exclusively in meshcoreManagerRegistry), but a brief comment noting this invariant would make the intent clear to future maintainers.


Test Coverage

Test coverage is good. Importantly, the MeshCore tests now use mockMeshcoreList (the real production path) rather than mockGetAllManagers (which the service never populates with MeshCore entries). This correctly validates the actual code path.

Minor observation — inactiveNodeNotificationService.test.ts line 15

The variable mockGetInactiveMonitoredNodesAsync (line 15) is declared as a const but then reused as the mock for databaseService.nodes.getInactiveMonitoredNodes (line 29). The variable name carries an Async suffix from the old pre-4.x direct-method path (databaseService.getInactiveMonitoredNodesAsync). The mock still works correctly because it's bound to the right repository path on line 29, but the name could confuse future readers. Low priority, easy cleanup when someone is next in the file.


Security / Permissions

The databaseService.checkPermissionAsync gate is correctly preserved for MeshCore sources — users lacking nodes:read on a MeshCore source are skipped before any alert query runs.


Overall Assessment

Approve. Root cause is correctly identified, fix is minimal and targeted, tests cover the real production code path, and no regressions are introduced. All suggestions above are non-blocking.

@Yeraze Yeraze merged commit fbed5b7 into main Jun 23, 2026
20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] Notify on Low Battery

2 participants