jif2.zhang fd4079c9c3
Docker Build and Push / build-image (push) Failing after 1m48s
feat(任务): 即时同步 —— 订阅本地库变更, 跨端改动无需切页自动刷新
现象: 多端同时打开同一项目时, 一端改完, 另一端必须切到别的页面再切回来才更新。

根因: 不是同步没到, 而是 UI 从未订阅本地库变化 ——
PowerSync 的下行推送 (PG 逻辑复制 → sync service → 客户端本地 SQLite) 本身正常,
但任务模块的读全是命令式 db().getAll() (api/task.js), 读完不再看;
lib/powersync/react.jsx 虽然导出了 useQuery/useStatus, 全项目一次都没用过;
TaskPage/ProjectView 只在 useEffect 里 load() 一次, 连 useRefreshOnVisible 都没接
(只有打卡与宏观页面在用)。「切面板能刷新」纯粹是因为组件重新挂载又 load 了一次。

实现 (docs/V0.2/离线编辑模块/即时同步方案.md, 方案 C):
- 新增 lib/powersync/useDbSignal.js: 用 db.watch() 的表级过滤 (tables) + 节流
  (throttleMs) + AbortSignal, 订阅本地库变更; 跳过订阅时的首次通知, 避免与
  挂载时的 load() 重复。
  (初版设计是手写 count/sum/max(updatedAt) 的「信号 SQL」, 实施时发现 SDK 原生
   支持 tables 过滤, 更可靠, 已改为表过滤并在文档中记录变更。)
- api/task.js 新增 TASK_TABLES / PROJECT_TABLES 常量 (表名须与客户端 schema 一致)。
- ProjectView: 订阅任务域 (Task/BoardColumn/TaskMember/TaskTag) → 重算;
  TaskPage: 订阅项目域 (Project/ProjectMember) → 重算。
- 不打断操作: BoardView/ListView 上报 onDragActiveChange, 拖拽期间收到变更只记
  pending, 松手后补一次刷新, 避免卡片在手指下跳位。
- 回前台兜底: 两页都接 useRefreshOnVisible (WebView 后台可能节流订阅回调)。

验证 (真实 Windows 外壳 + CDP, 服务端直接改库模拟另一端):
- 服务端改任务标题后, 页面在**无任何操作**的情况下自动更新 (三次复测均成功);
- 改回原标题后同样自动还原; 测试数据已恢复。

已知瓶颈 (不在 UI 层, 另见文档 §7.0):
- 实测端到端延迟约 13~14s, 未达 ≤1s 目标;
- CDP Network 观察客户端: 无 WebSocket, 仅 /sync/stream 长连接;
- NPM (baishe nginx-app-1, proxy_host/5.conf) 未关闭 proxy_buffering,
  流式响应被 nginx 缓冲后按批下发, 与观测到的固定批次延迟吻合;
- 修复属运维配置 (proxy_buffering off 等), 需单独评估后执行。
2026-10-27 20:57:05 +08:00
2026-08-03 18:09:08 +08:00
2026-08-07 17:10:07 +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:09:08 +08:00

iBoard

跨平台个人看板 (Tauri + React + Express + MariaDB)。用于收集和整理经济数据,但是以策略为主体重新组织。

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

首页

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

新闻模块

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

报警模块

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

监控

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


分支规则

分支类型与职责

分支 职责 部署 合并方向
master 主分支(生产分支) ✅ 唯一部署出口,push 即发布生产 —
dev-* 开发集成分支,如 dev-0.2 ❌ 不部署(只跑 CI 校验) 合到 master 后由 master 部署
tmp-* 临时分支,针对单个问题的修改 ❌ 不部署 合到对应 dev-* 后删除
android-* Android 外壳构建分支 ❌ 不部署,只触发 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 Generic Packages。

CI 触发范围

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

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

