# iBoard
用于收集和整理经济数据，但是以策略为主体重新组织。

## 分支规则

### 分支类型与职责

| 分支 | 职责 | 部署 | 合并方向 |
| --- | --- | --- | --- |
| `master` | **主分支**（生产分支） | ✅ 唯一部署出口，push 即发布生产 | — |
| `dev-*` | **开发集成分支**，如 `dev-0.2` | ❌ 不部署（只跑 CI 校验） | 合到 `master` 后由 master 部署 |
| `tmp-*` | **临时分支**，针对单个问题的修改 | ❌ 不部署 | 合到对应 `dev-*` 后删除 |
| `android-*` | **Android 外壳构建分支**，如 `android-1.0` | ❌ 不部署，只触发 APK 构建 | 构建前须与 `master` 同步 |

> **关于 `main`**：团队约定里的「main 主分支」在本仓库落地为 **`master`**（仓库默认分支即 `master`，不另建 `main`）。
> 下文所有「main 主分支」均指 `master`。

### 流程

```
tmp-xxx  ──合并──▶  dev-0.2  ──合并──▶  master  ──push──▶  自动部署生产
   │                    │
   │                    └──push──▶ 只跑 CI（版本一致性校验），不部署
   │
   └── 问题解决后删除

android-1.0 ── 从 master 同步代码后 push ──▶ 只触发 APK 构建 / 发布到 Packages
```

1. **日常开发**：从 `dev-*` 拉 `tmp-*`，改完合回 `dev-*`，`tmp-*` 用完即删。
2. **上线**：`dev-*` 验收通过后合并到 `master`，**推送 `master` 才部署生产**。
3. **打 Android 包**：`android-*` 与 `master` 同步（`git merge master` 或 `git reset --hard origin/master`）后推送，
   触发 [android-build.yml](.gitea/workflows/android-build.yml)，产物发布到 Gitea Generic Packages。

### CI 触发范围

| 工作流 | 触发分支 | 动作 |
| --- | --- | --- |
| [docker-build.yml](.gitea/workflows/docker-build.yml) | **仅 `master`** | 构建前端 + 生成签名 OTA 包 + 构建镜像 + 迁移数据库 + 部署生产 |
| [android-build.yml](.gitea/workflows/android-build.yml) | `android-*` | 构建并签名 release APK，上传到 Packages |
| [ota-build.yml](.gitea/workflows/ota-build.yml) | 仅手动 `workflow_dispatch` | 单独构建/签发 OTA 包 |
| [sync-registry.yml](.gitea/workflows/sync-registry.yml) | 见文件 | 同步注册中心 |

> ⚠️ **重要变更（2026-10）**：`dev-*` 分支**已取消** push 自动部署。
> 此前 `dev-*` 与 `master` 共用同一套生产环境，推送开发分支等于直接发布，容易把半成品带上线。
> 现在开发分支的提交只有合并进 `master` 并推送后才会部署。

## 版本管理

### 一句话规则

**同一个版本号，APK（原生包）裸写，OTA 热更包多一个 `-ota` 后缀。**

```
分支 dev-0.2  →  版本 0.2.0
                 ├─ APK / 桌面安装包   versionName = 0.2.0      （无后缀）
                 ├─ 前端运行时         APP_VERSION = 0.2.0      （无后缀）
                 └─ OTA 热更包         version     = 0.2.0-ota  （多一个 -ota）
```

版本号遵循 `主版本.次版本.修订号`，**唯一手动管理点是 `package.json` 的 `version` 字段**（当前 `0.2.0`），
该字段只写裸版本号，**不允许带 `-ota`**——`-ota` 由 `scripts/build-ota.mjs` 打包时自动追加。

版本号必须与所在分支对应：`dev-0.2` 分支上就该是 `0.2.x`。

### 改版本号时要同步的位置

| 文件 | 字段 | 说明 |
| --- | --- | --- |
| `package.json` | `version` | **唯一源**，前端 `src/version.js` 与 OTA 打包脚本均读取此值 |
| `src-tauri/tauri.conf.json` | `version` | Tauri 应用版本 → **APK 的 versionName**、桌面安装包版本 |
| `src-tauri/Cargo.toml` | `[package].version` | Rust 包版本，需与 tauri.conf.json 一致 |
| `src-tauri/Cargo.lock` | `name = "iboard"` 条目 | 同上，随 Cargo.toml 一起改 |

`scripts/ci/check-version.mjs` 会在 CI 第一段就校验这四处**完全相等**且不含 `-ota`，
并校验 `dev-<major>.<minor>` 分支与版本前缀对应；任何一项不满足直接构建失败（`main`/`master` 只跳过分支校验，一致性照样查）。

### APK 版本号是怎么来的

