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 nine staff categories as ntfy_topics. Keryx QR generation (POST /api/v1/keryx/qr) includes only the categories the admin left checked. Mobile Access default-checks SOS, watches, and Daily status. Condition topics (sensors, batteries, notseen, calibration, health) are optional; Overview loads them from the status API. test and debug access are unchecked in the collapsed Admin features section and require an explicit warning confirmation.
Keryx renders Overview cards for subscribed moment categories plus every unmuted condition category. Daily status is a wake, not a card. Each category has a phone-side notification policy and optional per-known-device overrides. Existing phones need a new QR after this cadence change.
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 (queryable; not a phone wake) | Status snapshot transitions | Stored on the status API; not posted to this topic | — | — |
status | {server_id}-status | Daily status | Denní stav | Once-a-day Overview reminder | Daily digest bucket (default 09:00 Europe/Prague) | Title = {server_id}: N conditions need attention or All clear; body tells the user to open Overview (no device list) | warning,clipboard or white_check_mark | default / min |
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 wakes from queryable state. SOS, watch, and pipeline tests are immediate ntfy wakes. Sensors, batteries, not-seen, calibration, and install health stay on the installation SQLite store, are PUT to keryx-status on the notify host, and contribute at most one {server_id}-status ntfy per day (skipped when the site is already all-clear). Phones GET the snapshot; they are not the database.
HTTP 429 from ntfy starts a global backoff (default 30 s, or Retry-After). Immediate SOS/watch/test alerts retain the existing retry queue. The daily status wake is retried until ntfy accepts it. Test publishes are not queued and intentionally bypass every cooldown.
| Id | Default server policy | Subject / grouping | Effective behavior |
|---|---|---|---|
sensors | Daily status (mute-able) | Stable asset | Evaluated locally; no per-event ntfy. Snapshot items + daily count |
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 | Daily status (mute-able) | Stable asset | Same as sensors |
notseen | Daily status (mute-able) | Stable asset and device type | Seeing the device again resolves the condition. Fridge timeout remains max(10 min, 2 × update_interval) |
calibration | Daily status (mute-able) | Device assignment / stable asset | Hourly checker updates state; never becomes dozens of ntfy parts |
test | Immediate, exempt | Unique test occurrence | Every Recheck / Test Send; no Keryx cadence control |
health | Daily status (mute-able) | Infrastructure key or fleet issue | Staff phones query Overview. Vendor ops is a separate topic (server_ntfy_ops_*) |
status | One ntfy per daily bucket when active_count > 0 (or a quiet all-clear after a day that had issues) | Site | Counts only. Tap opens Keryx Overview |
The daily bucket is anchored at 09:00 Europe/Prague by default. Timezone and time are editable in SafeCall settings. Per-category mute omits that category from the snapshot and the daily count. server_ntfy_enabled is the master switch for SOS/watch/test and the daily wake. Status PUT continues when ntfy is on so opening the app still works.
Vendor ops copies of health are independent (server_ntfy_ops_enabled + non-empty server_ntfy_ops_topic) and do not post to {server_id}-health.
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 PUTs the current condition snapshot to keryx-status so a newly scanned phone can leave Waiting for status without waiting for the next daily wake. It does not POST silent 3 KiB×N ntfy snapshots. Dashboard Recheck and Watch/Panic Test Send go to {server_id}-test, not -sos.
Keryx Active is SOS, watches, and test only. Condition ntfy from an old install is ignored. Daily -status tap opens Overview. FCM/APNs banners for condition topics are suppressed on the phone.
Scanning a new QR wipes local history and category state. Overview stays on Waiting for status until Keryx GETs the snapshot from keryx-status. Existing phones need a new QR for status_url and {server_id}-status. ACL notify hosts must mint a real ntfy subscribe token; tk_local_ QRs cannot poll and need a regenerated QR after token-manager is configured. FCM/APNs banners can still appear in that broken state because topic wake does not use the QR token.
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 stores each of these as health conditions on keryx-status. Vendor ops still POSTs when ops is on. Staff {server_id}-health is not a phone wake.
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 and ntfy-deployer.md |
Config that changes cadence
| Key | Default | Effect |
|---|---|---|
server_ntfy_enabled | true | Master switch for SOS/watch/test and the daily {server_id}-status wake |
server_ntfy_button_dedup_seconds | 40 | Shared SOS/watch cross-gateway event window |
server_ntfy_digest_timezone | Europe/Prague | Wall-clock timezone for the daily reminder |
server_ntfy_digest_time | 09:00 | Daily anchor |
server_ntfy_sensors_muted | false | Omit sensors from the snapshot and daily count |
server_ntfy_batteries_muted | false | Omit batteries |
server_ntfy_notseen_muted | false | Omit not-seen |
server_ntfy_calibration_muted | false | Omit calibration |
server_ntfy_health_muted | false | Omit install health (ops topic is independent) |
server_keryx_status_url | empty | Override; empty derives {ntfy origin}/keryx-status |
server_keryx_status_read_secret | empty | HMAC for phone GET; empty uses ntfy subscribe auth |
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.