Record the phone alarm done; drop the finished session file

Items 1 and 4 ship and are emulator-verified. Item 3 has NFC and the
steps+elapsed fallback wired into the ring screen; the QR scanner UI is the
remaining piece and is now the top entry under "Remaining".

Records what has NOT been verified rather than letting the deploy stand in for
it: the ring has only ever fired on the API 36 AVD, never on the Pixel, and
never with DND on — which is the one condition the STREAM_ALARM claim rests on.

Four new traps, all of which cost time here: the plan's LogStore.changes does
not exist (so re-arm needs three explicit hooks), the Android SDK was never
missing but merely unexported, a manifest permission is not a granted
permission, and a debug probe with its own UI proves nothing about the route
it was written to test.

TODO-phone-alarm-session.md deleted: its work is done, and a finished task
file must not be left behind.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EckWmyfetYq3Mi8MAnUudh
This commit is contained in:
Krzysztof Rudnicki 2026-08-27 21:30:38 +02:00
parent cb50e58a56
commit 9980b47cf4
2 changed files with 36 additions and 100 deletions

View File

@ -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

View File

@ -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.