From 2f634006d2a72b72cccc8c30354db2235f402458 Mon Sep 17 00:00:00 2001 From: Tianyu Liu Date: Fri, 12 Jun 2026 17:13:28 +0200 Subject: [PATCH] M1-T07: align docs to single-DB reality and re-export OpenAPI Rewrite README (single app.db + one alembic_app chain, legacy data moved once via scripts.migrate_legacy_data, accurate test list) and remove the Grafana Provisioning section. Update architecture-overview to the unified data layer (one Base, app-DB engine with WAL) and retire the alembic_location / alembic_poo sections. Mark M1 done in the roadmap. Re-export openapi/, which catches the spec up to the already-existing /config/smtp/test and /public-ip/check endpoints (purely additive; M1's DB-session dependency swap produced no schema change). pytest 95 passed; ruff clean (pre-existing only); OpenAPI export idempotent. --- README.md | 123 ++++++++++------------------------ docs/architecture-overview.md | 16 +---- docs/roadmap.md | 4 +- openapi/openapi.json | 72 ++++++++++++++++++++ openapi/openapi.yaml | 49 ++++++++++++++ 5 files changed, 161 insertions(+), 103 deletions(-) diff --git a/README.md b/README.md index 9fd0eee..c1fb3b9 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ 当前系统已经包含: - FastAPI Web 应用与服务端模板页面 -- SQLite + SQLAlchemy + Alembic 的三库结构 +- SQLite + SQLAlchemy + Alembic 的单库结构 - username/password + server-side session 鉴权 - runtime config 页面与 app DB 持久化 - public IPv4 monitor、历史持久化与定时检查 @@ -23,41 +23,32 @@ ## 当前配置现实 -当前系统仍然是三个独立的 SQLite 数据库文件,而不是单一数据库: +当前系统使用单一 SQLite 数据库文件(`app.db`),所有数据表都在其中: -- `app` 级共享数据使用自己的 DB 文件 -- `location` 模块使用自己的 DB 文件 -- `poo` 模块使用自己的 DB 文件 +- auth(单个 admin 用户、server-side session) +- runtime config 持久化(`app_config` 表) +- public IPv4 当前状态与变化历史 +- location 记录(`location` 表) +- poo 记录(`poo_records` 表) -当前阶段明确不借这次重构把这些 DB 合并。配置层已经显式反映这一点: +配置层只保留一个数据库环境变量: - `APP_DATABASE_URL` -- `LOCATION_DATABASE_URL` -- `POO_DATABASE_URL` -目前 auth、`location` 和 `poo` 都已经接到各自独立的数据库文件。 +`app.db` 不会在应用启动时自动创建,需要先运行: -其中 `app` 级共享 DB 当前主要用于: +```bash +python -m scripts.run_migrations +``` -- 单个 admin 用户 -- server-side session -- runtime config 持久化 -- public IPv4 当前状态与变化历史 - -这部分现在也使用 Alembic 管理: - -- `app db` 不会在应用启动时自动创建 -- 需要先运行 `python scripts/app_db_adopt.py` -- 这个脚本会创建新 DB 并建好 schema +该命令会通过 Alembic 将 `app.db` 初始化或升级到最新 head(含 `location` / `poo_records` 表)。 ## 当前目录 主要目录如下: - `app/`: FastAPI 应用代码 -- `alembic_app/`: App DB 的 Alembic migration 环境 -- `alembic_location/`: Location DB 的 Alembic migration 环境 -- `alembic_poo/`: Poo DB 的 Alembic migration 环境 +- `alembic_app/`: App DB 的 Alembic migration 环境(同时管理 `location` / `poo_records` 表) - `tests/`: pytest 测试 - `docs/`: 当前系统说明文档 - `scripts/`: 辅助脚本,例如 OpenAPI 导出 @@ -128,24 +119,22 @@ uvicorn app.main:app --reload --host 0.0.0.0 --port 8000 ## 数据库与 Alembic -当前默认使用 SQLite,并区分三个数据库文件: +当前使用单一 SQLite 数据库文件: - App DB:`sqlite:///./data/app.db` -- Location DB:`sqlite:///./data/locationRecorder.db` -- Poo DB:`sqlite:///./data/pooRecorder.db` - 数据目录:`./data/` -初始化 migration 环境后,可继续添加模型并生成迁移: +所有模型(auth / config / public_ip / location / poo)共用同一个 `Base`,均通过单一 Alembic 链管理: -当前 `app`、`location` 和 `poo` 都已经有各自独立的 Alembic 链路。 - -- App Alembic 环境:`alembic_app.ini` + `alembic_app/` -- Location Alembic 环境:`alembic_location.ini` + `alembic_location/` -- Poo Alembic 环境:`alembic_poo.ini` + `alembic_poo/` +- Alembic 环境:`alembic_app.ini` + `alembic_app/` - 统一 migration job:`python -m scripts.run_migrations` -- App DB 初始化:`python scripts/app_db_adopt.py` -- Location DB 接管 / 初始化:`python scripts/location_db_adopt.py` -- Poo DB 接管 / 初始化:`python scripts/poo_db_adopt.py` +- App DB 接管 / 初始化:`python scripts/app_db_adopt.py` + +历史 location / poo 数据(旧版本遗留的独立 DB 文件)已通过以下脚本一次性迁移至 `app.db`(幂等,不删除旧文件): + +```bash +python -m scripts.migrate_legacy_data +``` ## 基础鉴权 @@ -197,7 +186,7 @@ uvicorn app.main:app --reload --host 0.0.0.0 --port 8000 这意味着: -- location / poo / app DB 地址仍然属于 bootstrap 范畴 +- app DB 地址(`APP_DATABASE_URL`)仍然属于 bootstrap 范畴 - 运行时可编辑配置主要通过 `app_config` 表持久化 - token / secret 这类运行时必须可取回的配置,目前允许明文存储在 config 表中 - 登录密码仍然单独使用 Argon2 哈希,不走 config 表明文存储 @@ -318,55 +307,6 @@ docker compose -f docker-compose.yml up -d docker compose logs -f app ``` -## Grafana Provisioning - -当前仓库支持通过 Grafana provisioning 自动加载 SQLite datasource 和 repo 内的 dashboard 导出文件。 - -需要保留的文件路径如下: - -- `grafana/provisioning/datasources/locationrecorder.yaml` -- `grafana/provisioning/datasources/poorecorder.yaml` -- `grafana/provisioning/dashboards/provider.yaml` -- `grafana/dashboards/locationrecorder.json` -- `grafana/dashboards/poorecorder.json` - -这些文件的职责分别是: - -- `grafana/provisioning/datasources/locationrecorder.yaml`:声明 `locationrecorder` SQLite datasource,并指向 `/data/home-automation/locationRecorder.db` -- `grafana/provisioning/datasources/poorecorder.yaml`:声明 `poorecorder` SQLite datasource,并指向 `/data/home-automation/pooRecorder.db` -- `grafana/provisioning/dashboards/provider.yaml`:告诉 Grafana 从 `/var/lib/grafana/dashboards` 扫描并加载 dashboard JSON -- `grafana/dashboards/locationrecorder.json`:location recorder dashboard 导出文件,内容本身不需要在 compose 中改写 -- `grafana/dashboards/poorecorder.json`:poo recorder dashboard 导出文件,内容本身不需要在 compose 中改写 - -当前 `docker-compose.yml` 中,Grafana service 需要挂载以下目录: - -- `./grafana/provisioning -> /etc/grafana/provisioning:ro` -- `./grafana/dashboards -> /var/lib/grafana/dashboards:ro` - -同时保留现有 named volume `homeautomation_grafana_storage:/var/lib/grafana` 作为 Grafana 运行态数据存储。 - -一键启动前,至少需要以下文件已经存在: - -- `grafana/provisioning/datasources/locationrecorder.yaml` -- `grafana/provisioning/datasources/poorecorder.yaml` -- `grafana/provisioning/dashboards/provider.yaml` -- `grafana/dashboards/locationrecorder.json` -- `grafana/dashboards/poorecorder.json` - -启动方式: - -```bash -docker compose up -d -``` - -启动后会发生的事情: - -- Grafana 容器会安装 `frser-sqlite-datasource` 插件 -- Grafana 会读取 `/etc/grafana/provisioning/datasources/` 下的 datasource YAML -- Grafana 会读取 `/etc/grafana/provisioning/dashboards/provider.yaml` -- Grafana 会从 `/var/lib/grafana/dashboards/` 自动导入两个 dashboard JSON -- 现有 Grafana named volume 继续负责保存 Grafana 运行态数据,不会覆盖 repo 内的 dashboard 与 provisioning 文件 - ## Container Image CI 项目提供了一个 release image workflow: @@ -411,9 +351,16 @@ pytest 当前测试包含: -- app 基本启动测试 -- `/status` endpoint 测试 -- 登录 / session 基础流程测试 +- app 启动与 `/status` 检查 +- 登录 / session / 鉴权流程 +- runtime config 读写 +- public IPv4 monitor +- SMTP 配置与测试发信 +- location / poo recorder 端点 +- Home Assistant inbound 集成 +- TickTick OAuth +- 部署与迁移(`run_migrations`) +- legacy 数据迁移脚本(`migrate_legacy_data`) ## OpenAPI 导出 diff --git a/docs/architecture-overview.md b/docs/architecture-overview.md index c7c853c..41d7239 100644 --- a/docs/architecture-overview.md +++ b/docs/architecture-overview.md @@ -23,10 +23,8 @@ - 基础路由注册 - `config.py` - 环境变量驱动的 settings -- `auth_db.py` - - app 级共享 auth 数据库 - `db.py` - - SQLAlchemy engine / session / Base + - 统一数据层:一个 `Base`、一个绑定 `app_database_url` 的 cached engine(SQLite WAL)、`get_engine` / `get_session_local` / `reset_db_caches` / `get_db_session` - `dependencies.py` - 通用依赖注入 - `api/` @@ -37,7 +35,7 @@ - 当前已迁入 `POST /poo/record` 与 `GET /poo/latest` - `models/` - SQLAlchemy models - - 当前 `auth`、`location` 与 `poo` 使用各自独立的数据库 base + - 所有模型(auth / config / public_ip / location / poo)共用同一个 `Base`,均落在单一 `app.db` 中 - `schemas/` - Pydantic schemas - `services/` @@ -53,17 +51,9 @@ - `static/` - 极简静态资源 -### `alembic_location/` - -Location DB 的 migration 基础设施。 - ### `alembic_app/` -App DB 的 migration 基础设施。 - -### `alembic_poo/` - -Poo DB 的 migration 基础设施。 +App DB 的唯一 Alembic migration 链,同时管理 `location` / `poo_records` 表。M1 将三个独立 DB 合并进 `app.db` 后,`alembic_location/` 与 `alembic_poo/` 已退役,全部由此链统一管理。 ### `tests/` diff --git a/docs/roadmap.md b/docs/roadmap.md index fb36116..36771e6 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -34,7 +34,7 @@ | 里程碑 | 主题 | 一句话 | | --- | --- | --- | -| **M1** | 单库化地基 | 把三库合并成单一 `app.db`,清理散落数据层,删掉 Grafana | +| **M1** ✅ | 单库化地基 | 把三库合并成单一 `app.db`,清理散落数据层,删掉 Grafana | | **M2** | 前端 v2 | React SPA 取代 Jinja,承载 config + 可视化 + 记录增删改 | | **M3** | 开放与移动端(远期试水) | token 鉴权 + React Native 移动端 | @@ -42,7 +42,7 @@ --- -## M1 — 单库化地基 +## M1 — 单库化地基(✅ 已完成) ### 目标 diff --git a/openapi/openapi.json b/openapi/openapi.json index b03671e..9465aa3 100644 --- a/openapi/openapi.json +++ b/openapi/openapi.json @@ -249,6 +249,27 @@ } } }, + "/config/smtp/test": { + "post": { + "tags": [ + "pages" + ], + "summary": "Smtp Test Submit", + "operationId": "smtp_test_submit_config_smtp_test_post", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "text/html": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, "/homeassistant/publish": { "post": { "tags": [ @@ -325,6 +346,27 @@ } } }, + "/public-ip/check": { + "get": { + "tags": [ + "public-ip" + ], + "summary": "Run Public Ip Check", + "operationId": "run_public_ip_check_public_ip_check_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicIPCheckResponse" + } + } + } + } + } + } + }, "/ticktick/auth/start": { "get": { "tags": [ @@ -443,6 +485,36 @@ "type": "object", "title": "HTTPValidationError" }, + "PublicIPCheckResponse": { + "properties": { + "status": { + "type": "string", + "enum": [ + "first_seen", + "unchanged", + "changed", + "error" + ], + "title": "Status" + }, + "checked_at": { + "type": "string", + "format": "date-time", + "title": "Checked At" + }, + "changed": { + "type": "boolean", + "title": "Changed" + } + }, + "type": "object", + "required": [ + "status", + "checked_at", + "changed" + ], + "title": "PublicIPCheckResponse" + }, "StatusResponse": { "properties": { "status": { diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index b0dde44..a091b27 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -155,6 +155,19 @@ paths: text/html: schema: type: string + /config/smtp/test: + post: + tags: + - pages + summary: Smtp Test Submit + operationId: smtp_test_submit_config_smtp_test_post + responses: + '200': + description: Successful Response + content: + text/html: + schema: + type: string /homeassistant/publish: post: tags: @@ -203,6 +216,19 @@ paths: content: application/json: schema: {} + /public-ip/check: + get: + tags: + - public-ip + summary: Run Public Ip Check + operationId: run_public_ip_check_public_ip_check_get + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/PublicIPCheckResponse' /ticktick/auth/start: get: tags: @@ -285,6 +311,29 @@ components: title: Detail type: object title: HTTPValidationError + PublicIPCheckResponse: + properties: + status: + type: string + enum: + - first_seen + - unchanged + - changed + - error + title: Status + checked_at: + type: string + format: date-time + title: Checked At + changed: + type: boolean + title: Changed + type: object + required: + - status + - checked_at + - changed + title: PublicIPCheckResponse StatusResponse: properties: status: