I built the alert dashboard for a residential site's guard room. The site already had an AI box watching the cameras, and it was good at events: an intrusion, a fall, a car parked on the driveway. It sends an HTTP request when one happens and the dashboard shows it.
Two of the client's requirements weren't events. “Patrol not done” and “guard absent from post” are about something not happening. No detector anywhere fires when a guard fails to walk past a checkpoint. The box only ever says “someone was seen here”. So the rule had to be a timer over the silence between those messages, and it had to live in my code.
The shape of it
Each checkpoint is a row: which camera or input it listens to, which rule it enforces, a threshold in minutes, and when a guard was last seen there. Guards check in at four patrol points by triggering an input on the box, and each check-in resets that point's clock. The guard room has its own, much shorter threshold.
Once a minute the server asks one question of every checkpoint: has it been quiet for longer than its threshold? If so, raise “Patrol Not Done”. That's the whole idea. Everything interesting is in the edge cases.
A late message must not turn back the clock
Events don't always arrive in order. A retry, a slow network hop, a box that buffered while the link was down. If a check-in from 10:05 arrives after one from 10:20, a naive last_seen_at = ? moves the clock backwards and can raise an alert for a patrol that actually happened. So the update keeps whichever time is later:
UPDATE checkpoints
SET last_seen_at = MAX(last_seen_at, ?)
WHERE camera_id = ?;The timestamp is the event's own time when the box sends one, not the time my server received it. The question is when the guard was there, not when I heard about it.
One lapse, one alert
The minute tick keeps running while the checkpoint stays quiet. At 61 minutes the rule is broken; at 62, 90 and 200 it's still broken, and a guard room screen that adds a new alert every minute is a screen people learn to ignore. So before raising anything, the check looks for an alert that's already open for this checkpoint and rule:
for cp in checkpoints:
if cp.muted_until and cp.muted_until > now: continue
if now - cp.last_seen_at <= cp.threshold: continue
if open_alert(cp.camera_id, cp.rule): continue # already standing
raise_alert(cp, reason=f"no activity for {minutes} min")When a guard finally shows up, the check-in resolves the open alert and resets the clock. The next time the checkpoint goes quiet is a new lapse, and it gets a new alert. That distinction matters later, because the patrol report counts lapses.
The server will restart
It runs on a PC in a guard room. Someone will turn it off. If the clocks lived in memory, a restart would reset every checkpoint to “just seen” and hide a missed patrol, or forget which alerts were already open and raise them all again.
So none of it lives in memory. Last-seen times, open alerts and mutes are all rows in SQLite. On startup the site configuration is written with an upsert that updates the name and threshold but never touches last_seen_at. The first tick after a restart sees exactly the state the last tick before it saw.
Testing an hour without waiting an hour
The check takes now as a parameter instead of reading the clock. That one decision makes the whole thing testable in milliseconds:
addCheckpoint({ camera_id: "East Corner", threshold_min: 60, last_seen_at: T0 })
assert(checkTimers(T0 + 59 * MIN).length === 0) // not yet
assert(checkTimers(T0 + 61 * MIN).length === 1) // fires once
checkTimers(T0 + 90 * MIN); checkTimers(T0 + 200 * MIN)
assert(total("East Corner") === 1) // no pile-up
touchCheckpoint("East Corner", T0 + 210 * MIN)
assert(openCount("East Corner") === 0) // a visit clears it
assert(checkTimers(T0 + 275 * MIN).length === 1) // a new lapse fires againA bug here means a missed patrol goes unreported, which is the one thing the system exists to catch. It was the first part of the codebase to get a test file.
When the silence is the sensor
There's one more kind of quiet. If the AI box itself stops sending, every checkpoint goes silent at once, and the dashboard would blame every guard for a network cable. So the server separately tracks when it last heard anything at all from the box. After a configurable silence it shows a banner on every screen saying the box has gone quiet, so “nobody patrolled” and “we can't tell” don't look the same.
None of this is complicated code. The whole timer module is under a hundred lines. The work was in noticing that “alert when X doesn't happen” hides four separate questions: what time is it really, have I already said this, what survives a restart, and am I sure the silence means what I think it means.