diff --git a/README.md b/README.md
index 458b9f3..971739b 100644
--- a/README.md
+++ b/README.md
@@ -5,6 +5,21 @@
- 设计文档:见 `ring-health-docs` 仓库 `superpowers/specs/`
- 当前阶段:M1 静态页面(数据来自 Mock,未接入蓝牙 SDK)
+## 页面清单(M1)
+
+| 页面 | 路由 |
+|---|---|
+| 健康首页 | `home` |
+| 运动 | `workout` |
+| 运动历史 | `workout/history` |
+| 设备 | `device` |
+| 设备设置 | `device/settings` |
+| 我的 | `profile` |
+| 指标详情(步数/心率/睡眠/血氧/压力/体温/血压) | `detail/{metric}` |
+
+> 页面路由的唯一定义处是 `core/navigation/Routes.kt`,上表与其保持一致。
+> M1 的验收状态见 `docs/verification-m1.md`。
+
## 构建
```bash
diff --git a/app/src/main/java/com/placeholder/ringhealth/data/mock/MockRingDeviceRepository.kt b/app/src/main/java/com/placeholder/ringhealth/data/mock/MockRingDeviceRepository.kt
index c4f1b3e..669a9b8 100644
--- a/app/src/main/java/com/placeholder/ringhealth/data/mock/MockRingDeviceRepository.kt
+++ b/app/src/main/java/com/placeholder/ringhealth/data/mock/MockRingDeviceRepository.kt
@@ -33,6 +33,14 @@ class MockRingDeviceRepository(
_deviceState.value = provider.deviceState()
}
+ /**
+ * 切换某个功能开关。
+ *
+ * **必须返回新列表**(`map` / `+` 等),不要原地改写同一个 `MutableList`:
+ * `DeviceViewModel` 的 `stateIn(initialValue = …)` 在**构造时**就把当时的 List 引用冻结了,
+ * 而 `DeviceViewModelTest` 第一条用例的鉴别力正依赖于此 —— 若这里改成原地改写,
+ * `initialValue` 会被连带改动,那条断言就退化成自我确认,测试会静默失去意义。
+ */
override fun setFunctionEnabled(id: String, enabled: Boolean) {
_functionSettings.update { settings ->
settings.map { if (it.id == id) it.copy(enabled = enabled) else it }
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 1992dda..5520167 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -11,9 +11,6 @@
运动
设备
我的
- 设备设置
- 运动历史
- 指标详情
%1$s · 已连接
未连接设备
diff --git a/docs/verification-m1.md b/docs/verification-m1.md
new file mode 100644
index 0000000..c94dfce
--- /dev/null
+++ b/docs/verification-m1.md
@@ -0,0 +1,97 @@
+# M1 静态页面验收记录
+
+## 环境
+
+- 记录日期:2026-09-10
+- 构建版本:0.1.0 (versionCode 1)
+- 构建机:Windows 11,JDK 17.0.12,Android SDK `D:\Android\Sdk`(仅 `platform-tools` /
+ `platforms;android-36` / `build-tools;36.1.0`)
+- **测试设备:无** —— 本机没有安装任何模拟器镜像,也没有连接真机
+
+补充证据(说明「无设备」不是随口一说,而是逐项查过的):
+
+```
+$ ls D:\Android\Sdk
+build-tools cmdline-tools licenses platform-tools platforms
+ # 没有 emulator/,也没有 system-images/
+
+$ adb devices
+List of devices attached
+ # 空列表,没有连接真机
+```
+
+`C:\Users\10314\.android\avd\` 下**残留着两个 AVD 定义**(`Medium_Phone_API_36.1`、
+`Pixel_9_Pro`),但它们是**不可用的空壳**:二者都指向已删除的旧 SDK
+`C:\Users\10314\AppData\Local\Android\Sdk`(其 `emulator\emulator.exe` 与
+`system-images\android-36.1\google_apis_playstore\x86_64\` 均已不存在),且属于另一个项目
+(`D:\work\xiangmu\mediq-app`)。**不要因为看到 AVD 目录就以为本机可以起模拟器。**
+另注:当前 shell 的 `ANDROID_HOME` 仍指向那个已删除的旧路径,构建实际靠
+`local.properties` 的 `sdk.dir=D:\Android\Sdk` 生效。
+
+## 自动化验证(已执行)
+
+- 单元测试:`./gradlew :app:testDebugUnitTest` —— **21 项全绿**
+ (`FormattersTest` 7 + `MockDataProviderTest` 5 + `HomeViewModelTest` 1 + `WorkoutViewModelTest` 1
+ + `DeviceViewModelTest` 2 + `MetricDetailViewModelTest` 3 + `RoutesTest` 2)
+- 构建:`./gradlew clean assembleDebug` —— `BUILD SUCCESSFUL`,
+ 产物 `app/build/outputs/apk/debug/app-debug.apk`(18,907,329 字节)
+- **未执行**:各页面的 `@Preview` 在 Android Studio 中渲染。本机没有 IDE GUI,agent 无法打开
+ Preview 面板,**这一项与人工验收一样是未执行的** —— 不要把它写进「已执行」段落
+ (旧稿把它列在上面这一节里,那等于声称验证过一件没做过的事)。
+
+## 人工验收(未执行 —— 无设备)
+
+下表**全部未打勾**。这份清单已原样保留,交给有设备的人执行;不要在没有实测的情况下填入结果。
+
+| 项 | 结果 | 备注 |
+|---|---|---|
+| 应用启动无崩溃 | 未执行 | 无设备 |
+| 四 Tab 切换 | 未执行 | 无设备 |
+| 重按已高亮的父 Tab 的行为 | 未执行 | 需实测:推测会停在子页而非回到 Tab 根 |
+| 每个页面的 `@Preview` 可渲染 | 未执行 | 需在 Android Studio 中逐个打开确认 |
+| 次要文字灰度一致 | 未执行 | 不得出现 `TextSecondary` 与 `onSurfaceVariant` 混用 |
+| 首页:设备状态条 / 步数大环 8,426 / 六个指标卡(血压“未开启”) | 未执行 | 无设备 |
+| 指标详情页:周/月切换(7 根 / 30 根柱子) | 未执行 | 无设备 |
+| 睡眠详情:分期卡片只在睡眠指标出现,入睡/起床读起来像「一晚」 | 未执行 | 无设备 |
+| 运动页 / 运动历史页 | 未执行 | 无设备 |
+| 设备页 / 设备设置开关(返回后状态保持) | 未执行 | 无设备 |
+| 我的页 | 未执行 | 无设备 |
+| 旋转屏幕、深色系统设置下不崩溃 | 未执行 | 无设备 |
+
+### 需在有设备时重点确认的项(原清单细目,逐条保留)
+
+- 应用启动无崩溃,底部四个 Tab 显示为 健康 / 运动 / 设备 / 我的
+- 四个 Tab 可自由切换,切换后状态保留、无闪烁
+- **重按已高亮的父 Tab 的行为**:实现为 `popUpTo(start){saveState}` + `restoreState`,
+ 推测会**恢复到该 Tab 保存的子页栈**(停在子页而非回到 Tab 根)。请实测并记录实际行为;
+ 若实测是「停在子页」而产品希望「回到根」,那是一个独立的小改动,记下来即可
+- 首页:步数大环显示 8,426,目标 10,000,环按比例填充
+- 首页:点击任一指标卡进入详情页,标题为该指标中文名
+- 详情页:返回后回到首页
+- 运动页:显示“开始运动”卡片 + 5 条运动记录;「查看全部」进入历史页后底部导航栏仍可见
+- 设备页:戒指卡片显示电量 78%、固件版本 1.0.5
+- 睡眠卡片的入睡/起床时间**读起来像「一晚」**(如 入睡 22:41 / 起床 06:41),而不是跨好几天。
+ 阶段一按「最后一个数据点往前 8 小时」演示(R29);若显示成跨天,说明
+ `DEMO_SLEEP_WINDOW_MILLIS` 那段被改坏了
+
+## 已知限制(阶段一有意为之)
+
+- 数据全部为 Mock,不连蓝牙、不连后端
+- 血压恒为“未开启”占位(`HealthSummary.bloodPressure = null`)
+- 设备设置页的开关只改内存状态,重启即重置(对应 M2 的协议 0502/0504)
+- 睡眠分期条与入睡/起床时间是演示值(固定比例 + 最后一个数据点往前 8 小时),无真实分期数据
+- 详情页统计摘要一律保留一位小数(步数因此显示为「平均 7512.3」这类精度过剩的形式)
+- 仅浅色模式
+
+## 覆盖缺口声明
+
+**M1 的全部运行时行为目前只有编译期与单元测试保证,没有任何一次真机/模拟器运行。**
+`assembleDebug` 成功只说明代码能编译、能打包,**不说明界面正确、不说明不会崩溃**。
+这是本机没有设备导致的已知缺口(ledger Ruling R21),不是「已验证通过」。
+首次拿到设备时,请从上面那张人工验收表开始逐项补齐。
+
+具体而言,**没有任何一行 M1 的 Compose 界面代码被真正渲染过**:导航图能否起来、
+四个 Tab 是否显示正确文案、`MetricBarChart` 的柱子数量、`RingProgress` 的填充比例、
+睡眠分期卡片是否只在睡眠指标下出现、次要文字灰度是否统一 —— 这些全部未经运行时检验。
+单元测试覆盖的是 `Formatters` 的纯函数、`MockDataProvider` 的数据形状,以及七个 ViewModel
+的状态流;**它们不经过 Compose 渲染管线,也不能发现布局、主题或导航层面的错误。**