Summary
The pairing screen (lib/ui/pairing_screen.dart) has no way past it except a successful live BLE bond: no "Skip pairing" button, no "Pair later" option. The entire app shell sits behind this screen, including the screens that export or restore your own local data. So "no band in range" becomes "no access to your own data," whether this is a fresh install or a device that already has months of imported history sitting in its local SQLite file.
Root cause
lib/state/app_state.dart:850-857
AppRoute get route {
if (!initialized) return AppRoute.loading;
if (_onboardChoice == null && !isPaired) return AppRoute.welcome;
if (!isPaired) return AppRoute.pairing; // <-- no way past this without a live bond
if (!profileComplete) return AppRoute.profile;
return AppRoute.shell;
}
isPaired => paired != null, and paired is only set by a real BLE bond (PairedDevice.save() after the confirmed-write handshake in ble_engine.dart). Every export path (CSV export, full-database backup) lives in lib/ui/profile/profile_screen.dart, reachable only via AppRoute.shell. pairing_screen.dart itself (591 lines) has no skip, later, or continue-without-pairing option.
Repro 1: restore-then-lock
- Fresh install, or wipe app data.
- From the welcome screen, choose "Import from a file" → restore your own prior "Edge backup" (or a
.noopbak).
- Import succeeds; data is written to the local DB.
import_screen.dart:154-158's _continue() runs:
/// Finish: if still in onboarding (no choice made yet) mark it done so the gate
/// advances past `welcome` (→ pairing → shell)
await app.completeImportOnboard();
This always routes to AppRoute.pairing next, never to shell, regardless of import type.
- No band in range (lost/broken/not yet acquired) → stuck on the pairing screen with no way forward.
- Your own restored history is on disk, fully intact, and completely unreachable through the app. No export, no view, nothing, because the screen you're stuck on has no skip option.
Repro 2: forget-device lockout
- Already using the app normally, paired, shell full of months of data.
- Profile → "Forget device" (
profile_screen.dart:1804, confirm dialog at :1896) → calls app.unpair().
unpair() (app_state.dart:2972-2988) sets paired = null and calls notifyListeners().
_Gate re-evaluates route; isPaired is now false → immediately routes to AppRoute.pairing.
- Same dead end as repro 1: no band in range, and the pairing screen offers no way back into the app. "Forget device" reads as a benign disconnect action, but it silently seals off access to your own history with no exit besides re-bonding a band.
Use case: evaluating edge before switching
Beyond recovery, this also blocks a completely ordinary trial. Someone using noop as their primary app might want to import a noop backup into edge and compare the two apps' derived analytics before deciding whether to switch.
That person has no reason to pair a physical band to edge yet, and a real reason not to. Both apps drain-and-trim the band's flash on offload (lib/ble/ble_engine.dart:65-67: the day-commit "MUST complete before the engine writes the HISTORY_END ACK, so the band never trims flash we haven't banked"), so bonding edge to a strap noop currently owns risks losing history noop hasn't synced yet. The only way to try edge honestly right now is Repro 1's exact path: import the noop backup, then sit stuck on the pairing screen with that data imported and completely unreachable.
Why this matters
Requiring a band to initially set up the app is a reasonable product choice. Requiring one to re-enter an app that already has your data, after a restore or a routine "forget device" tap, or requiring one just to evaluate the app at all, is a different thing: a data-access bug, not an onboarding decision. Every scenario above lands on the exact same screen with the exact same problem: no way out except a physical band being in range right now.
Suggested fix
Add a "Skip pairing" / "Pair later" button to pairing_screen.dart that advances the gate straight to AppRoute.shell (or AppRoute.profile if incomplete) without requiring isPaired. One way to do it: a _pairingSkipped flag, persisted like _onboardChoice, that route treats as equivalent to isPaired for gating purposes only. Pairing itself stays available from within the shell (Profile → "Pair a band") whenever a band is actually in hand.
Summary
The pairing screen (
lib/ui/pairing_screen.dart) has no way past it except a successful live BLE bond: no "Skip pairing" button, no "Pair later" option. The entire app shell sits behind this screen, including the screens that export or restore your own local data. So "no band in range" becomes "no access to your own data," whether this is a fresh install or a device that already has months of imported history sitting in its local SQLite file.Root cause
lib/state/app_state.dart:850-857isPaired => paired != null, andpairedis only set by a real BLE bond (PairedDevice.save()after the confirmed-write handshake inble_engine.dart). Every export path (CSV export, full-database backup) lives inlib/ui/profile/profile_screen.dart, reachable only viaAppRoute.shell.pairing_screen.dartitself (591 lines) has no skip, later, or continue-without-pairing option.Repro 1: restore-then-lock
.noopbak).import_screen.dart:154-158's_continue()runs:AppRoute.pairingnext, never to shell, regardless of import type.Repro 2: forget-device lockout
profile_screen.dart:1804, confirm dialog at:1896) → callsapp.unpair().unpair()(app_state.dart:2972-2988) setspaired = nulland callsnotifyListeners()._Gatere-evaluatesroute;isPairedis now false → immediately routes toAppRoute.pairing.Use case: evaluating edge before switching
Beyond recovery, this also blocks a completely ordinary trial. Someone using noop as their primary app might want to import a noop backup into edge and compare the two apps' derived analytics before deciding whether to switch.
That person has no reason to pair a physical band to edge yet, and a real reason not to. Both apps drain-and-trim the band's flash on offload (
lib/ble/ble_engine.dart:65-67: the day-commit "MUST complete before the engine writes the HISTORY_END ACK, so the band never trims flash we haven't banked"), so bonding edge to a strap noop currently owns risks losing history noop hasn't synced yet. The only way to try edge honestly right now is Repro 1's exact path: import the noop backup, then sit stuck on the pairing screen with that data imported and completely unreachable.Why this matters
Requiring a band to initially set up the app is a reasonable product choice. Requiring one to re-enter an app that already has your data, after a restore or a routine "forget device" tap, or requiring one just to evaluate the app at all, is a different thing: a data-access bug, not an onboarding decision. Every scenario above lands on the exact same screen with the exact same problem: no way out except a physical band being in range right now.
Suggested fix
Add a "Skip pairing" / "Pair later" button to
pairing_screen.dartthat advances the gate straight toAppRoute.shell(orAppRoute.profileif incomplete) without requiringisPaired. One way to do it: a_pairingSkippedflag, persisted like_onboardChoice, thatroutetreats as equivalent toisPairedfor gating purposes only. Pairing itself stays available from within the shell (Profile → "Pair a band") whenever a band is actually in hand.