`tauri android init` 会按 `src-tauri/tauri.conf.json` 的 `version` 生成
`src-tauri/gen/android/app/tauri.properties`（该目录被 gitignore，CI 每次 `android init` 重新生成）：

```
tauri.android.versionName = 0.2.0     # 人类可见版本，与 tauri.conf.json 一致
tauri.android.versionCode = 2000      # 整数，升级判定用 = major*1000000 + minor*1000 + patch
```

注意：
- `versionCode` 必须**单调递增**才能覆盖安装，所以版本号只能往上走（`0.2.0` → 2000 > `0.1.0` → 1000）。
- 本地如果 APK 装完显示旧版本号，是 `src-tauri/gen/android` 缓存没刷新，
  删掉该目录重新 `npx tauri android init` 即可。

### 其它

- `src/version.js` 从 `package.json` 取值，前端"我"页与 OTA 检查接口展示的即此版本，不要另写死版本号。
- OTA 升级包的 `version` = `<package.json version>-ota`（见 `scripts/build-ota.mjs`），
  所以版本号错位会直接导致线上 OTA 判新失准。
- **`min_binary_version` 与这里的版本号是两件事**：它是 OTA 包要求的最低原生二进制版本
  （CI 里由 `OTA_MIN_BINARY` 显式指定，当前 `0.1.0`）。前端版本升到 0.2.0 时它不应跟着抬，
  否则装了 0.1.0 APK 的设备会直接收不到热更。
- `BUILD_ID` 每次编译自动生成（见 `vite.config.js` 的 `__BUILD_ID__`），无需手动维护。
- `ota-package/latest.json` 为运行时产物，不纳入版本管理。

## 分支与 CI/CD (Gitea Actions)

分支按版本命名：`dev-0.1`、`dev-0.2`…… 对应 `docs/V<版本>/` 下的需求/设计文档。
职责与合并方向见上文「分支规则」。

| 分支 | 触发的 workflow | 行为 |
| --- | --- | --- |
| `master`（主分支） | `docker-build.yml` | 生产发布：构建前端 → 生成签名 OTA 包 → 打 Docker 镜像 → 数据库自动升级 → 重启容器 `iboard` (宿主 3001) |
| `dev-*` | **不触发部署** | 推送只走版本一致性校验；上线须先合并到 `master` |
| `tmp-*` | 不触发 | 临时分支，合到 `dev-*` 后删除 |
| `android-*` | `android-build.yml` | 打签名 release APK 并上传到 Gitea Generic Package Registry（构建前须与 `master` 同步） |
| 手动 | `ota-build.yml` / `sync-registry.yml` | 单独出 OTA 包 / 同步 secrets 到注册中心 |

数据库升级约定：
- 唯一入口是 `prisma/migrations/` 下的迁移文件，由 `npx prisma migrate deploy` 在**启新容器之前**执行
  （幂等，无新迁移时为 no-op）；迁移失败会中断 workflow，不会用新镜像顶掉仍在运行的旧容器。
- 由于只有 `master` 会部署，**迁移真正作用到生产库的唯一时机就是 master 发布**；
  开发分支上的迁移不会被自动应用，请在合并到 `master` 前确认可回滚。
- 本地改了 `prisma/schema.prisma` 后：写迁移 SQL → `npx prisma migrate deploy` 验证 → 提交迁移目录。

- 首页主要是放经济文章
- 数据主要是放各种经济数据和图表。
- 策略是放按照经济理论组织的相关数据和图表的对比。



## 首页
首页显示新闻模块和报警模块的数据，按照时间最新优先，无限滚动，直到数据库中没有数据为止。


### 新闻模块
增加一个新闻模块，数据来源是每日夜间 3点，通过妙想 MCP 获取国内外最新经济政治新闻，按影响大小排序10条。
存入数据库。


### 报警模块
World Bank 中的数据只用来对比妙想 MCP 拉取的数据正确性。当同一时间的同一项数据不同时，记录一个报警。

## 监控
监控分为服务器监控和服务监控两部分。
服务器监控对接 komari，地址：https://komari.honor3.com/



## 前端 OTA 升级

