Appearance
SafeCall ntfy / Keryx alert topics
Canonical inventory of the ntfy topics SafeCall publishes and Keryx can subscribe to.
Code source of truth: server/src/ntfy_topics.ts. Admin-facing labels: admin.ntfy_topics.{id}_label in the UI catalogs. Publisher: server/src/ntfy.ts.
This page is for implementation and product work on Keryx and SafeCall notification cadence. Ordinary administrators should keep seeing alert categories, not topic IDs.
Naming
| Piece | Pattern | Example (server_id = TST000) |
|---|---|---|
| Topic id | stable enum | sos |
| ntfy topic name | {server_id}-{topic_id} | TST000-sos |
| Admin / QR label | translated catalog string | Panic button alerts |
GET /api/v1/features returns all eight staff categories as ntfy_topics. Keryx QR generation (POST /api/v1/keryx/qr) includes only the categories the admin left checked. Mobile Access checks the seven operational categories by default. test and debug access are unchecked in the collapsed Admin features section and require an explicit warning confirmation.
Keryx renders one category card for every topic in its QR. It keeps transport, topic names, and subscribe credentials out of the ordinary UI. Each category has a default notification cadence and optional per-known-device overrides; suppressed alerts are still stored and update category state.
Staff categories
| Id | ntfy name | Admin label (EN) | Admin label (CS) | What it is | Typical trigger | Title / body | Tags | Priority |
|---|---|---|---|---|---|---|---|---|
sensors | {server_id}-sensors | Sensor threshold alerts | Prah senzoru | Temperature / humidity out of range | MQTT adv1 or sData when a configured range is exceeded | Title = reason (Temperature (x°C) out of range); body adds for device: {name} ({mac or asset_uid}) | warning,thermometer | urgent |
sos | {server_id}-sos | Panic button alerts | Poplachové tlačítko | Panic button press | MQTT adv4 on a panic asset (beacon_is_pushed) | Title = KISS text or Panic button pushed; body adds device | warning,rotating_light | urgent |
watches | {server_id}-watches | Watch button alerts | Poplachové tlačítko hodinek | Watch button press | MQTT adv4 on a watch asset | Title = KISS text or Watch button pushed; body adds device | warning,rotating_light | urgent |
batteries | {server_id}-batteries | Low battery alerts | Slabá baterie | Low battery on button/sensor adv8 | MQTT adv8 when vbatt < 2800 mV | Title = Low battery (xV); body adds device | warning,battery | urgent |
notseen | {server_id}-notseen | Device not seen alerts | Zařízení neviděno | Panic, watch, or sensor stopped reporting | Inactivity timer in cache.ts | Title = Not seen for more than {n} minutes; body adds device | panic/watch: warning,rotating_light; sensor: warning,thermometer | urgent |
calibration | {server_id}-calibration | Calibration expiry | Vypršení kalibrace | Assigned sensor (or untyped) device past calibration interval | Hourly checker in calibration.ts | Title = Calibration expired: {asset or MAC}; body = asset uid, MAC, type, last calibrated, interval | warning,wrench | default |
test | {server_id}-test | Notification pipeline tests | Testy kanálu oznámení | Prove the ntfy path without KISS | Dashboard Recheck; Watch/Panic Test Send (is_test MQTT) | Recheck: Dashboard ntfy recheck / SafeCall {server_id} notification pipeline test. Test Send: Notification pipeline test / adds device and [adv] | Recheck: white_check_mark. Test Send: tags of the fake adv type | Recheck: min. Test Send: urgent |
health | {server_id}-health | Installation health alerts | Stav instalace | Install / fleet problems | Status snapshot transitions (see below) | Title/body are the health event; default tags warning,triangular_flag_on_post | warning,triangular_flag_on_post | high |
English catalog labels are what the admin UI and QR labels map use. The English fallbacks inside ntfy_topics.ts differ slightly (Sensor alerts, Device not-seen alerts, Install health) and are only used when a translation key is missing.
How often each topic can fire
SafeCall separates moment events from stateful conditions. SOS, watch, and test messages are moments. Sensors, batteries, not-seen, calibration, and health are conditions with explicit active and resolved transitions. Condition state, successful delivery time, and pending digest work persist in SQLite.
HTTP 429 from ntfy starts a global backoff (default 30 s, or Retry-After). Immediate production alerts retain the existing retry queue. Digest work stays in the durable outbox and is retried by the scheduler. Test publishes are not queued and intentionally bypass every cooldown.
| Id | Default server policy | Subject / grouping | Effective behavior |
|---|---|---|---|
sensors | Individual, immediate first alert, 6 h repeat | Stable asset; temperature/humidity facets remain detail and do not bypass the per-sensor cooldown | Recovery is an explicit quiet state update. A daily quiet snapshot lets Keryx establish current state |
sos | Immediate | One configurable 40 s per-device window shared by event logging, KISS, and ntfy | Multiple gateways reporting one press produce one event; a later press after the window is a new moment |
watches | Immediate | Same 40 s button window as SOS | Same as SOS |
batteries | Bulk every 24 h | One digest on the batteries topic with per-device items | Admin can choose a shorter fixed interval or individual per-device delivery |
notseen | Panic/watch immediate with 6 h critical repeat; sensors bulk every 24 h | Stable asset and device type | Seeing the device again resolves the condition. Fridge timeout remains max(10 min, 2 × update_interval) |
calibration | Bulk every 24 h | Device assignment / stable asset | Hourly checker updates state; admin can choose shorter or individual delivery |
test | Immediate, exempt | Unique test occurrence | Every Recheck / Test Send; no Keryx cadence control |
health | MQTT/KISS/ntfy failures immediate with 6 h critical repeat; fleet exceptions bulk every 24 h | Infrastructure key or individual fleet issue | Recoveries are explicit. Vendor ops follows the same split |
Bulk intervals use fixed wall-clock buckets anchored at 09:00 Europe/Prague by default. The timezone, anchor, modes, and intervals are editable in SafeCall settings. Digests are one message per existing category topic unless the 4 KiB push limit requires numbered parts.
server_ntfy_enabled gates staff production and test publishes. Health site posts also require that flag. Vendor ops copies of health are independent (server_ntfy_ops_enabled + non-empty server_ntfy_ops_topic).
SafeCall alert record
The ntfy title and first body section remain readable in the stock ntfy app. SafeCall appends a deterministic SafeCall alert data v1 section containing the category, moment/condition kind, phase, alert/state intent, stable subject, device identity, timestamp, sequence, and optional digest items. Keryx hides that transport block and uses it to update cards and apply per-device policy. Messages without the record remain visible as legacy history but cannot claim authoritative current state.
Minting a Keryx QR also publishes a silent current-state snapshot for each stateful category included in that QR so a newly scanned phone can leave Waiting for status without waiting for the next 09:00 digest. Those snapshots do not mark the scheduled digest as delivered. Dashboard Recheck and Watch/Panic Test Send go to {server_id}-test, not -sos.
Keryx-first compatibility rollout
Ship Keryx's dual parser and local policy support before enabling the new SafeCall envelope and cadence in a deployed installation. During the overlap:
- New Keryx parses
SafeCall alert data v1, updates category cards, and applies local cadence. Legacy bodies stay in history only. - Older Keryx and stock ntfy keep showing the human title and summary.
- Multiplexing reduces Keryx's own SSE count. It does not reduce connections opened by third-party stock ntfy subscribers.
MQTT / source → topic
| Source | Asset / device | Topic id |
|---|---|---|
MQTT adv1 (temp/humidity out of range) | sensor (or any asset with ranges) | sensors |
MQTT sData (temp/humidity out of range) | fridge/virtual-MAC sensor | sensors |
MQTT adv4 pushed, type panic | panic | sos |
MQTT adv4 pushed, type watch | watch | watches |
MQTT adv8 vbatt < 2800 mV | whatever asset owns that MAC | batteries |
| Inactivity timeout | panic, watch, sensor | notseen |
| Hourly calibration job | assigned sensor/untyped device | calibration |
| Dashboard Recheck; Watch/Panic Test Send | n/a | test |
| Status MQTT/KISS/ntfy/fleet transitions | install | health |
EVENTS_NTFY_ADV_TOPICS maps adv1/sdata → sensors, adv4 → sos, adv8 → batteries. Watch presses and not-seen alerts override that map to watches and notseen.
Low battery is not evaluated on adv1 vbatt, only on adv8.
Health event keys
status_report.ts dual-publishes each of these to {server_id}-health and, when ops is on, to the vendor topic.
cooldown_subject | When |
|---|---|
mqtt_down | MQTT was connected and becomes not connected |
kiss_down | KISS was up and becomes unreachable (only if KISS is configured) |
ntfy_down | ntfy reachability was ok and becomes not ok (ignores HTTP 429) |
ntfy_test_failed | Recheck POST to test failed and was not rate-limited |
fleet:{stale|never_seen}:{asset_uid} | One fleet condition appears or resolves |
Ops copy: same body, title prefixed with {server_id}:, tags warning,triangular_flag_on_post, priority high. Ops is not a Keryx category and is omitted from GET /features.
Topics that are not Keryx categories
| Name | Publisher | Audience | In QR / features? |
|---|---|---|---|
{binary_name}-ops (default safecall-ops) | ntfy_publish_ops_health | Vendor diagnostics | No |
{server_id}-service | Monit check script from scripts/cli/monit.ts | Process up/down | No |
APNs / FCM for com.sourcectl.keryxapp / com.sourcectl.keryx | ntfy firebase-key-file → Keryx Firebase → APNs/FCM | Wake of Keryx | n/a (not an alert category); see ntfy-fcm-apns.md |
Config that changes cadence
| Key | Default | Effect |
|---|---|---|
server_ntfy_enabled | true | Master switch for staff topic POSTs (including {server_id}-health) |
server_ntfy_sensors_cooldown_seconds | 21600 | Per-sensor repeat interval |
server_ntfy_button_dedup_seconds | 40 | Shared SOS/watch cross-gateway event window |
server_ntfy_critical_cooldown_seconds | 21600 | Panic/watch not-seen and infrastructure repeat interval |
server_ntfy_digest_timezone | Europe/Prague | Wall-clock timezone for digest boundaries |
server_ntfy_digest_time | 09:00 | Daily anchor for fixed intervals |
server_ntfy_{batteries,sensor_notseen,calibration,fleet_health}_mode | bulk | Bulk or individual delivery for the named policy |
server_ntfy_{batteries,sensor_notseen,calibration,fleet_health}_interval_seconds | 86400 | Digest interval or individual repeat interval |
server_ntfy_cooldown_seconds | 600 | Deprecated compatibility value for older binaries |
server_ntfy_server_url | brand notify host | Client ntfy base URL |
server_ntfy_ops_enabled | true | Vendor ops copy of health |
server_ntfy_ops_topic | {binary_name}-ops | Empty disables ops even if the flag is on |
Keryx uses ntfy's comma-separated multi-topic subscription endpoint, so an active app maintains one SSE connection instead of one per selected category. Reducing SafeCall publishes lowers request and cache work, but does not itself reduce connections opened by third-party stock ntfy clients.
Related: Configuration, Keryx app, Provisioning protocol.