Case Study - Operational Systems and Reporting
Nalaquq, LLC and Quinhagak Search and Rescue, western Alaska. May 2026 to present.
Quinhagak Search and Rescue covers a remote part of western Alaska. There are no roads in. Searches run across tundra and river country, often in cold, and the team is new enough that it has run only a handful of missions.
I am the Data Analyst and Integration Developer on the project, working remotely. I build features onto a shared situational awareness system that a lead developer owns and maintains. He owns the core; I build alongside it.
What shapes every decision here: when this software is wrong, the cost is not a bad quarter. It is an area marked searched that nobody walked, or a casualty request that never arrives.
Six tools, each built to answer one question a search leader actually asks. A coverage model, a casualty request bridge for iPhone users, a crew accountability board, a mission history dashboard, a callout summary report, and a vitals monitoring framework. Each was built to the point where it can demonstrate its own use case on its own, so the team could look at all six and adopt the ones they wanted rather than being handed a single thing to take or leave.
The team runs a mixed fleet. Some members are on Android, some on iPhone. The iPhone client is a stripped-down build of the same platform, and one of the things that did not make it across is the casualty evacuation tool. There is no CASEVAC button on iPhone at all.
That broke two things at once, and both of them matter during an actual emergency:
The iPhone client is closed source. I cannot fix it and neither can anyone else outside its vendor.
The client already handles one thing perfectly well: plain map markers. That is the whole solution. I wrote two small server-side services that use markers as the transport, both modeled on an existing bridge already running in the system, so they fit the patterns the lead developer had already established rather than introducing my own.
Mirror, which fixes seeing. Whenever a request exists, the service drops an ordinary pin at the same location with the nine line details written into the pin's notes. iPhone users see the casualty and tap it for the details. It runs roughly every thirty seconds. Android users change nothing about how they work.
Two design choices matter more than the feature. Each mirrored pin uses a fixed identifier, so re-running refreshes the same marker in place rather than creating duplicates, and a resolved request ages out on its own through the marker's own expiry. And it reuses the transport an existing bridge in the system already proved out, rather than introducing a second way of doing the same thing.
Intake, which fixes filing. The reverse direction. An iPhone user drops a marker named CASEVAC and types the nine line into the notes:
URGENT: 1 LITTER: 1 HOIST: yes HAZARD: open water, thin ice MARK: orange smoke NOTES: leg fracture, conscious
The server turns that into a logged CASEVAC on the command dashboard, identical to one authored on Android. And if someone types it loosely instead of in that format, which is what actually happens when a person is cold and frightened, it still files the casualty and preserves their exact words in the notes. Nothing is lost because the format was not followed.
These two services are each other's trigger. The mirror pins every real request as a plain marker whose name begins with the trigger word. The intake watches for plain markers with that name and files them. Left alone, one casualty report becomes an unbounded stream of fabricated casualties on a live server during an emergency.
Two independent guards prevent it, and the module docstring says plainly why there are two: one is not enough on a life-safety path.
The exception in the first guard is the interesting part. The system stages template pins for users to drag and fill, and those templates are generated by the same stack the guard is designed to reject. So the guard has to exclude its own output while still admitting its own staged templates, which means it cannot be a simple prefix match.
Both guards live in one function so they can be tested directly, and the database query that does the fast filtering mirrors that same logic. A comment on the query states that the two must change together, and the fetch re-runs the function in code after the query returns, with the note that the query is the fast filter and the function is the one that counts. Defence in depth, with the authoritative layer named rather than left ambiguous.
A staged template pin has all the field labels and no values. If the intake treats that as a filled form, it files an empty casualty. If it demands a strict format, it discards a real one from someone who typed in a hurry.
So the check is not "does this parse" but "did a person actually enter something":
def is_filled(remarks):
"""True if the author actually entered a value for a known field.
A parked template has the keys but no values ("URGENT: "), so it must not
file a casualty. Free text with no keys at all still counts as filled: a
person typed something, and the intake preserves it.
"""
Template keys present but all blank means an unused template, so no casualty is filed. Free text with no recognised keys at all counts as filled, because a person typed something and the point is not to lose it.
The rest of the parser is built for the same conditions. It accepts either a colon or an equals sign as a separator. It accepts abbreviations alongside full words, so AMB works as well as AMBULATORY and VENT as well as VENTILATOR. Anything it cannot interpret is skipped for the structured fields, and the author's entire note is preserved verbatim regardless.
A new team wants what an established one wants. Which months are busiest, how long searches take, which outcomes are common, where the training money should go.
The data cannot support most of those questions yet, and the obvious failure is to build the dashboard anyway and let someone make a real decision from eight records.
The module says why in its own docstring, which matters more than the code beneath it:
"""Turn callout records into summaries, gated by sample size.
The gate is the point: with a handful of records, seasonal and outcome
"patterns" are noise. So the report runs at three tiers by n:
n < 10 raw counts only, no pattern claims
10-29 breakdowns shown, flagged low-confidence
n >= 30 breakdowns reported normally
Thresholds are conventions, documented so they can be defended or changed.
"""
RAW_MAX = 10
TENTATIVE_MAX = 30
def tier(n):
if n < RAW_MAX:
return "raw"
if n < TENTATIVE_MAX:
return "tentative"
return "reported"
That last docstring line is the one I would defend. The thresholds are conventions, not laws. Someone will eventually disagree with thirty, and when they do the reasoning sits next to the number instead of in my head. The gate does not need removing later either. As the record grows, findings graduate through the tiers on their own.
The command line tool refuses outright to run against real data until the schema is confirmed and access is granted. It raises rather than guessing at column names.
This was not in scope. I added it after reading my own output.
The reporting breaks activity down by village. In a community of a few hundred people, a cell reading two is not a statistic. It is potentially a specific person, and their neighbors can work out who.
# Counts below this are personally identifying in a small community
# (a village of a few hundred, a known date range). Standard disclosure
# floor. Real counts below it are shown as "<5", never exact.
SMALL_COUNT = 5
def suppress_small(counts, floor=SMALL_COUNT):
"""Replace any exact count below the floor with a sentinel string, so a
category with 1-4 records is shown as "<N" rather than a number that could
identify a person. Only matters on real data; harmless on synthetic."""
out = {}
for k, v in counts.items():
out[k] = v if v >= floor else f"<{floor}"
return out
Callout records may or may not carry an outcome field, depending on upstream configuration. Rather than guessing or defaulting to zero, the analysis carries an explicit flag, and when outcomes are unavailable the report says so instead of rendering an empty chart that reads as a finding.
outcome_available = schema.OUTCOME_SOURCE is not None outcomes = Counter(_outcome(r) for r in rows) if outcome_available else Counter()
Every timestamp parse and every JSON field read is wrapped the same way, returning nothing rather than raising or inventing a default. A malformed record drops out of the duration statistics and the rest of the report still runs. Column names and field locations are indirected through a schema module, so upstream changes do not touch the analysis.
The coverage work takes GPS tracks searchers actually walked and estimates how much ground was genuinely covered, as opposed to how much was assigned. Those are very different numbers. A sector can be marked searched while half of it was never within sight of anyone.
I did not invent the method. I implemented published search and rescue methodology, drawing on Koester, Cooper, Frost, Robe, and the US Coast Guard 2004 addendum. The judgment I applied was in the defaults.
Every uncertain parameter, particularly effective sweep width, is set so the model understates coverage, and a flag in the source states plainly that the defaults are not field measured. If the model overstates, a team stops searching an area that was never really searched. If it understates, the cost is a redundant sweep. One of those errors is recoverable and the other is not.
An early version took sweep width as a single number. It does not work that way, since sweep width differs by search mode, so it now takes a value per mode. That correction came from reading the literature properly rather than from a bug report.
A demonstration run on simulated tracks reports something like twenty four percent chance of detection with half the sector never walked. Two numbers a search leader can act on, from tracks that already existed.
The design rule underneath it: the tool reports a number and never says searched. It has no binary output, because the binary is the judgment a search leader makes, not one a model should make on their behalf.
The methodology assumes ground search. Applying it to drone or boat search is not a matter of changing a constant. The detection curve is wrong for an aircraft flying a programmed grid, drone coverage credits detection to the flight when detection actually happens later during footage review, and on open water the subject drifts while the searched area does not. I raised these with the project lead rather than presenting the model as ready for all three.
Thirteen modules covering baselines, expected ranges, deterioration detection, cold weather bands that loosen the low side without touching the high side for a hypothermic patient, explicit sensor handoff that flags a searcher as unmonitored when their sensor moves to a casualty, a transmission policy tuned for a satellite link where bandwidth is genuinely scarce, and simulation so the whole path can be exercised without a patient.
One rule governs the whole thing: nothing calls an emergency by itself. The system raises attention and a human decides. That is a deliberate limit on the automation, not a gap in it, and it is the difference between a tool that supports a decision and one that makes it.
The clinical thresholds are the part I will not sign off on myself. They are tested and they need a medic's name on them before anyone uses this on a real person, and I have said so every time it has come up.
Who is out, who is accounted for, and alerting when that changes.
Authentication fails closed, and the reason is specific rather than stylistic: this board shows where people currently are. If the access check breaks, refusing everyone is a broken feature, while defaulting open is a location leak. So a failure in the check locks the door rather than opening it.
The test suite exercises the real guard through the framework's test client rather than mocking it, and the demo refuses to write its own output file at all if any of the four access paths returns the wrong status code. A demo that quietly produces output when the security model is broken is worse than no demo.
Filterable history of callouts by date, status and location, reading the live store fresh on every request with no caching, so there is no second copy of the truth to go stale.
None of this is standalone. It plugs into an application the lead developer wrote and maintains, and the constraint I set myself was to leave no trace anywhere except the routes I own.
The lead developer rewrote a large part of the core application while I was working. Rather than assume my branches still fit, I tested it.
I stacked all five feature branches onto a throwaway branch off the current main, one at a time, and ran the entire combined suite: 240 tests, 240 passing, zero conflicts. The method matters, because it had to prove compatibility without polluting any branch. Each check was a merge with no commit, run the tests, then abort.
The finding that made it worth doing: one branch silently contains all of another, because I had branched it from the wrong place months earlier. Adopting the first would have quietly brought the second along with it. That is not something a test suite tells you. I reported it in bold, along with the one trivial import-line conflict I found and exactly how to resolve it.
This describes my own work only. Substantial parts of the wider system were built by the project lead and are not discussed or claimed here.
It does not include the organization's system architecture, hostnames, infrastructure details, credentials, configuration, mission data, operational records, individual or community identifying information, vendor terms, device identifiers, or any code beyond the short excerpts above, all of which are pure logic from a single analysis module.
Published with written approval from Nalaquq. The organization's data stays with the organization.