# Watch Glance 0.6.2

Version: `0.6.2-dev` (feature candidate after the 0.6.1 daily-usable baseline).
Base: `b9d2503` (`main`, 0.6.1). Original candidate: `d8ca095`.
Physical acceptance fixes: `codex/watch-glance-0.6.2-fixes`.

## Goal

Make the Watch page a very light Agent/Host status terminal while it remains
"a watch first". The time stays the absolute visual focus; the page must not
become a dashboard and must not poll faster.

## Layout

```
        [battery] 98%

          SUN OCT 05

            21:42

Host           Busy
Task           Running  01:34
Codex          72%  23:40
```

- Date moved above the time; `WatchTime` remains the native 88 px resource and
  is the largest text role on the page.
- Three fixed rows under the time. Labels and values use the 24 px
  `StatusPrimary` role; labels retain the secondary color; values use a restrained per-state tone
  (`primary`/`accent`/`done`/`failed`/`approval`/`secondary`).
- All three rows share x=90 left and x=378 right edges. Date y=86 and
  time y=144 put the native time line bottom at y=273 (~59% of screen height),
  leaving room for row centers y=294/334/374 inside the round bezel.
  Host renderer `test_layout.py` asserts every box is round-safe.
- The battery row is drawn from existing primitives only
  (`drawRoundRect` + `fillRect`/`fillRoundRect`): a small outline, a nub and a
  proportional fill, with the numeric percentage always visible. No icon font,
  no bitmap asset. An invalid battery shows the outline plus `--`.
- Normal `Wi-Fi` and `Agent online` text is removed from the Watch. Network
  diagnostics remain in `Settings → System`; abnormal states surface through the
  `Host` and `Task` rows.

## Host row

Gateway derives `Offline` / `Idle` / `Active` / `Busy` from the existing 2 s
snapshot (`server/device_gateway/collectors/host_state.py`) and ships it as
`host_state` in the same frame. Thresholds: CPU ≥ 70% or MEM ≥ 90% → Busy;
CPU ≥ 20% → Active; otherwise Idle. Both gateway and firmware apply a 5%
hysteresis band so 69/71 sampling noise cannot flip the row every 2 s. The
firmware mirrors the same rules (`boards/common/include/watch_status.h`) so a
dropped link still derives a sensible state from cached values.

**No new WSS request.** The Watch reads only locally cached state.

## Task row

Authoritative agent state is mapped to a single word:

| Native state | Watch |
| --- | --- |
| `RUNNING` | `Running` (+ elapsed while selected and online) |
| `WAITING_APPROVAL` | `Approval` |
| `WAITING_INPUT` | `Waiting` |
| `DONE` | `Done` |
| `FAILED` | `Failed` |
| `IDLE` / `DORMANT` | `Idle` |
| `OFFLINE` | `Offline` |
| no selected target | `None` |

`Running` reuses the existing 1 Hz shell tick; no new timer was added.

## Codex quota row

Data comes from the installed Codex App Server (verified `codex-cli 0.154.0`,
local `generate-json-schema --experimental`):

- `account/rateLimits/read` is called **once** after a successful
  `initialize`. The device never requests it and no periodic poll exists.
- `account/rateLimits/updated` is sparse and is handled in the JSON socket
  reader: the observed values apply immediately and a single bounded
  `asyncio.create_task` refetch is scheduled. The reader never awaits an RPC,
  and a notification storm cannot create unbounded concurrent reads (lock +
  at-most-one in-flight task).
- Bucket selection prefers `rateLimitsByLimitId["codex"]`, falling back to the
  backward-compatible `rateLimits` only when its `limitId` is absent or `codex`.
- Window selection uses the tighter remaining percentage
  (`clamp(100 - usedPercent, 0, 100)`); ties break to the earlier valid reset.
- `resetsAt` (Unix seconds) renders as `23:40` when it is today, otherwise
  `Tue 08:00`, using the existing demo UTC+8 display. `windowDurationMins` is
  never shown.
- Only `codex_quota_valid`, `codex_remaining_percent`, `codex_resets_at` and
  `codex_window_minutes` reach the device. accountId, plan type and credits are
  dropped. The row is called `Codex`, never `Tokens`: this is a rate limit, not
  a context token budget.

## Frequency and redraw guarantees

- The Codex quota rides the **existing** `server_snapshot` frame; WSS frame
  frequency before == after. `GATEWAY_SNAPSHOT_INTERVAL` is unchanged at 2 s.
- The Watch redraws only when a displayed value actually changes
  (Host classification, Task label/Running timer, Codex percent/reset text,
  or the clock minute). Hidden Agent metadata never repaints Watch; bounded UTF-8
  names/results compare against the same prefix used during storage. Host is
  invalidated to Offline on disconnect and waits for a fresh snapshot. Two identical
  raw snapshots produce no repaint. The Agent page never repaints on a server
  snapshot. Monitor keeps its live per-frame updates.

## Verification

`./scripts/test-wearable-shell.sh`, `./scripts/test-network-resilience.sh`,
`./scripts/test-server.sh` and `./scripts/build.sh m5stack-stopwatch` all pass.

