Skip to content

fix(automation): timed events firing across all sources#3479

Merged
Yeraze merged 1 commit into
mainfrom
fix/timed-events-source-scope
Jun 15, 2026
Merged

fix(automation): timed events firing across all sources#3479
Yeraze merged 1 commit into
mainfrom
fix/timed-events-source-scope

Conversation

@Yeraze

@Yeraze Yeraze commented Jun 15, 2026

Copy link
Copy Markdown
Owner

Problem

Users reported that Timed Events (Timer Triggers) configured for one source fire on all sources.

Root cause

updateTimerTriggerResult (src/server/meshtasticManager.ts) had an asymmetric read/write:

// reads the PER-SOURCE key
const json = await databaseService.settings.getSettingForSource(this.sourceId, 'timerTriggers');
...
// writes the GLOBAL un-namespaced key  ← bug
await databaseService.settings.setSetting('timerTriggers', JSON.stringify(timerTriggers));

Every time a timer fired, it copied that source's full trigger list into the global timerTriggers setting. The settings GET-merge (settingsRoutes.ts { ...global, ...sourceSettings }) then surfaces that global value to any source that has no per-source override — so the trigger gets read, scheduled, and fired on those sources too. The scheduler and the fire action themselves are correctly per-source; the leak was purely this persistence write.

This is the same class of bug as #2839 (multi-source automation spam), which is exactly why getSettingForSource / setSourceSetting deliberately do not fall back to global.

Fix

Make the write source-scoped and symmetric with the read:

await databaseService.settings.setSourceSetting(this.sourceId, 'timerTriggers', JSON.stringify(timerTriggers));

Adds meshtasticManager.timerTriggerScope.perSource.test.ts asserting the result write targets source:<id>:timerTriggers and never the global key (success and error paths).

Scope notes

  • Secondary hardening (not changed here): the settings GET-merge still lets a source inherit a global timerTriggers value, and a null-sourceId save fans restartTimerScheduler out to every manager. Those are pre-existing behaviors that are only dangerous because this write poisoned the global key — with this fix the global key is no longer written by firing. Happy to follow up on excluding automation keys from the global merge if you want belt-and-suspenders.
  • Existing data: installs already contaminated may have a stale global timerTriggers and/or triggers copied into a source's namespace. This fix stops further contamination but does not retroactively clean those rows — let me know if you want a cleanup migration.

Validation

  • New test: 2/2 pass
  • tsc --noEmit: clean
  • Full Vitest suite: 6448 pass, 0 fail, 0 suite failures

🤖 Generated with Claude Code

…ss-source firing)

Timer Triggers ("Timed Events") configured for one source were firing on all
sources. Root cause: updateTimerTriggerResult READ the per-source settings key
(getSettingForSource(this.sourceId, 'timerTriggers')) but WROTE the
un-namespaced GLOBAL key (setSetting('timerTriggers', ...)). On every fire it
copied that source's trigger list into the global key; the settings GET-merge
then surfaced that global value to other sources lacking a per-source override,
so the trigger got scheduled and fired everywhere.

The per-source helpers deliberately do not fall back to global (see #2839), so
read and write must both be source-scoped. Switch the write to
setSourceSetting(this.sourceId, 'timerTriggers', ...).

Adds a perSource regression test asserting the result write targets the
source-scoped key and never the global key.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jun 15, 2026

Copy link
Copy Markdown

Claude finished @Yeraze's task —— View job


Code Review Complete ✅

Review Checklist:

  • Read and understand the problem description
  • Analyze the fix implementation in meshtasticManager.ts
  • Review the new test coverage
  • Evaluate code quality and best practices
  • Check for potential bugs or edge cases
  • Assess performance and security considerations
  • Provide comprehensive feedback

Summary

This is an excellent fix for a critical multi-source bug. The implementation is correct, well-documented, and thoroughly tested.

Code Quality & Best Practices ✅

Strengths:

  • Precise fix: Changes only the problematic line (setSettingsetSourceSetting)
  • Excellent documentation: The inline comment at lines 3787-3793 clearly explains the root cause and why source-scoped operations are required
  • Consistent with architecture: Follows MeshMonitor's multi-source design principles
  • References existing issue: Mentions [BUG] Multiple source : automation spam #2839, showing awareness of related problems

Code Quality:

  • Clean, minimal change with maximum impact
  • Proper error handling preserved
  • Logging maintained for debugging
  • Method signature unchanged (good for compatibility)

Test Coverage ✅

The new test file meshtasticManager.timerTriggerScope.perSource.test.ts is comprehensive:

  • Complete scenario coverage: Tests both success and error paths
  • Precise assertions: Verifies the fix writes to source-scoped key and never touches global key
  • Proper mocking: Well-structured mocks for database service
  • Clear test names: Self-documenting test descriptions
  • Regression protection: Explicitly tests the bug scenario to prevent reoccurrence

Lines 73-77 and 95-98 are particularly good - they assert the global key is NOT written to.

Security Considerations ✅

  • No security implications: This is purely a data scoping fix
  • Improves isolation: Actually enhances security by properly isolating source data
  • No credential exposure: Timer trigger data doesn't contain sensitive information

Performance Considerations ✅

  • No performance impact: Same number of database operations
  • Source-scoped operations: May actually be slightly faster as they don't fall back to global lookups
  • Minimal overhead: Single line change with no additional complexity

Potential Issues & Edge Cases

Minor Considerations (not blockers):

  1. Existing contaminated data: As noted in the PR description, installations may have stale global timerTriggers or triggers copied into wrong source namespaces. The fix stops further contamination but doesn't clean existing bad data.

  2. Cross-source inheritance: The settings GET-merge still allows sources to inherit global automation settings. While this isn't changed by this fix, it could theoretically still cause issues if global keys get polluted by other means.

Verification of Fix Scope:

I checked for other similar patterns in the codebase:

  • ✅ Auto-favorite operations use setSourceSetting correctly (lines 10850, 10889, 10966)
  • ✅ Announcement timing uses appropriate scoping (lines 11260-11263)
  • ✅ Geofence triggers still use global setSetting - this appears intentional as geofences may be designed as global

Recommendation: APPROVE

This fix:

  • ✅ Correctly addresses the root cause
  • ✅ Maintains backward compatibility
  • ✅ Has excellent test coverage
  • ✅ Follows established patterns
  • ✅ Includes clear documentation
  • ✅ No security or performance concerns

The implementation demonstrates deep understanding of MeshMonitor's multi-source architecture and the subtle but critical difference between setSetting and setSourceSetting.


@Yeraze Yeraze merged commit f4f28ba into main Jun 15, 2026
19 checks passed
@Yeraze Yeraze deleted the fix/timed-events-source-scope branch June 15, 2026 11:32
@Yeraze Yeraze mentioned this pull request Jun 15, 2026
Yeraze added a commit that referenced this pull request Jun 15, 2026
Bump version to 4.10.4 across package.json, package-lock.json,
helm/meshmonitor/Chart.yaml, desktop/package.json, and
desktop/src-tauri/tauri.conf.json.

Promote CHANGELOG [Unreleased] to [4.10.4] and add the user-facing fixes
merged since 4.10.3: timed-events cross-source firing (#3479), module
config save for Traffic Management/Status Message (#3464), Auto Favorites
firmware-support cache (#3482), MeshCore Share Contact error surfacing
(#3481), and the Firefox-Android delivered-icon CSS fix (#3477). The
air-quality particle-count fix (#3483) was already in [Unreleased].

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant