feat(notifications): lock-screen when a device goes quiet or the fuse is over - #983
Conversation
…-fuse house The phone app's catalogue had no sentence for a driver that stopped reporting. The engine already knew how; it just never rendered the app's words. driver.offline and fuse.over_limit use the same thresholds as the operator rules so a blip or a kettle is not a notification. Signed-off-by: Cursor Agent <cursoragent@cursor.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: add0210347
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| {Type: PushDriverOffline, Enabled: false, ThresholdS: DefaultThresholdS, Priority: 4, CooldownS: DefaultCooldownS}, | ||
| {Type: PushFuseOverLimit, Enabled: false, ThresholdS: 30, Priority: 5, CooldownS: 900}, |
There was a problem hiding this comment.
Coalesce alias rules before publisher fan-out
When a site already has driver_offline or fuse_over_limit enabled and the phone enables the corresponding new catalogue toggle, both rule entries independently dispatch while deliver sends every dispatch to every installed publisher. The same outage therefore produces two web pushes and two ntfy posts, rather than one notification per selected transport; coalesce these aliases or route catalogue events only to web push.
Useful? React with 👍 / 👎.
| if _, seen := s.fuseFirstOverAt[phase]; !seen { | ||
| s.fuseFirstOverAt[phase] = now |
There was a problem hiding this comment.
Start fuse timers only for enabled rules
When both fuse rules are disabled, this unconditional write still records how long each phase has been over its limit. If either rule is later enabled while the phase remains over, Reload does not clear fuseFirstOverAt, so the next evaluation can fire immediately using time accumulated before opt-in instead of waiting for the configured threshold; the previous evaluator returned before starting this timer when fuse_over_limit was disabled.
Useful? React with 👍 / 👎.
miravoss26
left a comment
There was a problem hiding this comment.
Reviewed the diff. This is the backend counterpart to ftw-webapp#56: adds driver.offline and fuse.over_limit as push-catalogue kinds, sharing thresholds with the existing driver_offline/fuse_over_limit ntfy rules.
evaluateFusenow checks both rule types (EventFuseOverLimit,PushFuseOverLimit) against a shared per-phase over-window (fuseFirstOverAt), but each keeps its own latch (alreadyFired[typ+phase]) and cooldown — checked the reset paths on both sides and they stay independent per rule, so enabling one doesn't silently affect the other's firing/cooldown behavior.dispatch()routes purely on whetherrule.Typehas aPushSentencesentry, which cleanly separates catalogue-rendered lock-screen text from the ntfy templates without a second flag to keep in sync.- Tests cover both new kinds end-to-end (threshold not yet met → met, disabled stays silent, exact rendered title/body).
- CI green across the full Go/web/full-stack suite; already has a human approval.
No blocking findings. Safe to merge from my read — matches its client-side pair for a consistent release.


Contract-pair: srcfl/ftw-webapp@cursor/push-and-restart-0d20
Accepted text proposal
Issue or Discussion: Slack follow-up from HannesB / Fredrik — notifications in the web app when a driver is down, and restart of FTW from the phone.
Maintainer comment that accepted this scope: Fredrik asked for both while the Pixii-offline work was in flight. Notifications already exist in the PWA; there was no sentence for a driver being down.
What changed
Two catalogue kinds, worded by
contract/push-catalogue.yaml(paired with the app):driver.offline— “A device went quiet” / “{name} stopped answering.” Same 10-minute silence threshold as the operatordriver_offlinerule.fuse.over_limit— “The house is drawing too much” / “{phase} is over the fuse rating.” Same 30-second over-rating threshold asfuse_over_limit.The engine already evaluated those conditions for ntfy. It now also renders the app’s sentences when the catalogue kinds are enabled. Turning notifications on in the phone app enables the new kinds with the rest of the catalogue; an existing subscription sees them as new toggles, off until saved.
Out of scope: recovered / concurrent-offline (too noisy or overlapping), restart (that is the paired webapp PR).
Why
Hannes’s Pixii went quiet after a network blip and nobody was told. The PWA already notifies for charging, updates, and an unreachable box. A device that stops reporting is the case he actually needed.
Boundaries and safety
DefaultRules; the phone opts in.box.unreachableremains the relay dead-man’s switch, not a rule on the box.Verification
go test ./internal/notifications/ ./internal/api/— includingTestCatalogueDriverOfflineRendersFromCatalogue,TestCatalogueFuseOverLimitRendersFromCatalogue,TestRulesGetOffersKindsAddedAfterTheConfigWasSaved.Checklist