jif2.zhang a66ddb457a 修复扫码弹窗一闪而过
现象: 米家授权过期后, 扫码弹窗从底部弹出但瞬间消失, 看不到二维码.

三处缺陷:
1. 服务端 startQrLogin() 在已有流程运行时提前 return, 未清 done.
   上一轮成功残留的 done=true 会让前端第一次轮询就拿到
   {running:false, done:true}, 误判为扫码成功立即关窗.
   改为复用流程时显式返回 done:false.
2. 前端 AuthPopup 的成功/失败判定没有幂等保护, 且 onClose/onOk 是
   父组件每次渲染新建的函数. 父级轮询触发重渲染后, 回调身份变化
   会波及弹窗关闭路径. 改用 ref 持有最新回调 + settledRef 保证
   一轮流程只结算一次.
3. 弹窗遮罩绑定 onMaskClick=cancel, 手机端极易误触; 关闭遮罩点击,
   只允许显式点「取消」关闭.

注意: 前端需重新构建发布, 服务端需重启后生效.
2026-10-24 07:27:03 +08:00
2026-08-03 18:09:08 +08:00
2026-08-07 17:10:07 +08:00
2026-08-03 18:05:50 +08:00
2026-10-24 07:27:03 +08:00
2026-10-24 07:27:03 +08:00
2026-04-25 19:48:19 +08:00
2026-08-07 14:36:57 +08:00
2026-08-07 14:36:57 +08:00
2026-08-03 18:05:50 +08:00
2026-08-03 18:09:08 +08:00
2026-08-06 17:27:23 +08:00

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# iBoard
用于收集和整理经济数据,但是以策略为主体重新组织。

## 版本管理

### 一句话规则

**同一个版本号,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 | 行为 |
| --- | --- | --- |
| `main` / `master` | `docker-build.yml` | 生产发布:构建前端 → 生成签名 OTA 包 → 打 Docker 镜像 → 数据库自动升级 → 重启容器 `iboard` (宿主 3001) |
| `dev-*` | `docker-build.yml` | **与生产同环境**:推送即构建、迁移数据库、部署(开发分支直接顶掉生产,含 OTA 热更) |
| `android-*` | `android-build.yml` | 打签名 release APK 并上传到 Gitea Generic Package Registry |
| 手动 | `ota-build.yml` / `sync-registry.yml` | 单独出 OTA 包 / 同步 secrets 到注册中心 |

数据库升级约定:
- 唯一入口是 `prisma/migrations/` 下的迁移文件,由 `npx prisma migrate deploy` 在**启新容器之前**执行
  (幂等,无新迁移时为 no-op);迁移失败会中断 workflow,不会用新镜像顶掉仍在运行的旧容器。
- `dev-*` 与 `main/master` 共用同一个 MariaDB 库(同一个 `DATABASE_URL` secret),
  因此开发分支上的迁移会直接作用到该库结构,合入前请确认迁移可回滚。
- 本地改了 `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 排除.
S
Description
No description provided
Readme
26 MiB
Languages
TypeScript 68.9%
JavaScript 25.5%
CSS 2.6%
Rust 1.7%
Shell 0.8%
Other 0.4%