日常操作规则(工作目录约定)

  • 主工作目录(iboard/)默认停留在 dev-0.2,所有修改都在 dev-0.2 上提交;dev-0.2 可随时 push(只跑 CI 校验,不部署)。

  • 独立功能开发使用独立分支 + 独立工作目录(如离线编辑在 iboard-dev-0.2.1/ 的 tmp-offline-edit 分支),不占用 dev-0.2。

  • 发布流程(仅在明确需要发布时执行,结束后立刻回到 dev-0.2):

    git switch master
    git merge --ff-only dev-0.2
    git push origin master        # push 即部署生产
    git switch dev-0.2
    

版本管理

版本号构成与口径(2026-10-25 决议)

V MAJOR.MINOR.PATCH   例: V0.2.0
       │      └── PATCH  小版本号: 仅在明确提出时自增, 否则沿用 (记录见 CHANGELOG.md)
       └── MINOR  大版本迭代, 对应一个开发分支 (V0.2 ↔ dev-0.2)

前端与外壳解绑,独立维护、独立发版(2026-10-25 决议):

组 版本 管理点 产物 发版节奏
前端 (Web/OTA) 0.2.0 package.json 的 version(唯一手动管理点,只写裸版本号,不允许带 -ota) 前端运行时 APP_VERSION = 0.2.0;OTA 包 version = 0.2.0-ota 随 master push 高频发版
外壳 0.2.0 src-tauri/tauri.conf.json 的 version(须与 Cargo.toml/Cargo.lock 组内一致) APK versionName、桌面安装包版本;后续 Windows 外壳同理 独立分支(如 android-*)低频发版
  • 两组版本号不要求相等,CI(scripts/ci/check-version.mjs)只校验组内一致 + 前端版本与 dev-<major>.<minor> 分支前缀对应。
  • 分支口径:dev-0.2 开发,发布一律走 master。

build 标识

  • 格式统一为 YYYYMMDDHHmmss(北京时间):
    • 前端 BUILD_ID:vite 编译时生成(vite.config.js#makeBuildId),无需手动维护;
    • OTA 包 build_id:打包时按同一格式生成(scripts/build-ota.mjs)。
  • OTA 包的 sequence(Unix 毫秒,单调递增)保留,仅用于判新与热插拔协议,不再用于展示。

PATCH 小版本号

  • 仅在明确提出时自增(改 package.json version 第三位),否则一直沿用。
  • 每次自增在 CHANGELOG.md 追加一行(patch 号 / 日期 / commit / 要点)。

改版本号时要同步的位置

组 文件 字段 说明
前端 package.json version 前端唯一源,src/version.js 与 OTA 打包脚本均读取此值
外壳 src-tauri/tauri.conf.json version APK 的 versionName、桌面安装包版本
外壳 src-tauri/Cargo.toml [package].version Rust 包版本,须与 tauri.conf.json 一致
外壳 src-tauri/Cargo.lock name = "iboard" 条目 随 Cargo.toml 一起改

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 检查接口展示的即此版本,不要另写死版本号。
  • min_binary_version 与这里的版本号是两件事:它是 OTA 包要求的最低原生二进制版本 (CI 里由 OTA_MIN_BINARY 显式指定,当前 0.1.0)。前端版本升到 0.2.0 时它不应跟着抬, 否则装了 0.1.0 APK 的设备会直接收不到热更。
  • ota-package/latest.json 为运行时产物,不纳入版本管理。

"我的"页版本展示口径

行 展示
当前版本 v0.2.0 · build 20261025204300(前端 APP_VERSION + BUILD_ID 原始格式)
运行资源 OTA 覆盖包版本 / 内置
新版本 v0.2.0-ota · build 20261025210000(OTA 包 build_id)
发布说明 触发构建的 commit message
提交 commit sha 前 10 位
页面底部 外壳 v0.2.0(Tauri API 读取;Web 端显示 —)

前端 OTA 升级

只升级前端资源 (dist/),不触碰 Tauri 框架/原生层,不重打 APK。 底层使用 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 优先命中覆盖包

发布说明与提交号(2026-10-25 起)

  • CI(docker-build.yml)把触发构建的 commit message(标题+正文,去空行,截断 400 字)作为 notes 写入 latest.json;
  • commit sha 经 --commit 参数单独写入 commit 字段;
  • "我的"页:发布说明展示 commit message,「提交」行展示短 sha。

首次部署

  1. 生成签名密钥 (一次性):
    # 安装 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):
    "hotswap": {
      "endpoint": "https://your-server/api/ota/check/{{current_sequence}}",
      "pubkey": "RWR...",
      "require_https": true
    }
    
  4. 服务端环境变量 OTA_PACKAGE_DIR 指向 ota-package/ 所在目录。

发布新版前端

npm run build                # 生成 dist/
node scripts/build-ota.mjs   # 打成 tar.gz + 签名 + latest.json
# sequence 默认 = Date.now(), 也可 --sequence 43 手动指定
# 也可加 --notes "修复登录按钮" --commit <sha> --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(可选,不填用时间戳)和 notes(可选)
  3. CI 跑完会在 artifact 里看到 ota-package/iboard-ota.tar.gz + latest.json + iboard-ota.tar.gz.minisig
  4. 下载 artifact,scp 到服务器 OTA_PACKAGE_DIR 目录
  5. 客户端"我"页 [检查更新] 即可触发升级

注意:

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

数据库升级约定

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

Windows 本地实时预览(不打包)

改完前端 UI 想立刻在 Windows 外壳里看效果,不必 tauri build、不必打 NSIS/便携包、也不必重装。

npm run win:dev                  # 构建(shell 模式) + 挂载 + 重启外壳
npm run win:dev -- --no-build    # 复用现有 dist, 只挂载+重启 (最快, 约 2 秒)
npm run win:dev -- --no-launch   # 只挂载, 自己手动启动外壳
npm run win:dev -- --exe "D:\path\iBoard.exe"
npm run win:dev -- --clear       # 卸载覆盖, 外壳回到内置前端

原理

外壳用 tauri-plugin-hotswap,启动时先看覆盖目录有没有生效的前端包,没有才回退到 exe 内置的 dist。 这和线上 OTA 走的是同一条路径,所以只要把新构建的 dist 写进覆盖目录再重启外壳即可:

%APPDATA%\com.iboard.app\hotswap\
  current              ← 文本文件, 内容为生效的目录名 (如 seq-1793068631298)
  seq-1793068631298\   ← 本次前端 (dist 内容) + hotswap-meta.json

hotswap-meta.json 里有两个字段是硬约束,改错会导致覆盖包被丢弃或立刻回滚:

字段 取值 原因
min_binary_version 必须等于外壳版本(当前 0.2.1) 默认策略 DiscardOnUpgrade 在 binary > min 时丢弃缓存;安全线又要求 binary >= min,故只能取等号
confirmed 必须为 true 默认策略 SingleLaunch 对未确认版本会在启动时立即回滚

另外 sequence 取 Date.now()(大值),让 OTA 检查认为"已是最新",避免线上更新把本地版本顶掉。

注意

  • 覆盖目录就是外壳真实的 OTA 落地目录,挂载期间"我的"页会显示本地版本(0.2.1-local); 测完建议 npm run win:dev -- --clear 复原,避免影响后续正常 OTA。
  • 脚本按 src-tauri/tauri.conf.json 的 identifier 推导覆盖目录,换 identifier 后无需改脚本。
  • 这只适用于前端改动。改了 src-tauri/(Rust/原生层、权限、CSP)必须重新打包外壳。
  • 想要带 HMR 的完整开发循环(改代码即时刷新、免手动重启)用 npm run tauri:dev, 代价是首次要编译一次 debug 版 Rust 二进制。
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%