Skip to content

feat: auto-timezone update from source node GPS position#2925

Draft
rancur wants to merge 1 commit into
Yeraze:mainfrom
rancur:feat/auto-timezone-from-position
Draft

feat: auto-timezone update from source node GPS position#2925
rancur wants to merge 1 commit into
Yeraze:mainfrom
rancur:feat/auto-timezone-from-position

Conversation

@rancur
Copy link
Copy Markdown
Contributor

@rancur rancur commented May 6, 2026

Closes/relates

Relates to #2924 — opening as DRAFT so the maintainer can react to concrete code without committing to merge. The issue thread has the full design proposal; this PR is the foundational backend implementation.

Motivation

Mobile MeshMonitor instances (RV / boat / overlanding setups) typically have a GPS-equipped node that moves with the deployment. The server's TZ env stays static, so log timestamps and scheduled tasks (backup_time, system_backup_time, maintenanceTime) drift relative to local time as the deployment crosses timezones.

This PR adds an opt-in service that reads a designated source node's GPS position and persists the detected IANA timezone to settings whenever the node crosses a configurable distance threshold or the computed tz changes.

Design summary

  • Opt-in via 3 user settings (default off):
    • geofenceTzEnabled: boolean — default false
    • geofenceTzSourceNodeId: string — accepts !hex or bare node-num
    • geofenceTzThresholdMiles: number — default 20
    • (also geofenceTzIntervalMinutes — default 15)
  • 4 service-managed settings (written by the service, surfaced read-only in a future UI):
    • geofenceTzDetected — last computed IANA tz
    • geofenceTzLastLat / geofenceTzLastLon — last-applied position (for debounce)
    • geofenceTzLastCheckedAt — ms timestamp
  • Service modeled after autoDeleteByDistanceService for consistency: same lifecycle (start / stop / runNow / getStatus), same isRunning guard, same staggered initial delay.
  • TZ lookup via geo-tz (8.1.6, MIT) — chosen over tz-lookup because:
    • Actively maintained (last release 2026-03 vs. tz-lookup's 2022-06)
    • Newer TZ boundary changes (e.g. Egypt 2023 DST flip) are captured
    • Bundle size cost (~72 MB unpacked) is acceptable for a backend service that doesn't ship to clients
    • 4 small deps (turf, geobuf, pbf) are themselves stable
  • Distance via the existing calculateDistance Haversine util (src/utils/distance.ts) — same primitive used by autoDeleteByDistanceService.
  • State persistence via the existing settings repository — no schema migration needed because settings is a key-value store; only VALID_SETTINGS_KEYS was extended.
  • Restart strategy NOT included. The current architecture's TZ env is startup-only — applying a detected tz to running schedulers/log timestamps requires a process restart. That's the harder design question and is deferred to a follow-up PR per the issue's "design alignment first" comment.

What's in this PR

File Change LOC
src/server/services/geofenceTimezoneService.ts New service (start/stop/runNow/getStatus + check cycle) +290
src/server/services/geofenceTimezoneService.test.ts 13 vitest tests (all paths covered) +268
src/server/constants/settings.ts 8 new keys added to VALID_SETTINGS_KEYS +9
src/server/server.ts Service import + startup gate + restart hook +24
src/server/routes/settingsRoutes.ts SettingsCallbacks extension + change-handler +32
package.json / package-lock.json geo-tz ^8.1.6 dep (lock churn)
README.md Feature line under "Key Features" +1

Net signal LOC (excluding lock file): ~624 added, ~14 removed.

What's NOT in this PR (deliberate)

  • React admin UI for the new settings — happy to scaffold based on review feedback (SettingsTab.tsx extension following the documented useCallback dep-array pattern in CLAUDE.md).
  • Auto-restart of the server on tz change — the harder design question. Current TZ env is startup-only in this architecture. Open design space:
    1. Use Node's process.env.TZ = newTz and accept that already-running setInterval/setTimeout schedules don't pick it up
    2. Trigger an internal restart via process.exit(0) and rely on container/PM2 restart policy
    3. Keep TZ env-only and just persist the detection so the operator/orchestrator can apply it on next deploy
      Option 3 is what this PR does. I think it's the right minimal foundation.

Tests

13 new tests in src/server/services/geofenceTimezoneService.test.ts:

  • No-op when disabled (default off)
  • No-op when no source node configured
  • No-op when source node not found in DB
  • No-op when source node has no GPS yet
  • First-run detection writes detected tz + position
  • Debounce: small movement below threshold doesn't update state
  • Distance trigger: > threshold causes re-detection (Phoenix → Albuquerque)
  • TZ-change trigger fires even when distance is below threshold (boundary crossing)
  • Graceful tz-lookup failure
  • start() / stop() lifecycle
  • start() is idempotent
  • Haversine sanity: Phoenix → Albuquerque ~330 mi
  • Haversine sanity: NYC → LA ~2450 mi

All pass under npx vitest run. Full suite (4696 tests) also green.

Backwards compat

Default OFF — zero behavior change unless the operator explicitly enables geofenceTzEnabled. No schema migration, no new tables. New settings keys are append-only additions to VALID_SETTINGS_KEYS.

Maintainer-respect notes

This is filed as DRAFT to respect the design-alignment-first workflow in CLAUDE.md. Happy to:

  • Iterate on naming/structure based on review (geofenceTz* prefix vs. autoTimezone* vs. something else)
  • Scope the UI follow-up however makes sense (SettingsTab extension vs. dedicated panel)
  • Revise the design entirely if there's a better fit — e.g. driving off Virtual Node position instead of a designated source node
  • Add an API route (POST /api/services/geofence-tz/run-now, GET /api/services/geofence-tz/status) if that pattern is preferred over implicit restart-on-settings-change

Reference implementation

A working personal-deployment version (Python single-script that polls + restarts the docker container) has been running on the contributor's mobile node for several weeks. This PR ports the algorithm into the upstream architecture rather than asking operators to bolt on a host-level cron.

Adds an opt-in service that periodically reads a designated source node's
GPS position and computes the local IANA timezone via geo-tz. When the
source node moves more than a configurable distance (default 20 mi) or
the computed tz changes, the new timezone is persisted to settings.

Designed for mobile MeshMonitor deployments (RV / boat / overlanding)
where the source node moves between timezones but the server's TZ env
stays static. Closes/relates to upstream issue Yeraze#2924.

Scope of this PR is intentionally backend-only:
- New service src/server/services/geofenceTimezoneService.ts modeled
  after autoDeleteByDistanceService for consistency.
- 3 user-configurable settings (default off): geofenceTzEnabled,
  geofenceTzSourceNodeId, geofenceTzThresholdMiles.
- 4 service-managed settings: geofenceTzDetected, geofenceTzLastLat,
  geofenceTzLastLon, geofenceTzLastCheckedAt.
- Service registration in startup flow (src/server/server.ts) plus
  settings-change restart hook (src/server/routes/settingsRoutes.ts).
- 13 vitest tests covering disabled / no-node / no-gps / first-run /
  debounce / threshold-trigger / tz-change / lookup-failure paths.

NOT in this PR (deliberate, see Yeraze#2924 discussion):
- React admin UI for the new settings (follow-up)
- Auto-restart of the server on tz change (TZ env is startup-only in
  current architecture; this is the harder design question)

Default off — zero behavior change unless operator opts in.
@Yeraze
Copy link
Copy Markdown
Owner

Yeraze commented May 7, 2026

So this is just to reconfigure the MeshMonitor install, not the actual node?

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.

3 participants