The Search-and-Rescue Button That Freezes a Position It Can't Invent
Clearspar Heli is a concept prototype of a day-of-operations system for a heli-ski/heli-charter operation — not a real operator, not real flights, not real guests. Every aircraft and person in the demo is invented sample data, and it is not a certified operational safety system, so no real group was ever overdue and no real search was ever triggered. But the design tension this feature ran into is real, and it's the kind that only shows up when the honest answer and the reassuring answer are different answers. When a group has gone quiet and someone has to act, the instinct is to put a position on the map. The honest thing is often to admit you don't have a current one — and this post is about building the button so that it can't lie about that even when a lie would feel more useful.
The feature is a one-tap declaration: a dispatcher looking at a group that's overdue or unreachable hits a control that freezes the assigned aircraft's last-known position, assembles the existing SAR packet, and pages the on-call roster. On the board it's deliberately a two-step action — the first tap opens a confirm panel with red warning text spelling out exactly what's about to happen (this freezes a real position, assembles the packet, and pages the ops_manager/dispatcher roster — a real safety action), plus an optional reason field, and only the second tap fires it. That friction is on purpose: this is not a status toggle, it's a record of a human judgment call, and the UI should feel like one.
Freezing a fact, not mirroring a feed. On trigger, the server queries the assigned aircraft's latest real Position row and writes frozen_lat, frozen_lon, frozen_position_reported_at, frozen_position_source, and frozen_position_age_seconds into an immutable SarDeclaration row. The word "frozen" is load-bearing. Those five columns are written exactly once, at insert, and never updated again — not even if the aircraft reports a fresh fix thirty seconds later. The declaration has to permanently answer one narrow question: where did we last know this group was at the moment we declared them overdue. That's a fixed historical fact, and a live-tracking mirror would quietly overwrite the one thing the record exists to preserve. The age is computed the same way — max(0, seconds since that fix) — so it's the staleness at declaration time, capped so a clock skew can't produce a negative number.
The harder case is the one most systems fudge: what if there's no position on file at all? A brand-new aircraft, a feed that never delivered, a demo asset nobody wired to a source. The cheap move is to interpolate, or fall back to a base location, or show the last dot from some other aircraft — anything to avoid a blank map. This one does none of that. If the position lookup comes back empty, all five frozen columns stay NULL together, by construction: they're only populated inside the branch that runs when a real row exists. The model's columns are all nullable as a set for exactly this reason, and the UI renders it as a plain "no position on file" instead of a fabricated fix. "We don't know where they are" is real, actionable information for a search — arguably the most important thing the declaration can tell you — and the schema is built so it can say that honestly rather than papering over it.
The part that made this more than a new table is that it connects three subsystems that already existed but had never touched each other: the positions feed (the frozen fix above), the escalation engine, and the SAR packet. It would have been easy to bolt on a second, parallel alert path just for manual declarations. Instead a declaration reuses the escalation engine's own row — either opening a new EscalationEvent straight at the SAR-notify step or advancing an already-active one to it, following the engine's "one active row per aircraft" discipline, and re-opening an escalation that had been acknowledged but was still live. From there it fires the same notifier, push fan-out, and webhook the automated engine uses, not copies of them. The default stub notifier records "not sent" unless an operator has real Twilio credentials configured, and the response hands back notifiedStaffCount — a real count of the roster it just paged, never a fabricated number. (One in-character detail: right after the commit it has to re-point the RLS org variable before the push queries run, the same GUC-reset discipline described in an earlier post — reusing the shared fan-out means reusing its footguns too.)
Then there's who's allowed to press it. Declaring uses DispatchWrite — dispatcher and ops_manager only — and the consequence I want to be precise about is that a guide or pilot cannot declare SAR on any group, including their own group. That can feel backwards: the guide is the one out there, so shouldn't they raise the alarm? But unilaterally triggering the on-call page for a group is a dispatch-desk decision, not a field one, and the enforcement lives server-side at the dependency layer, not in the UI. A guide credential hitting the declare endpoint gets a real 403 before the handler body ever runs — the button being hidden from their screen is a courtesy, not the control. Guides and pilots can still see the board and the packet (the read route is open to any staff role); they just can't pull the trigger. Resolving a declaration is gated tighter still, to ops_manager only, because closing out a serious record is the sensitive half — acknowledging doesn't end anything, but declaring it over does.
The underlying lesson isn't really about search-and-rescue. It's that a system under pressure to look complete will invent data if you leave it room to, and the countermeasure is to make "we don't know" a first-class value the schema can hold — five columns that go NULL together, a position label that reads "no position on file" without apology, an age that's honestly absent rather than defaulted to zero. Freezing the record instead of mirroring the feed is the same discipline pointed at time: the moment you declared is the fact worth keeping, so keep it and stop touching it. The temptation in a safety-adjacent feature is always to reassure. The more useful thing, when it's true, is to say plainly that you don't have the answer — and to build the data model so that staying silent about what you don't know isn't even an option the code can take.