New coverage:

- `server/tests/test_host_state.py` — Offline/Idle/Active/Busy, threshold
  boundaries and hysteresis.
- `server/tests/test_codex_quota.py` — primary/secondary/both, tightest-window
  selection, used 0/100, invalid reset, `rateLimitsByLimitId.codex`, fallback
  `rateLimits`, and that account metadata is not exposed.
- `server/tests/test_codex_quota_refresh.py` — one initial read, notification
  schedules a refresh, the reader is not blocked, storms stay bounded, failure
  keeps the last value.
- `server/tests/test_codex_quota_transport.py` — the existing snapshot carries
  the quota, no new periodic request/message exists, interval unchanged.
- `firmware/tests/watch_status_test.cpp` — host thresholds/hysteresis, task
  mapping (all states), Codex percent/reset rendering, redraw comparison.
- `firmware/tests/wearable_renderer_test.cpp` — date above time, time remains
  largest, three rows present, battery numeric, normal Wi-Fi/Agent online
  absent, all sizes/themes round-safe.
- `prototypes/wearable_shell_0_6a/test_layout.py` — Watch hierarchy and
  round-safety in the host renderer.

## Out of scope

Alarm, Double-B, touch, speech output, Agent/Monitor redesign, Session Picker,
Approval and Native UserInput behavior, Voice Confirm/Direct, network push
frequency and power-save policy were not touched.

## Physical acceptance closeout — 2026-10-05

Result: **READY TO MERGE**. The flashed and accepted firmware source is
`a34bbe0bcb885cd216df78944e379ede3cf8d8a9`, based on Pi candidate
`d8ca09506d0e749b87f51ea2cbdc0488528d14d4`; firmware reports `0.6.2-dev`.
This closeout keeps the accepted version and firmware behavior.

- Firmware: 3,256,448 bytes; SHA256
  `59a35a7b60d723427bb02ce3812a4d67a873bc086292e1ad29d5ff22bc15ff8b`.
- Merged image: 3,321,984 bytes; SHA256
  `e962ee2dea5613e36c42358290a3e9cd8ea298f510cc982a9819a5678e100724`.
  Mac checksums and flash verification passed; NVS/PHY settings were preserved.
- User confirmed common left/right row edges, date/time position and whitespace,
  time as the visual focus, larger 24 px status text, and no clipping/overlap.
- Wi-Fi/WSS/sync succeeded on the first connection attempt. Unexpected reboot,
  panic, watchdog and post-sync disconnect counts were all zero.
- The safe task progressed Running → Done, elapsed 0 → 24 seconds.
- Native quota, Gateway payload and user-observed Watch text matched:
  `56% / Sun 19:52`; native reset was `2026-10-11 19:52:25 UTC+8`.
- In the 60-second observation window, 30 server snapshots arrived at a median
  interval of 2.004 seconds. All 30 Done snapshots had `changed_view=0` despite
  a 224-byte UTF-8 session name. Four Watch redraws matched one clock minute
  change and three Host state changes; no periodic Agent metadata redraw remained.
- Host Offline invalidation passed production disconnect/reconnect callback
  tests; an additional physical disconnect was not performed.
- Wearable shell, Agent view/UTF-8, network resilience (88 Python tests and
  associated Node/C++/navigator checks), host layout (5 tests), firmware build,
  linked font coverage and vendor I2C ABI checks passed.
- Battery icon, A/B navigation, Agent/Monitor, Voice sanity, Settings Hold A Back
  and Existing Session passed the preceding candidate acceptance. Their behavior
  was unchanged; relevant production input regressions passed after the fixes.

Gateway server code is identical between the original Pi candidate and the
accepted fix commit. Acceptance retained that candidate Gateway with HTTP 200
health and `NRestarts=0`, without another restart. The original Pi target was
restored, repo-external state was retained, and one serial monitor remained.

## 下一步计划

本轮收口后停止开发。以下三项留待下一轮分别实现、修复并做真机验收：

1. **双击 / Double-B**：先明确动作映射与双击识别规则，再实现防抖和事件归属。
   验收覆盖单击、双击、长按及误触，保持现有 A/B 导航、Hold A Voice、
   Hold B Settings、Settings Back 和 Approval/Voice 的输入优先级。
2. **触屏 / Touch**：验证触控初始化、坐标方向和圆屏有效区域，再接入所需手势。
   验收覆盖边缘触点、误触、与实体按键及模态交互的冲突，并保留按键操作路径。
3. **Speech 问题修复**：从既有下载/播放及网络资源问题的证据开始，分别定位
   Gateway/TTS、公共下行、设备 HTTP/TLS 接收、缓冲与播放、取消/切换的边界。
   不把历史推测当作已确认根因。先用独立候选恢复可靠播放，再验收 Voice、WSS、
   输入响应和 Agent 状态不受影响；通过前保持 Speech Output 冻结。

本轮没有实现或启用上述功能，也没有改变 WSS cadence 或省电策略。
