Okay, doing this: today a hat can show a full 150-minute recording with no error while the camera delivered zero frames — the timer is just wall-clock and nothing checks frames. Fix: define "recording" as frames actually landing on the card, top to bottom, so the app can never show a session as recorded when it's empty.
This might happen: the silent loss stops — a stalled camera fails loud and immediately, so the operator re-records on the spot instead of finding out at decryption. This is the fix for today's incident report.
This might not happen: it's not one switch — it's a few coordinated changes (duration from frames, status gated on real progress, a stall watchdog, a post-session integrity check). Landing only some of them still leaves a hole.
Nothing might happen: on a healthy recording nothing changes — same behavior, just now provably true.
Frame-derived timer + progress-gated status + stall watchdog, then an app post-session check best
App-only check after each session — catches it after the fact, but the device still lies during recording.
Just the stall watchdog — stops runaway empties, but the false timer stays.
Okay, doing this: put the hat's ID in the session name so two hats can never produce the same file name, and take the time from the operator's phone (already sent as the witness clock t) instead of the board's own clock, which is wrong until NTP because these boards have no battery clock.
This might happen: the duplicate file names across devices stop, so nothing gets overwritten at offload; timestamps become trustworthy and correctly dated.
This might not happen: the phone-clock part is not new — the Pi 5 line already names from t (better than waiting for NTP, no boot delay). But Pi 5 is still second-resolution with no hat ID, so two hats in the same second (a 4-of-4 "Record all") still collide there too. The hat ID is the only part that actually closes it.
Nothing might happen: a hat recording alone, already on time, sees no difference.
Adopt Pi 5's phone-witness-clock naming on standard + add the hat ID best
PR #16's "wait for the board's NTP" — slower (boot delay), can time out, and still no hat ID.
PR #16 — likely superseded: the Pi 5 witness-clock approach beats its NTP-wait; still needs the hat-ID naming, which neither has
Okay, doing this: replace vague or missing failures with a specific, plain-language reason and a next step — "camera not detected", "camera stuck in recovery, power-cycle it", "no space left", "camera warming up" — written with the UX doctrine so a field operator knows what to do without calling support.
This might happen: the operator fixes it in the moment (reseat the cable, power-cycle, swap the card) instead of finding a dead session hours later; fewer silent failures walk off site.
This might not happen: it doesn't repair the fault — a genuinely broken camera still can't record, but now the person holding it knows why and what to try.
Nothing might happen: a clean start shows nothing new.
Map each real fault to a doctrine-written operator message + next step best
Keep the generic "couldn't start, turn it off and on" — what mostly exists; hides the actual cause.
Okay, doing this: watch bluetoothd; if it pegs a core and stays there while advertising is up, restart the bluetooth service, and only touch the shared radio if that alone doesn't clear it.
This might happen: a connected phone drops and reconnects; discovery blips for a few seconds; on Zero 3W / A7Z the shared radio can make Wi-Fi hiccup.
This might not happen: recording is not interrupted and no footage is lost.
Nothing might happen: if bluetooth never stalls, it never fires. Pure insurance.
Restart the light service first, escalate to the radio only if needed best
Always bounce the whole radio — simpler, but blips Wi-Fi every time.
Okay, doing this: when a record attempt hits the stale-key block, set the leftover key aside and retry, so the next tap records.
This might happen: the hat records immediately — no power-cycle, no field visit.
This might not happen: nothing is deleted (the key is kept aside, never destroyed); the normal path is untouched.
Nothing might happen: a hat that never lost power mid-record has no stale key, so it never triggers.
Clear it in place, records on the next tap best
Heal only on the next reboot — what the current fix does; still needs a power-cycle.
PR #26 — the next-boot fix, open, ready to test
Okay, doing this: drop the every-5s Wi-Fi check and update the app's connection status from NetworkManager's own change signals instead.
This might happen: the repeated background check disappears; the app still shows live connection status.
This might not happen: no change to the radio (it never scanned) and no change to how it auto-connects.
Nothing might happen: the operator sees the same status as before.
Event-driven from NetworkManager signals best
Just slow the check to ~15s — trivial, keeps the background work.
Read only when asked — simplest, but loses the live "Wi-Fi dropped" update.
Okay, doing this: the auto-enroll already works — on first internet a new hat installs Tailscale, joins with its one-time key, retries until it's on, then wipes the key. The change is to stop copy-pasting those ~150 lines into every board's first-boot script and keep one shared copy.
This might happen: one place to fix enrollment; the four board images stop drifting apart (they already have).
This might not happen: nothing changes in how a new hat actually enrolls — same steps, same behavior.
Nothing might happen: hats already on the tailnet are untouched; this only affects freshly-imaged cards.
One shared enroll unit every board image uses best
Leave it duplicated per board — works today, but drifts and has to be fixed four times.
Okay, doing this: if a hat's update state is stuck on "checking" past a timeout, reset it to idle automatically on the next check, instead of someone having to fix it by hand.
This might happen: a wedged hat can check and update on its own again; no manual intervention.
This might not happen: no change to a healthy update, and the safe A/B install + auto-rollback stays exactly as-is.
Nothing might happen: hats that never wedge never notice. The rest of the OTA is already clean — nothing else to simplify there.
Auto-reset a stale "checking" on the next check best
Keep resetting it by hand — what happens today.
Okay, doing this: ship the live camera preview (see through the hat before and during recording) and the polished Pi 5-line UI into the everyday app, for every hat — not just the Pi 5 test build.
This might happen: the operator sees the camera is actually capturing before committing to a long session — a black preview means no frames, which is exactly the silent-loss failure we just found. Preview becomes the human catch for it.
This might not happen (watch this one): it will not be free — turning preview on in the shipping app re-wakes the "recordings auto-stop a few seconds in" bug, which is dormant today only because production has no preview. The one-line record-start lock has to land first, or this reintroduces the loss it's meant to prevent.
Nothing might happen: on iPhone (preview unsupported today) nothing changes there yet.
Ship the existing preview module everywhere, after the start-lock fix best
Keep preview on the Pi 5 build only — safe, but the fork and the drift stay.
Okay, doing this: keep one set of screens and flows for the whole app; the transport underneath (BLE, direct preview) and the hat type swap behind a capability layer, so the UI never forks per device.
This might happen: design and fix once, and it lands on every hat and phone; the per-line drift (like the four first-boot scripts that already diverged) stops happening.
This might not happen: hardware differences don't vanish — they move behind the capability layer, not into separate screens. And this is the app UI only; the Pi 5 receiver firmware stays its own channel as before.
Nothing might happen: the operator sees the same app no matter which hat — ideally they never know which adapter is underneath.
One UI, device differences behind a capability layer best
Keep the live-preview build as a separate fork — what we have now; it drifts.
Lowest common denominator — drop device-specific features to force sameness; loses capability.
What we found: the receiver has no auto-stop — no inactivity, disconnect, timeout, or second-phone takeover ends a recording on its own. The one exception, a bluetoothd restart, self-stops cleanly with error=""; that was verified absent on the unit checked (no bluez_owner_lost, boot continuity intact). So all three stops were genuine external stop commands, and the app only emits a stop from the "Stop all" button.
So the realistic causes are: a "Stop all" press from the second connected phone, an accidental toggle, or the LAN API. The req-id cross-check against the app logs is what separates them.
Side note: the bluetoothd-restart exception is already shipping (present in live 1.8.2, and in 1.8.6 — built from product/standard). Nothing to merge.
Okay, doing this: make it a hard rule, enforced in the build, that every OTA release is built from the product/standard branch — never a feature or release side-branch — so nothing ships that isn't on the canonical, tested line.
This might happen: the gap between "what's tested on standard" and "what's on devices" closes; fixes that land on standard actually reach the fleet.
This might not happen: it doesn't slow a release — it just fixes which branch the artifact comes from.
Nothing might happen: if a build was already from standard, nothing changes; this makes it explicit and unbreakable.
Enforce in the build/CI — refuse to build from a non-standard ref best
Rely on discipline — what we have; it drifts.