docs: add next-phase roadmap, milestone design docs, and CLAUDE.md
pytest / test (push) Successful in 54s
pytest / test (push) Successful in 54s
- roadmap.md: M1 (DB consolidation) -> M2 (React SPA) -> M3 (token/mobile) - docs/design/: agent-pipeline design docs with atomic tasks for M1-M3 - CLAUDE.md: workflow, doc map, commit conventions, review-notes briefing flow - .gitignore: ignore local review-notes/
This commit is contained in:
@@ -5,3 +5,4 @@
|
||||
__pycache__/
|
||||
*.pyc
|
||||
data/
|
||||
review-notes/
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
# CLAUDE.md — Home Automation Backend
|
||||
|
||||
本文件每次会话自动加载。它定义本项目的**工作流程、文档位置、commit 规范**。请在动手前先读完。
|
||||
|
||||
## 项目速览
|
||||
|
||||
- 个人用 home-automation 后端:**FastAPI + SQLite + SQLAlchemy + Alembic**,服务端模板(Jinja,M2 将换成 React SPA)。
|
||||
- 单 admin 鉴权(Argon2 + server-side session cookie),runtime config 落 `app_config` 表。
|
||||
- 模块:public IPv4 monitor、SMTP 通知、location recorder、poo recorder、Home Assistant in/out、TickTick OAuth。
|
||||
- 已发布 `v1.0.3`。下一阶段方向:**M1 单库化 → M2 React 前端 → M3 token/移动端(远期,M2 后再说)**。
|
||||
- **当前现实**:在 M1 完成前仍是**三个独立 SQLite 库**(app / location / poo),三套 DeclarativeBase、三条 Alembic 链。不要假设已经单库——以代码现状为准。
|
||||
- 明确不做:Notion 模块。
|
||||
|
||||
## 文档地图与「开工前必读」
|
||||
|
||||
文档都在 `docs/`:
|
||||
|
||||
| 路径 | 作用 |
|
||||
| --- | --- |
|
||||
| `docs/roadmap.md` | 全局规划与里程碑总览 |
|
||||
| `docs/design/README.md` | **协作契约**:任务卡格式、原子任务定义、校验闸门、数据安全红线 |
|
||||
| `docs/design/m1-db-consolidation.md` | M1 原子任务(含真实代码现状盘点 + 人工 runbook) |
|
||||
| `docs/design/m2-frontend-v2.md` | M2 原子任务 + API 契约 + 前端校验闸门 |
|
||||
| `docs/design/m3-token-mobile.md` | M3(远期,暂缓) |
|
||||
| `docs/*.md`(auth / public-ip-monitor / location-recorder …) | 各模块说明,按需读 |
|
||||
|
||||
**开工时读取顺序**:
|
||||
1. `docs/design/README.md`(每轮都读,它是流程与验收的共同契约)。
|
||||
2. 本轮对应的 milestone 文档(如 `docs/design/m1-db-consolidation.md`),定位要做的任务卡。
|
||||
3. 任务卡 `Files` 列出的源文件 + 该模块的 `docs/*.md`(按需)。
|
||||
4. `docs/roadmap.md` 仅在需要全局视角时读。
|
||||
|
||||
## 工作流程
|
||||
|
||||
### 实现模式(由用户的提示词决定)
|
||||
|
||||
- **默认逐步**:给一个 milestone 文档,按其中原子任务**一步一步**实现。
|
||||
- **(a) 只实现一步**:用户说"只实现一步 / 这一个任务"时,**只做那一个任务卡**,跑完校验闸门后停下,等用户确认,不要顺手往下做。
|
||||
- **(b) 完成整个 milestone**:仅当用户在提示词里**显式要求启用 sub-agent 并指定模型**时,才用指定模型起 implementer sub-agent,按任务依赖顺序跑完整条链。
|
||||
- **Sub-agent 纪律**:只在用户显式要求时才 spawn sub-agent;单步/小改动在主线内联完成。起 sub-agent 时用用户**指定的模型**(Agent 工具的 `model` 覆盖)。
|
||||
|
||||
### 角色(Orchestrator → Implementer → Reviewer)
|
||||
|
||||
- 我(主线)= **Orchestrator**:挑依赖已满足的下一个任务、派发、转述结果、维护任务 `Status`。
|
||||
- **Implementer**(便宜模型,用户指定):一次一个任务,严格按任务卡,不扩范围。
|
||||
- **Reviewer**(强模型,用户指定):实现完成后起 Reviewer sub-agent,按任务卡 `Acceptance criteria` + `Reviewer checklist` 复核、**独立重跑校验闸门**,驱动 implementer 返工直到本轮 PASS。
|
||||
|
||||
### 校验闸门(每个任务结束都要全绿)
|
||||
|
||||
根目录、激活 `.venv` 后:
|
||||
```bash
|
||||
pytest # 权威闸门(CI 跑的就是它)
|
||||
ruff check . # line-length=100
|
||||
python scripts/export_openapi.py && git diff --exit-code openapi/ # 改了路由/schema 才需要,且产物须入库
|
||||
```
|
||||
前端任务(M2)在 `frontend/` 下另跑 `npm run lint && npm run typecheck && npm run test && npm run build`(详见 m2 文档 §8)。
|
||||
**不过闸门就不算完成**,不得跳过、不得留红给下一轮。
|
||||
|
||||
## 每轮简报(`review-notes/`)
|
||||
|
||||
每轮工作都要在 `review-notes/` 下产出**中文简报**。该目录**已在 `.gitignore` 忽略**,纯本地、不入库——它是 agent 之间和与人之间的交接载体,不是仓库产物。
|
||||
|
||||
- **实现 / 返工简报**:每轮实现完成后(无论首次实现还是返工),写一份。文件名建议 `<task-id>-impl-<n>.md` / `<task-id>-rework-<n>.md`(如 `M1-T03-impl-1.md`、`M1-T03-rework-1.md`)。至少包含:
|
||||
1. **本轮修改的具体内容**(改了哪些文件、做了什么、为什么)。
|
||||
2. **自动化测试结果**(`pytest` / `ruff` / 前端闸门的实际输出或结论,通过/失败逐项写清)。
|
||||
3. **若需人工 walkthrough**:写明具体步骤(怎么启动、点哪里、预期看到什么);若无需人工验证,明确写"无需人工 walkthrough"。
|
||||
- **review 简报**:每轮 review 后写一份,文件名建议 `<task-id>-review-<n>.md`(如 `M1-T03-review-1.md`)。至少包含:评审结论(`PASS` 或带编号的返工清单)、对照任务卡 `Acceptance criteria` + `Reviewer checklist` 的逐条核对、reviewer 独立重跑校验闸门的结果。
|
||||
|
||||
**用途**:① reviewer 审核时参考对应的实现简报;② implementer 返工时参考对应的 review 简报;③ 人类(用户)通读这些简报确认有无问题。简报之间用文件名里的 `<task-id>` 与轮次 `<n>` 对应起来。
|
||||
|
||||
### Orchestrator 派发契约(让简报真正被读到)
|
||||
|
||||
**关键**:sub-agent 冷启动、不继承主线上下文,**不会因为本文件提到简报就自动去读**对应文件。简报能流转,靠的是 orchestrator(主线)在**每次 spawn 时把路径显式写进 prompt**,而不是被动约定。所以派发时必须做到:
|
||||
|
||||
- **显式告诉它「先读哪个简报」**:
|
||||
- 派 implementer 做**首次实现** → 传任务卡位置(milestone 文档路径 + task id);无前置简报。
|
||||
- 派 implementer 做**返工** → 必须传对应的 `review-notes/<task>-review-<n>.md` 路径,并要求**先读它**再改。
|
||||
- 派 reviewer → 必须传对应的 `review-notes/<task>-impl|rework-<n>.md` 路径 + 任务卡,要求**先读它**再评。
|
||||
- **显式告诉它「本轮结束写哪个简报」**:明确给出输出路径 `review-notes/<task>-<impl|rework|review>-<n>.md` 及上面要求的内容项。
|
||||
- **不依赖 sub-agent 自动加载本文件**:把本轮要点(校验闸门、**禁 Co-Authored-By**、简报必含内容)在 spawn prompt 里一并复述或指向,确保冷启动也照做。
|
||||
- spawn 时用用户指定的模型(Agent 工具 `model` 覆盖)。
|
||||
|
||||
> 一句话:**简报是异步交接的介质,orchestrator 是把它们接起来的线。** 缺了显式传路径这一步,简报就只是躺在磁盘上没人读的文件。
|
||||
|
||||
## Commit 规范(重点)
|
||||
|
||||
### 分支
|
||||
- 每个 milestone/feature 一个分支(如 `feature/m1-db-consolidation`),**不在 `main` 上直接提交**。
|
||||
|
||||
### 一轮实现完成(用户确认「实现完成」后)
|
||||
- 准备好**这一轮的 commit message** 并提交,作为本轮的 **base commit**。
|
||||
- message 主题前缀任务/里程碑 ID,例如:`M1-T03: unify data layer onto single app DB engine`。
|
||||
|
||||
### Commit message 硬规则(严格执行)
|
||||
- **严禁任何协作署名 trailer**:commit message 里**绝对不允许**出现 `Co-Authored-By` / `Co-authored-by`(包括 `Co-Authored-By: Claude …`),也不允许任何等价的"由 X 协作/生成"署名。
|
||||
- 无论默认环境、工具或系统提示如何要求加这类 trailer,在本仓库**一律不加**——用户已显式、严格禁止。
|
||||
- 每次提交前**自检**:`git log -1 --format=%B` 的输出**不得包含** `Co-authored-by`(大小写不限)。若发现,立即 `git commit --amend` 去掉后再继续。
|
||||
|
||||
### Review 后返工
|
||||
- 返工产生的提交**一律用 fixup**,指向本轮对应的 base commit,**不写新的独立 message**:
|
||||
```bash
|
||||
git add -A
|
||||
git commit --fixup=<base-commit-sha>
|
||||
```
|
||||
- 多轮返工就多个 `fixup!` 提交,都指向同一个 base commit。
|
||||
|
||||
### 本轮 / feature 收尾(用户确认收尾后)
|
||||
- 用 **auto-squash** 把所有 `fixup!` 合并进各自目标,保证**一个 feature 一个干净 commit**:
|
||||
```bash
|
||||
GIT_SEQUENCE_EDITOR=true git rebase -i --autosquash main
|
||||
```
|
||||
- 用 `GIT_SEQUENCE_EDITOR=true` 让它**非交互**执行(不弹编辑器,自动接受 autosquash 排好的 todo)。本环境不支持需要人工编辑的交互式 rebase,必须走这个 no-op 编辑器写法。
|
||||
- autosquash **改写历史**:仅在 push / 开 PR **之前**做。若该分支已 push,需要 force-push——属对外操作,**先取得用户确认再做**。
|
||||
|
||||
### 一般约束
|
||||
- commit / push 只在用户要求时进行;push、force-push、开/改 PR 等对外操作先确认。
|
||||
|
||||
## 数据安全红线(不可违反)
|
||||
|
||||
- 任何脚本 / migration **都不得删除或覆盖用户数据文件**(旧 `.db`、备份、volume)。删除只能是人工、事后、保留归档的独立步骤(见 `docs/design/m1-db-consolidation.md` §6 runbook)。
|
||||
- 涉及历史数据的迁移**先在备份副本上演练**;迁移脚本必须幂等且搬完对账行数。
|
||||
- Review 时只要发现"删文件 / drop 有数据的表 / truncate"出现在自动化任务里,直接判返工。
|
||||
|
||||
## 常用命令
|
||||
|
||||
```bash
|
||||
# 环境
|
||||
python -m venv .venv && source .venv/bin/activate && pip install -r dev-requirements.txt
|
||||
# 迁移(初始化/适配 DB)
|
||||
python -m scripts.run_migrations
|
||||
# 起服务
|
||||
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
|
||||
# 测试 / lint / OpenAPI 导出
|
||||
pytest
|
||||
ruff check .
|
||||
python scripts/export_openapi.py
|
||||
```
|
||||
@@ -0,0 +1,127 @@
|
||||
# 设计文档与多模型协作约定
|
||||
|
||||
本目录把 `docs/roadmap.md` 里的三个里程碑展开成**可被 coding agent 流水线执行**的详细设计文档。
|
||||
|
||||
- [`m1-db-consolidation.md`](./m1-db-consolidation.md) — 单库化地基
|
||||
- [`m2-frontend-v2.md`](./m2-frontend-v2.md) — React SPA 前端 v2
|
||||
- [`m3-token-mobile.md`](./m3-token-mobile.md) — token 鉴权与移动端(远期)
|
||||
|
||||
本文件定义**所有任务共用的格式与协作规则**,三个里程碑文档不再重复这些约定。
|
||||
|
||||
---
|
||||
|
||||
## 1. 协作模型:Orchestrator → Implementer → Reviewer
|
||||
|
||||
设计目标是让三个不同档位的模型分工协作:
|
||||
|
||||
| 角色 | 模型档位 | 职责 |
|
||||
| --- | --- | --- |
|
||||
| **Orchestrator(编排者)** | 强模型 | 读里程碑文档,挑出依赖已满足的下一个原子任务,派发给 implementer;收到 reviewer 的 PASS 后推进下一个任务 |
|
||||
| **Implementer(实现者)** | 便宜模型 | 一次只做**一个**原子任务,严格按任务卡执行,跑完本地校验后回报 |
|
||||
| **Reviewer(评审者)** | 强模型 | 对照验收标准 + Reviewer checklist 独立复核、独立跑校验闸门,返回 `PASS` 或一份编号返工清单 |
|
||||
|
||||
### 循环
|
||||
|
||||
```
|
||||
Orchestrator 选任务 T(其 Depends 全部为 done)
|
||||
│
|
||||
▼
|
||||
Implementer 实现 T ──► 跑校验闸门 ──► 回报 diff + 校验输出
|
||||
│
|
||||
▼
|
||||
Reviewer 复核 T
|
||||
├── PASS ─────────► Orchestrator 标记 T 为 done,进入下一个任务
|
||||
└── REWORK[1..n] ─► 退回 Implementer,按编号逐条修,直到 PASS
|
||||
```
|
||||
|
||||
### 角色边界(重要)
|
||||
|
||||
- **Implementer 不得扩大范围**:只能改任务卡 `Files` 里列出的文件;超出范围的问题要在回报里以 `OUT-OF-SCOPE:` 标注,交给 Orchestrator 决定是否新开任务,而不是顺手改掉。
|
||||
- **Reviewer 必须独立重跑校验闸门**,不能只信 implementer 的回报。
|
||||
- **Reviewer 的返工清单必须可执行、带编号**(`REWORK 1: ...`),不写主观感受。
|
||||
- 一个任务**不通过校验闸门就不算完成**,Orchestrator 不得跳过。
|
||||
|
||||
---
|
||||
|
||||
## 2. 原子任务的定义
|
||||
|
||||
一个"原子任务"必须同时满足:
|
||||
|
||||
1. **单一关注点**:一个任务只解决一件事(一次 schema 变更、一个端点、一个模块迁移……)。
|
||||
2. **PR 大小**:理想 diff < ~200 行(结构性 sweep 任务可放宽,但应在卡上标 `[structural]` 并优先派给较强 implementer)。
|
||||
3. **边界处可绿**:任务完成时,整个仓库通过校验闸门(见下)。**一个任务"拥有"它所改代码对应的测试**——如果改动会让某些现有测试失败,修这些测试就属于这个任务的范围,不允许留红给下一个任务。
|
||||
4. **可独立验收**:验收标准是客观、可机械检查的断言,不依赖人的主观判断。
|
||||
5. **依赖显式**:通过 `Depends` 字段声明前置任务。没有声明依赖的任务,Orchestrator 可并行派发。
|
||||
|
||||
---
|
||||
|
||||
## 3. 任务卡格式
|
||||
|
||||
每个任务在里程碑文档中以如下结构出现。Implementer 和 Reviewer 都只需要任务卡 + 本约定文件,**不需要读其它任务**即可工作。
|
||||
|
||||
```markdown
|
||||
### M{n}-T{nn} — <标题> [structural?]
|
||||
|
||||
- **Status**: `todo` | `in-progress` | `in-review` | `done`
|
||||
- **Depends**: M{n}-T{nn}, …(或 `none`)
|
||||
- **Context**: 1–2 句,为什么要做这个、它在里程碑里的位置。
|
||||
|
||||
**Files**(精确到路径,标注动作)
|
||||
- `create path/to/new_file.py`
|
||||
- `modify path/to/existing.py`
|
||||
- `delete path/to/old.py`
|
||||
|
||||
**Steps**(便宜模型可直接照做的有序步骤)
|
||||
1. …
|
||||
2. …
|
||||
|
||||
**Out of scope / 不要碰**
|
||||
- …(明确列出容易被误改的相邻区域,约束便宜模型漂移)
|
||||
|
||||
**Acceptance criteria**(客观、可勾选;Reviewer 逐条核)
|
||||
- [ ] …
|
||||
- [ ] 校验闸门全绿(见 §4)
|
||||
|
||||
**Reviewer checklist**(除验收标准外,强模型重点看的点)
|
||||
- …
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 校验闸门(每个任务结束都要全绿)
|
||||
|
||||
在仓库根目录、激活 `.venv` 后执行:
|
||||
|
||||
```bash
|
||||
# 1) 单元 / 集成测试(CI 同款,权威闸门)
|
||||
pytest
|
||||
|
||||
# 2) Lint(pyproject 已配置 ruff,line-length=100)
|
||||
ruff check .
|
||||
|
||||
# 3) 若本任务改动了任何 HTTP 路由 / schema:重导出 OpenAPI 并确认已提交
|
||||
python scripts/export_openapi.py
|
||||
git diff --exit-code openapi/ # 必须无未提交差异
|
||||
```
|
||||
|
||||
- `pytest` 是**权威闸门**:`.github/workflows/pytest.yml` 跑的就是它,任何任务都不得让它变红。
|
||||
- 改了路由 / Pydantic schema 的任务,`openapi/openapi.json` 和 `openapi/openapi.yaml` 必须在同一任务里重新生成并提交(`openapi/` 纳入版本控制)。
|
||||
- 前端(M2/M3)相关任务的前端侧闸门(lint / typecheck / build)在对应里程碑文档里单独定义。
|
||||
|
||||
---
|
||||
|
||||
## 5. 提交与集成约定
|
||||
|
||||
- 每个任务一个 commit,message 前缀任务 ID,例如:`M1-T03: unify data layer onto single app DB engine`。
|
||||
- 一个里程碑在一个 feature 分支上推进(如 `feature/m1-db-consolidation`),按任务依赖顺序合并。
|
||||
- 任务卡里的 `Status` 字段由 Orchestrator 维护,作为流水线的单一进度源。
|
||||
- 涉及**不可逆 / 数据破坏**的步骤(删旧 DB 文件、删 Grafana volume 等)一律不进自动化任务,只在文档里标为人工步骤(见 M1 的"人工操作"小节)。
|
||||
|
||||
---
|
||||
|
||||
## 6. 数据安全红线(贯穿所有里程碑,不可违反)
|
||||
|
||||
1. **任何脚本 / migration 都不得删除或覆盖用户数据文件**(旧 `.db`、备份、volume)。删除只能是人工、事后、确认无误的独立步骤。
|
||||
2. 涉及历史数据的迁移**先在备份副本上演练**,再对真实库执行。
|
||||
3. 数据迁移脚本必须**幂等**且**搬完对账行数**,对不上立即中止并非零退出。
|
||||
4. 破坏性 Reviewer 一票否决:只要任务里出现"删文件 / drop 有数据的表 / truncate",Reviewer 直接 REWORK,要求改为人工步骤。
|
||||
@@ -0,0 +1,348 @@
|
||||
# M1 — 单库化地基(DB Consolidation)
|
||||
|
||||
> 阅读前提:先读 [`README.md`](./README.md)(协作模型、任务卡格式、校验闸门、数据安全红线)。本文档只展开 M1 的现状、目标与原子任务。
|
||||
|
||||
## 1. 目标
|
||||
|
||||
把 location、poo 两个独立 SQLite 库合并进 `app.db`,收敛成**单库 + 单 engine + 单 DeclarativeBase + 单 Alembic 链**,清理项目早期散落的数据层代码,并移除 Grafana。历史数据零丢失。
|
||||
|
||||
## 2. 现状(实现者可据此工作,不必重新通读全仓库)
|
||||
|
||||
**三套数据层(散落点)**
|
||||
- `app/db.py`:`Base` + `engine`/`SessionLocal`/`get_db_session` —— 实际绑定 `settings.location_database_url`(即 location 库,命名有误导性)。
|
||||
- `app/models/base.py`:仅 `from app.db import Base` 转出。
|
||||
- `app/poo_db.py`:`PooBase` + `poo_engine`/`PooSessionLocal`/`get_poo_db_session` —— 绑定 `poo_database_url`。
|
||||
- `app/auth_db.py`:`AuthBase` + 带 `lru_cache` 的 `_get_auth_engine` / `get_auth_session_local` / `reset_auth_db_caches` / `get_auth_db_session` —— 绑定 `app_database_url`(真正的 app 库)。
|
||||
|
||||
**模型与归属**
|
||||
- `app/models/auth.py`:`AuthUser`、`AuthSession`(`AuthBase` → app 库)
|
||||
- `app/models/config.py`:`AppConfigEntry`(`AuthBase` → app 库)
|
||||
- `app/models/public_ip.py`:`PublicIPState`、`PublicIPHistory`(`AuthBase` → app 库)
|
||||
- `app/models/location.py`:`Location`(`Base` → location 库),表 `location`,PK(`person`,`datetime`),`latitude`/`longitude` NOT NULL,`altitude` nullable
|
||||
- `app/models/poo.py`:`PooRecord`(`PooBase` → poo 库),表 `poo_records`,PK(`timestamp`),`status`/`latitude`/`longitude` NOT NULL
|
||||
- `app/models/__init__.py`:导出除 `PooRecord` 外的模型(`PooRecord` 单独存在)
|
||||
|
||||
**三条 Alembic 链**
|
||||
- `alembic_app.ini` + `alembic_app/`(`env.py` 用 `AuthBase.metadata`),head = `20260429_05_public_ip_monitor`
|
||||
- `alembic_location.ini` + `alembic_location/`,head = `20260419_01_location_baseline`
|
||||
- `alembic_poo.ini` + `alembic_poo/`,head = `20260420_01_poo_baseline`
|
||||
|
||||
**adoption / 启动链路**
|
||||
- `scripts/app_db_adopt.py`(常量 `APP_BASELINE_REVISION = "20260429_05_public_ip_monitor"`)
|
||||
- `scripts/location_db_adopt.py`、`scripts/poo_db_adopt.py`(含 legacy 校验:`EXPECTED_USER_VERSION`、表结构断言)
|
||||
- `scripts/run_migrations.py`:依次调用三个 adopt 函数,返回 `{"app","location","poo"}`
|
||||
- `app/main.py` lifespan:`ensure_runtime_dirs`(app/location/poo 三路径)、`ensure_auth_db_ready`、`ensure_location_db_ready`、`ensure_poo_db_ready`,再起 APScheduler 每 4h 检查 public IP
|
||||
|
||||
**依赖与路由**
|
||||
- `app/dependencies.py`:`get_auth_db`(app session)、`get_db`(location session)、`get_poo_db`(poo session)、`get_app_settings`、`get_current_auth_session`、`get_homeassistant_client`、`get_ticktick_client`
|
||||
- `app/api/routes/location.py`:`POST /location/record`,依赖 `get_db`,**无鉴权**
|
||||
- `app/api/routes/poo.py`:`POST /poo/record`、`GET /poo/latest`,依赖 `get_poo_db`,**无鉴权**
|
||||
- `app/api/routes/homeassistant.py`:同时用 `get_db`(location)和 `get_poo_db`
|
||||
|
||||
**config**
|
||||
- `app/config.py`:`app_database_url` / `location_database_url` / `poo_database_url` 三字段 + computed `app_sqlite_path` / `location_sqlite_path` / `poo_sqlite_path`
|
||||
- `app/services/config_page.py`:`build_runtime_settings` 用到 `reset_auth_db_caches`;配置页 sections 暴露 `location_database_url` / `poo_database_url`(约 263–264 行)
|
||||
|
||||
**测试耦合点(M1 必然要改)**
|
||||
- `tests/conftest.py`:`test_database_urls` 设三套环境变量;`ready_location_database` / `ready_poo_database` / `auth_database` / `location_client`(monkeypatch `app_db.engine`/`SessionLocal`)/ `poo_client`(monkeypatch `poo_db.poo_engine`/`PooSessionLocal`)
|
||||
- `tests/test_location.py` / `tests/test_poo.py`:用上述 client + 各自 adopt 脚本的 adoption 测试
|
||||
- `tests/test_deployment.py`:断言 `run_all_migrations()` 返回 `{app,location,poo}` 三库各自 revision;断言 entrypoint 不含 `*_db_adopt`
|
||||
- `tests/test_homeassistant_inbound.py`:monkeypatch `app.poo_db`
|
||||
- `tests/test_config.py` / `tests/test_public_ip.py` / `tests/test_smtp.py`:硬编码三套 URL / 路径
|
||||
- `reset_auth_db_caches` 被 `conftest`、`test_app`、`test_auth`、`test_deployment`、`test_ticktick` 引用
|
||||
|
||||
## 3. 目标架构(M1 完成态)
|
||||
|
||||
**单数据层 `app/db.py`**
|
||||
```python
|
||||
class Base(DeclarativeBase): ...
|
||||
# 绑定 settings.app_database_url 的 cached engine;建连时启用 WAL(PRAGMA journal_mode=WAL)
|
||||
def get_engine() -> Engine: ...
|
||||
def get_session_local() -> sessionmaker: ...
|
||||
def reset_db_caches() -> None: ...
|
||||
def get_db_session() -> Generator[Session, None, None]: ...
|
||||
```
|
||||
- 所有模型(auth / config / public_ip / location / poo)都继承这一个 `Base`。
|
||||
- 删除 `app/auth_db.py`、`app/poo_db.py`、`app/models/base.py`。
|
||||
- 依赖收敛为**单一** `get_db`(app session);移除 `get_poo_db`、旧 `get_auth_db`。
|
||||
- 一条 Alembic 链(`alembic_app`),`location` / `poo_records` 成为其管理对象;删除 `alembic_location*` / `alembic_poo*`。
|
||||
- `config.py` 只保留 `app_database_url`;移除 location/poo 的 url 与 path。
|
||||
- `docker-compose.yml` 去掉 grafana service;删除 `grafana/`。
|
||||
- 数据搬迁由 `scripts/migrate_legacy_data.py` 一次性完成(不进 Alembic 链)。
|
||||
|
||||
## 4. 任务依赖图
|
||||
|
||||
```
|
||||
T01 (app 链建 location+poo 空表)
|
||||
├─► T02 (数据搬迁脚本) # 逻辑上需要新表存在
|
||||
└─► T03 [structural] (统一数据层/模型/依赖/路由)
|
||||
└─► T04 (lifespan + run_migrations 收敛, 删 adopt 脚本)
|
||||
└─► T05 (config 去 location/poo url + 配置页 + 测试硬编码)
|
||||
T06 (删 Grafana) # 独立, 可并行
|
||||
T07 (文档 + OpenAPI 重导出) # 收尾, 依赖 T03/T04/T05
|
||||
```
|
||||
|
||||
`T01`、`T06` 无前置可先开;`T02` 依赖 `T01`;`T03` 依赖 `T01`;`T04`/`T05` 依赖 `T03`;`T07` 最后。
|
||||
|
||||
---
|
||||
|
||||
## 5. 原子任务
|
||||
|
||||
### M1-T01 — app 链新增 revision:建 `location` + `poo_records` 空表 `[schema]`
|
||||
|
||||
- **Status**: `todo`
|
||||
- **Depends**: none
|
||||
- **Context**: 让 app 库的 Alembic 链能建出这两张表,schema 与旧库**完全一致**。本任务只动 schema,不搬数据、不移模型。
|
||||
|
||||
**Files**
|
||||
- `create alembic_app/versions/20260611_06_merge_location_poo_tables.py`
|
||||
- `modify scripts/app_db_adopt.py`(更新 `APP_BASELINE_REVISION`)
|
||||
|
||||
**Steps**
|
||||
1. 新 revision:`revision = "20260611_06_merge_location_poo_tables"`,`down_revision = "20260429_05_public_ip_monitor"`。
|
||||
2. `upgrade()` 用 `op.create_table` 手写建 `location` 与 `poo_records`,列/约束严格照抄现有 baseline(`location`: person TEXT, datetime TEXT, latitude REAL NOT NULL, longitude REAL NOT NULL, altitude REAL nullable, PK(person,datetime);`poo_records`: timestamp TEXT, status TEXT, latitude REAL NOT NULL, longitude REAL NOT NULL, PK(timestamp))。
|
||||
3. `downgrade()`:`op.drop_table("poo_records")` + `op.drop_table("location")`。
|
||||
4. 把 `scripts/app_db_adopt.py` 的 `APP_BASELINE_REVISION` 更新为新 head。
|
||||
|
||||
**Out of scope / 不要碰**
|
||||
- 不要把 `Location` / `PooRecord` 模型改到 app Base(那是 T03)。
|
||||
- 不要触碰 `alembic_location*` / `alembic_poo*`(T03/T04 删)。
|
||||
- 不要在本 revision 里写任何数据拷贝。
|
||||
|
||||
**Acceptance criteria**
|
||||
- [ ] 在一个全新临时 app 库上 `command.upgrade(alembic_app head)` 后,`sqlite_master` 含 `location`、`poo_records`、且与旧 baseline 表结构一致(`PRAGMA table_info` 对齐)。
|
||||
- [ ] `downgrade -1` 能干净回滚这两张表。
|
||||
- [ ] `APP_BASELINE_REVISION == "20260611_06_merge_location_poo_tables"`。
|
||||
- [ ] 校验闸门全绿(`pytest` 中 `test_deployment` 对 app head 的断言仍通过,因为它用的是常量)。
|
||||
|
||||
**Reviewer checklist**
|
||||
- 表结构与旧 baseline **逐列逐约束**一致(类型 TEXT/REAL、nullable、PK 顺序)。
|
||||
- `down_revision` 正确指向旧 head,链上只有一个 head。
|
||||
|
||||
---
|
||||
|
||||
### M1-T02 — 数据搬迁脚本 `scripts/migrate_legacy_data.py`
|
||||
|
||||
- **Status**: `todo`
|
||||
- **Depends**: M1-T01
|
||||
- **Context**: 把旧 `locationRecorder.db` / `pooRecorder.db` 的行幂等拷进 app 库的新表,搬完对账。**不进 Alembic 链**,人工运行一次。
|
||||
|
||||
**Files**
|
||||
- `create scripts/migrate_legacy_data.py`
|
||||
- `create tests/test_migrate_legacy_data.py`
|
||||
|
||||
**Steps**
|
||||
1. 入口 `migrate_legacy_data(app_url, location_url, poo_url, *, dry_run=False) -> dict`,CLI 默认从 env 读三个 url(即便 location/poo url 已从 `Settings` 移除,本脚本可直接读环境变量或接受 `--location-db`/`--poo-db` 参数,保持自包含)。
|
||||
2. 对每个旧库:若文件不存在 → 该表 `skipped`(**不报错**,保证 CI / 全新部署可安全 no-op)。
|
||||
3. 拷贝用 SQLite `ATTACH DATABASE '<old>' AS legacy` + `INSERT OR IGNORE INTO main.<table> SELECT <显式列> FROM legacy.<table>`(显式列名,禁用 `SELECT *`)。`INSERT OR IGNORE` 保证幂等(PK 冲突跳过)。
|
||||
4. 搬完对账:对每张表比对 `源行数` 与 `目标行数中来自源的部分`;目标行数 < 源行数则 `raise` 并以非零码退出。
|
||||
5. `dry_run` 模式只读统计、不写入。
|
||||
6. 打印每表结果:`{location: {source, copied, skipped, final}, poo_records: {...}}`。
|
||||
|
||||
**Out of scope / 不要碰**
|
||||
- **绝不** `os.remove` / 覆盖任何旧文件(数据安全红线)。
|
||||
- 不修改 Alembic 链,不在 app 启动链路里调用本脚本。
|
||||
- 不改 `config.py`。
|
||||
|
||||
**Acceptance criteria**
|
||||
- [ ] 单测:给定含 N 行的临时旧库 + 已 upgrade 的临时 app 库,运行后 app 库对应表有 N 行;**再运行一次**仍是 N 行(幂等)。
|
||||
- [ ] 单测:旧库文件不存在时该表返回 `skipped`,不抛异常,app 库该表保持为空。
|
||||
- [ ] 单测:构造"目标缺行"场景,断言对账失败抛错且退出码非零。
|
||||
- [ ] 脚本中不出现任何文件删除/覆盖调用(`grep -nE "os\.remove|unlink|shutil|truncate|DROP TABLE" scripts/migrate_legacy_data.py` 为空)。
|
||||
- [ ] 校验闸门全绿。
|
||||
|
||||
**Reviewer checklist**
|
||||
- 幂等机制确实是 PK 冲突安全(`INSERT OR IGNORE` 或等价 upsert),不是靠"先清空目标"。
|
||||
- 对账逻辑会在丢行时**真的中止**(非零退出),不是只打印 warning。
|
||||
- 列名显式,与两表 schema 完全对应。
|
||||
|
||||
---
|
||||
|
||||
### M1-T03 — 统一数据层、模型、依赖、路由到单库 `[structural]`
|
||||
|
||||
- **Status**: `todo`
|
||||
- **Depends**: M1-T01
|
||||
- **Context**: M1 的核心 sweep。把三套 engine/Base/session 收敛成 `app/db.py` 一套(绑 app 库、开 WAL),所有模型挂到同一个 `Base`,依赖收敛为单一 `get_db`,所有路由改用它。**本任务必须原子落地**——删除旧模块会同时打断所有 importer,无法分多次保持绿色。Orchestrator 可按下方 Steps 的自然分段派给较强 implementer。
|
||||
|
||||
**Files**
|
||||
- `modify app/db.py`(改写为统一数据层:`Base` + 绑 `app_database_url` 的 cached engine + WAL + `get_session_local` + `reset_db_caches` + `get_db_session`)
|
||||
- `delete app/auth_db.py`
|
||||
- `delete app/poo_db.py`
|
||||
- `delete app/models/base.py`
|
||||
- `modify app/models/location.py`(`from app.db import Base`)
|
||||
- `modify app/models/poo.py`(改继承统一 `Base`,import 改 `app.db`)
|
||||
- `modify app/models/auth.py`、`app/models/config.py`、`app/models/public_ip.py`(`AuthBase` → 统一 `Base`)
|
||||
- `modify app/models/__init__.py`(补导出 `PooRecord`,保证 `from app import models` 注册所有表到同一 metadata)
|
||||
- `modify app/dependencies.py`(单一 `get_db`;删 `get_poo_db`;`get_app_settings`/`get_current_auth_session` 改用 `get_db`)
|
||||
- `modify app/api/routes/auth.py`、`pages.py`、`public_ip.py`、`ticktick.py`(`get_auth_db` → `get_db`)
|
||||
- `modify app/api/routes/location.py`、`poo.py`、`homeassistant.py`(location/poo session 改用 `get_db`;删 `get_poo_db` 引用)
|
||||
- `modify app/services/config_page.py`(`reset_auth_db_caches` → `reset_db_caches`)
|
||||
- `modify app/main.py`(`import app.auth_db as auth_db` → 统一层;`get_auth_session_local` → `get_session_local`)
|
||||
- `modify tests/conftest.py`、`tests/test_app.py`、`tests/test_auth.py`、`tests/test_ticktick.py`、`tests/test_homeassistant_inbound.py`、`tests/test_location.py`、`tests/test_poo.py`(import sweep + 把 location/poo client 改成写 app 库的统一 session;移除对 `app.poo_db`/`app.db`(location) monkeypatch 的依赖)
|
||||
|
||||
**Steps**
|
||||
1. 改写 `app/db.py`:`Base(DeclarativeBase)`;沿用 `auth_db.py` 的 cached-engine + reset 模式但绑 `app_database_url`;为 sqlite 连接注册 `PRAGMA journal_mode=WAL`(用 `event.listens_for(engine, "connect")` 或建连后执行)。导出 `get_engine`/`get_session_local`/`reset_db_caches`/`get_db_session`。
|
||||
2. 模型 sweep:所有 `from app.auth_db import AuthBase` / `from app.poo_db import PooBase` / `from app.db import Base` 统一成 `from app.db import Base`;类继承统一 `Base`。`app/models/__init__.py` 增加 `from app.models.poo import PooRecord` 并补进 `__all__`。
|
||||
3. 删 `app/auth_db.py`、`app/poo_db.py`、`app/models/base.py`。
|
||||
4. 依赖 sweep:`app/dependencies.py` 留单一 `get_db`(yield 统一 session),删 `get_poo_db`;`get_app_settings`、`get_current_auth_session` 的 `Depends(get_auth_db)` → `Depends(get_db)`。
|
||||
5. 路由 sweep:所有 `Depends(get_auth_db)`、`Depends(get_poo_db)`、`Depends(get_db)` 统一为 `Depends(get_db)`(变量名 `auth_db_session`/`poo_db`/`db` 可保留,不强制改)。
|
||||
6. `app/services/config_page.py`:`reset_auth_db_caches` → `reset_db_caches`。
|
||||
7. `app/main.py`:把 `_run_scheduled_public_ip_check` / `ensure_auth_db_ready` 里的 `auth_db.get_auth_session_local()` 换成统一 `get_session_local()`。(lifespan 里 location/poo 的 ready 检查留到 T04 删。)
|
||||
8. 测试 sweep:`reset_auth_db_caches` → `reset_db_caches`(6 个文件);conftest 的 `location_client`/`poo_client` 改成"写入统一 app session 即可"的形式(不再 monkeypatch 已删除的 `app.poo_db`/location `app.db`);`test_homeassistant_inbound` 同理。
|
||||
|
||||
**Out of scope / 不要碰**
|
||||
- 不删 `scripts/location_db_adopt.py` / `scripts/poo_db_adopt.py`,不改 lifespan 的 location/poo ready 调用(那是 T04,避免与本任务交叉冲突)。
|
||||
- 不动 `config.py` 的字段(T05)。
|
||||
- 不改业务逻辑(service 内部算法、HA 集成行为保持不变)。
|
||||
|
||||
**Acceptance criteria**
|
||||
- [ ] `grep -rnE "auth_db|poo_db|PooBase|AuthBase|get_auth_db|get_poo_db|reset_auth_db_caches|app\.models\.base" app | grep -v __pycache__` 结果为空。
|
||||
- [ ] `app/db.py` 的 engine 绑定 `app_database_url`,sqlite 下 `PRAGMA journal_mode` 实测为 `wal`。
|
||||
- [ ] 所有模型 `Base.metadata.tables` 同时包含 auth/config/public_ip/location/poo_records 五类表。
|
||||
- [ ] `pytest` 全绿(含 location/poo/homeassistant_inbound 测试在单库下通过)。
|
||||
- [ ] `ruff check .` 无新增告警。
|
||||
|
||||
**Reviewer checklist**
|
||||
- WAL 真的生效(实际连接 `PRAGMA journal_mode` 返回 `wal`),不是只写了注释。
|
||||
- location/poo 的读写在单库下行为不变(端点仍返回 200、行落库)。
|
||||
- 没有遗留指向已删模块的死 import;没有把业务逻辑顺手改了。
|
||||
- `get_db` 现在产出的是 app 库 session(不是旧 location 库)。
|
||||
|
||||
---
|
||||
|
||||
### M1-T04 — 收敛启动链路:lifespan + run_migrations,删除 location/poo adopt 脚本
|
||||
|
||||
- **Status**: `todo`
|
||||
- **Depends**: M1-T03
|
||||
- **Context**: 单库后只需保证 app 库就绪;location/poo 的 adoption 链路整条退役。
|
||||
|
||||
**Files**
|
||||
- `modify app/main.py`(`ensure_runtime_dirs` 只建 app 路径;删 `ensure_location_db_ready`/`ensure_poo_db_ready` 及其调用与 import)
|
||||
- `modify scripts/run_migrations.py`(只 `adopt_or_initialize_app_db`,返回 `{"app": ...}`)
|
||||
- `delete scripts/location_db_adopt.py`
|
||||
- `delete scripts/poo_db_adopt.py`
|
||||
- `delete alembic_location.ini`、`alembic_location/`(含 env.py、versions)
|
||||
- `delete alembic_poo.ini`、`alembic_poo/`
|
||||
- `modify tests/test_deployment.py`(`run_all_migrations` 期望值改为单 `{"app": ...}`;删/改 legacy location/poo 迁移断言;保留"app DB 不存在则 fail-closed"用例)
|
||||
- `modify tests/test_location.py`、`tests/test_poo.py`(删除针对已删 adopt 脚本的 adoption 测试;保留端点行为测试)
|
||||
- `modify tests/conftest.py`(删 `_make_alembic_config`/`_make_poo_alembic_config`/`ready_location_database`/`ready_poo_database` 等已无意义的 fixture)
|
||||
|
||||
**Steps**
|
||||
1. `app/main.py`:移除 `from scripts.location_db_adopt ...` / `poo_db_adopt` import;删两个 `ensure_*_db_ready` 函数及 lifespan 中调用;`ensure_runtime_dirs` 只处理 `settings.app_sqlite_path`。
|
||||
2. `scripts/run_migrations.py`:`run_all_migrations` 只返回 app 一项。
|
||||
3. 删除两套 adopt 脚本与两套 alembic 环境/ini。
|
||||
4. 测试:把 `test_migration_runner_*` 改成单库口径;删掉引用已删脚本常量(`LOCATION_BASELINE_REVISION` 等)的用例。
|
||||
|
||||
**Out of scope / 不要碰**
|
||||
- 不动 `scripts/app_db_adopt.py` 的核心逻辑(仅 T01 已更新其常量)。
|
||||
- 不动数据搬迁脚本(T02)。
|
||||
|
||||
**Acceptance criteria**
|
||||
- [ ] `grep -rnE "location_db_adopt|poo_db_adopt|alembic_location|alembic_poo" app scripts tests | grep -v __pycache__` 为空。
|
||||
- [ ] 仓库不再有 `alembic_location*` / `alembic_poo*` 文件。
|
||||
- [ ] `python -m scripts.run_migrations` 在全新临时 app 库上成功初始化(含 location/poo_records 表)。
|
||||
- [ ] 校验闸门全绿。
|
||||
|
||||
**Reviewer checklist**
|
||||
- lifespan 仍对 app 库 fail-closed(缺库时明确报错),未弱化启动安全。
|
||||
- 没有残留对已删 alembic 环境的引用(包括 `.ini` 路径字符串)。
|
||||
|
||||
---
|
||||
|
||||
### M1-T05 — config 去除 location/poo URL 与路径,清理配置页与测试硬编码
|
||||
|
||||
- **Status**: `todo`
|
||||
- **Depends**: M1-T03
|
||||
- **Context**: 配置层只剩 `app_database_url`,运行时不再有 location/poo 库概念。
|
||||
|
||||
**Files**
|
||||
- `modify app/config.py`(删 `location_database_url`/`poo_database_url` 字段与 `location_sqlite_path`/`poo_sqlite_path` computed 属性)
|
||||
- `modify app/services/config_page.py`(配置页 sections 移除 `location_database_url`/`poo_database_url` 展示项)
|
||||
- `modify .env.example`(移除两行 legacy DB URL;保留 `APP_DATABASE_URL`)
|
||||
- `modify tests/test_config.py`(删对两个 URL/路径的断言)
|
||||
- `modify tests/test_public_ip.py`、`tests/test_smtp.py`(构造 `Settings` 时去掉 location/poo url 入参)
|
||||
- `modify tests/conftest.py`(`test_database_urls` 不再 set `LOCATION_DATABASE_URL`/`POO_DATABASE_URL`)
|
||||
|
||||
**Out of scope / 不要碰**
|
||||
- 不动 `migrate_legacy_data.py`(它自带读旧库路径的能力,与 `Settings` 解耦)。
|
||||
- 不改其它配置项(SMTP / TickTick / HA 等)。
|
||||
|
||||
**Acceptance criteria**
|
||||
- [ ] `grep -rnE "location_database_url|poo_database_url|location_sqlite_path|poo_sqlite_path" app tests | grep -v __pycache__` 为空。
|
||||
- [ ] 配置页渲染不再出现 location/poo DB URL 字段。
|
||||
- [ ] 校验闸门全绿。
|
||||
|
||||
**Reviewer checklist**
|
||||
- 没有别的代码还假设 `Settings` 上存在这两个属性(运行期不会 AttributeError)。
|
||||
|
||||
---
|
||||
|
||||
### M1-T06 — 移除 Grafana
|
||||
|
||||
- **Status**: `todo`
|
||||
- **Depends**: none(可与 T01 并行)
|
||||
- **Context**: 可视化将由 M2 的 React 承担;Grafana 直接删除,不再 re-point。
|
||||
|
||||
**Files**
|
||||
- `modify docker-compose.yml`(删 `grafana` service 及其 `depends_on`/挂载;删顶层 `volumes.homeautomation_grafana_storage`)
|
||||
- `delete grafana/`(`provisioning/`、`dashboards/` 全部)
|
||||
- `modify tests/test_deployment.py`(若有针对 grafana service 的断言则同步移除)
|
||||
- `modify README.md`(删"Grafana Provisioning"整节——也可并入 T07,二选一,避免重复改同段)
|
||||
|
||||
**Out of scope / 不要碰**
|
||||
- **不在脚本里删除** named volume `homeautomation_grafana_storage` 的实际数据卷——这是人工 ops 步骤(见 §6),compose 里移除声明即可。
|
||||
- 不动 app/migration service。
|
||||
|
||||
**Acceptance criteria**
|
||||
- [ ] `docker-compose.yml` 不再含 `grafana` 与 `homeautomation_grafana_storage`。
|
||||
- [ ] 仓库不再有 `grafana/` 目录。
|
||||
- [ ] `docker compose config` 能成功解析(语法有效)。
|
||||
- [ ] 校验闸门全绿。
|
||||
|
||||
**Reviewer checklist**
|
||||
- 没有遗留对 `./grafana/...` 挂载路径的引用。
|
||||
- 没有顺手删 `./data` 卷或改动 app service 端口/卷。
|
||||
|
||||
---
|
||||
|
||||
### M1-T07 — 文档与 OpenAPI 收尾
|
||||
|
||||
- **Status**: `todo`
|
||||
- **Depends**: M1-T03, M1-T04, M1-T05
|
||||
- **Context**: 让文档反映单库现实,并把"前后端不分离 / 三库不合并 / Grafana"约束在 architecture 文档中正式退役。
|
||||
|
||||
**Files**
|
||||
- `modify README.md`(三库 → 单库;删 location/poo DB 初始化与 adopt 说明;更新"运行测试"段落使其与实际测试一致)
|
||||
- `modify docs/architecture-overview.md`(退役"三库不合并";location/poo Alembic 链合并说明)
|
||||
- `modify docs/roadmap.md`(勾掉 M1 范围项)
|
||||
- `run python scripts/export_openapi.py` 并提交 `openapi/` 变更(location/poo 路由依赖在 T03 改过,schema 可能变化)
|
||||
|
||||
**Acceptance criteria**
|
||||
- [ ] README / architecture 不再描述 location/poo 独立库与 adopt 脚本。
|
||||
- [ ] `python scripts/export_openapi.py` 后 `git diff --exit-code openapi/` 无未提交差异。
|
||||
- [ ] 校验闸门全绿。
|
||||
|
||||
**Reviewer checklist**
|
||||
- 文档无残留的旧命令(`location_db_adopt.py` 等)。
|
||||
- OpenAPI 已重导出且入库。
|
||||
|
||||
---
|
||||
|
||||
## 6. 人工操作 runbook(生产切换,不进自动化任务)
|
||||
|
||||
按数据安全红线,下列步骤由人执行,**不**写进 implementer 任务:
|
||||
|
||||
1. **备份**:停服前复制 `data/app.db`、`data/locationRecorder.db`、`data/pooRecorder.db` 到带时间戳的归档目录。
|
||||
2. **演练**:把上述备份恢复到 scratch 目录,先在副本上跑完整流程(升级 + `migrate_legacy_data.py --dry-run` 再实跑),核对行数。
|
||||
3. **部署新镜像**:新镜像的 migration job 会把 app 库升级到新 head,建出空的 `location` / `poo_records`。
|
||||
4. **搬数据**:在生产机运行 `python scripts/migrate_legacy_data.py`(指向归档前的旧库),核对对账输出。
|
||||
5. **验证**:app 起来后确认 location/poo 端点与历史查询正常、行数与旧库一致。
|
||||
6. **(事后,确认无误再做)撤旧库**:归档旧 `.db` 文件、删除 `homeautomation_grafana_storage` 卷。**这一步人工、可回退地保留归档,永不在脚本中自动执行。**
|
||||
|
||||
## 7. 里程碑完成定义(Definition of Done)
|
||||
|
||||
- 运行期只存在 `app.db` 一个库、一个 engine、一个 `Base`、一条 Alembic 链。
|
||||
- `grep` 不到任何 `auth_db` / `poo_db` / location 独立库 / adopt 脚本 / grafana 的残留引用。
|
||||
- 旧库历史数据已通过 `migrate_legacy_data.py` 搬入且对账通过。
|
||||
- `pytest`、`ruff check .`、`export_openapi` 全绿且 `openapi/` 已入库。
|
||||
- README / architecture / roadmap 反映单库现实。
|
||||
@@ -0,0 +1,232 @@
|
||||
# M2 — 前端 v2(React SPA)
|
||||
|
||||
> 阅读前提:先读 [`README.md`](./README.md)。M2 依赖 M1 完成(单库 + 干净的数据层 + API 建立在合并后的 schema 上)。
|
||||
|
||||
## 1. 目标
|
||||
|
||||
用 **React SPA** 取代现有 Jinja 页面,由 FastAPI **同源**托管(同一容器、同一 origin)。一步合并 roadmap 的"前端重写"与"前端做厚":配置界面 + 数据可视化(热力图 / 地图,接管 Grafana)+ 记录的按需展示与小幅增删改。
|
||||
|
||||
> **元目标(agentic 实验)**:这是用 agent 写 React 的试水,全程尽量不读代码。因此本里程碑**强约束 OpenAPI → 类型化 TS client 作为契约护栏**:后端 API 先稳,前端永远对着强类型契约写,便宜模型不易跑偏,reviewer 也有客观依据。
|
||||
|
||||
## 2. 现状(M1 完成后)
|
||||
|
||||
- 页面仍是服务端 Jinja:`app/api/routes/pages.py`(`GET/POST /config`、`/`、`/admin`、`POST /config/smtp/test`)+ `app/templates/`(`base/config/home/login.html`、`styles.css`)。
|
||||
- 鉴权:`get_current_auth_session`(读 `auth_session_cookie_name` cookie),server-side session + 每会话 `csrf_token` 内嵌在表单。
|
||||
- `app/main.py` 已 `app.mount("/static", StaticFiles(...))`。
|
||||
- 配置读写逻辑在 `app/services/config_page.py`(`build_config_sections` / `save_config_updates` / `build_runtime_settings`)。
|
||||
- 业务数据:单库中的 `location`、`poo_records`、`public_ip_state`、`public_ip_history`。
|
||||
|
||||
## 3. 目标架构
|
||||
|
||||
### 3.1 后端:JSON API + SPA 托管
|
||||
|
||||
- 所有数据交互走 **JSON API**,统一前缀 `/api`(SPA 是客户端渲染,必须有 API——这与"同源/同容器"无关)。
|
||||
- FastAPI 既挂 `/api/*`,又挂 SPA 静态产物,并对非 `/api`、非静态资源的路径**回退到 `index.html`**(支持前端路由 deep-link)。
|
||||
- Jinja 页面在 SPA 达到功能对齐后移除。
|
||||
|
||||
### 3.2 鉴权:复用 session cookie + SPA 版 CSRF
|
||||
|
||||
- 继续用现有 **HttpOnly session cookie**(同源自动携带),M2 **不引入 token**(token 属 M3)。
|
||||
- CSRF:新增 `GET /api/session` 返回当前用户 + 该会话的 `csrf_token`;SPA 在所有写请求(POST/PUT/PATCH/DELETE)放 `X-CSRF-Token` header,后端校验其与 session 内 `csrf_token` 一致。等价于把现有表单 CSRF 平移到 header。
|
||||
- 浏览器面向的所有新端点一律 session 保护;**裸 ingestion 端点(设备调用的 `POST /location/record`、`POST /poo/record`)维持现状到 M3**。
|
||||
|
||||
### 3.3 前端工程
|
||||
|
||||
- `frontend/`:**Vite + React + TypeScript**。
|
||||
- API client:由后端 `openapi/openapi.json` **自动生成** TS 类型与请求函数(如 `openapi-typescript` + 轻量 fetch 封装,或同类工具)。生成物入库或在 build 时生成(见 T06 决策)。
|
||||
- 可视化:地图 + 热力图(location 轨迹 / poo 点位)。建议 **MapLibre GL 或 Leaflet + heatmap 插件**(最终选型见 §5 决策)。
|
||||
- 状态/数据请求:轻量即可(如 TanStack Query),不引入重型框架。
|
||||
|
||||
### 3.4 构建与部署
|
||||
|
||||
- 多阶段 `Dockerfile`:node 阶段 `npm ci && npm run build` → 把 `frontend/dist` 拷进 python 镜像的静态目录;运行镜像不带 node。
|
||||
- compose 仍是单 app 容器(同源)。
|
||||
|
||||
## 4. API 契约(M2 要落地的端点)
|
||||
|
||||
> 全部 `/api` 前缀、session 保护、JSON 进出。具体 schema 在各任务里用 Pydantic 定义,并经 `export_openapi.py` 固化。
|
||||
|
||||
| 分组 | 端点 | 用途 |
|
||||
| --- | --- | --- |
|
||||
| 会话 | `GET /api/session` | 返回当前用户 + csrf_token;未登录 401 |
|
||||
| 会话 | `POST /api/auth/login` | 账号密码登录,下发 session cookie |
|
||||
| 会话 | `POST /api/auth/logout` | 注销 |
|
||||
| 会话 | `POST /api/auth/password` | 改密(沿用现有强制改密语义)|
|
||||
| 配置 | `GET /api/config` | 返回配置 sections(secret 不回显)|
|
||||
| 配置 | `PUT /api/config` | 保存配置(留空保留旧 secret 语义不变)|
|
||||
| 配置 | `POST /api/config/smtp/test` | 触发测试发信 |
|
||||
| 数据 | `GET /api/locations` | location 记录查询(时间范围/分页,供地图/热力图)|
|
||||
| 数据 | `GET /api/poo` | poo 记录列表(分页)|
|
||||
| 数据 | `GET /api/public-ip` | 当前状态 + 变化历史 |
|
||||
| CRUD | `PATCH /api/locations/{person}/{datetime}` | 修正单条 location |
|
||||
| CRUD | `DELETE /api/locations/{person}/{datetime}` | 删除单条 location |
|
||||
| CRUD | `PATCH /api/poo/{timestamp}` | 修正单条 poo |
|
||||
| CRUD | `DELETE /api/poo/{timestamp}` | 删除单条 poo |
|
||||
|
||||
> 记录 CRUD 依赖现有 PK 作行标识(location PK=`person+datetime`,poo PK=`timestamp`)。路径参数需对 `datetime`/`timestamp` 做 URL 编码处理。
|
||||
|
||||
## 5. 需先拍板的决策(Orchestrator 在派 T06 前确认)
|
||||
|
||||
1. **地图/热力图库**:MapLibre GL(矢量、现代)vs Leaflet(简单、生态大)。推荐 Leaflet + `leaflet.heat`(试水门槛低)。
|
||||
2. **OpenAPI client 生成物**:入库(确定性、便于 review)vs build 时生成(仓库干净)。推荐**入库**,并加一个 `npm run codegen` + CI 校验"生成物与 openapi 同步"。
|
||||
3. **CSRF 落地**:header `X-CSRF-Token` + `GET /api/session` 下发(推荐)vs 双提交 cookie。
|
||||
4. **是否保留少量 Jinja**:建议 SPA 对齐后**全量移除** `templates/`,只留 SPA。
|
||||
|
||||
> 这些可用 1 个轻量"决策任务"或直接由 Orchestrator 在本节记录选择,再开 T06。
|
||||
|
||||
## 6. 任务依赖图
|
||||
|
||||
```
|
||||
后端 API(可与前端 scaffold 并行)
|
||||
M2-T01 config API
|
||||
M2-T02 session/auth API ─┐
|
||||
M2-T03 data read API ├─► 都产出 OpenAPI 契约
|
||||
M2-T04 record CRUD API │
|
||||
M2-T05 smtp/action API ─┘
|
||||
│ (openapi 稳定后)
|
||||
▼
|
||||
M2-T06 前端 scaffold + codegen ──► M2-T07 auth UI
|
||||
├─► M2-T08 config UI
|
||||
├─► M2-T09 可视化 UI
|
||||
└─► M2-T10 records 管理 UI
|
||||
▼
|
||||
M2-T11 FastAPI 托管 SPA + 移除 Jinja(依赖 T07–T10 达到对齐)
|
||||
▼
|
||||
M2-T12 多阶段 Dockerfile + CI/compose
|
||||
▼
|
||||
M2-T13 文档 + OpenAPI 收尾
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 原子任务(任务卡)
|
||||
|
||||
> 后端任务沿用 M1 的校验闸门(`pytest` / `ruff` / `export_openapi`)。前端任务的闸门见 §8。
|
||||
|
||||
### M2-T01 — config JSON API
|
||||
- **Status**: `todo` · **Depends**: none(M1 完成后)
|
||||
- **Context**: 把 `config_page` 的读写能力暴露成 JSON,复用现有 service,不重写业务逻辑。
|
||||
- **Files**: `create app/api/routes/api/config.py`、`create app/schemas/config.py`;`modify app/main.py`(注册路由);`create tests/test_api_config.py`
|
||||
- **Steps**: 用 `build_config_sections`/`save_config_updates` 包出 `GET/PUT /api/config`;session 保护;secret 不回显、留空保留旧值语义照搬。
|
||||
- **Acceptance**:
|
||||
- [ ] 未登录访问 `GET /api/config` 返回 401。
|
||||
- [ ] 登录后 `GET` 返回 sections,secret 字段被遮罩。
|
||||
- [ ] `PUT` 留空 secret 时保留旧值;非法值返回 4xx 且不写库。
|
||||
- [ ] 校验闸门全绿(含 `openapi/` 重导出入库)。
|
||||
- **Reviewer**: 复用了 service 而非复制逻辑;CSRF 校验存在;secret 不泄漏到响应或 OpenAPI 示例。
|
||||
|
||||
### M2-T02 — session / auth JSON API
|
||||
- **Status**: `todo` · **Depends**: none
|
||||
- **Context**: 给 SPA 提供登录/注销/会话探测 + CSRF 下发。
|
||||
- **Files**: `create app/api/routes/api/session.py`、`app/schemas/session.py`;`modify app/main.py`;`create tests/test_api_session.py`
|
||||
- **Steps**: `GET /api/session`(401 或 user+csrf)、`POST /api/auth/login`、`POST /api/auth/logout`、`POST /api/auth/password`,复用 `app/services/auth.py`。
|
||||
- **Acceptance**:
|
||||
- [ ] 正确账号密码登录后置下 HttpOnly session cookie;`GET /api/session` 返回 user + csrf_token。
|
||||
- [ ] 错误凭据 401,不下发 cookie。
|
||||
- [ ] 写端点缺 `X-CSRF-Token` 或不匹配 → 403。
|
||||
- [ ] 强制改密语义与现有一致。
|
||||
- [ ] 校验闸门全绿。
|
||||
- **Reviewer**: cookie 仍 HttpOnly、`Secure` 跟随 `app_env`、`SameSite=Lax`;密码仍 Argon2,不明文。
|
||||
|
||||
### M2-T03 — 数据读取 API(locations / poo / public-ip)
|
||||
- **Status**: `todo` · **Depends**: none
|
||||
- **Files**: `create app/api/routes/api/data.py`、`app/schemas/data.py`;`modify app/main.py`;`create tests/test_api_data.py`
|
||||
- **Steps**: `GET /api/locations`(时间范围 + 分页)、`GET /api/poo`(分页)、`GET /api/public-ip`(state + history);session 保护;查询参数有上限防全表导出。
|
||||
- **Acceptance**:
|
||||
- [ ] 分页/时间范围参数生效且有上限;越权未登录 401。
|
||||
- [ ] 返回 schema 经 OpenAPI 固化。
|
||||
- [ ] 校验闸门全绿。
|
||||
- **Reviewer**: 查询走索引/PK,无 N+1;时间过滤边界正确。
|
||||
|
||||
### M2-T04 — 记录 CRUD API(修正 / 删除)
|
||||
- **Status**: `todo` · **Depends**: M2-T03
|
||||
- **Files**: `modify app/api/routes/api/data.py`、`app/services/location.py`、`app/services/poo.py`;`create tests/test_api_record_crud.py`
|
||||
- **Steps**: `PATCH`/`DELETE` location(PK person+datetime)与 poo(PK timestamp);session + CSRF 保护;PK 路径参数 URL 解码;删除是**硬删单行**(不是清表)。
|
||||
- **Acceptance**:
|
||||
- [ ] PATCH 改单行字段、DELETE 删单行,行数变化精确为 1。
|
||||
- [ ] 不存在的 PK → 404。
|
||||
- [ ] 缺 CSRF → 403。
|
||||
- [ ] 没有任何"批量删/清表"路径。
|
||||
- [ ] 校验闸门全绿。
|
||||
- **Reviewer**: 删除限定单 PK;编辑校验输入;ingestion 裸端点未被顺手加保护或改动。
|
||||
|
||||
### M2-T05 — SMTP 测试 / 动作类 JSON API
|
||||
- **Status**: `todo` · **Depends**: M2-T01
|
||||
- **Files**: `modify app/api/routes/api/config.py`;`modify tests/test_api_config.py`
|
||||
- **Steps**: `POST /api/config/smtp/test` 复用 `send_smtp_test_email`,返回结构化结果(success / config-error / failed)。
|
||||
- **Acceptance**:
|
||||
- [ ] 三种结果都有明确 JSON 状态码/字段;session + CSRF 保护。
|
||||
- [ ] 校验闸门全绿。
|
||||
|
||||
### M2-T06 — 前端 scaffold + OpenAPI codegen `[structural]`
|
||||
- **Status**: `todo` · **Depends**: M2-T01..T05(OpenAPI 已稳定)
|
||||
- **Context**: 建 `frontend/` 工程与类型化 client 流水线,这是后续所有前端任务的地基。
|
||||
- **Files**: `create frontend/`(Vite+React+TS 脚手架、`package.json`、`tsconfig.json`、eslint、vitest、`.gitignore`)、`frontend/src/api/`(codegen 产物 + fetch 封装,自动注入 `X-CSRF-Token`)、`frontend/README.md`、`npm run codegen` 脚本
|
||||
- **Steps**: 初始化 Vite React-TS;接 `openapi/openapi.json` 生成类型;写一个最小 App 壳 + 受保护路由骨架;fetch 封装统一带 cookie、写请求注入 CSRF header、401 跳登录。
|
||||
- **Acceptance**:
|
||||
- [ ] `npm ci && npm run build` 成功产出 `frontend/dist`。
|
||||
- [ ] `npm run lint`、`npm run typecheck`、`npm run test` 全绿(哪怕只有 1 个 smoke 测试)。
|
||||
- [ ] `npm run codegen` 生成物与当前 `openapi/openapi.json` 一致(CI 可校验)。
|
||||
- **Reviewer**: client 全部基于生成类型;CSRF/cookie/401 处理在统一封装层;无手写、与契约不符的请求类型。
|
||||
|
||||
### M2-T07 — 鉴权 UI(登录 / 会话引导 / 改密)
|
||||
- **Status**: `todo` · **Depends**: M2-T06
|
||||
- **Acceptance**: 登录成功进受保护区;未登录访问受保护路由跳登录;强制改密流程可走完;`build/lint/typecheck/test` 全绿。
|
||||
|
||||
### M2-T08 — 配置 UI(取代 Jinja config 页)
|
||||
- **Status**: `todo` · **Depends**: M2-T06
|
||||
- **Acceptance**: 能读/存所有现有配置 section;secret 不回显、留空保留;SMTP 测试按钮反映三态;前端闸门全绿。
|
||||
|
||||
### M2-T09 — 数据可视化 UI(地图 + 热力图)
|
||||
- **Status**: `todo` · **Depends**: M2-T06(数据来自 T03)
|
||||
- **Context**: 接管 Grafana 原职责:location 轨迹/热力图、poo 点位。
|
||||
- **Acceptance**: 地图渲染 location/poo 点;热力图层可切换;时间范围筛选生效;前端闸门全绿。
|
||||
|
||||
### M2-T10 — 记录管理 UI(按需展示 + 增删改)
|
||||
- **Status**: `todo` · **Depends**: M2-T06(CRUD 来自 T04)
|
||||
- **Acceptance**: 列表分页展示 poo/location;可编辑、可删除单条并即时刷新;删除有二次确认;前端闸门全绿。
|
||||
|
||||
### M2-T11 — FastAPI 托管 SPA + 移除 Jinja
|
||||
- **Status**: `todo` · **Depends**: M2-T07, T08, T09, T10
|
||||
- **Files**: `modify app/main.py`(挂载 SPA 静态目录 + 非 `/api` 路径回退 `index.html`);`delete app/templates/`、`app/api/routes/pages.py`(功能对齐后);`modify tests`(移除 Jinja 页面测试,新增 SPA fallback 测试)
|
||||
- **Acceptance**:
|
||||
- [ ] `/config` 等路径返回 SPA(`index.html`),`/api/*` 不被 fallback 吞掉,`/static`/资源正常。
|
||||
- [ ] 旧 Jinja 模板与 pages 路由移除后 `pytest` 全绿。
|
||||
- [ ] 校验闸门全绿(含 OpenAPI 重导出)。
|
||||
- **Reviewer**: fallback 不拦截 `/api`、`/docs`、`/openapi.json`、静态资源;未登录访问 API 仍 401(不是被 SPA 壳吞掉)。
|
||||
|
||||
### M2-T12 — 多阶段 Dockerfile + CI/compose
|
||||
- **Status**: `todo` · **Depends**: M2-T11
|
||||
- **Files**: `modify Dockerfile`(node build 阶段 → 拷 `dist` 进 python 镜像);`modify .github/workflows/*`(加前端 build/lint/typecheck);`modify tests/test_deployment.py`(镜像断言更新)
|
||||
- **Acceptance**:
|
||||
- [ ] 镜像构建成功且运行镜像不含 node 运行时。
|
||||
- [ ] CI 跑前端闸门 + 后端 `pytest`。
|
||||
- [ ] 校验闸门全绿。
|
||||
|
||||
### M2-T13 — 文档 + OpenAPI 收尾
|
||||
- **Status**: `todo` · **Depends**: M2-T12
|
||||
- **Acceptance**: README 增"前端 v2"段(开发/构建说明);architecture 退役"不前后端分离"约束;roadmap 勾选 M2;`openapi/` 已同步入库。
|
||||
|
||||
---
|
||||
|
||||
## 8. 前端校验闸门(前端任务每次结束都要全绿)
|
||||
|
||||
在 `frontend/` 下:
|
||||
```bash
|
||||
npm ci
|
||||
npm run codegen # 生成类型化 client;产物须与 openapi/openapi.json 同步
|
||||
npm run lint
|
||||
npm run typecheck
|
||||
npm run test
|
||||
npm run build # 必须产出 dist
|
||||
```
|
||||
- 后端若同任务改了路由/schema,仍需根目录 `python scripts/export_openapi.py` 并提交 `openapi/`。
|
||||
- "codegen 产物与 OpenAPI 同步"应在 CI 校验(生成后 `git diff --exit-code`)。
|
||||
|
||||
## 9. 里程碑完成定义(DoD)
|
||||
|
||||
- 访问应用得到 React SPA;配置、可视化、记录增删改都在 SPA 内完成。
|
||||
- 所有浏览器交互走 `/api` JSON 端点,session + CSRF 保护;ingestion 裸端点维持现状(留给 M3)。
|
||||
- Jinja `templates/` 与 pages 路由移除;FastAPI 同源托管 SPA。
|
||||
- 多阶段镜像构建通过;CI 含前端闸门。
|
||||
- 后端 `pytest`/`ruff`/`export_openapi` + 前端 `build/lint/typecheck/test` 全绿。
|
||||
@@ -0,0 +1,109 @@
|
||||
# M3 — Token 鉴权与移动端(远期试水)
|
||||
|
||||
> 阅读前提:先读 [`README.md`](./README.md)。M3 依赖 M2(已有 `/api` JSON 契约与 session 鉴权)。
|
||||
>
|
||||
> **定位**:远期、低投入、探索性。React Native 部分主要是"没做过、试试水"。范围**可能收缩**——其中**token 鉴权 + ingestion 端点收口**是有持久价值的安全改进,应优先;RN app 是加分项。Orchestrator 可只取前半。
|
||||
|
||||
## 1. 目标
|
||||
|
||||
1. 引入 **bearer token 鉴权**,让非浏览器客户端(移动端、设备脚本)能安全访问。
|
||||
2. 把 M2 暂时维持裸奔的 **ingestion 端点**(`POST /location/record`、`POST /poo/record`)收口到 token 鉴权下。
|
||||
3. 做一个 **React Native** 移动端,用类 OAuth 流程拿 token 后消费现有 `/api`。
|
||||
|
||||
## 2. 现状(M2 完成后)
|
||||
|
||||
- `/api/*` 走 session cookie + `X-CSRF-Token`。
|
||||
- `app/services/auth.py` 有 server-side session(`auth_sessions` 表,token_hash 存储)。
|
||||
- `POST /location/record`、`POST /poo/record` 仍**无鉴权**(设备/脚本裸调用)。
|
||||
|
||||
## 3. 目标架构
|
||||
|
||||
### 3.1 Token 模型
|
||||
- 新表 `auth_tokens`:`id`、`user_id`、`token_hash`(仅存哈希)、`label`(设备名)、`created_at`、`expires_at`(可空=长期)、`revoked_at`。
|
||||
- bearer 校验:`Authorization: Bearer <token>` → 哈希比对 `auth_tokens` → 命中且未撤销未过期则认定身份。
|
||||
|
||||
### 3.2 类 OAuth 签发流程(无第三方的 Authorization Code 简化版)
|
||||
1. 移动端在**内置浏览器**打开 `/authorize?...`。
|
||||
2. 用户账号密码登录(走现有 session),页面展示"授权此设备"。
|
||||
3. 批准后服务端生成**一次性 authorization code**,重定向到 app 深链 `homeautomation://callback?code=...`。
|
||||
4. app 用 code 调 `POST /api/auth/token` 换取 bearer token 并存本地。
|
||||
> 简化兜底:批准页直接展示一次性 token 由 app 捕获。优先实现重定向 + code 交换的正规版。
|
||||
|
||||
### 3.3 统一鉴权依赖
|
||||
- `/api` 的数据/CRUD 端点接受**session cookie 或 bearer**两者之一(同一套端点同时服务 Web 与移动端)。
|
||||
- ingestion 端点(location/poo record)改为**要求 bearer**。
|
||||
|
||||
### 3.4 React Native
|
||||
- **Expo + TypeScript**,复用 M2 的 OpenAPI 类型化 client(共享契约)。
|
||||
- 内置浏览器走 §3.2 流程拿 token;之后所有请求带 `Authorization: Bearer`。
|
||||
|
||||
## 4. 迁移注意(重要)
|
||||
- ingestion 端点一旦要求 bearer,**现有调用方(HA/设备脚本)必须先配置 token**,否则记录会中断。
|
||||
- 上线顺序:先签发 token 能力(T01–T02)→ 给现有设备配 token → 再对 ingestion 端点强制 bearer(T03),避免断流。可设一个过渡开关或灰度。
|
||||
|
||||
## 5. 任务依赖图
|
||||
```
|
||||
M3-T01 token 模型 + 迁移
|
||||
└─► M3-T02 签发流程(authorize + code 交换)
|
||||
└─► M3-T03 统一鉴权依赖 + ingestion 端点收口(含过渡开关)
|
||||
├─► M3-T04 Web 端 token 管理 UI(列出/撤销设备)
|
||||
└─► M3-T05 React Native app(试水)
|
||||
└─► M3-T06 文档收尾
|
||||
```
|
||||
|
||||
## 6. 原子任务(任务卡)
|
||||
|
||||
### M3-T01 — token 数据模型 + Alembic 迁移
|
||||
- **Status**: `todo` · **Depends**: none(M2 完成后)
|
||||
- **Files**: `create app/models/token.py`、`alembic_app/versions/<new>_auth_tokens.py`;`modify app/models/__init__.py`;`create tests/test_token_model.py`
|
||||
- **Acceptance**:
|
||||
- [ ] 迁移在全新库 upgrade 后建出 `auth_tokens` 表;downgrade 可回滚。
|
||||
- [ ] token 仅以哈希存储(与 `auth_sessions` 同等强度),明文不入库。
|
||||
- [ ] 校验闸门全绿。
|
||||
- **Reviewer**: 哈希算法/长度与现有 session token 一致;`expires_at` 可空语义明确。
|
||||
|
||||
### M3-T02 — 签发流程:authorize 页 + code 交换端点
|
||||
- **Status**: `todo` · **Depends**: M3-T01
|
||||
- **Files**: `create app/api/routes/api/token.py`、`app/schemas/token.py`;前端 `/authorize` 页(M2 SPA 内);`create tests/test_api_token.py`
|
||||
- **Acceptance**:
|
||||
- [ ] 登录用户在 `/authorize` 批准后得到一次性 code;`POST /api/auth/token` 用 code 换取 bearer,code 一次性且短时效。
|
||||
- [ ] 未登录访问 `/authorize` 跳登录;无效/过期 code 换取失败。
|
||||
- [ ] 返回的 bearer 仅此一次明文出现,库中只存哈希。
|
||||
- [ ] 校验闸门全绿。
|
||||
- **Reviewer**: code 一次性、绑定用户、短 TTL;深链 redirect 白名单校验,防开放重定向。
|
||||
|
||||
### M3-T03 — 统一鉴权依赖 + ingestion 端点收口
|
||||
- **Status**: `todo` · **Depends**: M3-T02
|
||||
- **Files**: `modify app/dependencies.py`(新增"cookie 或 bearer"统一身份依赖);`modify app/api/routes/location.py`、`poo.py`(要求 bearer,带过渡开关);`modify tests`
|
||||
- **Acceptance**:
|
||||
- [ ] `/api` 数据/CRUD 端点用合法 bearer 可访问(等价于 session)。
|
||||
- [ ] ingestion 端点:带合法 bearer 通过,缺/错 token 在强制模式下 401;过渡开关可临时放行(默认关)。
|
||||
- [ ] 撤销的 token 立即失效。
|
||||
- [ ] 校验闸门全绿。
|
||||
- **Reviewer**: 过渡开关默认安全(强制);bearer 与 session 两路鉴权不产生绕过;ingestion 行为变更有测试覆盖。
|
||||
|
||||
### M3-T04 — Web 端 token 管理 UI
|
||||
- **Status**: `todo` · **Depends**: M3-T03
|
||||
- **Acceptance**: 在 SPA 内可列出已签发设备 token(label/创建时间/最近使用)、可撤销;撤销后该 token 立即失效;前端闸门全绿。
|
||||
|
||||
### M3-T05 — React Native app(试水)
|
||||
- **Status**: `todo` · **Depends**: M3-T03 · `[experimental]`
|
||||
- **Files**: `create mobile/`(Expo + TS,复用 OpenAPI 类型化 client)
|
||||
- **Acceptance**:
|
||||
- [ ] 内置浏览器走签发流程拿到 token 并安全存储(Keychain/Keystore)。
|
||||
- [ ] 至少跑通:登录拿 token → 拉取一类数据展示 → 记一条 ingestion。
|
||||
- [ ] `npm run lint`/`typecheck`/`build`(或 Expo 等价) 全绿。
|
||||
- **Reviewer**: token 存安全存储而非明文;client 基于共享 OpenAPI 类型。
|
||||
|
||||
### M3-T06 — 文档收尾
|
||||
- **Status**: `todo` · **Depends**: M3-T05
|
||||
- **Acceptance**: README/architecture 增 token 鉴权与移动端说明;roadmap 勾选 M3;`openapi/` 同步。
|
||||
|
||||
## 7. 里程碑完成定义(DoD)
|
||||
- 存在 bearer token 鉴权与签发流程;token 仅哈希存储、可撤销。
|
||||
- ingestion 端点已收口到 bearer(过渡完成后强制)。
|
||||
- `/api` 同时支持 session 与 bearer。
|
||||
- (加分)React Native app 能拿 token 并消费 `/api`。
|
||||
- 后端 + 前端 + 移动端各自校验闸门全绿,`openapi/` 入库。
|
||||
|
||||
> 提醒:本里程碑探索性强,T05 可作为独立试水随时叫停,不影响 T01–T04 带来的安全收口价值。
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
# Roadmap
|
||||
|
||||
本文档记录 `home-automation` 在 `v1.0.3` 之后的下一阶段规划。这一阶段不是小修补,而是几次较大的结构性改动:单库化、前端重写、以及远期的移动端试水。
|
||||
|
||||
> 每个里程碑的**可执行原子任务**展开在 [`docs/design/`](./design/README.md):M1 [`m1-db-consolidation.md`](./design/m1-db-consolidation.md)、M2 [`m2-frontend-v2.md`](./design/m2-frontend-v2.md)、M3 [`m3-token-mobile.md`](./design/m3-token-mobile.md)。这些文档为 Orchestrator→Implementer→Reviewer 的多模型流水线设计。
|
||||
|
||||
## 当前基线(v1.0.3)
|
||||
|
||||
- FastAPI + 服务端 Jinja 模板页面(目前只有 `/login`、`/config`)
|
||||
- 三个独立 SQLite 库:
|
||||
- App DB:`sqlite:///./data/app.db`
|
||||
- Location DB:`sqlite:///./data/locationRecorder.db`
|
||||
- Poo DB:`sqlite:///./data/pooRecorder.db`
|
||||
- 三条独立 Alembic 链:`alembic_app/`、`alembic_location/`、`alembic_poo/`
|
||||
- 单 admin 鉴权(Argon2 + server-side session cookie)
|
||||
- Public IPv4 monitor、SMTP 通知、Location / Poo recorder、Home Assistant in/out、TickTick OAuth
|
||||
- 数据可视化目前由 Grafana provisioning 承担(仅 location / poo dashboard)
|
||||
- 已有 OpenAPI 导出脚本:`scripts/export_openapi.py`
|
||||
|
||||
## 本阶段正式退役的架构约束
|
||||
|
||||
`docs/architecture-overview.md` 里有几条当时刻意写死的约束,这一阶段明确退役:
|
||||
|
||||
- **“不引入前后端分离”** → 退役。本阶段改为 React SPA(仍由 FastAPI 同源托管,但渲染移到客户端)。
|
||||
- **“三个独立 DB 不合并”** → 退役。本阶段把 location / poo 合并进 `app.db`。
|
||||
- **Grafana 作为可视化方案** → 退役。可视化由 React 前端自己承担(热力图、地图等)。
|
||||
|
||||
保持不变的约束:
|
||||
|
||||
- 继续使用 **SQLite**,本阶段不上 Postgres。
|
||||
- 不引入 Notion。
|
||||
|
||||
## 里程碑总览
|
||||
|
||||
| 里程碑 | 主题 | 一句话 |
|
||||
| --- | --- | --- |
|
||||
| **M1** | 单库化地基 | 把三库合并成单一 `app.db`,清理散落数据层,删掉 Grafana |
|
||||
| **M2** | 前端 v2 | React SPA 取代 Jinja,承载 config + 可视化 + 记录增删改 |
|
||||
| **M3** | 开放与移动端(远期试水) | token 鉴权 + React Native 移动端 |
|
||||
|
||||
排序原则:**先清地基,再在干净结构上盖楼。** M2 的新 API 和 React 必须建立在合并后的单库之上,否则就是在准备推倒的旧数据层上盖新楼、之后回头返工。
|
||||
|
||||
---
|
||||
|
||||
## M1 — 单库化地基
|
||||
|
||||
### 目标
|
||||
|
||||
把 location / poo 两个独立库合并进 `app.db`,借机清理项目早期散落各处的数据访问代码,并移除 Grafana。
|
||||
|
||||
### 范围
|
||||
|
||||
- **Alembic 收敛为单链(app 链)**:location / poo 的表此后纳入 app 链管理;`alembic_location/`、`alembic_poo/` 退出活跃使用(保留在 git 历史)。
|
||||
- **新建表(schema only)**:在 app 链上加一条 upgrade revision,把原来两个旧库里的表**原样**建到 `app.db` 中。Alembic **不需要知道任何旧数据**——它只负责把 app DB 往上升一个版本、建出这两张新表。
|
||||
- **数据搬迁交给独立脚本**:`scripts/migrate_legacy_data.py`(见下方“迁移策略”),手动跑一次。
|
||||
- **配置层收敛**:去掉 `LOCATION_DATABASE_URL` / `POO_DATABASE_URL`,统一到 `APP_DATABASE_URL`。
|
||||
- **开启 SQLite WAL**:单文件 + Web + APScheduler 并发写入,开 WAL 更稳。
|
||||
- **删除 Grafana**:移除 compose 中的 grafana service、`grafana/provisioning/`、`grafana/dashboards/`。直接删除,不再 re-point datasource。
|
||||
- **更新文档**:README、architecture-overview 同步反映单库现实。
|
||||
|
||||
### 注意
|
||||
|
||||
- **可视化空窗可接受**:M1 删掉 Grafana 后、到 M2 React 可视化落地之前会有一段没有可视化面板的时间。已确认可以接受。
|
||||
- **历史数据是第一优先级,绝不能丢**(见“数据安全原则”)。
|
||||
|
||||
---
|
||||
|
||||
## 迁移策略(M1 核心)
|
||||
|
||||
职责拆分得很清楚:**Alembic 管 schema,脚本管数据。**
|
||||
|
||||
### Alembic revision(只建结构)
|
||||
|
||||
- 一条 app 链上的 upgrade revision,建出与旧库**完全相同**的表结构。
|
||||
- 确定性、与环境无关:在生产机、CI、全新部署上都一样地建空表,不依赖任何旧文件是否存在。
|
||||
- 本步**只原样挪表,不顺手改 schema**。任何表结构清理留到之后一条单独的 migration 去做——不可替代的历史数据,一次只承担一种风险。
|
||||
|
||||
### 数据搬迁脚本(`scripts/migrate_legacy_data.py`)
|
||||
|
||||
- 把旧 `locationRecorder.db` / `pooRecorder.db` 里的行,拷进 `app.db` 的新表(SQLite `ATTACH DATABASE` 或单独连接均可)。
|
||||
- **幂等**:重复运行不会重复插入。
|
||||
- **搬完对账**:逐表核对源 / 目标行数,对不上就报错中止。
|
||||
- 只在生产机上**手动跑一次**,不进 Alembic 永久链路(避免把一次性历史搬迁焊死进每次全新建库都要跑的链路里)。
|
||||
|
||||
### 旧库的“撤掉”
|
||||
|
||||
- “撤掉旧库” = ① 配置不再指向它们 + ② 文件**归档保留**。
|
||||
- **绝不**在任何脚本 / migration 里 `os.remove` 旧文件——那不可逆,且踩数据安全红线。
|
||||
- 真正的删除是**人工、最后、确认无误之后**单独的一步。
|
||||
|
||||
---
|
||||
|
||||
## 数据安全原则
|
||||
|
||||
历史数据(location / poo 记录)是这个项目里最不可替代的东西,迁移期间一律按以下原则:
|
||||
|
||||
1. **迁移前先归档**旧 `.db` 文件一份。
|
||||
2. **先在副本上演练**:把每日备份恢复到一个 scratch 目录,在副本上跑完整迁移、核对行数无误,再对真实库动手。
|
||||
3. **脚本幂等 + 行数对账**,对不上立即中止。
|
||||
4. **旧文件只读归档、绝不自动删除**,删除是事后人工动作。
|
||||
|
||||
---
|
||||
|
||||
## M2 — 前端 v2(React SPA)
|
||||
|
||||
### 目标
|
||||
|
||||
用 React SPA 取代现有 Jinja 页面,由 FastAPI 同源托管(同一容器、同一 origin)。这一步合并了“前端重写为 React”和“前端做厚”两件原本分开的事——它们本质是同一坨活。
|
||||
|
||||
> 备注:React 是一次 agentic programming 试水。之前只手写过 Vue、没手写过 React,这一轮想全程靠 agent、尽量不读代码地把它做出来。OpenAPI 导出 → 生成类型化 TS client 作为 agent 的契约护栏,正好服务这个目标。
|
||||
|
||||
### 范围
|
||||
|
||||
- **React SPA**,FastAPI 挂载打包后的静态产物(同源,省掉 CORS)。
|
||||
- **Config 界面**:取代现有 Jinja config 页。
|
||||
- **数据可视化**:热力图、地图等,接管原先 Grafana 干的事。
|
||||
- **按需展示 DB 数据**(例如 poo 记录)。
|
||||
- **记录的小幅增删改**:用于修正不准确的记录。
|
||||
|
||||
### 后端配套
|
||||
|
||||
- **补一套 JSON API**:SPA 是客户端渲染,需要后端提供 config 读写、数据查询、记录 CRUD 等 JSON 端点。(同源不等于不需要 API——API 是“客户端怎么拿数据”,与文件托管在哪无关。)
|
||||
- **鉴权**:浏览器面向的新端点(含记录 CRUD)复用现有 session cookie 保护。
|
||||
- **类型化 client**:用 `scripts/export_openapi.py` 的输出生成 TS client。
|
||||
|
||||
### 鉴权边界(与 M3 衔接)
|
||||
|
||||
- 现在那个“裸 API 记小狗日志”的 ingestion 端点(设备 / 脚本调用,非浏览器)**维持现状到 M3**。
|
||||
- M2 新增的、浏览器调用的 CRUD 端点,用 session 保护即可,本步不引入 token。
|
||||
|
||||
---
|
||||
|
||||
## M3 — 开放与移动端(远期试水)
|
||||
|
||||
### 目标
|
||||
|
||||
引入 token 鉴权并做一个 React Native 移动端。**明确是很远期、低投入的试水**——先把 React 前端做出来,之后才会碰移动端,且主要是想试试没做过的 React / React Native。
|
||||
|
||||
### 范围
|
||||
|
||||
- **OAuth-lite token 签发**:移动端在内置浏览器里用账号密码登录,走一遍类 OAuth 流程,服务端签发一个 bearer token 给 app 存起来使用。(本质是没有第三方的 Authorization Code 简化版。)
|
||||
- **React Native 移动端**:试水性质。
|
||||
- **给 ingestion 端点上 token**:把 M2 暂时维持裸奔的设备端点收口到 token 鉴权下。
|
||||
|
||||
### 为什么放最后
|
||||
|
||||
- 移动端是这一阶段最远期、最不确定的部分。
|
||||
- token 主要是移动端的前置条件;Web 端 React 用现有 session cookie 即可,不需要为它提前引入 token。
|
||||
Reference in New Issue
Block a user