diff --git a/TODO-hardening-continue.md b/TODO-hardening-continue.md index a8dc221..1465487 100644 --- a/TODO-hardening-continue.md +++ b/TODO-hardening-continue.md @@ -20,16 +20,30 @@ starting; this file deliberately does not duplicate it. | 2 power button | **Done.** `_inhibit.py` (logind block, self-terminating, stale cleanup by pid via `/proc`) + `_alarm_session.py` fire-time record, re-fired by `_trigger` outside the catch-up window. | | 10 vmbox | **Done.** `scripts/vmbox/harness.sh` + `DOCS-vmbox.md`. 8 cases, fails closed. Found 2 live bugs the host could not reach. | | 5 curtains | **Done.** `_curtains.py` driver protocol + no-op default, called from `prepare_hardware()`. `DOCS-curtains-hardware.md` has the buy list. | +| 1/4 phone alarm | **Done.** Five Kotlin files behind one MethodChannel + the Dart ring screen. The phone arms and rings its own alarm; no Clock-app confirmation. Verified on the API 36 AVD (screen Asleep→Awake, AlarmActivity over the lockscreen) and deployed to the Pixel 6a. | +| 3 room proofs | **Mostly done.** NFC (hand-written `NfcReader.kt`) and the steps+elapsed fallback ship and are wired into the ring screen. QR is specified and unit-tested (`checkQr`) but has **no scanner UI** — `mobile_scanner` was left as the droppable extra. | ## Remaining, in order -1. **Items 4/3/1, phone.** Biggest chunk by far. Own session. Everything here - is new — the app has zero alarm capability today, only a time picker. -2. **Item 8, mutual gating.** Needs 1 done first. This is the phase that - legitimately touches `_sync.py`, so it carries that split (see below). +1. **Item 3 leftovers.** The QR scanner UI (`mobile_scanner`) and a Settings + screen to register the NFC tag id / QR payload. `RoomProofSettings` already + persists both and `RoomProof.checkQr` is unit-tested — what is missing is + only the UI that feeds them. NFC and the steps fallback already work, so + the alarm is dismissable without this. +2. **Item 8, mutual gating.** Phone half is now in place. This is the phase + that legitimately touches `_sync.py`, so it carries that split (see below). 3. **Item 13, black screen.** Different repo (`~/testsAndMisc/ linux_configuration`), host-only, blocks nothing. +### Not yet verified on the phone + +The ring itself has only been fired on the API 36 AVD. On the Pixel 6a the +build installs and the runtime permission prompts appear, but **an actual +alarm has never rung on it** — that needs an alarm armed through the picker +(which needs a sync backend connected) and a wait to the fire instant. Ring +with **DND on**, since "STREAM_ALARM ignores DND" is the load-bearing claim +and has not been observed on hardware. + ## Decisions already made — do not re-litigate - Failsafe stops **sound only**; lock, challenge and workout gate persist. @@ -49,6 +63,24 @@ starting; this file deliberately does not duplicate it. - Curtains: no hardware; pluggable driver + logging no-op default. - Quality bar: 100% branch coverage, no new `# noqa` / `type: ignore`. +## Traps found in the phone session + +- **The plan's `LogStore.changes` does not exist.** `crdt_sync_dart` has no + Stream at all. Re-arm therefore needs three explicit hooks — startup, + BOOT_COMPLETED, dismiss — and dropping any one makes the alarm ring once + and then silently stop forever. +- **The Android SDK was never missing, only unexported.** `~/Android/Sdk` has + `cmdline-tools`, `emulator` and `platform-tools`; only `adb` was on PATH. + Export `ANDROID_HOME` rather than installing anything. +- **A permission declared in the manifest is not a permission granted.** + Both `POST_NOTIFICATIONS` and `ACTIVITY_RECOGNITION` are runtime grants, and + each one missing silently breaks a different half of the dismiss path + (no full-screen intent / no step sensor). The AVD caught both because a + freshly installed app there sits at `importance=NONE`. +- **A debug probe entrypoint with its own UI proves nothing about the route.** + The first probe rendered its own widget, so the AVD run validated the Kotlin + and nothing about `/ring`. Point the probe at the real app widget. + ## Traps that cost time this session - **The 250-line gate fires on *staged* files.** Touching a file means diff --git a/TODO-phone-alarm-session.md b/TODO-phone-alarm-session.md deleted file mode 100644 index ea59662..0000000 --- a/TODO-phone-alarm-session.md +++ /dev/null @@ -1,96 +0,0 @@ -# Next session: make the phone a real alarm (items 4, 3, 1) - -REMOVE ME AFTER FINISH - -Paste the block at the bottom into a fresh session. Everything above it is -context for *you*, the human, deciding whether the framing is right. - -## Why this is its own session - -The PC half is done (phases 0–7, `4286fcb`..`c131923`). What is left is -almost entirely new Kotlin + Dart on a bleeding-edge Android toolchain, needs -an emulator that is not installed yet, and ends in physical-device -verification. The plan calls it "biggest chunk by far. Own session." - -## State of the phone app, measured 2026-08-27 (not guessed) - -- `phone_app/lib/` has 3 screens and 7 services. **Zero alarm capability** — - it is a time picker that syncs a value. -- `MainActivity.kt` exists at - `android/app/src/main/kotlin/com/kuhy/wake_alarm_sync/MainActivity.kt` and - is bare. `applicationId = "com.kuhy.wake_alarm_sync"`. -- **`minSdk`/`targetSdk`/`compileSdk` are all delegated** to the Flutter - Gradle plugin (`flutter.minSdkVersion` etc.) — there is no floor, and - full-screen-intent and foreground-service semantics cannot be reasoned - about without one. Pin them first. -- **Only `/usr/bin/adb` is on PATH.** No `emulator`, no `avdmanager`, no - `sdkmanager`. The emulator work in the plan needs these installed first, - and the emulator image must be **API 36** to match the phone — the API 34 - image validates against semantics the target device does not have. - -## The prompt - -``` -Implement items 4, 3 and 1 from TODO-hardening-continue.md in ~/wake-alarm — -the phone half of the wake alarm. - -Read first, in this order: - 1. TODO-hardening-continue.md (state, decisions, traps) - 2. ~/.claude/plans/wake-alarm-still-does-witty-toucan.md, phases 4 and 5 - (the full design, with every measured finding) - 3. CLAUDE.md (repo invariants) - -Scope, in order: - -1. Prerequisites, before any feature work: - - Pin explicit minSdk/targetSdk/compileSdk in - phone_app/android/app/build.gradle.kts. They are all delegated to the - Flutter plugin today, so there is no floor to reason about. - - Install emulator/avdmanager/sdkmanager (only adb is on PATH) and create - an API 36 AVD. API 34 validates against semantics the phone lacks. - -2. Item 4 + 1 — the phone actually rings. Hand-written Kotlin behind ONE - MethodChannel on the existing bare MainActivity.kt, never a plugin: the - stack is AGP 9.0.1 / Gradle 9.1 / Kotlin 2.3.20, and a plugin that will - not compile breaks the build outright rather than degrading. Five files: - AlarmScheduler, AlarmService, AlarmActivity, BootReceiver, AlarmChannel. - AlarmManager.setAlarmClock(), a foreground service with a wakelock, - STREAM_ALARM (load-bearing — it is what ignores DND and silent mode, never - STREAM_MUSIC), a full-screen intent over the lockscreen, re-arm on - BOOT_COMPLETED *and* on dismiss. - Dart side: main.dart has no init seam today — it needs - WidgetsFlutterBinding.ensureInitialized(), a navigatorKey and a route. - Port the five challenge kinds from _challenges.py so both halves behave - identically. - -3. Item 3 — separate-room proofs. NFC or QR qualifies; steps alone does NOT - (the phone is stationary while you walk to it, so steps only accrue after - pickup and can be faked by shaking it in bed). Steps + elapsed time is the - fallback when both others fail. This is a decision already taken — do not - re-litigate it. - -Constraints: -- 100% branch coverage, no new # noqa / type: ignore. -- The 250-line gate fires on STAGED files. Do not edit an oversized file this - change does not already have to touch — that cascade cost a full revert - last session. A new constant goes in its single consumer. -- Verify on the PHONE (23181JEGR08034) via the phone-deploy skill, not just - the emulator. A companion-app change is not done on desktop. -- Do not touch the PC half's invariants: no [Install] on wake-alarm.service, - _state.py stays UTC-keyed (screen_locker mirrors that helper), the trigger - starts morning-routine.service. - -Item 8 (mutual gating) depends on this and is the session after. Item 13 -(black screen) is a different repo, host-only, and blocks nothing. -``` - -## After that session - -- **Item 8, mutual gating.** Phone challenge first, then PC, with a bounded - escape: if sync is unreachable ~600 s the PC gate degrades to - locally-solvable and logs a WARNING. Without it, a Firebase outage means you - can neither dismiss nor get to work. This is the phase that legitimately - touches `_sync.py`, so it carries that file's 371-line split. -- **Item 13, black screen.** `~/testsAndMisc/linux_configuration` plus /boot - and /etc. Host-only — vmbox is BIOS/GRUB and this machine is UEFI, so a - guest pass there is actively misleading.