Runbook¶
One page for whoever is on call, not necessarily whoever built this. See INSTALL.md for setup, DEVELOPMENT.md for dev, and UNIFI.md for the UniFi side.
Add or remove a PIN¶
Admin UI (fastest): /admin → PINs → "Add PIN", or "Revoke" on an existing row. Needs ADMIN_TOKEN. PINs are exactly 4 digits and can't duplicate another PIN.
CLI, on the box:
pnpm pin:add "Alex" 1234
Remote, if you can reach the box over HTTP but have no shell on it: curl -X POST http://<box>:3000/api/admin/pins -H "X-Admin-Token: <that box's ADMIN_TOKEN>" -H 'content-type: application/json' -d '{"label":"Alex","pin":"1234","isDuress":false}'.
PINs are hashed on write and never returned by any API or shown in any UI. To replace one, revoke it and issue a new one.
What an alert means¶
- ALARM: a sensor tripped while armed (instantly for an "instant" sensor, or after the entry delay ran out with nobody disarming). The alert (Slack, ntfy, Telegram or Signal) carries a camera snapshot and a live-view link when one is mapped. The admin header pill shows
ALARM - <sensor name>. - ALARM from a PIN lockout: too many wrong PINs at the panel. It fires whatever the arm state, and the audit log shows
alarm_pin_lockout. Check the panel and the camera by the door. - DURESS: someone disarmed with a duress PIN. It looks and behaves like a normal disarm at the panel, on purpose. The only places it shows are the separate "DURESS" Slack/ntfy message and the raw event type in the audit log. Treat it as someone possibly under coercion, not a false alarm.
- HEALTH: no sensor event for 2h and the realtime stream isn't connected, so the feed may be dead. See "Sensor events stopped" below.
Sensor events stopped¶
A quiet office is normal. The HEALTH alert means it's quiet and this app can't see the console's own event stream either.
curl localhost:3000/api/health
lastWebhookAgeSecis how long since the last event.connectivity.streamStatusisconnected,disconnectedordisabled. Ifdisconnected, readconnectivity.streamLastError(a TLS error means the console CA certificate or "Skip certificate check" setting is missing).disabledmeans the SuperAdmin account isn't set in Settings > UniFi console, so only the Alarm Manager webhook feeds the app. Check the automation in the console.
The kiosk tablet is unreachable¶
The panel is a web page (/panel). Arming and disarming were never tied to that one tablet. Open the same URL on any phone or laptop on the office LAN. Keep a cheap spare tablet for a longer outage.
If the backend is down, the panel shows an offline banner and nothing can arm or disarm until it's back. See "The app itself is down".
Restoring from a backup¶
Every PIN, the audit history, config and sensor mappings live in one SQLite file. A background job snapshots it daily to <directory of DATABASE_PATH>/backups/alarm-YYYY-MM-DD-HHmm.db and keeps the 14 most recent.
sudo systemctl stop unifi-alarm-panel
cp /path/to/data/backups/alarm-2026-09-01-0300.db /path/to/data/alarm.db
sudo systemctl start unifi-alarm-panel
Everything between that backup and now (PIN changes, audit events) is lost. This is a last resort for a corrupted DB or a bad manual edit.
An update is stuck¶
"Update to latest" in the admin UI queues a request. A root-owned timer applies it within a minute (download, symlink swap, restart). If the button stays on "Update requested", the timer is probably missing or stopped:
systemctl status unifi-alarm-panel-updater.timer
If it's inactive or not found, install it (INSTALL.md) or apply the update by hand:
cd /opt/unifi-alarm-panel
./deploy/update.sh # latest release
./deploy/update.sh v1.0.2 # a specific tag, also how you roll back
When an update finishes, the admin UI reloads itself within a couple of seconds.
The app itself is down¶
sudo systemctl status unifi-alarm-panel
sudo journalctl -u unifi-alarm-panel -f # live logs
curl localhost:3000/api/health # engine and DB status, heartbeat age, version, connectivity
ok: false means something is wrong. A stale lastWebhookAgeSec alone may just be a quiet office (see above). If the process is running but wedged, sudo systemctl restart unifi-alarm-panel. State is persisted to SQLite, and a countdown that was running resumes on its original schedule (resumed an in-flight alarm countdown after a restart in the log). panel_recovered_default_disarmed in the audit log means the stored state was unreadable and the app came up disarmed. Verify the real state.
Camera streaming (go2rtc) is a separate service and restarts independently of arm/disarm: sudo systemctl restart unifi-alarm-panel-go2rtc. Its stream registry doesn't survive its own restart, but the app re-registers on boot. If a stream is still dark a minute after restarting go2rtc, restart unifi-alarm-panel too.