只升级前端资源 (`dist/`), **不触碰** Tauri 框架/原生层, 不重打 APK.
底层使用 [`tauri-plugin-hotswap`](https://github.com/denniskribl/tauri-plugin-hotswap)
(签名/回滚/资源替换全部由插件处理).

### 工作流

```
开发者                                   服务端
npm run build                ──┐
scripts/build-ota.mjs          │  生成 iboard-ota.tar.gz + minisign 签名
                               │  输出 latest.json (sequence/version/...)
                               ▼
                          ota-package/   ── 部署到服务器 ──▶   /srv/iboard/ota-package
                                                                    │
                                                                    ▼
                                                          GET /api/ota/check/<seq>
客户端 (Tauri)                                                     │
启动 ─▶ notifyReady()                                              ▼
我页 ─▶ checkUpdate()  ◀──── 200 + manifest (含 url/signature) ─────┘
       applyUpdate()  ─▶  下载 tar.gz ─▶ minisign 校验 ─▶ 解压到 AppData/ota-dist
       location.reload() ─▶  后续资源从 tauri://localhost 优先命中覆盖包
```

### 首次部署

1. **生成签名密钥** (一次性):
   ```bash
   # 安装 minisign: Windows (scoop) / macOS (brew) / Linux (apt)
   node scripts/ota-genkey.mjs
   # 产物: ota-package/hotswap.{key, pub}
   ```
2. **把公钥写入 `src-tauri/tauri.conf.json`** 的 `plugins.hotswap.pubkey` 字段
   (脚本会打印).
3. **配置 endpoint** (`src-tauri/tauri.conf.json`):
   ```json
   "hotswap": {
     "endpoint": "https://your-server/api/ota/check/{{current_sequence}}",
     "pubkey": "RWR...",
     "require_https": true
   }
   ```
4. **服务端环境变量** `OTA_PACKAGE_DIR` 指向 `ota-package/` 所在目录.

### 发布新版前端

```bash
npm run build                # 生成 dist/
node scripts/build-ota.mjs   # 打成 tar.gz + 签名 + latest.json
# sequence 默认 = Date.now(), 也可 --sequence 43 手动指定
# 也可加 --notes "修复登录按钮" --min-binary 0.1.0
```

然后把 `ota-package/` 下两个文件 (`iboard-ota.tar.gz` + `latest.json`) 同步到
服务端 `OTA_PACKAGE_DIR` 指向的目录. sequence 单调递增即可.

### 客户端使用

打开 App 的"我"页, 点 [检查更新] → [立即升级] 即可. 失败会自动回滚到内置 dist.

> **注意:** Tauri 2 的 dev 模式 (`tauri dev`) 会代理到 Vite dev server, **OTA 不生效**.
> OTA 仅在 release/打包构建中可用, 测试请用 `tauri build` 产物.

### 客户端判新口径（V0.2 新增）

"我"页 OTA 组内有一个 **开发版本** 开关（默认关闭），切换"怎么算新版本"：

| 开关 | 判新依据 | 适用 |
| --- | --- | --- |
| 关（默认） | **版本号**逐字段比大小（`0.2.0-ota` > `0.1.0` 才算新） | 生产。旧分支被重建（sequence 变大但版本号更小）时不会把设备拉回旧版 |
| 开 | **build sequence**（服务端 sequence 比本地大就算新） | 开发联调。每次构建都能推到设备 |

- 开关值持久化在设备端 `localStorage.iboard_ota_dev_mode`（`'1'`/`'0'`），默认 `'0'`。
- 版本比较在 `src/utils/version.js`（纯函数，`src/api/ota.js` 统一封装两种模式）。
- **`min_binary_version` 与应用版本是两件事**：前者是前端 bundle 要求的最低原生二进制版本，
  当前 APK 原生层仍是 `0.1.0`，所以 CI 里固定传 `--min-binary 0.1.0`（见 `docker-build.yml` 的
  `OTA_MIN_BINARY`）。只改前端版本号时**不要**跟着抬这个值，否则旧 APK 会收不到更新。

### Web 端

Web 浏览器访问 (同源) 时, 服务端返回简化版 manifest, 仅显示"有新版本请刷新".
Web 端不会自动下载包, 也不会替换前端代码.


### CI 自动签名 (Gitea Actions)

.gitea/workflows/ota-build.yml 是手动触发的 OTA 打包工作流 (每次发版前点一次).

**配置 secret** (Gitea 仓库 -> Settings -> Actions -> Secrets):
- MINISIGN_PRIVATE_KEY: 完整 .key 文件内容, **整段** (含首行 `untrusted comment: minisign encrypted secret key` 和 base64 行), 整段作为多行 secret 粘贴即可.

**使用流程**:
1. 在 Gitea Actions 页面选 OTA Frontend Build & Sign -> Run workflow
2. 填 sequence (可选, 不填用时间戳) 和 
otes (可选)
3. CI 跑完会在 artifact 里看到 ota-package/iboard-ota.tar.gz + latest.json + iboard-ota.tar.gz.minisig
4. 下载 artifact, scp 到服务器 OTA_PACKAGE_DIR 目录
5. 客户端"我"页 [检查更新] 即可触发升级

**注意**:
- CI 用的私钥必须**与** 	auri.conf.json 里 plugins.hotswap.pubkey **配对**; 换密钥时需要同时更新 CI secret 和 conf.
- 不要把私钥 commit 到 git, 整个 ota-package/hotswap.key 已被 .gitignore 排除.
