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 don't name or timestamp a session from a clock that hasn't synced yet (these boards have no battery clock and boot with the wrong time until Wi-Fi/NTP corrects it).
This might happen: the duplicate file names across devices stop, so nothing gets overwritten at offload; timestamps become trustworthy.
This might not happen: waiting for the clock adds a few seconds at cold boot before the first record; the hat's ID in the name is what actually guarantees uniqueness, independent of the clock.
Nothing might happen: hats that were already on time and recording alone see no difference.
Hat ID in the name + wait for a synced clock before naming best
Wait for the clock only — what the current PR does; two hats at the same second still collide.
PR #16 — the clock-wait half, open but rotted; needs a rebase and the hat-ID naming added
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.