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.
This commit is contained in:
@@ -5,7 +5,7 @@
|
|||||||
当前系统已经包含:
|
当前系统已经包含:
|
||||||
|
|
||||||
- FastAPI Web 应用与服务端模板页面
|
- FastAPI Web 应用与服务端模板页面
|
||||||
- SQLite + SQLAlchemy + Alembic 的三库结构
|
- SQLite + SQLAlchemy + Alembic 的单库结构
|
||||||
- username/password + server-side session 鉴权
|
- username/password + server-side session 鉴权
|
||||||
- runtime config 页面与 app DB 持久化
|
- runtime config 页面与 app DB 持久化
|
||||||
- public IPv4 monitor、历史持久化与定时检查
|
- public IPv4 monitor、历史持久化与定时检查
|
||||||
@@ -23,41 +23,32 @@
|
|||||||
|
|
||||||
## 当前配置现实
|
## 当前配置现实
|
||||||
|
|
||||||
当前系统仍然是三个独立的 SQLite 数据库文件,而不是单一数据库:
|
当前系统使用单一 SQLite 数据库文件(`app.db`),所有数据表都在其中:
|
||||||
|
|
||||||
- `app` 级共享数据使用自己的 DB 文件
|
- auth(单个 admin 用户、server-side session)
|
||||||
- `location` 模块使用自己的 DB 文件
|
- runtime config 持久化(`app_config` 表)
|
||||||
- `poo` 模块使用自己的 DB 文件
|
- public IPv4 当前状态与变化历史
|
||||||
|
- location 记录(`location` 表)
|
||||||
|
- poo 记录(`poo_records` 表)
|
||||||
|
|
||||||
当前阶段明确不借这次重构把这些 DB 合并。配置层已经显式反映这一点:
|
配置层只保留一个数据库环境变量:
|
||||||
|
|
||||||
- `APP_DATABASE_URL`
|
- `APP_DATABASE_URL`
|
||||||
- `LOCATION_DATABASE_URL`
|
|
||||||
- `POO_DATABASE_URL`
|
|
||||||
|
|
||||||
目前 auth、`location` 和 `poo` 都已经接到各自独立的数据库文件。
|
`app.db` 不会在应用启动时自动创建,需要先运行:
|
||||||
|
|
||||||
其中 `app` 级共享 DB 当前主要用于:
|
```bash
|
||||||
|
python -m scripts.run_migrations
|
||||||
|
```
|
||||||
|
|
||||||
- 单个 admin 用户
|
该命令会通过 Alembic 将 `app.db` 初始化或升级到最新 head(含 `location` / `poo_records` 表)。
|
||||||
- server-side session
|
|
||||||
- runtime config 持久化
|
|
||||||
- public IPv4 当前状态与变化历史
|
|
||||||
|
|
||||||
这部分现在也使用 Alembic 管理:
|
|
||||||
|
|
||||||
- `app db` 不会在应用启动时自动创建
|
|
||||||
- 需要先运行 `python scripts/app_db_adopt.py`
|
|
||||||
- 这个脚本会创建新 DB 并建好 schema
|
|
||||||
|
|
||||||
## 当前目录
|
## 当前目录
|
||||||
|
|
||||||
主要目录如下:
|
主要目录如下:
|
||||||
|
|
||||||
- `app/`: FastAPI 应用代码
|
- `app/`: FastAPI 应用代码
|
||||||
- `alembic_app/`: App DB 的 Alembic migration 环境
|
- `alembic_app/`: App DB 的 Alembic migration 环境(同时管理 `location` / `poo_records` 表)
|
||||||
- `alembic_location/`: Location DB 的 Alembic migration 环境
|
|
||||||
- `alembic_poo/`: Poo DB 的 Alembic migration 环境
|
|
||||||
- `tests/`: pytest 测试
|
- `tests/`: pytest 测试
|
||||||
- `docs/`: 当前系统说明文档
|
- `docs/`: 当前系统说明文档
|
||||||
- `scripts/`: 辅助脚本,例如 OpenAPI 导出
|
- `scripts/`: 辅助脚本,例如 OpenAPI 导出
|
||||||
@@ -128,24 +119,22 @@ uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
|
|||||||
|
|
||||||
## 数据库与 Alembic
|
## 数据库与 Alembic
|
||||||
|
|
||||||
当前默认使用 SQLite,并区分三个数据库文件:
|
当前使用单一 SQLite 数据库文件:
|
||||||
|
|
||||||
- App DB:`sqlite:///./data/app.db`
|
- App DB:`sqlite:///./data/app.db`
|
||||||
- Location DB:`sqlite:///./data/locationRecorder.db`
|
|
||||||
- Poo DB:`sqlite:///./data/pooRecorder.db`
|
|
||||||
- 数据目录:`./data/`
|
- 数据目录:`./data/`
|
||||||
|
|
||||||
初始化 migration 环境后,可继续添加模型并生成迁移:
|
所有模型(auth / config / public_ip / location / poo)共用同一个 `Base`,均通过单一 Alembic 链管理:
|
||||||
|
|
||||||
当前 `app`、`location` 和 `poo` 都已经有各自独立的 Alembic 链路。
|
- Alembic 环境:`alembic_app.ini` + `alembic_app/`
|
||||||
|
|
||||||
- App Alembic 环境:`alembic_app.ini` + `alembic_app/`
|
|
||||||
- Location Alembic 环境:`alembic_location.ini` + `alembic_location/`
|
|
||||||
- Poo Alembic 环境:`alembic_poo.ini` + `alembic_poo/`
|
|
||||||
- 统一 migration job:`python -m scripts.run_migrations`
|
- 统一 migration job:`python -m scripts.run_migrations`
|
||||||
- App DB 初始化:`python scripts/app_db_adopt.py`
|
- App DB 接管 / 初始化:`python scripts/app_db_adopt.py`
|
||||||
- Location DB 接管 / 初始化:`python scripts/location_db_adopt.py`
|
|
||||||
- Poo DB 接管 / 初始化:`python scripts/poo_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` 表持久化
|
- 运行时可编辑配置主要通过 `app_config` 表持久化
|
||||||
- token / secret 这类运行时必须可取回的配置,目前允许明文存储在 config 表中
|
- token / secret 这类运行时必须可取回的配置,目前允许明文存储在 config 表中
|
||||||
- 登录密码仍然单独使用 Argon2 哈希,不走 config 表明文存储
|
- 登录密码仍然单独使用 Argon2 哈希,不走 config 表明文存储
|
||||||
@@ -318,55 +307,6 @@ docker compose -f docker-compose.yml up -d
|
|||||||
docker compose logs -f app
|
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
|
## Container Image CI
|
||||||
|
|
||||||
项目提供了一个 release image workflow:
|
项目提供了一个 release image workflow:
|
||||||
@@ -411,9 +351,16 @@ pytest
|
|||||||
|
|
||||||
当前测试包含:
|
当前测试包含:
|
||||||
|
|
||||||
- app 基本启动测试
|
- app 启动与 `/status` 检查
|
||||||
- `/status` endpoint 测试
|
- 登录 / session / 鉴权流程
|
||||||
- 登录 / session 基础流程测试
|
- runtime config 读写
|
||||||
|
- public IPv4 monitor
|
||||||
|
- SMTP 配置与测试发信
|
||||||
|
- location / poo recorder 端点
|
||||||
|
- Home Assistant inbound 集成
|
||||||
|
- TickTick OAuth
|
||||||
|
- 部署与迁移(`run_migrations`)
|
||||||
|
- legacy 数据迁移脚本(`migrate_legacy_data`)
|
||||||
|
|
||||||
## OpenAPI 导出
|
## OpenAPI 导出
|
||||||
|
|
||||||
|
|||||||
@@ -23,10 +23,8 @@
|
|||||||
- 基础路由注册
|
- 基础路由注册
|
||||||
- `config.py`
|
- `config.py`
|
||||||
- 环境变量驱动的 settings
|
- 环境变量驱动的 settings
|
||||||
- `auth_db.py`
|
|
||||||
- app 级共享 auth 数据库
|
|
||||||
- `db.py`
|
- `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`
|
- `dependencies.py`
|
||||||
- 通用依赖注入
|
- 通用依赖注入
|
||||||
- `api/`
|
- `api/`
|
||||||
@@ -37,7 +35,7 @@
|
|||||||
- 当前已迁入 `POST /poo/record` 与 `GET /poo/latest`
|
- 当前已迁入 `POST /poo/record` 与 `GET /poo/latest`
|
||||||
- `models/`
|
- `models/`
|
||||||
- SQLAlchemy models
|
- SQLAlchemy models
|
||||||
- 当前 `auth`、`location` 与 `poo` 使用各自独立的数据库 base
|
- 所有模型(auth / config / public_ip / location / poo)共用同一个 `Base`,均落在单一 `app.db` 中
|
||||||
- `schemas/`
|
- `schemas/`
|
||||||
- Pydantic schemas
|
- Pydantic schemas
|
||||||
- `services/`
|
- `services/`
|
||||||
@@ -53,17 +51,9 @@
|
|||||||
- `static/`
|
- `static/`
|
||||||
- 极简静态资源
|
- 极简静态资源
|
||||||
|
|
||||||
### `alembic_location/`
|
|
||||||
|
|
||||||
Location DB 的 migration 基础设施。
|
|
||||||
|
|
||||||
### `alembic_app/`
|
### `alembic_app/`
|
||||||
|
|
||||||
App DB 的 migration 基础设施。
|
App DB 的唯一 Alembic migration 链,同时管理 `location` / `poo_records` 表。M1 将三个独立 DB 合并进 `app.db` 后,`alembic_location/` 与 `alembic_poo/` 已退役,全部由此链统一管理。
|
||||||
|
|
||||||
### `alembic_poo/`
|
|
||||||
|
|
||||||
Poo DB 的 migration 基础设施。
|
|
||||||
|
|
||||||
### `tests/`
|
### `tests/`
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -34,7 +34,7 @@
|
|||||||
|
|
||||||
| 里程碑 | 主题 | 一句话 |
|
| 里程碑 | 主题 | 一句话 |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| **M1** | 单库化地基 | 把三库合并成单一 `app.db`,清理散落数据层,删掉 Grafana |
|
| **M1** ✅ | 单库化地基 | 把三库合并成单一 `app.db`,清理散落数据层,删掉 Grafana |
|
||||||
| **M2** | 前端 v2 | React SPA 取代 Jinja,承载 config + 可视化 + 记录增删改 |
|
| **M2** | 前端 v2 | React SPA 取代 Jinja,承载 config + 可视化 + 记录增删改 |
|
||||||
| **M3** | 开放与移动端(远期试水) | token 鉴权 + React Native 移动端 |
|
| **M3** | 开放与移动端(远期试水) | token 鉴权 + React Native 移动端 |
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## M1 — 单库化地基
|
## M1 — 单库化地基(✅ 已完成)
|
||||||
|
|
||||||
### 目标
|
### 目标
|
||||||
|
|
||||||
|
|||||||
@@ -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": {
|
"/homeassistant/publish": {
|
||||||
"post": {
|
"post": {
|
||||||
"tags": [
|
"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": {
|
"/ticktick/auth/start": {
|
||||||
"get": {
|
"get": {
|
||||||
"tags": [
|
"tags": [
|
||||||
@@ -443,6 +485,36 @@
|
|||||||
"type": "object",
|
"type": "object",
|
||||||
"title": "HTTPValidationError"
|
"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": {
|
"StatusResponse": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"status": {
|
"status": {
|
||||||
|
|||||||
@@ -155,6 +155,19 @@ paths:
|
|||||||
text/html:
|
text/html:
|
||||||
schema:
|
schema:
|
||||||
type: string
|
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:
|
/homeassistant/publish:
|
||||||
post:
|
post:
|
||||||
tags:
|
tags:
|
||||||
@@ -203,6 +216,19 @@ paths:
|
|||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema: {}
|
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:
|
/ticktick/auth/start:
|
||||||
get:
|
get:
|
||||||
tags:
|
tags:
|
||||||
@@ -285,6 +311,29 @@ components:
|
|||||||
title: Detail
|
title: Detail
|
||||||
type: object
|
type: object
|
||||||
title: HTTPValidationError
|
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:
|
StatusResponse:
|
||||||
properties:
|
properties:
|
||||||
status:
|
status:
|
||||||
|
|||||||
Reference in New Issue
Block a user