112 Commits
Author SHA1 Message Date
jif2.zhang 5622e4b3d4 fix(打卡): 修复日历切上月看不到记录; 当日进度改按当天份额统计
Docker Build and Push / build-image (push) Successful in 1m40s
- date.js: addMonths 对 "YYYY-MM" 月粒度保持月粒度, 不再生成 "YYYY-MM-01"。
  旧逻辑使日历切上月时请求 month=2026-10-01, 服务端判为非法后静默回退本月,
  表现为上个月一片空白。
- MergedView: 「回到今天」同时把月份切回当前月; 日标题「已完成 x/y」改为
  按 items 的 dayDone 现算, 避免把同周期后续日期的打卡计入历史日期,
  并让打卡/撤销的乐观更新立即反映到标题。
- server /calendar: 兼容旧的 month=YYYY-MM-DD 请求并截回月份, 不再静默回退。
- server /habits: 回看历史日期时过滤当天尚未创建的打卡项, 与 /calendar 口径一致;
  summary.done 改用 dayDone (当天份额) 而非 completed (周期完成度)。

验证: Vite dev + 生产数据 (admin) 2026-10 显示 10 个环 / 完成率 32%,
10-22 的标题由错误的 3/14 修正为 1/14; 「回到今天」回到 2026-11 (14%)。
2026-11-02 13:54:59 +08:00
jif2.zhang afbb9bc603 feat(宏观): ifinance 分类动态挂载为宏观 tab, 报警迁入监控, 下线油价/本地经济数据/策略研究
Docker Build and Push / build-image (push) Successful in 2m37s
- 宏观只保留本地 新闻/经济文章, 外接 ifinance categories[] 动态成为 tab (纯前端直连, CORS/_headers, https)
- 报警从宏观迁到监控第 4 个 tab (保留 Alert 模块, 供未来新增报警源)
- 彻底删除油价/经济数据/策略研究: 前端组件+api、后端 apizero/data 路由、cron、fetchers、种子数据
- 新增迁移 DROP Indicator/IndicatorPoint/Strategy/OilPriceHistory; 重新生成 Prisma Client
- CSP 放开 *.honor3.com (含端口通配), 避免新增子域反复重装外壳
2026-10-30 17:17:10 +08:00
jif2.zhang 82c4ae8a84 fix(项目设置): 修复「隐藏的任务自动归档」提示成功但不生效
Docker Build and Push / build-image (push) Successful in 1m34s
现象: 项目设置 -> 归档设置 -> 隐藏的任务自动归档, 选完档位提示「已保存」,
重新打开仍是「永不」, 且不会产生任何归档。

根因 (读写两条路都断了, 缺一不可):
1. 读: projectSummary() 没有回传 autoArchive, 而 ProjectManage 读的是
   project.autoArchive -> 永远是 undefined -> 界面恒显示默认值「永不」;
2. 写: projectApi.update() 的字段白名单里没有 autoArchive 分支
   (只有 name/code/taskKinds/icon/archived/notes), 传进来直接被忽略,
   data 为空 -> 一条 SQL 都不执行 -> 但函数正常返回, 调用方照常弹「已保存」。

服务端一直是对的: powersync 上传分支处理了 autoArchive, REST 也有校验,
所以不是同步丢字段。

修复:
- projectSummary 增加 autoArchive: p.autoArchive || 'never';
- projectApi.update 增加 autoArchive 分支, 校验取值与 AUTO_ARCHIVE_OPTIONS 同口径。

验证 (真实 Windows 外壳, 临时暴露 API 直调被修改的函数):
- update({autoArchive:'3d'}) 返回 3d; 重新 list/detail 仍为 3d (已落库);
- 进项目设置, 该行显示「一周后 ›」(设成 1w 后), 不再恒为「永不」;
- 还原为 never 后界面同步回「永不」。
- 顺带确认「同样隐藏已完成」的列级设置 retention 读写都正常:
  Done 列 retention=3d, 服务端 26 条已完成中 9 条超 3 天,
  App 实际只返回 14 条 -> 过滤本身是生效的, 此前不生效的只有自动归档档位。
- 控制台无异常; 验证用的临时钩子已移除。
2026-10-28 18:32:40 +08:00
jif2.zhang c5fc328f2b feat(任务/打卡): 已归档页面按原型重做; 打卡日历完成度改 4 档配色
Docker Build and Push / build-image (push) Successful in 1m27s
一、已归档任务管理: 按评审通过的原型落地 (docs/V0.2/任务模块/已归档任务管理-原型.html)

- 去掉「任务类型」过滤: 类型是弱维度, 归档场景几乎不按它检索, 反而占掉一整行;
  类型标签仍在行内展示。
- 关键词输入框前加常驻标签「标题」, 框内占位改为「输入关键词」。
- 归档时间新增快捷项: 全部 / 本周 / 本月 / 本季度 / 自定义;
  选中后下方灰字回显实际区间 (本周=周一起算, 区间截至今天, 不跨未来);
  「自定义」才展开两个日期选择器。
- 补上「筛选」按钮 (带生效条件数徽标) 与「重置」: 条件先编辑, 点按钮统一提交。
  原实现改任一条件就即时重查, 连点会连发请求, 输关键词时列表会闪。
- 列表上方显示已应用条件标签 (可单独 ✕ 撤销) + 命中条数。
- 列表行重排: 标题独占一行, 元信息收成一行 (#编号 · 看板列 · 归档于日期),
  「恢复 / 删除」由右侧竖排改到底部右侧横排, 降低行高。
- 空状态区分「这个项目还没有归档任务」(提示去 ⋯ 菜单归档) 与
  「没有符合条件的归档任务」(建议放宽条件)。
- 修正日期边界: archivedList 的 from/to 原来直接拼 'T00:00:00Z',
  本地选的日期会偏移 8 小时, 当天凌晨的归档被漏掉; 改为按本地时区换算成 UTC 瞬时。

二、打卡日历完成度配色: 由 3 档改 4 档

    小半 (ratio < 0.5)  -> 红 #ff3141
    大半 (0.5 <= r < 1) -> 橘 #ff8f1f
    完成 (r = 1)        -> 蓝 #1677ff
    超额                -> 绿 #00b578

  注意服务端统计时把 actual 封顶在 expected (`Math.min(cnt, exp)`),
  所以 ratio 永远 <= 1,**超额只能靠 status === 'over' 判断**,不能看比例;
  DayRing 内新增 tierOf() 统一分级,图例同步改为四档。

验证 (真实 Windows 外壳 + CDP):
- 打卡页: 图例显示「小半/大半/完成/超额」, 计算样式分别为
  rgb(255,49,65) / rgb(255,143,31) / rgb(22,119,255) / rgb(0,181,120);
  当月实际渲染出 low=5 / high=1 / over=1。
- 已归档页: 筛选行标签为 标题/看板列/归档时间; 时间选项五项齐全;
  按钮为 重置/筛选; 点「本周」回显 2026-10-26 ~ 2026-10-28, 点「筛选」后
  徽标显示 1、条件标签可撤销、命中条数正确。
- 归档 → 已归档列表可见 → 恢复 全链路走通 (含上次修好的 TDZ 问题),
  服务端确认 archived 由 t 变回 f。
- 控制台无异常。
2026-10-28 10:19:17 +08:00
jif2.zhang 77e057c580 docs(任务): 已归档任务管理界面重设计原型 (待评审, 未改业务代码)
Docker Build and Push / build-image (push) Successful in 1m29s
按评审意见重做「已归档任务」全屏弹层的界面, 产出可交互原型:
docs/V0.2/任务模块/已归档任务管理-原型.html

四点改动:
1. 去掉「类型」过滤 —— 类型是弱维度, 归档场景几乎不按类型检索, 反而占掉一整行;
2. 关键词输入框前加常驻标签「标题」, 框内占位改为「输入关键词」;
3. 归档时间新增快捷预设: 全部 / 本周 / 本月 / 本季度 (+ 自定义才展开日期区间),
   选完在下方灰字回显实际区间, 避免"以为选的是整周";
4. 补上「筛选」按钮 —— 原版改任何条件都立即查询且没有确认动作; 改为条件填完一次性提交,
   按钮带生效条件数徽标, 另有「重置」。

附带改进:
- 列表上方显示已应用条件标签(可单独 × 撤销) + 命中条数;
- 列表行重排: 标题独占一行, 元信息收成一行(#编号 · 看板列 · 归档于日期),
  「恢复/删除」从右侧竖排改到底部右侧横排, 降低行高;
- 空状态区分「还没归档过」与「筛选无结果」, 分别给出下一步提示;
- 类型标签仍在行内展示, 只是不再作为筛选项。

原型为单文件 HTML (无外部依赖), 可交互: 点预设/筛选/恢复/删除均有反馈。
已用无头浏览器验证: 无 JS 错误, 本周预设算出 2026-10-26~2026-10-28, 筛选命中条数正确。

业务代码未改动, 等待评审确认后再落地。
2026-10-28 09:51:00 +08:00
jif2.zhang 75cabe8339 fix(任务): 修复归档/恢复归档报 "Cannot access 'a' before initialization"
根因: api/task.js 的 archive / unarchive 里写了 `const now = now()`。
同一函数作用域内的 `const now` 会遮蔽模块级的 now 函数, 而初始化表达式的右侧
在求值时该 const 正处于 TDZ, 于是抛 ReferenceError。
源码里报错信息是 "Cannot access 'now' before initialization",
经 vite 压缩后变量被重命名为单字母, 用户端看到的就是 "Cannt access 'a' before i"。

影响: 点「归档」确认后必然失败 (弹错误框); 恢复归档同理。删除/编辑不受影响。

修复: 局部变量改名为 ts, 并补注释避免后人再写回同名。

验证 (真实 Windows 外壳 + CDP 走 UI):
- 点任务行 ⋯ → 归档 → 确认: 无异常, 行从列表消失, 任务数减少;
- 服务端确认已落库: archived=t / archivedAt / archiveSource=manual;
- 测试数据已还原 (archived=f)。

顺带全仓搜索 `const X = X()` 自遮蔽写法, 仅这两处, 无其它同类隐患。
2026-10-28 09:45:43 +08:00
jif2.zhang 58879024a2 docs+feat(即时同步): 实测端到端 1~2s 达标; 补回滚开关; 订正两次错误排查结论
Docker Build and Push / build-image (push) Successful in 1m26s
一、结论订正 (第二次, 前两次均被测量方式误导)

初次把 ~14s 延迟归因于 nginx 未关 proxy_buffering —— 实测被推翻:
加了 proxy_buffering off / proxy_read_timeout 1h 并热重载, nginx -T 确认生效, 延迟毫无变化。
(该配置已回滚: advanced_config 清空、生效 conf 还原、nginx 已重载。保留它反而会让 OTA 大包
 下载失去缓冲保护, 属净负收益。)

随后把 ~15-30s 延迟归因于客户端 db.watch/渲染滞后 —— 同样被推翻:
错误在于测量方法, 用 CDP 反复读 document.body.innerText (长列表页开销大) 去轮询,
且把 ssh + docker exec 的 ~6s 开销算进了延迟。

改为**页面内自计时**后 (50ms 轮询本地库 + MutationObserver 观察 DOM, 各自记录 performance.now),
一次干净测量的结果:
- psql 命令总耗时 (ssh + docker exec + UPDATE): 2138ms
- 数据出现在客户端本地库: 2251ms  (≈ 命令返回后 113ms)
- 界面更新: 2580ms
- 同步数据到达 -> 渲染完成: 329ms

即端到端约 1~2s, 其中 UI 渲染只占 ~0.33s, useDbSignal 的表级 watch 工作正常,
无需改用 useQuery。服务端侧的 powersync 日志也一致 (checkpoint_complete 与改库同一秒,
stream_ms 达 42 分钟、close_reason: client closing stream, 长连接稳定)。

二、补回滚开关

文档验收标准里的"回滚开关"此前并未实现。useDbSignal 现在读取
localStorage['iboard_realtime'] === '0' 时完全不订阅, 退回手动刷新,
用于线上出问题时的快速止损 (重启应用生效)。

三、文档

§7.0 改为"端到端约 1~2s (达标)", 写入可复现的页面内自计时测量方法,
并留档两次被推翻的结论及各自错因, 避免后人重复踩坑。§6.2 验收标准同步更新。
2026-10-27 22:20:53 +08:00
jif2.zhang 96f4e09bd5 docs(即时同步): 订正延迟排查结论 —— 不是 nginx 缓冲, 瓶颈在客户端应用/渲染段
Docker Build and Push / build-image (push) Successful in 1m28s
上一版文档把 ~14s 延迟归因于 nginx 未关闭 proxy_buffering, 实测推翻了该判断:
- 已为 iboard.honor3.com 配置 proxy_buffering off / proxy_read_timeout 1h 并热重载,
  nginx -T 确认生效, 但端到端延迟无任何改善;
- powersync 服务日志 (docker logs -t): 改库与服务端 checkpoint_complete 几乎同一秒
  (13:46:53.738), 另一次约 6s —— 服务端推送很快;
- 同步流是长连接 (stream_ms 达 42 分钟, close_reason: client closing stream), 非频繁重连;
- 临时把 db 挂到 window.__psdb 轮询: 数据确实已写入客户端本地库, 但 DOM 更新明显滞后。

结论修正为: 瓶颈在客户端「同步数据应用 → db.watch 通知 → React 渲染」这一段,
延迟 15~30s 且波动 (1s/6s/17s/27s), 更像排队或重试退避。文档补充了下一步排查方向。

nginx 那组配置未回滚: 对长连接流式接口属合理配置, 且已验证不影响其它站点。
2026-10-27 21:54:20 +08:00
jif2.zhang fd4079c9c3 feat(任务): 即时同步 —— 订阅本地库变更, 跨端改动无需切页自动刷新
Docker Build and Push / build-image (push) Failing after 1m48s
现象: 多端同时打开同一项目时, 一端改完, 另一端必须切到别的页面再切回来才更新。

根因: 不是同步没到, 而是 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
jif2.zhang 86335dfaa4 fix(离线库): 关闭 PowerSync 多标签共享 worker —— 修复 Windows 升级后任务页永久「加载中」
Docker Build and Push / build-image (push) Successful in 1m30s
现象: Windows 外壳 OTA 升级后, 任务页一直「加载中」; 其它不访问本地库的页面正常。
Android 正常。仅「原地升级/reload 之后」出现, 全新启动进程则正常。

根因: PowerSync Web 的 enableMultiTabs 默认在桌面端开启 (有 SharedWorker 时),
开启后会额外用 **共享 IndexedDB worker** 承载本地库。该 SharedWorker 跨页面 reload
存活, OTA applyUpdate 后 location.reload() 的新页面连上去时, 本地库读取永久挂起
—— 无异常、纯 pending, 所以 TaskPage 的 projects 一直是 null, 停在「加载中…」。
Android WebView 不支持 SharedWorker, 走独立 worker (随页面销毁), 因此不受影响。

实测 (真实 Windows 外壳 + WebView2 CDP):
- 修复前: 目标页 /me 正常; Page.reload 后落到 /task 显示「加载中…」, 再导航仍卡住;
  shared_worker 目标仍在 (worker-*.js 前后同 URL)。
- 修复后: reload 后 /task 正常渲染 25 个任务; shared_worker 目标消失;
  控制台仅剩 PowerSync「未启用多标签」提示。

修复: database.enableMultiTabs = false。本应用是单窗口外壳 (dock.html 不访问本地库),
不需要多标签同步; 该选项关闭后本地库改用随页面销毁的独立 worker, reload 干净。

注: 该问题与 OTA 包内容无关 (同一个包全新启动正常), 属 reload 路径的运行时缺陷。
2026-10-27 19:56:25 +08:00
jif2.zhang a23b10959c fix(同步): 上传路径补上 Task.status 与 Project.icon —— 此前静默丢弃
Docker Build and Push / build-image (push) Successful in 1m35s
现象: 任务点完成后被自动移入 Done 分组, 但详情页状态仍是「未开始」。

根因: /api/powersync/upload 的 **更新分支** 漏了 status 字段 ——
Task 更新分支逐个处理了 title/description/kind/markDate/extras/sortOrder/
assigneeId/doneAt/archived*/columnId/parentId, 唯独没有 d.status
(离线新建分支是处理了的, 所以只有"改已有任务"这条路丢状态)。
客户端 api/task.js 本地写入时 status 与 columnId 是一起算、一起写的
(改状态会按列绑定归位到 Done 列), connector 上行回读整行, 所以 payload 里
status 一定存在 —— 是服务端 prisma.task.update 时没把它放进 data。

结果: 列变更落地、状态被丢弃; 下行同步以服务端为权威, 又把本地 status 覆盖
回去, 于是"看板在 Done、详情未开始"。

数据实证 (生产 PG): 9 条顶层任务处于 Done 列 (boundStatus=done) 但 status
非 done (8 条 todo + 1 条 doing), 且全部 doneAt 有值; 反向 (status=done 却
不在 done 列) 0 条 —— 单向故障, 排除下行覆盖/并发冲突。这 9 条的 columnId 与
doneAt 在同一个 update 里都写成功了, 唯独 status 没有, 只可能是它压根没进 data。

修复 (稳妥方案, 复用唯一口径):
- resolveStatus.js 新增 resolveTaskUpload(): 客户端上行的是**本地整行**, 字段
  总是存在, 不能用"字段是否存在"判断变更, 必须先与服务端现值比较, 再交给
  resolveTaskWrite 裁决。不这样做会踩 T-BD-16: 任务从 Plan 拖到未绑定状态的
  Action 列时, 整行里 status 仍是 todo, 若当作"本次变更"提交, 场景 B 会把任务
  从 Action 归位回 Plan (拖过去又被弹回来)。
- powersync.js Task 更新分支改为调用 resolveTaskUpload, 并把 task.column 与
  columnId 一起指向目标列 (否则场景 B 会拿旧列判断, 同样误归位);
  doneAt 与最终状态保持一致 (进入 done 记时 / 离开 done 清空, 口径同 REST)。
- Project 更新分支补 icon (客户端本地写了 icon, 服务端同样丢弃), 校验口径与
  REST 写接口一致: 必须 data:image/ 前缀, 长度 <= 300_000。
- 上传处理器新增按项目缓存的 loadColumns (ops 已按表序排列, BoardColumn 先于
  Task 落地, 故 Task 阶段读到的列是本批次最终结果)。

用例 (10 项, 含关键回归与旧思路对照):
仅换列到未绑定列保持列与状态 / 换列到绑定列状态跟随 / 仅改状态归位 /
无变化返回 null / 脏数据自愈 / 子任务豁免 / 无匹配列不移动 /
对照证明旧思路会把 Action 里的任务弹回 Plan。

存量脏数据 (9 条) 需另行订正为 done, 不在本提交内。
2026-10-27 17:28:38 +08:00
jif2.zhang c460f09821 chore(dev): 新增 Windows 本地实时预览脚本 win:dev (不打包)
Docker Build and Push / build-image (push) Successful in 2m31s
改完前端 UI 想立刻在 Windows 外壳里看效果, 原本要 tauri build 打包。实际外壳
用 tauri-plugin-hotswap, 启动时优先读 %APPDATA%\<identifier>\hotswap\ 下的覆盖
包, 没有才回退 exe 内置 dist —— 与线上 OTA 同一条路径。因此只构建前端并写进
覆盖目录 + 重启外壳即可, 不编译 Rust、不打 NSIS/便携包、不重装。

  npm run win:dev                  构建(shell 模式) + 挂载 + 重启
  npm run win:dev -- --no-build    复用现有 dist, 只挂载+重启 (~2s)
  npm run win:dev -- --clear       卸载覆盖, 回到内置前端
  npm run win:dev -- --no-launch   只改覆盖目录, 不碰外壳进程

读插件源码得到的两个硬约束 (写错会被静默丢弃或启动即回滚), 脚本已处理:
- min_binary_version 必须 == 外壳版本: 默认 DiscardOnUpgrade 在 binary > min 时
  丢弃缓存, 而安全线又要求 binary >= min, 只能取等号;
- confirmed 必须为 true: 默认 SingleLaunch 对未确认版本启动即回滚。
另: 目录名须为 seq-<数字>; sequence 取 Date.now() 让 OTA 检查认为已是最新,
避免线上更新顶掉本地版本。

README 补「Windows 本地实时预览(不打包)」章节: 用法/原理/约束表/注意事项。
2026-10-27 15:49:39 +08:00
jif2.zhang 1d800c536e feat(任务): 详情页编号右置到副标题行、换列后自动收起、固定头尾滚动
- 完整任务编号从标题下方独立一行, 移到「看板列 · 状态」同一行右侧
  (左侧可省略号截断, 编号 flex-shrink:0 不被挤压)
- 切换看板列后 ActionSheet 未收起: 补 handler.close?.(), 且放在 onAction
  最前 (选中即收, 不等接口返回), 与项目视图 ⋯ 菜单同口径
- 滚动优化: 原整个弹层是一个滚动容器, 标题与保存按钮都会随内容滚走。
  改为三段式弹性布局 —— .td-head 固定 / .td-body 唯一滚动区 /
  .td-foot 固定 (保存按钮), 键盘弹出时底部随 kb 上抬

验证: 390x720 矮屏渲染真实 TaskDetail (14 条子任务撑出溢出), 11 项断言通过 ——
主体 scrollH 1531 > clientH 465; 滚动 1066px 到底后头部 top 86->86、
底部 top 657->657 均不变; 保存按钮 bottom 708 <= 720 始终在屏内;
头+体+底高度之和等于弹层总高 (无重叠/无空隙)。
2026-10-27 15:49:06 +08:00
jif2.zhang 0fc45aa6ec feat(打卡): 日历日期标记改为比例环, 并移除无引用死代码
日期标记 (F-CL-06): 旧实现是只按状态上色的 4px 圆点, 看不出完成度 ——
3/5 与 5/5 是同一个绿点。改为环形进度, 弧长 = 当天完成比例:
  expected = 0        -> 不画环 (当天没有应打卡项)
  expected > 0, 0 次  -> 空心环 (未开始)
  部分完成            -> 弧长按比例 (半填充)
  达标 / 超额         -> 整圈 (实心; 超额换色区分)
未来日期不画环 (计划 != 未完成); 选中日改用白环, 弧长仍表完成度;
「本月完成率」右侧补图例。

- 新增 components/checkin/DayRing.jsx (共享环形标记, 颜色交由 CSS 按视图上色)
- MergedView 日历格子接入该环
- 删除无引用死代码: pages/checkin/CalendarView.jsx (173 行) 与
  HabitView.jsx (338 行) —— V0.2.1 合并「打卡/日历」tab 后遗留, 全库扫描确认
  无任何 import/动态 import 引用; 连带清理仅服务于它们的 CSS
  (保留仍被 ListView 使用的 .habit-link)

验证: 桩 fetch 渲染真实 MergedView, 15 项断言通过 —— 空心环 / 弧长 25%·50%
精确匹配 / 整圈 / 三态配色 / 未来不画环 / 选中日白弧 60% / 图例 / 旧圆点已移除。
2026-10-27 15:48:14 +08:00
jif2.zhang 32f404ac90 fix(任务): 修复列表与看板拖拽「能触发但放不下」—— uuid 主键被 Number() 成 NaN
Docker Build and Push / build-image (push) Successful in 1m34s
0.2.1 切本地库后 Task.id / BoardColumn.id 为 uuid 字符串, 拖拽落点却用
Number(dataset.xxxId) 取值得到 NaN:
  - 看板 hitColumn 返回 NaN -> NaN 为 falsy, onDrop 永不触发; 目标列也不高亮
  - 列表 colId/taskId 为 NaN -> 组内命中与换列 findIndex 恒失败, 松手无操作
(打卡侧仍走 REST 数字 id, 故未受影响)

- 新增 src/pages/task/ids.js: idStr / sameId / datasetId, 统一按字符串比对
- useColumnDrag / useRowDrag: 落点、组内命中、换列、提交判定改用 sameId
- ProjectView: moveToColumn / reorderTask / changeStatus 改用 sameId,
  落点为空直接忽略, 不把坏 id 写进本地库
- api/task.js: update 拦截 "NaN"/"undefined" 列 id, 报「看板列无效」

验证: headless Edge + CDP 派发真实指针事件驱动两个 hook (uuid 数据),
修复前 4 条落点断言全挂, 修复后全部通过。
2026-10-27 10:06:37 +08:00
jif2.zhang 06e9a3e694 fix(任务): 编号改为 #序号展示、详情页完整编号可复制、修复详情改列报「看板列不存在」
- 列表/看板任务编号徽标改为 `#序号`(如 #18), 移除不再需要的 codePrefix 透传
- 详情页标题下新增完整任务编号胶囊 (项目编号-序号, 如 ADMIN-18), 点按复制:
  Clipboard API 优先, 非安全上下文/旧 WebView 回退 execCommand, 并提示已复制
- 修复详情页「看板列」切换报错: 0.2.1 本地库列 id 为 uuid 字符串,
  ActionSheet 的 key 恒为字符串, 旧代码 Number(key) 得到 NaN ->
  update 校验失败抛「看板列不存在」。改为字符串比对 (currentCol/勾选标记同步)
- 详情页保存后先就地替换该行再回源刷新
2026-10-27 10:06:12 +08:00
jif2.zhang 317072539d docs(0.2.1): 记录 OTA 链路打通与遗留清单实测数据
Docker Build and Push / build-image (push) Successful in 1m27s
2026-10-27 08:12:54 +08:00
jif2.zhang 08dd766886 fix(ci): OTA 包写入路径对齐 compose 挂载点 (deploy/prod/data/ota)
Docker Build and Push / build-image (push) Successful in 1m26s
2026-10-27 08:12:12 +08:00
jif2.zhang c9477b562e ci(ota): 新增 OTA 包同步步骤 —— 用 docker cp 经 bind-mount 容器写入宿主 data/ota
Docker Build and Push / build-image (push) Successful in 1m27s
2026-10-27 08:09:00 +08:00
jif2.zhang 50283269fb fix(ota): 生产 app 挂载宿主 data/ota 为 OTA 包目录 (此前镜像内为空, 线上 OTA 不可用)
Docker Build and Push / build-image (push) Successful in 1m14s
2026-10-27 08:06:05 +08:00
jif2.zhang 04bc48b6a7 fix(ota): OTA_MIN_BINARY 提到 0.2.1 —— 旧外壳 CSP 不含新域名, 拉新前端会白屏; 补充遗留清单与外壳升级策略
Docker Build and Push / build-image (push) Successful in 1m16s
2026-10-27 07:52:25 +08:00
jif2.zhang 702c8daf6c docs(0.2.1): 记录发布结果 —— master CI 10/10、Android 16/16、Windows 打包通过
Docker Build and Push / build-image (push) Successful in 1m18s
2026-10-27 07:30:30 +08:00
jif2.zhang a1c267b0d4 feat(0.2.1): 生产切换完成 —— 单端口上线 (3001 + 内网 PG/PowerSync), 迁移 MySQL 全量数据, 补 uuid 镜像触发器
Docker Build and Push / build-image (push) Successful in 2m14s
2026-10-27 07:26:47 +08:00
jif2.zhang 836035f4cd docs(0.2.1): 新增 021任务.md —— 目标/现状/卡点/待确认信息速查 2026-10-27 07:08:04 +08:00
jif2.zhang b29be62747 fix(deps): dev-0.2 同步 package-lock (tauri CLI 2.12.4), 修复镜像构建 npm ci 失败 2026-10-26 22:53:46 +08:00
jif2.zhang a9dc632986 chore(deploy): 生产 compose 增加 app 构建段 (服务器 buildKit 仅 compose 可用) 2026-10-26 22:52:27 +08:00
jif2.zhang cc2677ce06 chore(deploy): 生产 compose 支持 HOST_PORT 变量 (灰度验证时先用备用端口) 2026-10-26 22:18:31 +08:00
jif2.zhang 71dcf7e1b1 ci(0.2.1): 部署段改为调用宿主 /app/iboard-prod 部署脚本 —— 无 PG secrets 时跳过自动部署 2026-10-26 22:18:14 +08:00
jif2.zhang 30bc489b5c feat(0.2.1): 单端口生产架构 —— /sync 同域反代 (去掉 13002 独立同步端口), 新增 deploy/prod 生产编排 2026-10-26 21:57:40 +08:00
jif2.zhang 4b19d1d929 chore: artifacts 打包产物不入库 2026-10-26 21:22:57 +08:00
jif2.zhang 921f3496ea chore(0.2.1): OTA 基址切正式域名 https://iboard.honor3.com 2026-10-26 21:22:49 +08:00
jif2.zhang 7234b4b40a chore: 忽略 .esbuild-tmp 临时目录 2026-10-26 20:53:46 +08:00
jif2.zhang eef296942f feat(0.2.1): 合并 Windows 外壳 + 离线编辑, 版本 0.2.1, 正式域名 https://iboard.honor3.com 2026-10-26 20:53:38 +08:00
jif2.zhang e689e44923 Merge branch 'dev-0.2.1' into dev-0.2
# Conflicts:
#	generated/prisma/internal/class.ts
#	generated/prisma/internal/prismaNamespace.ts
#	generated/prisma/internal/prismaNamespaceBrowser.ts
#	generated/prisma/models/BoardColumn.ts
#	generated/prisma/models/Project.ts
#	generated/prisma/models/ProjectMember.ts
#	generated/prisma/models/Task.ts
#	generated/prisma/models/TaskMember.ts
#	generated/prisma/models/TaskTag.ts
#	generated/prisma/models/User.ts
#	server/routes/projects.js
#	server/routes/tasks.js
#	src/api/task.js
2026-10-26 20:46:25 +08:00
jif2.zhang fc23919a4e chore(prisma): 补齐 generated 任务模块模型 (Project/Task/BoardColumn 等 6 个), 与 schema 对齐 2026-10-26 20:42:05 +08:00
jif2.zhang f34c8c3bae feat(离线): 界面离线可看 —— GET 响应本地缓存 + 同步凭证持久化 (离线不清库) + 左上角离线徽标 (与 Windows 外壳分支同源) 2026-10-26 16:05:59 +08:00
jif2.zhang 9f16db4286 feat(0.2.1): 同步端点支持 VITE_POWERSYNC_URL 覆写 (Tauri 外壳等打包端按需指定) 2026-10-26 08:58:37 +08:00
jif2.zhang fd029ad124 fix(0.2.1): PowerSync 升级 v2 线 (web 2.5.0 + react 2.1.0 + wa-sqlite 2.0.8) —— 1.39.1 与自动装配的 wa-sqlite 1.7.2 不兼容 (缺 sqlite3_prepare_v3) 2026-10-26 02:24:49 +08:00
jif2.zhang e963140bf9 feat(0.2.1): TLS 前置 13443 —— PowerSync 网页端必须安全上下文 (navigator.locks), 自签证书按路径分流 app/sync 2026-10-26 01:59:31 +08:00
jif2.zhang 533a05832e fix(0.2.1): crypto.randomUUID 仅安全上下文可用 —— http 明文部署下用 getRandomValues 兜底生成 uuid 2026-10-26 01:52:38 +08:00
jif2.zhang 086f7aecf9 fix(0.2.1): 验证脚本依赖移出 package.json (dev-only 工具不进镜像), 锁文件同步 2026-10-26 01:42:17 +08:00
jif2.zhang 1edf57b75d fix(0.2.1): 运行时镜像 npm ci 加 --omit=optional (跳过 devOptional 的 better-sqlite3, 免 node-gyp) 2026-10-26 01:40:38 +08:00
jif2.zhang 4ce701e99a feat(0.2.1): M5 闭环验证通过 —— verify 脚本 7 步全绿 (下行/离线写/上行/定号/回填/REST确认/删除同步); 方案文档补 Sequential ID Mapping 2026-10-26 01:37:24 +08:00
jif2.zhang 8813766aff fix(0.2.1): 镜像列 Prisma 侧可空 (库级 NOT NULL, 触发器填充), 避免 create 校验拦截 2026-10-26 01:27:46 +08:00
jif2.zhang aca8a05c0f feat(0.2.1): Sequential ID Mapping —— 外键 uuid 镜像列+触发器, 同步流改 uuid 主键 (离线行回填后本地 id 稳定), 客户端零改动 2026-10-26 01:21:40 +08:00
jif2.zhang 9caaa303e5 fix(0.2.1): upload 契约对齐客户端 —— 外键列即父行 uuid (projectId/columnId/parentId/taskId); 附 Node 闭环验证脚本 2026-10-26 01:14:34 +08:00
jif2.zhang a714013e3e feat(0.2.1): M3 前端接入 —— PowerSyncProvider/登录连接/登出清库, 任务模块数据层切本地库 (projectApi/columnApi/taskApi 本地实现, 离线读写+自动上行), 任务编号徽标展示 2026-10-26 00:41:51 +08:00
jif2.zhang 1a695920ce fix(0.2.1): ETL 按目标 PG 列白名单取数 + boolean/bigint/json 类型感知转换 (现网库 schema 领先于本分支) 2026-10-26 00:29:08 +08:00
jif2.zhang 025fcc3244 feat(0.2.1): M4-b ETL —— 现网 MariaDB 只读快照导入 PG (uuid/code/number 回填, 时序表限量, 序列校准) 2026-10-26 00:27:20 +08:00
jif2.zhang 636e566657 fix(0.2.1): initdb 增加 powersync publication (服务不自建, PSYNC_S1141) 2026-10-26 00:21:00 +08:00
jif2.zhang 040d560aff fix(0.2.1): !env 为 YAML 整值标签 —— URI 由 compose 组装后整值注入 (PS_SOURCE_URI/PS_STORAGE_URI) 2026-10-26 00:19:18 +08:00
jif2.zhang a4af607c41 feat(0.2.1): M3 前端 PowerSync 基础 (schema/connector/db) + 依赖; fix storage sslmode 2026-10-26 00:07:33 +08:00
jif2.zhang a386660833 fix(0.2.1): compose build context 修正为 ../.. (仓库根) 2026-10-26 00:00:18 +08:00
jif2.zhang 034c87f955 feat(0.2.1): M4-a 部署栈 —— compose(PG逻辑复制+powersync+app:13001), sync streams(任务域6表+User), initdb, DISABLE_CRON 开关, 健康检查端口跟随 PORT 2026-10-25 23:57:51 +08:00
jif2.zhang f83deefa47 feat(V0.2): 视图切换移入项目栏 —— 紧凑段控 + 按钮收纳 (按设计稿)
Docker Build and Push / build-image (push) Successful in 1m5s
- 项目栏一行: 项目名(省略) + 列表/看板紧凑段控(vseg 26px) + 已归档/管理/+新增
- 「项目管理」文案收窄为「管理」, mini-btn 内边距收窄
- 移除原 .pv-head 大段控; 视图记忆逻辑不变 (T-VW-02/03)
2026-10-25 23:51:09 +08:00
jif2.zhang a4ab22588c feat(0.2.1): M2 服务端同步接入 —— /api/powersync token+upload 幂等上行, 项目/任务/拷贝链路接入 uuid 与编号分配 (identity 助手) 2026-10-25 23:49:09 +08:00
jif2.zhang 61d3028909 feat(0.2.1): M1 数据库迁移 PostgreSQL —— schema 切 pg, 任务域六表加 uuid, 项目 code/任务 number, db.js 切 adapter-pg, 重建 PG 基线迁移; 附实施方案文档 2026-10-25 23:38:54 +08:00
jif2.zhang 368e99a380 feat(任务模块): 任务归档 T-ARC-01~08 实现
Docker Build and Push / build-image (push) Successful in 1m27s
- 数据: Project.autoArchive + Task.archived/archivedAt/archiveSource,
  迁移 20261026000000_task_archive (两索引)
- 领域: server/task-domain/archive.js —— hiddenAtOf 与保留口径精确同源,
  autoArchiveDue 到期判定, runAutoArchiveForProject (读时惰性) +
  runAutoArchiveSweep (每小时 cron 兜底)
- 接口: POST /tasks/:id/archive|unarchive (连带子任务, 幂等);
  GET /projects/:id/tasks/archived (keyword/kind/columnId/from/to 过滤,
  archivedAt+id 游标分页); 项目 PUT 收 autoArchive (枚举校验);
  主列表/子任务/项目 taskCount 全部排除归档
- 前端: ⋯ 菜单「归档」(确认含子任务条数); 项目栏「已归档」入口 →
  ArchivedTasks 弹层 (关键词/类型/列/归档时段过滤 + 加载更多 + 恢复/删除);
  项目管理「归档设置」四档 Picker
2026-10-25 23:09:52 +08:00
jif2.zhang 39282b9633 docs(任务模块): 任务归档 T-ARC 需求与设计 (T-ARC-01~08)
- 需求文档 §3.15: 手动归档(⋯菜单+连带子任务)/自动归档(项目级档位
  3天/一周/一月/永不)/归档排除/游标分页大量查询/恢复/彻底删除/配置入口/权限
- 功能设计 §15: Project.autoArchive + Task.archived/archivedAt/archiveSource、
  归档/恢复/归档查询 API、自动归档双保险执行(读时惰性+小时cron)、
  边界处理; 覆盖自检表更新为 157 条
- 数据库设计: Project 补 notes/taskKinds/icon/autoArchive 字段,
  Task 补归档三字段与两索引, 迁移清单标注 20261026000000_task_archive 待实施
2026-10-25 22:59:08 +08:00
jif2.zhang 7f213b5c9e style(V0.2): 看板列滚动条右移进列间隙, 不再压住任务卡片右沿 2026-10-25 22:39:00 +08:00
jif2.zhang a4cc4e9871 docs: 日常操作规则 —— 主目录默认 dev-0.2, 发布才切 master 推送后即回 2026-10-25 22:33:53 +08:00
jif2.zhang 1cd5b85a4b Merge remote-tracking branch 'origin/master' into dev-0.2
Docker Build and Push / build-image (push) Successful in 1m6s
# Conflicts:
#	CHANGELOG.md
2026-10-25 21:59:52 +08:00
jif2.zhang 8203a7f893 fix(V0.2): 版本规则收尾 —— 手动 OTA 工作流对齐发布说明/commit 口径
Docker Build and Push / build-image (push) Successful in 1m1s
- ota-build.yml: notes 默认取检出 commit 的 message, --commit 写入
  manifest (与 docker-build 一致); 参数加引号防空格断裂
- docker-build.yml: 去掉 head -c 400 字节截断 (会把多字节汉字切成
  非法 UTF-8 导致 latest.json 解析失败), 截断统一由打包脚本按字符处理
- build-ota.mjs: CI 日志输出 build_id 与 commit
2026-10-25 21:40:13 +08:00
jif2.zhang 0f71323c11 docs: CHANGELOG 按程序/Android外壳/Windows外壳 三部分独立记录当前值与变更
Docker Build and Push / build-image (push) Successful in 1m2s
2026-10-25 21:36:01 +08:00
jif2.zhang 93b7c7bb0a docs(changelog): 记录 V0.2.1 —— 主要解决离线编辑与不同终端数据合并问题 2026-10-25 21:34:17 +08:00
jif2.zhang 67be8d8f7b feat(V0.2): 版本方案落地 —— 前端/外壳解绑, build 标识统一, 外壳版本展示
Docker Build and Push / build-image (push) Successful in 1m5s
- check-version: 分组校验 (前端 package.json / 外壳 tauri.conf+Cargo),
  两组不再要求相等; 分支前缀校验只针对前端版本
- build-ota: build_id 改为 YYYYMMDDHHmmss (北京时间, 与前端 BUILD_ID
  同格式); sequence 保持毫秒仅用于判新
- 我的页: 当前版本恢复 v0.2.0 · build <原始BUILD_ID>; 新版本附 OTA
  build_id; 底部替换为外壳版本 (Tauri getVersion, Web 端 —)
- README: 合并遗留 readme 全部内容并按决议重写版本管理章节;
  新增 CHANGELOG.md (PATCH 仅明确提出时自增)
2026-10-25 21:28:06 +08:00
jif2.zhang 06aa48f6ad docs: 版本方案 (V0.2) 整理稿 —— 版本构成/分支映射/OTA 口径/PATCH 记录/展示修复清单, 含待确认问题 A-E
Docker Build and Push / build-image (push) Successful in 1m7s
2026-10-25 20:46:11 +08:00
jif2.zhang 6bd39aeed2 feat(V0.2): 看板列内纵向滚动加固 + 滚动时右缘滚动条 (方案 B)
Docker Build and Push / build-image (push) Successful in 1m6s
- 列体外包 .bcol-main (relative): 叠加自绘细滚动条, 滚动时出现、
  停止约 700ms 后淡出, 宽 3px 不占卡片空间
- bcol-body 补 -webkit-overflow-scrolling/overscroll-behavior,
  高度链显式化 (height:100%), 修真机列内滚不动的问题
- 列头保持固定不随滚动
2026-10-25 18:57:35 +08:00
jif2.zhang fd66c529c2 fix(V0.2): 智能截断去尾部标点; 看板左列收窄; 项目头像三处统一
Docker Build and Push / build-image (push) Successful in 1m7s
- splitTitle: 截断后用 \p{P} 去掉标题尾部连续分隔符 (原字符类重组有
  转义坑, 换 Unicode 属性类)
- 看板卡片左列 (状态点+⋯) 收窄至 14px 并负外距悬进卡片内边距
- 项目头像统一: 新增 .pavatar 基类与 projectIcon.colorOfProject,
  项目管理预览改用同配色/同圆角/居中首字, 修复灰底与文字歪斜
2026-10-25 18:38:16 +08:00
jif2.zhang cee21a39d9 feat(V0.2): 快速添加输入框三行/失焦收起; ⋯菜单点选即收; 项目图标可设图片
Docker Build and Push / build-image (push) Successful in 1m22s
- 快速添加改 3 行 textarea, Enter 创建/Shift+Enter 换行/Esc 关闭,
  焦点离开且无内容时自动收起
- 任务 ⋯ ActionSheet 点选动作后 handler.close() 收回菜单
- Project.icon (TEXT data URL, 迁移 20261025130000): 项目管理可选图片,
  客户端裁为 96x96 cover JPEG (~15KB); 项目列表/最近项目/管理页图标位
  显示图片, 保持原尺寸与圆角; 可恢复默认首字色块
2026-10-25 12:02:29 +08:00
jif2.zhang e3ac00a641 fix(V0.2): OTA 发布说明改为 commit message, sha 单独展示; 版本附构建时间
Docker Build and Push / build-image (push) Successful in 1m2s
- CI: notes 取 git log -1 的 commit message (标题+正文, 截断 400 字),
  commit sha 经 --commit 写入 manifest (不再把裸 sha 塞进发布说明)
- build-ota: 新增 --commit 参数; latest.json 增加 commit 字段
- 服务端 /ota/check 与 /manifest-web 透传 commit 与 sequence
- 我的页: 当前版本/新版本后附 seq 时间戳 (MM-dd HH:mm),
  发布说明下方新增「提交」行显示短 sha
2026-10-25 11:56:04 +08:00
jif2.zhang 74e1e7dd13 feat(V0.2): 标题30字+智能拆分 / 时间标记 MM-dd / 时间词前缀 (T-TK-14~16)
Docker Build and Push / build-image (push) Successful in 1m25s
- 标题上限收紧至 30 字 (服务端校验+详情编辑框); 快速添加输入框
  放宽到 200 字: 超长时在 30 字内分隔符处截断为标题, 完整原文进描述
- Task.markDate (DateTime, 迁移 20261025120000): 详情页 DatePicker
  选择/清除, 列表行与看板卡显示 MM-dd 小标签
- 快速添加时间词前缀: 今天/明天/后天/本周/下周/本月/下月 开头
  (后跟分隔符) 自动设为时间标记并移出标题; 本周=本周日, 本月=月末
- 需求/功能/数据库设计文档同步更新 T-TK-14~16 与 Task 表字段
2026-10-25 11:47:44 +08:00
jif2.zhang fd27242e4e fix(V0.2): 全局禁用非编辑框文字选中 + 标题类文本溢出省略
Docker Build and Push / build-image (push) Successful in 1m6s
- * 级 user-select/touch-callout 禁用, input/textarea/contenteditable 放行
- 项目列表标题、项目管理导航标题、看板列名: 单行省略
- 弹层 sheet-title 限两行截断, sheet-sub 单行省略 (任务详情 200 字标题
  不再撑爆弹层头部)
2026-10-25 10:41:29 +08:00
jif2.zhang 20cc1ccde9 feat(V0.2): 列表/看板负责人改为头像 (姓氏首字+按名稳定配色)
Docker Build and Push / build-image (push) Successful in 59s
2026-10-25 03:04:15 +08:00
jif2.zhang 9401bd2b3d refactor(V0.2): 列表行等高 (无标签保留元信息行); 看板类型标签与负责人同行
Docker Build and Push / build-image (push) Successful in 1m1s
2026-10-25 02:59:52 +08:00
jif2.zhang fb9e2350b3 feat(V0.2): 任务类型标签 —— 项目级自定义 + 任务标记 + 列表/看板展示
Docker Build and Push / build-image (push) Successful in 1m21s
- 数据: Project.taskKinds (JSON, 未配置回落默认 需求/Bug/FEATURE/优化),
  Task.kind (≤20字); 迁移 20261025030000_task_kind
- 服务: 项目 PUT 支持整体替换 taskKinds (去重/上限30/至少1个);
  任务 create/update 支持 kind (空串清空)
- 项目管理: 新增「任务类型」分区, chips 展示 + 弹层编辑 (删除/新增)
- 任务详情: 新增「类型」字段, 项目类型 chips 点选, 可清空
- 展示: 列表行左下与看板卡左下显示彩色小标签
  (需求蓝/Bug红/FEATURE紫/优化橙, 自定义类型灰)
2026-10-25 02:53:25 +08:00
jif2.zhang 2e6cc986a0 refactor(V0.2): 列表主行去掉状态圆点, 元信息行仅在有成员时显示
Docker Build and Push / build-image (push) Successful in 1m1s
2026-10-25 02:44:03 +08:00
jif2.zhang d20837f9f1 refactor(V0.2): 看板 ⋯ 沉到左下角; 列表负责人右下角, 去掉状态文字
Docker Build and Push / build-image (push) Successful in 59s
- 看板卡片左列改为上下撑满: 状态点在上, ⋯ 落到卡片左下角
- 列表行右列改为上下撑满: ⋯ 在右上, 负责人落到右下角
- 列表行第二行不再显示未开始/执行中/完成小字, 只留状态点与成员
2026-10-25 02:40:02 +08:00
jif2.zhang c0b55277a4 fix(V0.2): 列表组内排序乐观更新未重排数组, 松手后界面不动
Docker Build and Push / build-image (push) Successful in 1m0s
列表分组渲染按 tasks 数组顺序, reorderTask 此前只给任务打新
sortOrder 字段而保持原数组顺序 —— 松手后界面无任何变化, 看起来
就是组内无法排序 (数据其实已提交)。改为直接按新顺序重建数组,
不在分组内的任务 (孤儿) 追加在尾部。
2026-10-25 02:37:39 +08:00
jif2.zhang 49037371f8 refactor(V0.2): 看板卡片左列布局 / 列表负责人右移 / 把手再收窄
Docker Build and Push / build-image (push) Successful in 1m0s
- 看板卡片: 状态点与 ⋯ 竖排移到卡片左侧, 负责人+成员靠右, 标题独占中间
- 列表行: 负责人从第二行元信息移到行右侧 (⋯ 左边)
- ⠿ 把手: 宽度收到约 10px, 负外距悬进卡片内边距, 基本不占横向空间
2026-10-25 02:32:49 +08:00
jif2.zhang 2fa41c774b fix(V0.2): 拖动浮层残留蓝色背景 —— 类名与遗留 .drag-ghost 规则冲突
Docker Build and Push / build-image (push) Successful in 1m3s
globals.css 里有一条无人使用的旧 .drag-ghost 规则 (蓝色气泡,
max-width 180px, 看板早期方案遗留), 排在新增规则之后同特异性胜出,
克隆卡片容器被套上蓝色背景并被压缩到 180px 宽, 即真机上手下面的
蓝色 div。删除遗留规则; 新规则显式清零背景/内边距并禁用
tap-highlight/user-select (克隆脱离 .mv-view/.task-content 祖先后
需自带防选中)。CDP 实测拖动中容器背景已透明、宽度正常。
2026-10-25 02:28:09 +08:00
jif2.zhang 1673cbdb7d refactor(V0.2): 拖动交互收紧 —— 仅把手触发 / 缩小把手 / 去多余元素 / 打卡跨组
Docker Build and Push / build-image (push) Successful in 1m3s
- 打卡与列表: 去掉卡片整体长按拖动路径, 只有 ⠿ 把手可拖; 把手缩小
- 列表行去掉跟动的蓝色目标列徽标 (drag-pill), 占位/克隆浮层承担全部反馈
- 打卡支持跨组拖动: 落到其他周期组的目标卡 = 改周期 + 落位一次完成
  (updateHabit period + reorder), 组内排序不变
- 看板: 去掉 ⠿ 把手, 恢复长按拖动
2026-10-25 02:19:03 +08:00
jif2.zhang 5066007d9d feat(V0.2): 克隆卡片跟手浮层 + 列表组内排序 + 把手统一移到最左
Docker Build and Push / build-image (push) Successful in 1m1s
- 浮层改为激活时克隆原卡片 (DOM clone, 淡化+投影) 挂 body 顶层,
  移动直接改 transform —— 替代蓝色信息条, 所见即所拖
- 任务列表支持组内排序: 拖到本组某行上/下半显示插入指示, 松手提交
  POST /projects/:id/tasks/reorder (顶层任务按展平顺序重写 sortOrder);
  拖到其他分组仍为换列
- ⠿ 把手统一移到卡片最左: 打卡 / 列表任务行 / 看板卡片 (看板新增把手
  即按即拖路径, 与列表同款 touchstart preventDefault 双保险)
- useColumnDrag onDrop 经 ref 桥接防陈旧闭包
2026-10-25 02:07:58 +08:00
jif2.zhang 9a8313173d fix(V0.2): 浮层 Portal 修复被遮挡 + 列表落点改为手指所在分组
Docker Build and Push / build-image (push) Successful in 1m1s
- 打卡/任务浮层 createPortal 挂 document.body: 页签滑动容器 transform
  破坏 position:fixed 包含块, 浮层被压进卡片流只能看到边缘
- 任务列表落点: 弃用 80px/列步进换算 (视觉位置与换算不一致, 落点随机),
  改为 elementFromPoint 命中 [data-column-id] 分组 —— 手指在哪个分组
  松手就落到哪, 与看板行为一致; 划出分组时保持上一目标
2026-10-25 01:51:17 +08:00
jif2.zhang 83cc218c5e feat(V0.2): 拖动跟手浮层 —— 半透明副本随指针, 原位虚线占位
Docker Build and Push / build-image (push) Successful in 1m5s
- useRowDrag/useHabitDrag 暴露 ghostRef, 拖动激活与移动时直接改
  transform (绕过 React 重渲染, 保证跟手流畅)
- 打卡浮层带颜色点 + 名称; 任务列表浮层带任务名 + 实时目标列名
- 原位卡片降为低透明度虚线占位, 落点指示不变
2026-10-25 01:42:29 +08:00
jif2.zhang 215263bf2f fix(V0.2): 打卡排序闭包陈旧 + 把手拖动双保险
Docker Build and Push / build-image (push) Successful in 1m1s
- useHabitDrag: onDrop 经 ref 桥接实时闭包 —— bind/bindHandle 空依赖
  useCallback 固化了首次渲染(groups=[] 数据未加载)的落位函数, 松手
  静默早退, 顺序永远不变
- 把手路径挂非 passive touchstart preventDefault: 规范上禁止浏览器对
  整个触摸的默认处理 (含滚动), 与 touch-action:none 双保险
- 把手触区放大 (36px 高), 降低按到行体触发不可靠长按路径的概率
2026-10-25 01:31:27 +08:00
jif2.zhang 2725f00f68 fix(V0.2): 拖动可靠性重做 —— 把手入口 + 打卡排序接入实际渲染视图
Docker Build and Push / build-image (push) Successful in 1m1s
问题定位:
- 打卡页实际渲染的是 MergedView (CheckinPage 路由), 上次改的 HabitView
  是未使用的旧组件, 打包时被 tree-shake, 线上自然无任何效果
- 任务列表长按抬起后一动即脱手: touch-action:pan-y 下手指出现纵向分量,
  WebView/渲染器直接把触摸判给滚动并 pointercancel, touchmove preventDefault
  拦不住已决定的滚动 (看板可用只因该模式下页面无纵向可滚祖先)

修复:
- 新增 ⠿ 拖动把手 (touch-action:none): 浏览器按规范完全不接管该触摸,
  按下立即拖动, CDP 触摸仿真验证: 垂直拖动穿过可滚列表正常落点,
  跨组(每天/每周/每月)正确拒绝; 任务行把手拖动换列, 打卡卡片把手组内排序
- useRowDrag/useHabitDrag 抽出 armListeners, 新增 bindHandle 立即激活路径
- MergedView: 组内排序改按 sortOrder (服务端已返回), 拖动落位乐观更新 +
  POST /checkin/habits/reorder, 失败回滚; 打卡点击加拖动余波保护
- 长按路径保留为补充入口 (不可滚场景仍可用)
2026-10-25 01:21:42 +08:00
jif2.zhang b1c1b16e66 ci: minisign 静态包混多平台二进制, 试运行挑可用者, 失败回退 apt
Docker Build and Push / build-image (push) Successful in 3m31s
2026-10-25 00:56:35 +08:00
jif2.zhang 51256335ba ci: 优化构建速度与资源消耗
Docker Build and Push / build-image (push) Failing after 39s
- docker-build: 停用 docker system prune --volumes (保住 build 层缓存), 改为 image prune -f
- 新增 .dockerignore, node_modules/.git/src-tauri 等不再进 build context
- 前端只构建一次: CI 构建 dist 供 OTA 与镜像复用 (PREBUILT_DIST 构建参数)
- Dockerfile: npm install → npm ci + BuildKit cache mount
- setup-node 开启 npm 缓存; master/android-* 加 concurrency 取消旧 run
- android: 只构建 aarch64; tauri target 缓存 key 按 Cargo.lock hash
- minisign 改为静态二进制下载, 去掉 apt-get
2026-10-25 00:48:50 +08:00
jif2.zhang 20a5105c23 refactor(V0.2): 详情页精简 + 列表行样式调整
Docker Build and Push / build-image (push) Successful in 4m20s
- 任务详情: 移除底部「拷贝到其他项目 / 删除任务」按钮 (⋯ 菜单仍保留入口), 清理相关死代码
- 列表子任务: 改为与普通任务同构的两行布局 (标题 + 左侧状态点/状态文字, 字体略小)
- 列表任务卡片第二行不再显示看板分组标签
2026-10-25 00:23:01 +08:00
jif2.zhang 9467d4c242 feat(V0.2): 列表拖动修复 + 打卡项组内拖动排序
- 任务列表/看板拖动: 激活后用非 passive touchmove preventDefault 拦截浏览器滚动接管, 修复列表长按抬起后 pointercancel 导致无法真正拖动
- 打卡: 列表按周期分组展示(每天/每周/每月/每年), 长按拖动组内排序(上下落点指示), 新增 POST /checkin/habits/reorder 重写 sortOrder; 新建项排到末尾
- 打卡卡片加 touch-action/user-select 防长按误触; 拖动结束 350ms 内抑制点击余波
2026-10-25 00:06:44 +08:00
jif2.zhang d6ca7573f3 fix(V0.2): 三轮反馈 5 项 —— 回滚键盘方案 / 列表卡片拖动 / 看板防误触 / 横滑整屏 / 详情页恢复
Docker Build and Push / build-image (push) Successful in 4m9s
1. 回滚键盘遮挡统一方案 (真机测试不通过, 需求搁置): 恢复 TaskDetail/
   UserPicker/CopyTaskPopup/ProjectManage 与 useKeyboardInset 到 9fea1a5^,
   删除 useKeyboardInsetWatcher / .kb-safe / interactive-widget=resizes-content
2. 任务详情页样式随 1 一并恢复提交前状态 (含底部拷贝/删除按钮)
3. 列表视图换列改为卡片拖动: 新 useRowDrag (长按 400ms 行抬起, 左右
   80px/列步进, 行尾徽标显示目标列, 松手落列), 移除长按 ActionSheet
   —— 旧长按与复选框/行点击互相误触, 抬手合成 click 落在控件上触发状态切换
4. 看板拖动防误触: 任务区禁长按文字选择与系统菜单 (输入框除外);
   pointercancel 只放弃不提交; 拖动结束 350ms 内抑制 click 误开详情;
   状态点/⋯/子任务行 pointerdown 不冒泡, 不发起拖动
5. 看板横滑扩展到整屏: .page-transition 补 height:100% 打通
   app-main → page-with-navbar 百分比链, board-wrap 撑满剩余高度

已验证: kb-safe/Watcher/onLongPress 残留引用清零, 逐文件 diff 审阅通过;
本机无 Node 运行时, 未跑 vite build
2026-10-24 23:24:31 +08:00
jif2.zhang 9fea1a5a3a fix(V0.2): 二轮评审 6 项 —— BUILD_ID 格式 / 菜单与详情精简 / 拖动重做 / 键盘统一方案
Docker Build and Push / build-image (push) Successful in 4m5s
1. BUILD_ID 改为 yyyyMMddHHmmss (去掉中间点), 仍 UTC+8
2. 任务 ⋯ 菜单去掉顶部任务名标题
3. 任务详情去掉底部「拷贝到其他项目」「删除任务」按钮 (⋯ 菜单已有)
4+6. 键盘遮挡统一方案 (重点设计 §7.2):
   - index.html 加 interactive-widget=resizes-content
   - App 级 useKeyboardInsetWatcher 写 --kb-inset (visualViewport+window 双源)
   - 底部弹层统一 .kb-safe 类 (安全区+20px+键盘插入量, dvh 收缩+内部滚动)
   - TaskDetail/UserPicker/CopyTaskPopup/信息块弹层全部迁移, 删除 per-component hook
   - 信息块 textarea 补 focus scrollIntoView 兜底
5. 拖动方案重做 (重点设计 §7.1):
   - 新 useColumnDrag: Pointer Events + setPointerCapture, 长按400ms原位抬起,
     elementFromPoint 高亮目标列, 边缘 rAF 自动横滚; 移除手指跟随浮层(任务名)
   - 列表行长按 500ms → 「移动到列」ActionSheet (不做自由拖动)
   - 移除「拖到卡片转子任务」误触路径, 统一走详情页层级区
   - 删除旧 useLongPressDrag

已验证: 99 文件语法 0 错误、导入 0 缺失
2026-10-24 22:15:52 +08:00
jif2.zhang 506db0795b feat(V0.2): 任务操作菜单 / 状态指示 / 移动与转子任务 / 移除扩展信息
Docker Build and Push / build-image (push) Successful in 4m13s
1. 任务 ⋯ 菜单 (列表行与看板卡片): 编辑 / 拷贝到其他项目 / 删除
   (antd-mobile ActionSheet, 带任务名标题; 拷贝抽出为 CopyTaskPopup 复用)
2. 点击任务非按钮区域 = 打开编辑页 (原本即如此, ⋯ 不再误触为编辑)
3. 修复状态选择器堆成三行: 此前 .seg 只有 .pv-head 作用域样式,
   详情弹层裸用导致纵向堆叠; 新增通用 .seg 分段样式, 当前项高亮点按即切
4. 移除任务的扩展信息 UI (T-TK-04~07 标注已取消, 决策 35):
   ExtrasCard 组件删除, DB extras 字段保留不再使用;
   项目相关长文本由项目信息块 (T-PJ-13) 承载
5. 任务移动入口: 详情页新增「看板列」ActionSheet 换列(联动裁决不变)
   与「层级」区(转为子任务=选父任务 / 转为顶层), 覆盖无法拖拽的场景
6. 列表页移除「+ 子任务」入口: 添加子任务只在详情页, 或经转子任务

已验证: 99 文件语法 0 错误、导入 0 缺失、文档 149 条/决策 35 连续
2026-10-24 21:51:26 +08:00
jif2.zhang b71ceca3f2 fix(V0.2): BUILD_ID 时区 / 新建弹框残留 / 项目信息块
Docker Build and Push / build-image (push) Successful in 4m19s
1. BUILD_ID 改用 UTC+8 生成: CI runner 是 UTC 时区, 原来取本地时间会错位;
   统一偏移 +8h 后按 UTC 读取 (vite.config.js makeBuildId)

2. 修复「我的项目」页反复弹新建框: createSignal 一次性消费,
   ProjectList 处理后回调清零 (此前 TaskPage 状态在 /task 与 /task/list
   间保留, 信号残留导致每次进入都触发)

3. 新增项目信息块 (T-PJ-13):
   - Project.notes Json ([{title,content}], <=50 块 / 标题<=50 字 / 内容<=20000 字)
   - 迁移 20261024130000: 加列(NULL)->回填'[]'->收紧 NOT NULL, 存量行安全
   - GET 详情返回 notes; PUT 接受 notes 整体替换
   - 项目管理新增「信息块」分区: 列表预览 + 底部弹层独立编辑(标题+多行
     textarea+删除), 键盘弹出复用 useKeyboardInset 抬升
   - 文档: 需求 T-PJ-13 (149 条) + 功能设计对应行

已验证: 109 文件语法 0 错误、文档 149/149 覆盖
2026-10-24 21:37:05 +08:00
jif2.zhang 1fd7522fa6 fix(V0.2): 任务模块 4 个 bug —— 覆盖层安全区/返回键/项目列表排序/键盘遮挡
Docker Build and Push / build-image (push) Successful in 4m17s
1. 项目管理等 cs-page 覆盖层与系统状态栏重合:
   .cs-page 补 padding-top: var(--safe-top) (复用 main.jsx 实测校准的变量)

2. 项目管理按返回键跑到上一个 Tab:
   新增 useOverlayBack —— 覆盖层打开时 pushState 哨兵, 返回键先关覆盖层
   应用于 项目管理(内层视图先回主区) 与 任务详情弹层

3. 更多项目页:
   - 头部补「+ 新建」入口
   - 项目列表支持长按拖动排序(纵向), 顺序存设备端, 与首屏最近项目共用 recentOrder
   - 顺带修复首屏 RecentProjects 拖动排序的闭包缺陷(监听器读到旧下标, 首次移动
     会 splice(-1)); 两处均改为 ref 取当前下标

4. 任务详情输入子任务被键盘遮挡:
   新增 useKeyboardInset (visualViewport 差值), 键盘弹出时抬升弹层内容并收缩
   maxHeight; 输入框聚焦后再 scrollIntoView 到可视区中部 (双保险)

已验证: 99 文件语法 0 错误、相对导入 0 缺失
2026-10-24 21:09:19 +08:00
jif2.zhang 0cdbfb434f feat(V0.2): 任务模块界面调整 —— 首屏/项目管理/底部安全距离
Docker Build and Push / build-image (push) Successful in 4m8s
按 2026-10-24 评审的 5 条调整实现 (需求 T-VW-08~10 / T-PJ-11/12 / T-UI-11):

首屏 (T-VW-08/09):
- /task 改为「上=最近项目条(3 个+更多入口), 下=当前项目任务管理」
- 完整项目列表迁至 /task/list, 从「更多项目」进入; /task/:pid 保留供深链

最近项目排序 (T-VW-10):
- 卡片长按 300ms 拖动换位, 结果存设备端 prefs.recentOrder
- 顺序 = 手动排序 > 最近访问(recentIds) > 列表序, 归档项目不展示

项目管理 (T-PJ-11/12):
- 新增 ProjectManage: 成员 / 看板列 / 项目信息 三分区统一入口
- 项目信息含 改名 / 归档切换 / 删除(个人项目隐藏) / 退出(仅创建人)
- 项目页头部只留「项目管理 + 新增」, 移除散置的成员/列设置按钮

底部安全距离 (T-UI-11):
- 任务详情 / 人员选择 / 拷贝弹层 padding-bottom = 安全区 + 20px
- 小屏下弹层 maxHeight + 内部滚动, 底部按钮完整可见

已验证: 97 文件语法 0 错误、相对导入 0 缺失
2026-10-24 20:32:44 +08:00
jif2.zhang 6153b9db99 fix(V0.2): 清除 schema.prisma 中段 BOM, 修复 CI prisma generate 失败
Docker Build and Push / build-image (push) Successful in 4m24s
追加任务模块模型时临时文件带入 U+FEFF, Prisma 解析在 383 行报 Validation Error (run 200)
2026-10-24 17:43:30 +08:00
jif2.zhang 6c9cd3fd99 feat(V0.2): 任务模块 —— 项目 / 列表与看板 / 列-状态绑定 / 一级子任务
Docker Build and Push / build-image (push) Failing after 3m51s
需求 142 条 (T-*), 设计见 docs/V0.2/任务模块/

数据层:
- 新增 6 张表: Project / ProjectMember / BoardColumn / Task / TaskMember / TaskTag
- 看板列是实体(可增删改排序), Task.columnId 外键 RESTRICT 防误删列丢任务
- Task.status(todo/doing/done) 与 columnId 双落库, 二者正交

领域层 server/task-domain/ (唯一口径, 前后端不重复实现):
- resolveStatus: 列决定状态(只看目标列, 无拖出回退); 状态与列不匹配时按列序取第一个绑定该状态的列自动归位, 无匹配列则不移动; 子任务完全豁免
- retention: 逐列 6 档保留窗口(永不显示/当天/3天/本周/本月/永久显示), 以 doneAt 判定
- copyTask: 跨项目深拷贝, 人员按目标项目成员过滤并回报丢弃名单

接口: /api/projects, /api/columns, /api/tasks (均校验项目成员)

前端:
- 底部 Tab 插入「任务」, 位于「打卡」之后; 默认首页仍为打卡
- 列表视图(子任务缩进+折叠) / 看板视图(长按拖拽换列、拖到卡片降为子任务)
- 列设置(列管理+状态绑定+保留选项) / 任务详情(扩展信息卡片默认收起) / 成员管理
- 视图记忆与上次项目存设备端

已验证: 95 文件语法 0 错误、相对导入 0 缺失、领域逻辑用例全部通过
2026-10-24 11:21:16 +08:00
jif2.zhang 42acf72f1e 合并 dev-0.2 到 master: 修复打卡按钮点不动
Docker Build and Push / build-image (push) Successful in 4m9s
2026-10-24 09:53:14 +08:00
jif2.zhang e57b7902be 修复打卡页所有打卡/再打一次按钮点不动
回归原因: 合并视图引入 doCheck 时加了 `if (!item.canCheck || isFuture) return`.
但 canCheck 字段**只有 /day 接口返回**, 本视图数据取自 /habits, 该接口
不返回 canCheck → item.canCheck === undefined → !undefined === true
→ 每个按钮点击都在第一行直接 return, 表现为"点不动".

修复: doCheck 只按 isFuture 拦截, 与后端 POST /habits/:id/check 的校验
口径一致 (后端只拒绝未来日期与超过补打窗口的日期).

影响范围: 打卡按钮、再打一次按钮、环形点击 (三处共用 doCheck).
2026-10-24 09:53:08 +08:00
jif2.zhang 6a48b7311b 合并 dev-0.2 到 master: 打卡页新建按钮调整
Docker Build and Push / build-image (push) Successful in 4m4s
2026-10-24 08:40:15 +08:00
jif2.zhang 7e229ecc84 打卡页: 「点环即打卡」提示换成新建按钮, 去掉底部歪掉的按钮
问题:
- 当日栏右侧的「点环即打卡」是纯文字提示, 看着像可点但实际无交互, 容易误导
- 分组列表底部另有一个「+ 新建打卡项」按钮, 带内联 margin,
  与卡片左右边距不一致, 视觉上是歪的

改动:
- 当日栏右侧改为「+ 新建」按钮 (antd-mobile Button, mini) 右对齐,
  复用原有 openCreate, 一个入口完成新建
- 删除底部「+ 新建打卡项」按钮及其 .mv-add-wrap 样式
- .mv-daybar 移除废弃的 .sum 样式, 新增 .mv-new-btn (margin-left:auto 右对齐)

空态「创建第一个打卡项」保留 (无任何打卡项时的引导)。
2026-10-24 08:40:09 +08:00
jif2.zhang 3250b94bc6 合并 dev-0.2 到 master: 打卡与日历合并视图 (按天/周/月分组)
Docker Build and Push / build-image (push) Successful in 4m1s
2026-10-24 08:30:53 +08:00
jif2.zhang 330e6585d9 feat(V0.2): 打卡与日历合并为单视图, 打卡内容按 每天/每周/每月 分组
实现设计文档 §11.5「日历与打卡合并」待确认项 (界面设计.html 2.12 ②).

结构:
- CheckinPage 由 3 个 tab 收敛为 2 个: 打卡(含日历) / 清单
- 新增 MergedView.jsx: 上为月历, 下为当日打卡项
  (原 CalendarView + HabitView 的展示/交互合并到一个组件)

按规格落地:
- 月历: 月份切换 + 回到今天; 去掉「昨天/今天/明天」分段; 日期格 38→31px
- 分组: 每天 → 每周 → 每月, 组标题左侧 3px 主色竖条 + 右侧项数; 去掉「每年」分组
  (若存在历史 year 项则归入"其它周期"分组, 避免数据凭空消失)
- 卡片紧凑化: 内边距 12→8px, 环形 58→42px (环宽 6→5), 间距 10→7px
- 操作: 撤销 / 编辑 / 归档 由卡片底部三个文字链接收进「⋯」底部面板
- 联动: 点日期即切换当日数据; 未来日期只读, 按钮灰显「计划」
- 组内仍按「未完成在前、已完成在后」排序 (F-CK-16)

样式: globals.css 新增 .mv-* 段 (月历/分组/紧凑卡片/操作面板), 数值与设计稿一致.

服务端无需改动: habits 接口已返回 period/periodLabel/expectedPerDay/canCheck 等字段.
2026-10-24 08:30:48 +08:00
jif2.zhang 99cedce4ed 修复「我」页顶部安全区与蓝色渐变头部割裂
现象: 顶部安全区修好后, 其他页面正常, 但「我」页顶部多出一条白色/色块,
把原本从屏幕顶端开始的蓝色渐变隔断.

原因: .app-safe-top 是独立占位元素, 固定使用页面底色.
「我」页头部 me-profile 是通栏蓝色渐变 (无 PageHeader), 安全区夹在中间
就形成一条异色带, 视觉上把蓝色切成两段.

修复:
- MePage 挂载时给 body 加 me-safe-top 标记, 卸载时移除
- 该标记下把 .app-safe-top 高度归零, 改由 .me-profile 的
  padding-top: calc(24px + var(--safe-top)) 吸收状态栏高度
- 这样蓝色渐变直接从屏幕最顶端开始, 是一整块连续色域, 无接缝,
  同时内容仍正确避开电量/信号栏

其他页面不受影响 (无该 body 标记, 安全区照常占位).
2026-10-24 08:15:09 +08:00
jif2.zhang a150706cc4 修复「我」页顶部安全区与蓝色渐变头部割裂
Docker Build and Push / build-image (push) Successful in 4m3s
现象: 顶部安全区修好后, 其他页面正常, 但「我」页顶部多出一条白色/色块,
把原本从屏幕顶端开始的蓝色渐变隔断.

原因: .app-safe-top 是独立占位元素, 固定使用页面底色.
「我」页头部 me-profile 是通栏蓝色渐变 (无 PageHeader), 安全区夹在中间
就形成一条异色带, 视觉上把蓝色切成两段.

修复:
- MePage 挂载时给 body 加 me-safe-top 标记, 卸载时移除
- 该标记下把 .app-safe-top 高度归零, 改由 .me-profile 的
  padding-top: calc(24px + var(--safe-top)) 吸收状态栏高度
- 这样蓝色渐变直接从屏幕最顶端开始, 是一整块连续色域, 无接缝,
  同时内容仍正确避开电量/信号栏

其他页面不受影响 (无该 body 标记, 安全区照常占位).
2026-10-24 08:15:05 +08:00
jif2.zhang a915a08f4d 修复 OTA 后顶部内容顶到状态栏 (未让出电量/信号栏)
现象: 直装 APK 时头部正常让开状态栏; 经 OTA 升级到 V0.2 前端后,
页面从屏幕最顶部开始, 与电量/信号栏重叠.

根因: 顶部让位一直只依赖 antd-mobile 的 <SafeArea position="top" />,
它只是一个空 div, 高度完全来自 env(safe-area-inset-top). 而 Android
WebView (Tauri) 下该变量常返回 0.
旧版每页顶部都有 45px 的 antd-mobile <NavBar>, 视觉上兜住了状态栏,
掩盖了 inset 失效; V0.2 把 NavBar 换成 23px 的 PageHeader
(「我」页甚至无头部), 兜底消失后问题暴露. 与 OTA 机制本身无关.

修复:
- MobileShell: <SafeArea> 换成 .app-safe-top, 高度由 --safe-top 控制
- main.jsx: 新增 calibrateSafeTop(), 用临时元素测 env(safe-area-inset-top),
  并与 screen.height - visualViewport.height 交叉校验, 取较大值写入
  --safe-top; 两者都不可用时保底 24px; 监听旋转/视口变化重算
- globals.css: --safe-top 保留 env() 表达式, 新增 .app-safe-top 样式

这样无论 WebView 是否上报 inset, 顶部都能正确让位.
2026-10-24 08:05:19 +08:00
jif2.zhang 2ea0da758f 修复 OTA 后顶部内容顶到状态栏 (未让出电量/信号栏)
Docker Build and Push / build-image (push) Successful in 4m2s
现象: 直装 APK 时头部正常让开状态栏; 经 OTA 升级到 V0.2 前端后,
页面从屏幕最顶部开始, 与电量/信号栏重叠.

根因: 顶部让位一直只依赖 antd-mobile 的 <SafeArea position="top" />,
它只是一个空 div, 高度完全来自 env(safe-area-inset-top). 而 Android
WebView (Tauri) 下该变量常返回 0.
旧版每页顶部都有 45px 的 antd-mobile <NavBar>, 视觉上兜住了状态栏,
掩盖了 inset 失效; V0.2 把 NavBar 换成 23px 的 PageHeader
(「我」页甚至无头部), 兜底消失后问题暴露. 与 OTA 机制本身无关.

修复:
- MobileShell: <SafeArea> 换成 .app-safe-top, 高度由 --safe-top 控制
- main.jsx: 新增 calibrateSafeTop(), 用临时元素测 env(safe-area-inset-top),
  并与 screen.height - visualViewport.height 交叉校验, 取较大值写入
  --safe-top; 两者都不可用时保底 24px; 监听旋转/视口变化重算
- globals.css: --safe-top 保留 env() 表达式, 新增 .app-safe-top 样式

这样无论 WebView 是否上报 inset, 顶部都能正确让位.
2026-10-24 08:05:15 +08:00
jif2.zhang 2f55865092 合并 dev-0.2 到 master: V0.2 功能 + 米家修复 + 分支策略调整
Docker Build and Push / build-image (push) Successful in 8m0s
2026-10-24 07:36:58 +08:00
jif2.zhang d1f36b3b8f 修复扫码弹窗一闪而过
Docker Build and Push / build-image (push) Successful in 4m5s
现象: 米家授权过期后, 扫码弹窗从底部弹出但瞬间消失, 看不到二维码.

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

注意: 前端需重新构建发布, 服务端需重启后生效.
2026-10-24 06:30:00 +08:00
jif2.zhang 95b9985e8b 修复米家会话失效后不弹登录二维码
Docker Build and Push / build-image (push) Successful in 5m46s
根因一: 过期检测形同虚设
- @zythum02/mijia-api 的 refreshToken() 在本地凭证字段齐全时直接返回成功,
  根本不发请求, 因此 checkAuth() 永远判定 ok, 过期后既不出横幅也不出扫码入口
- checkAuth() 改为真打一次 getHomesList() 验证 token; 成功缓存 5 分钟,
  失败只缓存 30 秒(便于网络恢复后自愈); 增加并发探测合并; 状态未变不写库
- getStatus() 以实时探测结果为准, DB 里的 status 仅在探测为 unknown 时兜底,
  避免残留的 ok 遮蔽已失效状态

根因二: 二维码图片 401 导致弹窗空白
- /api/mihome/reauthorize/qr-image 在 authRequired 之后, 而 <img src> 无法携带
  Authorization 头, 图片请求被拒, 用户看到的是空白/裂图
- 新增 fetchMihomeQrImage(): 带 JWT fetch 后转 blob URL 给 <img>, 并在卸载/换码时释放

其他修复
- EnvView 的 load 依赖 [status] 且成功回调又 setStatus, 造成 /status 无限重复请求;
  改为空依赖 + 函数式 setStatus; 首屏与重试走 ?force=1 强查
- 会话失效(非网络类错误)时自动弹出扫码窗, 每次挂载仅弹一次
- 扫码状态机新增 done 标记, 只有真正登录成功才算成功, 避免取消后误判
- 二维码 120s 超时自动续码(最多 2 次), 慢扫不再直接失败
- 取消后不再丢弃可能已成功的扫码结果
- scripts/mihome-login.mjs 适配 done 语义
- 同步更新设计文档 §5.2 / §5.4 / §6

注意: 前端需重新构建发布后生效。
2026-10-23 22:21:08 +08:00
287 changed files with 38396 additions and 13368 deletions
+12
View File
@@ -0,0 +1,12 @@
# 大目录/与镜像无关的内容不进 build context
# 注意: dist/ 和 ota-package/ 不能忽略 —— CI 预构建产物要通过 context 进入镜像
.git
.gitea
.local
.pnpm-store
node_modules
src-tauri
docs
*.log
readme
deploy
+10
View File
@@ -0,0 +1,10 @@
# Windows 外壳构建模式 (npm run build:shell / vite build --mode shell)
# 外壳内前端跑在 tauri://localhost, 没有同源后端, API 必须指向远端 Express。
# 与 android 打包脚本 (scripts/wsl/build-apk.sh) 注入的值保持一致;
# 进程环境变量优先级高于本文件, android 流程不受影响。
#
# 0.2.1 起正式域名: https://iboard.honor3.com (NPM 反代 -> baihu:3001, 受信证书)
# PowerSync 同步端点默认同域 (NPM 把 /sync 反代到同步服务); 若改用独立子域,
# 打包时用 VITE_POWERSYNC_URL=https://sync.example.com 覆盖即可。
VITE_API_BASE=https://iboard.honor3.com
VITE_POWERSYNC_URL=https://iboard.honor3.com
+190 -182
View File
@@ -1,182 +1,190 @@
name: Android Build & Publish
# 仅在 android-* 分支 push 时触发 (如 android/release, android-1.0)
# 构建 release APK (keystore 签名) 并上传到 Gitea Generic Package Registry
on:
push:
branches:
- 'android-*'
jobs:
build-android:
runs-on: ubuntu-latest
# 需要写权限: 上传 packages
permissions:
contents: read
packages: write
env:
# 固定工具链版本, 便于 actions/cache 命中
# Android 后端地址 (bake 进 APK); 与 scripts/wsl/build-apk.sh 保持一致
VITE_API_BASE: http://bh.vps.honor3.com:3001
steps:
- name: Checkout
uses: https://gitea.com/actions/checkout@v4
- name: Setup Node
uses: https://gitea.com/actions/setup-node@v4
with:
node-version: 22
cache: 'npm'
# 版本号一致性: package.json / tauri.conf.json / Cargo.toml / Cargo.lock 必须完全相等。
# tauri.conf.json 的 version 就是 APK 的 versionName, 漏改会打出一个版本号错误的安装包。
- name: Check version consistency
run: node scripts/ci/check-version.mjs
- name: Setup JDK 17
uses: https://gitea.com/actions/setup-java@v4
with:
distribution: temurin
java-version: '17'
# ---------- 缓存: 命中后跳过下面的安装 ----------
- name: Cache Android SDK & NDK
id: cache-android
uses: https://gitea.com/actions/cache@v4
with:
path: ~/android-sdk
key: android-sdk-ndk-27.2-${{ runner.os }}
- name: Cache Rust toolchain
id: cache-rust
uses: https://gitea.com/actions/cache@v4
with:
path: |
~/.cargo
~/.rustup
# 工具链升级时 bump 版本号使缓存失效重装 (含 cargo-ndk)
key: rust-android-v1-${{ runner.os }}
- name: Cache Tauri cargo target
uses: https://gitea.com/actions/cache@v4
with:
path: src-tauri/target
key: tauri-target-android-${{ runner.os }}
# ---------- 安装工具链 (仅缓存未命中时) ----------
- name: Install Rust + Android targets
if: steps.cache-rust.outputs.cache-hit != 'true'
run: |
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
. "$HOME/.cargo/env"
rustup target add aarch64-linux-android armv7-linux-androideabi x86_64-linux-android i686-linux-android
cargo install cargo-ndk --locked
- name: Install Android SDK & NDK
if: steps.cache-android.outputs.cache-hit != 'true'
run: |
export ANDROID_HOME="$HOME/android-sdk"
sudo apt-get update
sudo apt-get install -y --no-install-recommends unzip
mkdir -p "$ANDROID_HOME/cmdline-tools"
wget -q https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip -O /tmp/cmdline-tools.zip
unzip -q /tmp/cmdline-tools.zip -d /tmp/
mv /tmp/cmdline-tools "$ANDROID_HOME/cmdline-tools/latest"
export PATH="$ANDROID_HOME/cmdline-tools/latest/bin:$PATH"
yes | sdkmanager --licenses >/dev/null || true
sdkmanager --install \
"platforms;android-36" \
"build-tools;36.0.0" \
"platform-tools" \
"ndk;27.2.12479018" \
"cmake;3.22.1"
- name: Install frontend deps
run: npm ci --no-audit --no-fund
# ---------- 全局 Android 环境 (对后续所有 step 生效, 无论缓存是否命中) ----------
- name: Set Android env
run: |
echo "ANDROID_HOME=$HOME/android-sdk" >> "$GITHUB_ENV"
echo "ANDROID_NDK_HOME=$HOME/android-sdk/ndk/27.2.12479018" >> "$GITHUB_ENV"
echo "$HOME/android-sdk/cmdline-tools/latest/bin" >> "$GITHUB_PATH"
echo "$HOME/android-sdk/platform-tools" >> "$GITHUB_PATH"
# ---------- 构建 ----------
- name: Init Android project
run: |
# tauri android init 需要 cargo (运行 cargo metadata)。
# Rust 工具链缓存命中时 "Install Rust" 步骤被跳过, 不会把 cargo 加进全局 PATH,
# 因此这里显式加载 cargo 环境 (与 Build release APK 步骤保持一致)。
. "$HOME/.cargo/env"
cd src-tauri
npx tauri android init --ci
- name: Inject release signing config
run: bash scripts/ci/android-signing.sh
env:
ANDROID_KEYSTORE_B64: ${{ secrets.ANDROID_KEYSTORE_B64 }}
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
- name: Build release APK
run: |
export ANDROID_HOME="$HOME/android-sdk"
export ANDROID_NDK_HOME="$ANDROID_HOME/ndk/27.2.12479018"
export PATH="$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/platform-tools:$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin:$PATH"
export CARGO_HOME="$HOME/.cargo"
. "$HOME/.cargo/env"
cd src-tauri
npx tauri android build --apk
# 断言签名配置仍在且 release 块实际引用了它 (防止 tauri 重新生成 gen/android 或正则未命中)
if ! grep -q 'create("release")' gen/android/app/build.gradle.kts || ! grep -q 'signingConfig = signingConfigs.getByName("release")' gen/android/app/build.gradle.kts; then
echo "ERROR: 构建后签名配置丢失/未生效, APK 未签名" >&2
exit 1
fi
- name: Locate APK
id: apk
run: |
# 优先 universal 已签名 APK, 找不到再回退到其他已签名 APK
APK=""
UNIVERSAL=$(find src-tauri/gen/android/app/build/outputs/apk/universal/release -name '*.apk' ! -name '*unsigned*' 2>/dev/null | head -1)
if [ -n "$UNIVERSAL" ]; then
APK="$UNIVERSAL"
else
APK=$(find src-tauri/gen/android/app/build/outputs/apk -name '*.apk' ! -name '*unsigned*' | head -1)
fi
if [ -z "$APK" ]; then
echo "ERROR: 未找到已签名 APK" >&2
exit 1
fi
echo "path=$APK" >> $GITHUB_OUTPUT
ls -lh "$APK"
# ---------- 上传到 Gitea Generic Package ----------
- name: Publish APK to Gitea packages
env:
# 注意: 不能用 GITEA_TOKEN (内置 job token, 无组织 packages 写权限, 且命名规则禁止 GITEA_ 前缀)
# 用一个自定义名 secret 存放有 packages 写权限的 user token
PACKAGE_TOKEN: ${{ secrets.PACKAGE_TOKEN }}
APK_PATH: ${{ steps.apk.outputs.path }}
run: |
set -euo pipefail
# 包版本: 用分支名去掉 android- 前缀 + 短 SHA, 保证每次 push 唯一
BRANCH="${GITHUB_REF_NAME#android-}"
PKG_VERSION="${BRANCH}-${GITHUB_SHA:0:8}"
FILE_NAME="iboard-$(basename "$APK_PATH")"
OWNER="${GITHUB_REPOSITORY_OWNER}"
PKG_NAME="iboard"
# Gitea Generic Package Registry 上传端点 (PUT)
# 注意: GITHUB_SERVER_URL 是仓库 web 根地址, 例如 http://git.honor3.com
URL="${GITHUB_SERVER_URL}/api/packages/${OWNER}/generic/${PKG_NAME}/${PKG_VERSION}/${FILE_NAME}"
echo "上传到: ${URL}"
curl -sS --fail-with-body \
-H "Authorization: token ${PACKAGE_TOKEN}" \
--upload-file "$APK_PATH" \
"${URL}"
echo ""
echo "已发布: ${URL}"
name: Android Build & Publish
# 仅在 android-* 分支 push 时触发 (如 android/release, android-1.0)
# 构建 release APK (keystore 签名) 并上传到 Gitea Generic Package Registry
on:
push:
branches:
- 'android-*'
# 同分支连续 push 时取消旧的构建, 省资源也避免上传过时 APK
concurrency:
group: android-${{ github.ref_name }}
cancel-in-progress: true
jobs:
build-android:
runs-on: ubuntu-latest
# 需要写权限: 上传 packages
permissions:
contents: read
packages: write
env:
# 固定工具链版本, 便于 actions/cache 命中
# Android 后端地址 (bake 进 APK); 与 scripts/wsl/build-apk.sh 保持一致
VITE_API_BASE: https://iboard.honor3.com
steps:
- name: Checkout
uses: https://gitea.com/actions/checkout@v4
- name: Setup Node
uses: https://gitea.com/actions/setup-node@v4
with:
node-version: 22
cache: 'npm'
# 版本号一致性: package.json / tauri.conf.json / Cargo.toml / Cargo.lock 必须完全相等。
# tauri.conf.json 的 version 就是 APK 的 versionName, 漏改会打出一个版本号错误的安装包。
- name: Check version consistency
run: node scripts/ci/check-version.mjs
- name: Setup JDK 17
uses: https://gitea.com/actions/setup-java@v4
with:
distribution: temurin
java-version: '17'
# ---------- 缓存: 命中后跳过下面的安装 ----------
- name: Cache Android SDK & NDK
id: cache-android
uses: https://gitea.com/actions/cache@v4
with:
path: ~/android-sdk
key: android-sdk-ndk-27.2-${{ runner.os }}
- name: Cache Rust toolchain
id: cache-rust
uses: https://gitea.com/actions/cache@v4
with:
path: |
~/.cargo
~/.rustup/toolchains
# 工具链升级时 bump 版本号使缓存失效重装 (含 cargo-ndk)
key: rust-android-v1-${{ runner.os }}
- name: Cache Tauri cargo target
uses: https://gitea.com/actions/cache@v4
with:
path: src-tauri/target
# 依赖变化 → 新 key; 命不中时回退最近一次的 target 目录做增量编译
key: tauri-target-android-${{ hashFiles('src-tauri/Cargo.lock') }}
restore-keys: tauri-target-android-
# ---------- 安装工具链 (仅缓存未命中时) ----------
- name: Install Rust + Android targets
if: steps.cache-rust.outputs.cache-hit != 'true'
run: |
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
. "$HOME/.cargo/env"
# 只构建 aarch64 (现代 Android 手机); 需要模拟器/x86 设备时再加回其他 target
rustup target add aarch64-linux-android
cargo install cargo-ndk --locked
- name: Install Android SDK & NDK
if: steps.cache-android.outputs.cache-hit != 'true'
run: |
export ANDROID_HOME="$HOME/android-sdk"
sudo apt-get update
sudo apt-get install -y --no-install-recommends unzip
mkdir -p "$ANDROID_HOME/cmdline-tools"
wget -q https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip -O /tmp/cmdline-tools.zip
unzip -q /tmp/cmdline-tools.zip -d /tmp/
mv /tmp/cmdline-tools "$ANDROID_HOME/cmdline-tools/latest"
export PATH="$ANDROID_HOME/cmdline-tools/latest/bin:$PATH"
yes | sdkmanager --licenses >/dev/null || true
sdkmanager --install \
"platforms;android-36" \
"build-tools;36.0.0" \
"platform-tools" \
"ndk;27.2.12479018" \
"cmake;3.22.1"
- name: Install frontend deps
run: npm ci --no-audit --no-fund
# ---------- 全局 Android 环境 (对后续所有 step 生效, 无论缓存是否命中) ----------
- name: Set Android env
run: |
echo "ANDROID_HOME=$HOME/android-sdk" >> "$GITHUB_ENV"
echo "ANDROID_NDK_HOME=$HOME/android-sdk/ndk/27.2.12479018" >> "$GITHUB_ENV"
echo "$HOME/android-sdk/cmdline-tools/latest/bin" >> "$GITHUB_PATH"
echo "$HOME/android-sdk/platform-tools" >> "$GITHUB_PATH"
# ---------- 构建 ----------
- name: Init Android project
run: |
# tauri android init 需要 cargo (运行 cargo metadata)。
# Rust 工具链缓存命中时 "Install Rust" 步骤被跳过, 不会把 cargo 加进全局 PATH,
# 因此这里显式加载 cargo 环境 (与 Build release APK 步骤保持一致)。
. "$HOME/.cargo/env"
cd src-tauri
npx tauri android init --ci
- name: Inject release signing config
run: bash scripts/ci/android-signing.sh
env:
ANDROID_KEYSTORE_B64: ${{ secrets.ANDROID_KEYSTORE_B64 }}
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
- name: Build release APK
run: |
export ANDROID_HOME="$HOME/android-sdk"
export ANDROID_NDK_HOME="$ANDROID_HOME/ndk/27.2.12479018"
export PATH="$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/platform-tools:$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin:$PATH"
export CARGO_HOME="$HOME/.cargo"
. "$HOME/.cargo/env"
cd src-tauri
npx tauri android build --apk --target aarch64
# 断言签名配置仍在且 release 块实际引用了它 (防止 tauri 重新生成 gen/android 或正则未命中)
if ! grep -q 'create("release")' gen/android/app/build.gradle.kts || ! grep -q 'signingConfig = signingConfigs.getByName("release")' gen/android/app/build.gradle.kts; then
echo "ERROR: 构建后签名配置丢失/未生效, APK 未签名" >&2
exit 1
fi
- name: Locate APK
id: apk
run: |
# 优先 universal 已签名 APK, 找不到再回退到其他已签名 APK
APK=""
UNIVERSAL=$(find src-tauri/gen/android/app/build/outputs/apk/universal/release -name '*.apk' ! -name '*unsigned*' 2>/dev/null | head -1)
if [ -n "$UNIVERSAL" ]; then
APK="$UNIVERSAL"
else
APK=$(find src-tauri/gen/android/app/build/outputs/apk -name '*.apk' ! -name '*unsigned*' | head -1)
fi
if [ -z "$APK" ]; then
echo "ERROR: 未找到已签名 APK" >&2
exit 1
fi
echo "path=$APK" >> $GITHUB_OUTPUT
ls -lh "$APK"
# ---------- 上传到 Gitea Generic Package ----------
- name: Publish APK to Gitea packages
env:
# 注意: 不能用 GITEA_TOKEN (内置 job token, 无组织 packages 写权限, 且命名规则禁止 GITEA_ 前缀)
# 用一个自定义名 secret 存放有 packages 写权限的 user token
PACKAGE_TOKEN: ${{ secrets.PACKAGE_TOKEN }}
APK_PATH: ${{ steps.apk.outputs.path }}
run: |
set -euo pipefail
# 包版本: 用分支名去掉 android- 前缀 + 短 SHA, 保证每次 push 唯一
BRANCH="${GITHUB_REF_NAME#android-}"
PKG_VERSION="${BRANCH}-${GITHUB_SHA:0:8}"
FILE_NAME="iboard-$(basename "$APK_PATH")"
OWNER="${GITHUB_REPOSITORY_OWNER}"
PKG_NAME="iboard"
# Gitea Generic Package Registry 上传端点 (PUT)
# 注意: GITHUB_SERVER_URL 是仓库 web 根地址, 例如 http://git.honor3.com
URL="${GITHUB_SERVER_URL}/api/packages/${OWNER}/generic/${PKG_NAME}/${PKG_VERSION}/${FILE_NAME}"
echo "上传到: ${URL}"
curl -sS --fail-with-body \
-H "Authorization: token ${PACKAGE_TOKEN}" \
--upload-file "$APK_PATH" \
"${URL}"
echo ""
echo "已发布: ${URL}"
+94 -40
View File
@@ -18,6 +18,11 @@ on:
branches:
- master
# 连续 push master 时取消还在跑的旧部署, 避免排队执行过时版本的完整部署
concurrency:
group: deploy-master
cancel-in-progress: true
jobs:
build-image:
runs-on: ubuntu-latest
@@ -25,12 +30,12 @@ jobs:
# 关键: 显式固定 Android/OTA 后端地址, 覆盖 runner 环境里可能存在的
# 其他 VITE_API_BASE 值。否则 OTA 包里的前端可能连错后端, 导致
# "OTA 更新后登录失败/进不去内部"。与 android-build.yml 保持一致。
VITE_API_BASE: http://bh.vps.honor3.com:3001
# OTA bundle 要求的最低原生二进制版本。
# 与 package.json 的应用版本是**两个概念**: 前端 OTA 不碰原生层,
# 现有 APK 仍是 0.1.0, 所以这里固定 0.1.0 (否则旧 APK 会被判定为不兼容而收不到更新)。
# 重打 APK 并确认原生层无破坏后, 再提升此值。
OTA_MIN_BINARY: '0.1.0'
VITE_API_BASE: https://iboard.honor3.com
# OTA bundle 要求的最低原生二进制版本 (hotswap 据此拒绝不兼容的旧外壳)。
# 0.2.1 起必须为 0.2.1: 旧外壳 (0.1.0) 的 CSP 白名单编译进了原生二进制,
# 不含 https://iboard.honor3.com, 若让它拉到 0.2.1 前端会因 CSP 拦截而白屏。
# 旧外壳请整包升级 (Windows 重装 exe / Android 换装 APK), 不要靠 OTA。
OTA_MIN_BINARY: '0.2.1'
steps:
- uses: https://gitea.com/actions/checkout@v4
@@ -38,6 +43,7 @@ jobs:
uses: https://gitea.com/actions/setup-node@v4
with:
node-version: 22
cache: 'npm'
- name: Check version consistency & branch
# 1) package.json / tauri.conf.json / Cargo.toml / Cargo.lock 四处版本必须完全相等且不带 -ota
@@ -53,62 +59,110 @@ jobs:
run: npm ci --no-audit --no-fund
- name: Build frontend
# dist/ 供 OTA 打包用 (Dockerfile 内也会构建一份)
# 全流程唯一一次前端构建: dist/ 既供 OTA 打包, 也通过 build context 进入镜像
# (docker build 传 PREBUILT_DIST=true 跳过镜像内的重复构建)
run: npm run build
- name: Install minisign
run: sudo apt-get update && sudo apt-get install -y minisign
# 静态二进制直接下载, 免去每次 apt-get update + install
# 注意: release tarball 是 macOS 上打包的, 混有多个平台的 minisign 二进制,
# 逐个试运行挑出本机可执行的, 都不行再回退 apt
run: |
set -euo pipefail
mkdir -p /tmp/ms
curl -sL https://github.com/jedisct1/minisign/releases/download/0.12/minisign-0.12-linux.tar.gz | tar xz -C /tmp/ms
BIN=""
for c in $(find /tmp/ms -type f -name minisign); do
if "$c" -v >/dev/null 2>&1; then BIN="$c"; break; fi
done
if [ -n "$BIN" ]; then
sudo install -m755 "$BIN" /usr/local/bin/minisign
else
echo "未找到可执行的静态二进制, 回退 apt 安装"
sudo apt-get update && sudo apt-get install -y minisign
fi
minisign -v
- name: Build & sign OTA package
# 每次主分支提交生成 OTA 升级包, sequence 默认时间戳 (单调递增)
# 发布说明 = 触发构建的 commit message (标题+正文; 打包脚本内安全截断 400 字符),
# commit sha 通过 --commit 单独写入 manifest, 前端分行展示
# 产物 ota-package/ (tar.gz + latest.json + .minisig) 进入 docker build context, 由 Dockerfile COPY 进镜像
env:
MINISIGN_PRIVATE_KEY: ${{ secrets.MINISIGN_PRIVATE_KEY }}
OTA_NOTES: ${{ github.sha }}
run: |
set -euo pipefail
node scripts/build-ota.mjs --out ota-package --notes ${OTA_NOTES} --min-binary ${OTA_MIN_BINARY}
# 不用 head -c 截断: 那会把多字节汉字切成非法 UTF-8, latest.json 解析失败
MSG="$(git log -1 --pretty=format:%B | sed -e '/^[[:space:]]*$/d')"
echo "notes: ${MSG}"
node scripts/build-ota.mjs --out ota-package --notes "${MSG}" --commit "${GITHUB_SHA}" --min-binary ${OTA_MIN_BINARY}
echo "===== latest.json ====="
cat ota-package/latest.json
echo "===== bundle sha256 ====="
sha256sum ota-package/iboard-ota.tar.gz
- name: Publish OTA package to server
# CI 跑在容器内, 无法直接写宿主文件系统; 用 docker cp 经bind-mount容器
# 把 ota-package 落到宿主 /app/iboard-prod/deploy/prod/data/ota
# (该目录被生产 app 容器挂载为 /app/ota-package)。
# docker 不可用或目录缺失时仅告警, 不影响发布。
run: |
set -euo pipefail
HOST_DIR=/app/iboard-prod
if ! docker info >/dev/null 2>&1; then
echo "::warning::docker 不可用, 跳过 OTA 包同步"; exit 0
fi
docker run -d --name ota-bridge -v "${HOST_DIR}/deploy/prod/data/ota:/dst" alpine:3.20 sleep 30 >/dev/null
docker cp ota-package/. ota-bridge:/dst/ || echo "::warning::OTA 包复制失败"
docker rm -f ota-bridge >/dev/null 2>&1 || true
echo "== 宿主 OTA 目录 =="
docker run --rm -v "${HOST_DIR}/deploy/prod/data/ota:/d" alpine:3.20 sh -c 'ls -la /d | head -6'
docker restart iboard-prod-app-1 >/dev/null 2>&1 || true
- name: Build Docker image
# DATABASE_URL 仅在 prisma generate 阶段需要, 可用任何合法占位串
run: docker build -t iboard:latest --build-arg DATABASE_URL=mysql://build:build@127.0.0.1:3306/build .
# PREBUILT_DIST=true: 复用上一步构建好的 dist/, 镜像内跳过前端构建
# DOCKER_BUILDKIT=1: Dockerfile 用了 cache mount 语法
run: docker build -t iboard:latest --build-arg DATABASE_URL=mysql://build:build@127.0.0.1:3306/build --build-arg PREBUILT_DIST=true .
env:
DOCKER_BUILDKIT: '1'
- name: List images
run: docker images
- name: Apply DB migrations
# 数据库自动升级: 按 prisma/migrations/ 下的迁移顺序应用 (生产库由 master 部署时统一升级)
# migrate deploy 幂等, 无新迁移时为 no-op
# 位置关键: 在 "Remove old container" 之前 —— 建表/改表成功后才切换流量,
# 迁移失败则整个 workflow 中断, 不会用新镜像顶掉仍在运行的旧容器
- name: Deploy 0.2.1 production stack
# 0.2.1 起生产为单端口架构: app(3001 对外) + pg(内网) + powersync(内网),
# 客户端同步经 app 的 /sync 反代进入同步服务 (无独立同步端口)。
# 部署逻辑在服务器的 /app/iboard-prod/scripts/deploy/prod.sh (幂等), 这里只负责:
# 1) 把刚构建好的镜像交给该脚本
# 2) 未配置 PG 相关 secrets 或脚本缺失时跳过部署 (发布不失败, 仅提示)
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
PG_PASSWORD: ${{ secrets.PG_PASSWORD }}
PS_STORAGE_PASSWORD: ${{ secrets.PS_STORAGE_PASSWORD }}
JWT_SECRET: ${{ secrets.JWT_SECRET }}
POWERSYNC_JWT_SECRET: ${{ secrets.POWERSYNC_JWT_SECRET }}
POWERSYNC_JWT_KEY_B64: ${{ secrets.POWERSYNC_JWT_KEY_B64 }}
run: |
set -euo pipefail
npx prisma migrate status || true
npx prisma migrate deploy
- name: Remove old container
run: docker rm -vf iboard || true
- name: Recycle container
run: docker system prune --volumes -f
- name: Run container
# DATABASE_URL 通过 gitea Actions secret 注入, 不进镜像层
# OTA_PACKAGE_DIR 已在 Dockerfile 设为 /app/ota-package
# 米家凭证 (auth.txt) 不进 git: 运维放宿主机 /app/iboard/cache, bind-mount 进容器 (MIHOME_CACHE_DIR=/app/cache)
run: |
docker run -d --name iboard -p 3001:3001 \
-e DATABASE_URL="${{ secrets.DATABASE_URL }}" \
-e JWT_SECRET="${{ secrets.JWT_SECRET }}" \
-e MIHOME_CACHE_DIR=/app/cache \
-v /app/iboard/cache:/app/cache \
iboard:latest
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
JWT_SECRET: ${{ secrets.JWT_SECRET }}
HOST_DIR=/app/iboard-prod
if [ -z "${PG_PASSWORD:-}" ]; then
echo "::warning::未配置 PG_PASSWORD 等 secrets, 跳过自动部署 (镜像已构建完成)"
echo "手工部署: ssh baihu 'bash $HOST_DIR/scripts/deploy/prod.sh'"
exit 0
fi
if ! docker run --rm -v "$HOST_DIR:/w" alpine:3.20 test -f /w/scripts/deploy/prod.sh; then
echo "::warning::$HOST_DIR/scripts/deploy/prod.sh 不存在, 跳过部署"
exit 0
fi
docker run --rm \
-v /var/run/docker.sock:/var/run/docker.sock \
-v "$HOST_DIR:$HOST_DIR" \
-w "$HOST_DIR" \
-e PG_PASSWORD="$PG_PASSWORD" \
-e PS_STORAGE_PASSWORD="$PS_STORAGE_PASSWORD" \
-e JWT_SECRET="$JWT_SECRET" \
-e POWERSYNC_JWT_SECRET="$POWERSYNC_JWT_SECRET" \
-e POWERSYNC_JWT_KEY_B64="$POWERSYNC_JWT_KEY_B64" \
-e PUBLIC_URL=https://iboard.honor3.com \
-e ROOT="$HOST_DIR" \
docker:27-cli sh -c 'apk add -q bash curl >/dev/null 2>&1; bash /app/iboard-prod/scripts/deploy/prod.sh'
+97 -75
View File
@@ -1,75 +1,97 @@
name: OTA Frontend Build & Sign
# 前端 OTA 升级包构建 + 签名 + 上传 artifact
# 触发: 仅手动 (workflow_dispatch), 发版前用
# - 主分支 push 的自动 OTA 生成已并入 docker-build.yml (随镜像发布), 本 workflow 不再监听 push 避免重复构建
# 输入: sequence / notes / min_binary (可覆盖默认)
# Secret 依赖: MINISIGN_PRIVATE_KEY (整段 .key 文件内容, 含首行 untrusted comment)
# 注意: 此 workflow 不会修改 src-tauri/tauri.conf.json 里的 pubkey,
# 换密钥请手动改 conf 并 commit.
on:
workflow_dispatch:
inputs:
sequence:
description: 'OTA sequence (单调递增整数, 留空用 timestamp)'
required: false
type: string
notes:
description: '发布说明 (写入 latest.json.notes)'
required: false
type: string
default: ''
min_binary:
description: '最低 binary 版本 (默认 0.1.0 = 现有 APK 的原生版本, 与前端应用版本无关)'
required: false
type: string
default: '0.1.0'
jobs:
build-ota:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: https://gitea.com/actions/checkout@v4
- name: Setup Node
uses: https://gitea.com/actions/setup-node@v4
with:
node-version: 22
- name: Install deps
run: npm ci --no-audit --no-fund
- name: Build frontend
# 复用 vite build 产物; 不打 Docker (本 workflow 只关心前端)
run: npm run build
- name: Install minisign
run: sudo apt-get update && sudo apt-get install -y minisign
- name: Build & sign OTA package
env:
MINISIGN_PRIVATE_KEY: ${{ secrets.MINISIGN_PRIVATE_KEY }}
OTA_NOTES: ${{ inputs.notes }}
run: |
set -euo pipefail
OUT="ota-package"
ARGS="--out ${OUT}"
# sequence 留空 → 脚本默认用时间戳; min_binary 默认 0.1.0 (与前端版本解耦)
if [ -n "${{ inputs.sequence }}" ]; then ARGS="${ARGS} --sequence ${{ inputs.sequence }}"; fi
if [ -n "${OTA_NOTES}" ]; then ARGS="${ARGS} --notes ${OTA_NOTES}"; fi
if [ -n "${{ inputs.min_binary }}" ]; then ARGS="${ARGS} --min-binary ${{ inputs.min_binary }}"; fi
node scripts/build-ota.mjs ${ARGS}
echo "===== latest.json ====="
cat ${OUT}/latest.json
echo "===== bundle sha256 ====="
sha256sum ${OUT}/iboard-ota.tar.gz
- name: Upload OTA package as artifact
uses: https://gitea.com/actions/upload-artifact@v4
with:
name: iboard-ota-${{ inputs.sequence || github.run_number }}
path: ota-package/
retention-days: 30
if-no-files-found: error
name: OTA Frontend Build & Sign
# 前端 OTA 升级包构建 + 签名 + 上传 artifact
# 触发: 仅手动 (workflow_dispatch), 发版前用
# - 主分支 push 的自动 OTA 生成已并入 docker-build.yml (随镜像发布), 本 workflow 不再监听 push 避免重复构建
# 输入: sequence / notes / min_binary (可覆盖默认)
# Secret 依赖: MINISIGN_PRIVATE_KEY (整段 .key 文件内容, 含首行 untrusted comment)
# 注意: 此 workflow 不会修改 src-tauri/tauri.conf.json 里的 pubkey,
# 换密钥请手动改 conf 并 commit.
on:
workflow_dispatch:
inputs:
sequence:
description: 'OTA sequence (单调递增整数, 留空用 timestamp)'
required: false
type: string
notes:
description: '发布说明 (写入 latest.json.notes)'
required: false
type: string
default: ''
min_binary:
description: '最低 binary 版本 (默认 0.1.0 = 现有 APK 的原生版本, 与前端应用版本无关)'
required: false
type: string
default: '0.1.0'
jobs:
build-ota:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: https://gitea.com/actions/checkout@v4
- name: Setup Node
uses: https://gitea.com/actions/setup-node@v4
with:
node-version: 22
- name: Install deps
run: npm ci --no-audit --no-fund
- name: Build frontend
# 复用 vite build 产物; 不打 Docker (本 workflow 只关心前端)
run: npm run build
- name: Install minisign
# 静态二进制直接下载, 免去每次 apt-get update + install
# 注意: release tarball 是 macOS 上打包的, 混有多个平台的 minisign 二进制,
# 逐个试运行挑出本机可执行的, 都不行再回退 apt
run: |
set -euo pipefail
mkdir -p /tmp/ms
curl -sL https://github.com/jedisct1/minisign/releases/download/0.12/minisign-0.12-linux.tar.gz | tar xz -C /tmp/ms
BIN=""
for c in $(find /tmp/ms -type f -name minisign); do
if "$c" -v >/dev/null 2>&1; then BIN="$c"; break; fi
done
if [ -n "$BIN" ]; then
sudo install -m755 "$BIN" /usr/local/bin/minisign
else
echo "未找到可执行的静态二进制, 回退 apt 安装"
sudo apt-get update && sudo apt-get install -y minisign
fi
minisign -v
- name: Build & sign OTA package
# 发布说明默认 = 检出 commit 的 message (标题+正文), commit sha 单独写入;
# 手动填写的 notes 优先
env:
MINISIGN_PRIVATE_KEY: ${{ secrets.MINISIGN_PRIVATE_KEY }}
run: |
set -euo pipefail
OUT="ota-package"
ARGS="--out ${OUT} --commit ${GITHUB_SHA}"
if [ -n "${{ inputs.sequence }}" ]; then ARGS="${ARGS} --sequence ${{ inputs.sequence }}"; fi
if [ -n "${{ inputs.notes }}" ]; then
ARGS="${ARGS} --notes \"${{ inputs.notes }}\""
else
MSG="$(git log -1 --pretty=format:%B | sed -e '/^[[:space:]]*$/d')"
ARGS="${ARGS} --notes \"${MSG}\""
fi
if [ -n "${{ inputs.min_binary }}" ]; then ARGS="${ARGS} --min-binary ${{ inputs.min_binary }}"; fi
eval "node scripts/build-ota.mjs ${ARGS}"
echo "===== latest.json ====="
cat ${OUT}/latest.json
echo "===== bundle sha256 ====="
sha256sum ${OUT}/iboard-ota.tar.gz
- name: Upload OTA package as artifact
uses: https://gitea.com/actions/upload-artifact@v4
with:
name: iboard-ota-${{ inputs.sequence || github.run_number }}
path: ota-package/
retention-days: 30
if-no-files-found: error
+25
View File
@@ -47,3 +47,28 @@ cache/apizero/
server/mihome/.cache/
# 宸ヤ綔鍖鸿�蹇�/浼氳瘽鏁版嵁 (鏈�湴绉佹湁, 涓嶅叆搴�)
.workbuddy/
# 鏈�湴 pnpm 瀹夎�浜х墿 (椤圭洰浣跨敤 package-lock.json)
/.pnpm-store
# 0.2.1 楠岃瘉閮ㄧ讲: 鏈嶅姟鍣ㄦ湰鍦版暟鎹�嵎涓庡瘑閽� (缁濅笉鍏ュ簱)
deploy/0.2.1/data/
deploy/0.2.1/.env
# 构建/工具临时目录 (local)
.esbuild-tmp/
# Windows 打包产物 (本地生成, 不入库)
/artifacts/
# 生产部署密钥与数据卷
/deploy/prod/.env
/deploy/prod/data/
# 本地临时脚本 (含口令, 不入库)
scripts/_*.mjs
.dsh-ops/
+78
View File
@@ -0,0 +1,78 @@
// CDP 触摸手势驱动: 长按 400ms 激活 + 水平拖动, 观察 useRowDrag 行为
import WebSocket from 'ws'
import { spawn } from 'node:child_process'
const C = JSON.parse((await (await fetch('http://127.0.0.1:9223/json/version')).text()))
const ws = new WebSocket(C.webSocketDebuggerUrl, { perMessageDeflate: false })
let id = 0
const pending = new Map()
const send = (method, params = {}, sessionId) => new Promise((res) => {
const i = ++id
pending.set(i, res)
ws.send(JSON.stringify({ id: i, method, params, sessionId }))
})
ws.on('message', (d) => {
const m = JSON.parse(d)
if (m.id && pending.has(m.id)) { pending.get(m.id)(m.result); pending.delete(m.id) }
if (m.method === 'Runtime.consoleAPICalled') {
console.log('[console]', m.params.type, m.params.args.map((a) => a.value ?? a.description).join(' '))
}
})
await new Promise((r) => ws.on('open', r))
const { targetId } = await send('Target.createTarget', { url: 'about:blank' })
const { sessionId } = await send('Target.attachToTarget', { targetId, flatten: true })
const S = (m, p) => send(m, p, sessionId)
await S('Runtime.enable')
await S('Page.enable')
await S('Emulation.setDeviceMetricsOverride', { width: 400, height: 800, deviceScaleFactor: 2, mobile: true })
await S('Emulation.setTouchEmulationEnabled', { enabled: true, maxTouchPoints: 5 })
await S('Page.navigate', { url: 'http://127.0.0.1:5199/dragtest.html' })
await new Promise((r) => setTimeout(r, 3000))
const sleep = (ms) => new Promise((res) => setTimeout(res, ms))
const ev = async (expr) => (await S('Runtime.evaluate', { expression: expr, returnByValue: true })).result.value
let rect = null
for (let i = 0; i < 30 && !rect; i++) {
await sleep(1000)
const v = await ev(`(() => { const el = document.querySelector('[data-task-id="3"]'); return el ? JSON.stringify(el.getBoundingClientRect()) : null })()`)
if (v) rect = JSON.parse(v)
}
if (!rect) { console.log('page never loaded'); ws.close(); process.exit(1) }
console.log('row rect', JSON.stringify(rect))
const r = rect
const x0 = r.left + r.width / 2
const y0 = r.top + r.height / 2
const touch = (type, x, y) => S('Input.dispatchTouchEvent', {
type,
touchPoints: type === 'touchEnd' ? [] : [{ x, y, id: 1 }],
})
// 1) touchStart + 按住 500ms (激活长按)
await touch('touchStart', x0, y0)
console.log('--- touchStart, holding 500ms')
await sleep(500)
// 2) 水平拖动 +200px, 每步 +25px / 40ms (纯水平)
for (let dx = 25; dx <= 200; dx += 25) {
await touch('touchMove', x0 + dx, y0)
await sleep(40)
}
console.log('--- moved to +200px (pure horizontal)')
// 3) 带纵向分量的拖动 (模拟真实手指): +160px 横 / +40px 纵
await touch('touchMove', x0 + 60, y0 + 12)
await sleep(40)
await touch('touchMove', x0 + 120, y0 + 26)
await sleep(40)
await touch('touchMove', x0 + 160, y0 + 40)
await sleep(40)
await touch('touchEnd')
await sleep(600)
console.log('--- events on page:')
console.log((await ev('window.__ev.join("\\n")')))
ws.close()
+77
View File
@@ -0,0 +1,77 @@
// CDP 触摸手势驱动 v2: 把手立即拖动 + 长按路径, 组内/跨组落点
import WebSocket from 'ws'
const C = JSON.parse((await (await fetch('http://127.0.0.1:9223/json/version')).text()))
const ws = new WebSocket(C.webSocketDebuggerUrl, { perMessageDeflate: false })
let id = 0
const pending = new Map()
const send = (method, params = {}, sessionId) => new Promise((res) => {
const i = ++id
pending.set(i, res)
ws.send(JSON.stringify({ id: i, method, params, sessionId }))
})
ws.on('message', (d) => {
const m = JSON.parse(d)
if (m.id && pending.has(m.id)) { pending.get(m.id)(m.result); pending.delete(m.id) }
})
await new Promise((r) => ws.on('open', r))
const { targetId } = await send('Target.createTarget', { url: 'about:blank' })
const { sessionId } = await send('Target.attachToTarget', { targetId, flatten: true })
const S = (m, p) => send(m, p, sessionId)
await S('Runtime.enable'); await S('Page.enable')
await S('Emulation.setDeviceMetricsOverride', { width: 400, height: 800, deviceScaleFactor: 2, mobile: true })
await S('Emulation.setTouchEmulationEnabled', { enabled: true, maxTouchPoints: 5 })
await S('Page.navigate', { url: 'http://127.0.0.1:5199/dragtest.html' })
const sleep = (ms) => new Promise((res) => setTimeout(res, ms))
const ev = async (expr) => (await S('Runtime.evaluate', { expression: expr, returnByValue: true })).result.value
const center = async (sel) => {
const r = JSON.parse(await ev(`JSON.stringify(document.querySelector('${sel}').getBoundingClientRect())`))
return { x: r.left + r.width / 2, y: r.top + r.height / 2 }
}
for (let i = 0; i < 30; i++) { await sleep(1000); if (await ev(`!!document.querySelector('[data-habit-id="1"]')`)) break }
const touch = (type, x, y) => S('Input.dispatchTouchEvent', {
type, touchPoints: type === 'touchEnd' ? [] : [{ x, y, id: 1 }],
})
// 用例 1: 按住 habit-1 的把手, 立即垂直拖到 habit-4 中部, 松手 → 应 DROP before/after 4
let p = await center('[data-habit-id="1"] .grip')
await touch('touchStart', p.x, p.y)
await sleep(60)
const t = await center('[data-habit-id="4"]')
await touch('touchMove', p.x, p.y + 20)
await sleep(30)
await touch('touchMove', t.x, t.y)
await sleep(60)
const cls1 = await ev(`document.querySelector('[data-habit-id="4"]').className`)
console.log('case1 target class:', cls1)
await touch('touchEnd')
await sleep(300)
// 用例 2: 按住 habit-1 把手拖到每周组 (habit-5) → 跨组, 应无落点无 DROP
p = await center('[data-habit-id="1"] .grip')
await touch('touchStart', p.x, p.y)
await sleep(60)
const w = await center('[data-habit-id="5"]')
await touch('touchMove', w.x, w.y)
await sleep(60)
const cls2 = await ev(`document.querySelector('[data-habit-id="5"]').className`)
console.log('case2 target class (应无 dragover):', cls2)
await touch('touchEnd')
await sleep(300)
// 用例 3: 长按卡片主体 500ms 后拖到 habit-3 下半 → DROP after 3
p = await center('[data-habit-id="2"]')
await touch('touchStart', p.x, p.y)
await sleep(520)
const t3 = await center('[data-habit-id="3"]')
await touch('touchMove', t3.x, t3.y + 20)
await sleep(60)
await touch('touchEnd')
await sleep(300)
console.log('--- events:')
console.log(await ev('window.__ev.join("\\n")'))
ws.close()
+27
View File
@@ -0,0 +1,27 @@
import WebSocket from 'ws'
const C = JSON.parse((await (await fetch('http://127.0.0.1:9223/json/version')).text()))
const ws = new WebSocket(C.webSocketDebuggerUrl, { perMessageDeflate: false })
let id = 0; const pending = new Map()
const send = (m, p = {}, s) => new Promise((r) => { const i = ++id; pending.set(i, r); ws.send(JSON.stringify({ id: i, method: m, params: p, sessionId: s })) })
ws.on('message', (d) => { const m = JSON.parse(d); if (m.id && pending.has(m.id)) { pending.get(m.id)(m.result); pending.delete(m.id) } })
await new Promise((r) => ws.on('open', r))
const targets = await send('Target.getTargets')
const t = targets.targetInfos.find((x) => x.url.includes('dragtest'))
const { sessionId } = await send('Target.attachToTarget', { targetId: t.targetId, flatten: true })
const S = (m, p) => send(m, p, sessionId)
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
const ev = async (e) => (await S('Runtime.evaluate', { expression: e, returnByValue: true })).result.value
// 长按 habit-2 主体, 分别在 450ms 后检查激活态, 再拖动
const r = JSON.parse(await ev(`JSON.stringify(document.querySelector('[data-habit-id="2"]').getBoundingClientRect())`))
const x = r.left + r.width / 2, y = r.top + r.height / 2
await S('Input.dispatchTouchEvent', { type: 'touchStart', touchPoints: [{ x, y, id: 1 }] })
await sleep(460)
console.log('after 460ms class:', await ev(`document.querySelector('[data-habit-id="2"]').className`))
const r3 = JSON.parse(await ev(`JSON.stringify(document.querySelector('[data-habit-id="3"]').getBoundingClientRect())`))
await S('Input.dispatchTouchEvent', { type: 'touchMove', touchPoints: [{ x: r3.left + r3.width / 2, y: r3.top + r3.height - 8, id: 1 }] })
await sleep(80)
console.log('class of 3 after move:', await ev(`document.querySelector('[data-habit-id="3"]').className`))
await S('Input.dispatchTouchEvent', { type: 'touchEnd', touchPoints: [] })
await sleep(300)
console.log('events:', await ev('window.__ev.join(" | ")'))
ws.close()
+32
View File
@@ -0,0 +1,32 @@
import WebSocket from 'ws'
const C = JSON.parse((await (await fetch('http://127.0.0.1:9223/json/version')).text()))
const ws = new WebSocket(C.webSocketDebuggerUrl, { perMessageDeflate: false })
let id = 0; const pending = new Map()
const send = (m, p = {}, s) => new Promise((r) => { const i = ++id; pending.set(i, r); ws.send(JSON.stringify({ id: i, method: m, params: p, sessionId: s })) })
ws.on('message', (d) => { const m = JSON.parse(d); if (m.id && pending.has(m.id)) { pending.get(m.id)(m.result); pending.delete(m.id) } })
await new Promise((r) => ws.on('open', r))
const { targetId } = await send('Target.createTarget', { url: 'about:blank' })
const { sessionId } = await send('Target.attachToTarget', { targetId, flatten: true })
const S = (m, p) => send(m, p, sessionId)
await S('Runtime.enable'); await S('Page.enable')
await S('Emulation.setDeviceMetricsOverride', { width: 400, height: 800, deviceScaleFactor: 2, mobile: true })
await S('Emulation.setTouchEmulationEnabled', { enabled: true, maxTouchPoints: 5 })
await S('Page.navigate', { url: 'http://127.0.0.1:5199/dragtest.html' })
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
const ev = async (e) => (await S('Runtime.evaluate', { expression: e, returnByValue: true })).result.value
for (let i = 0; i < 30; i++) { await sleep(1000); if (await ev(`!!document.querySelector('[data-task-id="101"]')`)) break }
const touch = (type, x, y) => S('Input.dispatchTouchEvent', { type, touchPoints: type === 'touchEnd' ? [] : [{ x, y, id: 1 }] })
// 用例1: 任务B(102) 把手拖到任务D(104) 下半 → 期望 REORDER 102 after 104
const g = JSON.parse(await ev(`JSON.stringify(document.querySelector('[data-task-id="102"] .grip').getBoundingClientRect())`))
await touch('touchStart', g.left + g.width / 2, g.top + g.height / 2)
await sleep(60)
const d = JSON.parse(await ev(`JSON.stringify(document.querySelector('[data-task-id="104"]').getBoundingClientRect())`))
await touch('touchMove', d.left + d.width / 2, d.top + d.height - 10)
await sleep(120)
console.log('row104 class:', await ev(`document.querySelector('[data-task-id="104"]').className`))
console.log('drag state:', await ev(`JSON.stringify({drag: !!document.querySelector('.task-row.dragging'), ghost: !!document.querySelector('.drag-ghost')})`))
await touch('touchEnd')
await sleep(200)
console.log('events:', await ev('window.__ev.join(" | ")'))
ws.close()
+40
View File
@@ -0,0 +1,40 @@
import WebSocket from 'ws'
const C = JSON.parse((await (await fetch('http://127.0.0.1:9223/json/version')).text()))
const ws = new WebSocket(C.webSocketDebuggerUrl, { perMessageDeflate: false })
let id = 0; const pending = new Map()
const send = (m, p = {}, s) => new Promise((r) => { const i = ++id; pending.set(i, r); ws.send(JSON.stringify({ id: i, method: m, params: p, sessionId: s })) })
ws.on('message', (d) => { const m = JSON.parse(d); if (m.id && pending.has(m.id)) { pending.get(m.id)(m.result); pending.delete(m.id) } })
await new Promise((r) => ws.on('open', r))
const targets = await send('Target.getTargets')
const t = targets.targetInfos.find((x) => x.url.includes('dragtest'))
const { sessionId } = await send('Target.attachToTarget', { targetId: t.targetId, flatten: true })
const S = (m, p) => send(m, p, sessionId)
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
const ev = async (e) => (await S('Runtime.evaluate', { expression: e, returnByValue: true })).result.value
const touch = (type, x, y) => S('Input.dispatchTouchEvent', { type, touchPoints: type === 'touchEnd' ? [] : [{ x, y, id: 1 }] })
const r = JSON.parse(await ev(`JSON.stringify(document.querySelector('[data-habit-id="2"] .grip').getBoundingClientRect())`))
const x = r.left + r.width / 2, y = r.top + r.height / 2
await touch('touchStart', x, y)
await sleep(60)
const t4 = JSON.parse(await ev(`JSON.stringify(document.querySelector('[data-habit-id="4"]').getBoundingClientRect())`))
const fx = t4.left + t4.width / 2, fy = t4.top + t4.height / 2
await touch('touchMove', fx, fy)
await sleep(150)
const scan = await ev(`(() => {
const out = []
document.querySelectorAll('*').forEach((el) => {
const cs = getComputedStyle(el)
const b = el.getBoundingClientRect()
if (b.width === 0 || b.height === 0) return
const bg = cs.backgroundColor
// 找带蓝色背景的元素
if (/rgb\\((22|2[0-9]|1[0-9]), ?1[0-9]{2}/.test(bg) || bg.includes('22, 119, 255') || bg.includes('0, 149, 255')) {
out.push({ tag: el.tagName, cls: el.className && String(el.className).slice(0, 40), bg, rect: [Math.round(b.left), Math.round(b.top), Math.round(b.width), Math.round(b.height)], text: (el.textContent || '').slice(0, 12) })
}
})
return JSON.stringify(out)
})()`)
console.log('blue elements:', scan)
console.log('ghost children:', await ev(`(() => { const g = document.querySelector('.drag-ghost'); return g ? g.children.length + ' kids, inner w ' + g.getBoundingClientRect().width : 'none' })()`))
await touch('touchEnd')
ws.close()
+35
View File
@@ -0,0 +1,35 @@
import WebSocket from 'ws'
import fs from 'node:fs'
const C = JSON.parse((await (await fetch('http://127.0.0.1:9223/json/version')).text()))
const ws = new WebSocket(C.webSocketDebuggerUrl, { perMessageDeflate: false })
let id = 0; const pending = new Map()
const send = (m, p = {}, s) => new Promise((r) => { const i = ++id; pending.set(i, r); ws.send(JSON.stringify({ id: i, method: m, params: p, sessionId: s })) })
ws.on('message', (d) => { const m = JSON.parse(d); if (m.id && pending.has(m.id)) { pending.get(m.id)(m.result); pending.delete(m.id) } })
await new Promise((r) => ws.on('open', r))
const { targetId } = await send('Target.createTarget', { url: 'about:blank' })
const { sessionId } = await send('Target.attachToTarget', { targetId, flatten: true })
const S = (m, p) => send(m, p, sessionId)
await S('Runtime.enable'); await S('Page.enable')
await S('Emulation.setDeviceMetricsOverride', { width: 400, height: 800, deviceScaleFactor: 2, mobile: true })
await S('Emulation.setTouchEmulationEnabled', { enabled: true, maxTouchPoints: 5 })
await S('Page.navigate', { url: 'http://127.0.0.1:5199/dragtest.html' })
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
const ev = async (e) => (await S('Runtime.evaluate', { expression: e, returnByValue: true })).result.value
for (let i = 0; i < 30; i++) { await sleep(1000); if (await ev(`!!document.querySelector('[data-habit-id="1"]')`)) break }
const touch = (type, x, y) => S('Input.dispatchTouchEvent', { type, touchPoints: type === 'touchEnd' ? [] : [{ x, y, id: 1 }] })
const r = JSON.parse(await ev(`JSON.stringify(document.querySelector('[data-habit-id="2"] .grip').getBoundingClientRect())`))
const x = r.left + r.width / 2, y = r.top + r.height / 2
await touch('touchStart', x, y)
await sleep(60)
const t = JSON.parse(await ev(`JSON.stringify(document.querySelector('[data-habit-id="4"]').getBoundingClientRect())`))
await touch('touchMove', t.x ?? t.left + t.width / 2, t.top + t.height / 2)
await sleep(120)
// 拖动中截图
const shot = await S('Page.captureScreenshot', { format: 'png' })
fs.writeFileSync(process.env.USERPROFILE + '\\AppData\\Local\\Temp\\drag-mid.png', Buffer.from(shot.data, 'base64'))
console.log('shot saved; ghost exists:', await ev(`!!document.querySelector('.drag-ghost')`))
console.log('ghost html head:', (await ev(`document.querySelector('.drag-ghost') ? document.querySelector('.drag-ghost').outerHTML.slice(0, 200) : 'NONE'`)))
await touch('touchEnd')
await sleep(200)
console.log('events:', await ev('window.__ev.join(" | ")'))
ws.close()
+36
View File
@@ -0,0 +1,36 @@
import WebSocket from 'ws'
const C = JSON.parse((await (await fetch('http://127.0.0.1:9223/json/version')).text()))
const ws = new WebSocket(C.webSocketDebuggerUrl, { perMessageDeflate: false })
let id = 0; const pending = new Map()
const send = (m, p = {}, s) => new Promise((r) => { const i = ++id; pending.set(i, r); ws.send(JSON.stringify({ id: i, method: m, params: p, sessionId: s })) })
ws.on('message', (d) => { const m = JSON.parse(d); if (m.id && pending.has(m.id)) { pending.get(m.id)(m.result); pending.delete(m.id) } })
await new Promise((r) => ws.on('open', r))
const { targetId } = await send('Target.createTarget', { url: 'about:blank' })
const { sessionId } = await send('Target.attachToTarget', { targetId, flatten: true })
const S = (m, p) => send(m, p, sessionId)
await S('Runtime.enable'); await S('Page.enable')
await S('Emulation.setDeviceMetricsOverride', { width: 400, height: 800, deviceScaleFactor: 2, mobile: true })
await S('Emulation.setTouchEmulationEnabled', { enabled: true, maxTouchPoints: 5 })
await S('Page.navigate', { url: 'http://127.0.0.1:5199/dragtest.html' })
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
const ev = async (e) => (await S('Runtime.evaluate', { expression: e, returnByValue: true })).result.value
for (let i = 0; i < 30; i++) { await sleep(1000); if (await ev(`!!document.querySelector('[data-task-id="3"]')`)) break }
await sleep(1000) // 等延迟数据加载完
const touch = (type, x, y) => S('Input.dispatchTouchEvent', { type, touchPoints: type === 'touchEnd' ? [] : [{ x, y, id: 1 }] })
// 用例: 按住任务3的把手 → 水平拖 +200px (带 +30 纵向漂移, 复现真机抖动) → 松手
const r = JSON.parse(await ev(`JSON.stringify(document.querySelector('[data-task-id="3"] .grip').getBoundingClientRect())`))
let x = r.left + r.width / 2, y = r.top + r.height / 2
console.log('grip rect', JSON.stringify(r))
await touch('touchStart', x, y)
await sleep(60)
for (const [dx, dy] of [[25, 4], [60, 10], [110, 18], [160, 26], [200, 30]]) {
await touch('touchMove', x + dx, y + dy)
await sleep(50)
}
console.log('row class after moves:', await ev(`document.querySelector('[data-task-id="3"]').className`))
console.log('meta:', await ev(`document.querySelector('[data-task-id="3"] .task-meta').textContent`))
await touch('touchEnd')
await sleep(300)
console.log('events:', await ev('window.__ev.join(" | ")'))
ws.close()
+30
View File
@@ -0,0 +1,30 @@
import WebSocket from 'ws'
import fs from 'node:fs'
const C = JSON.parse((await (await fetch('http://127.0.0.1:9223/json/version')).text()))
const ws = new WebSocket(C.webSocketDebuggerUrl, { perMessageDeflate: false })
let id = 0; const pending = new Map()
const send = (m, p = {}, s) => new Promise((r) => { const i = ++id; pending.set(i, r); ws.send(JSON.stringify({ id: i, method: m, params: p, sessionId: s })) })
ws.on('message', (d) => { const m = JSON.parse(d); if (m.id && pending.has(m.id)) { pending.get(m.id)(m.result); pending.delete(m.id) } })
await new Promise((r) => ws.on('open', r))
const targets = await send('Target.getTargets')
const t = targets.targetInfos.find((x) => x.url.includes('dragtest'))
const { sessionId } = await send('Target.attachToTarget', { targetId: t.targetId, flatten: true })
const S = (m, p) => send(m, p, sessionId)
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
const ev = async (e) => (await S('Runtime.evaluate', { expression: e, returnByValue: true })).result.value
await S('Page.reload')
await sleep(2500)
const touch = (type, x, y) => S('Input.dispatchTouchEvent', { type, touchPoints: type === 'touchEnd' ? [] : [{ x, y, id: 1 }] })
const r = JSON.parse(await ev(`JSON.stringify(document.querySelector('[data-habit-id="2"] .grip').getBoundingClientRect())`))
const x = r.left + r.width / 2, y = r.top + r.height / 2
await touch('touchStart', x, y)
await sleep(60)
const t4 = JSON.parse(await ev(`JSON.stringify(document.querySelector('[data-habit-id="4"]').getBoundingClientRect())`))
await touch('touchMove', t4.left + t4.width / 2, t4.top + t4.height / 2)
await sleep(150)
console.log('ghost bg:', await ev(`getComputedStyle(document.querySelector('.drag-ghost')).backgroundColor`))
console.log('ghost rect:', await ev(`JSON.stringify(document.querySelector('.drag-ghost').getBoundingClientRect())`))
const shot = await S('Page.captureScreenshot', { format: 'png' })
fs.writeFileSync(process.env.USERPROFILE + '\\AppData\\Local\\Temp\\drag-fixed.png', Buffer.from(shot.data, 'base64'))
await touch('touchEnd')
ws.close()
+8
View File
@@ -0,0 +1,8 @@
fix(V0.2): 拖动浮层残留蓝色背景 —— 类名与遗留 .drag-ghost 规则冲突
globals.css 里有一条无人使用的旧 .drag-ghost 规则 (蓝色气泡,
max-width 180px, 看板早期方案遗留), 排在新增规则之后同特异性胜出,
克隆卡片容器被套上蓝色背景并被压缩到 180px 宽, 即真机上手下面的
蓝色 div。删除遗留规则; 新规则显式清零背景/内边距并禁用
tap-highlight/user-select (克隆脱离 .mv-view/.task-content 祖先后
需自带防选中)。CDP 实测拖动中容器背景已透明、宽度正常。
+4
View File
@@ -0,0 +1,4 @@
const s = '问题,;、!'
console.log(JSON.stringify(s.replace(/[\s\p{P}]+$/u, '')))
console.log(JSON.stringify('正常标题'.replace(/[\s\p{P}]+$/u, '')))
console.log(JSON.stringify('a-b_c'.replace(/[\s\p{P}]+$/u, '')))
@@ -0,0 +1,5 @@
Exception during registration:
System.ComponentModel.Win32Exception (0x80004005): 系统找不到指定的文件。
在 System.Diagnostics.Process.StartWithCreateProcess(ProcessStartInfo startInfo)
在 System.CommandLine.Invocation.Process.StartProcess(String command, String args, String workingDir, Action`1 stdOut, Action`1 stdErr, ValueTuple`2[] environmentVariables)
在 System.CommandLine.Builder.CommandLineBuilderExtensions.<>c.<<RegisterWithDotnetSuggest>b__5_1>d.MoveNext()
+31
View File
@@ -0,0 +1,31 @@
# CHANGELOG
版本号规则见 [README.md](README.md)「版本管理」。前端与外壳版本号**互相独立**、独立发版,
分三部分记录;PATCH 仅在明确提出时自增, 否则沿用, 每次变更在对应部分追加一行。
---
## 程序版本 (Web / OTA)
**当前值: 0.2.0** (管理点: `package.json` 的 `version`; OTA 包 = 同值 + `-ota`)
| 日期 | 版本 | commit | 要点 |
| --- | --- | --- | --- |
| 2026-10-25 | 0.2.0 | — | V0.2 基线。前端与外壳版本解绑; build 标识统一 `YYYYMMDDHHmmss` (北京时间); PATCH 规则确立 (仅明确提出时自增) |
| 2026-10-25 | 0.2.1 | 93b7c7b | 主要解决离线编辑与不同终端数据合并问题 |
## Android 外壳版本
**当前值: 0.2.0** (管理点: `src-tauri/tauri.conf.json` 的 `version`, 须与 Cargo.toml/Cargo.lock 组内一致; 产物 = APK versionName)
| 日期 | 版本 | commit | 要点 |
| --- | --- | --- | --- |
| 2026-10-25 | 0.2.0 | — | V0.2 基线。外壳与前端版本解绑, 独立维护 (android-* 分支出包) |
## Windows 外壳版本
**当前值: —** (尚未建立; 建立后管理点同为 `src-tauri/tauri.conf.json` + Cargo 三件套, 走独立分支/打包工作流)
| 日期 | 版本 | commit | 要点 |
| --- | --- | --- | --- |
| — | — | — | 暂无 |
+84 -72
View File
@@ -1,73 +1,85 @@
# ===============================================================
# iBoard 一体化镜像: 静态前端 (dist/) + Express API (server/) 同进程
# 构建: docker build --build-arg DATABASE_URL=... -t iboard:latest .
# 可选 --build-arg VITE_API_BASE=https://api.example.com 让前端走绝对地址
# 运行: docker run -d --name iboard -p 3001:3001 -e DATABASE_URL=... iboard:latest
# ===============================================================
FROM node:22-alpine AS builder
WORKDIR /app
# 国内构建时可换源
RUN npm config set registry https://registry.npmmirror.com
# 先拷贝 package 文件, 利用 Docker 缓存
COPY package*.json ./
COPY prisma ./prisma/
COPY prisma.config.ts ./
COPY generated ./generated/
# 装全量依赖 (dev 也装, 后面 build 要用)
RUN npm install
# 拷贝源码
COPY . .
# 构建前端 (输出到 dist/) 与 Prisma 客户端
RUN npx prisma generate
# build 时把 VITE_API_BASE 写进 bundle, 默认空 = 走相对路径 /api (同源 Express)
ARG VITE_API_BASE=
ENV VITE_API_BASE=$VITE_API_BASE
RUN npm run build
# 运行时阶段
FROM node:22-alpine
WORKDIR /app
# 阿里源加速
RUN npm config set registry https://registry.npmmirror.com
# 只装运行时依赖
COPY package*.json ./
RUN npm install --omit=dev
# 拷贝构建产物
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/server ./server
COPY --from=builder /app/generated ./generated
COPY --from=builder /app/prisma ./prisma
COPY --from=builder /app/prisma.config.ts ./prisma.config.ts
COPY --from=builder /app/scripts ./scripts
COPY --from=builder /app/content ./content
COPY --from=builder /app/public ./public
# OTA 升级包: CI (docker-build.yml) 生成的 ota-package/, 随镜像发布
# 本地构建时只有占位文件 (ota-package/README.md), server 找不到 latest.json 则 OTA 返回"无更新"
COPY ota-package ./ota-package
ENV OTA_PACKAGE_DIR=/app/ota-package
# OTA bundle 下载用的绝对 base (manifest.url 必须是绝对 URL, 插件不拼 base, 相对路径会导致下载 0% 失败)
# 生产为 http 明文 (无 https), 客户端 tauri.conf.json 的 hotswap.require_https 已设为 false 放行 http
ENV OTA_PUBLIC_BASE=http://bh.vps.honor3.com:3001
# 健康检查
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget -qO- http://127.0.0.1:3001/api/health || exit 1
EXPOSE 3001
ENV NODE_ENV=production
ENV PORT=3001
# Node 22 默认未开 strip-types, 显式开启以便加载 generated/prisma/*.ts
# syntax=docker/dockerfile:1
# ===============================================================
# iBoard 一体化镜像: 静态前端 (dist/) + Express API (server/) 同进程
# 构建: docker build --build-arg DATABASE_URL=... -t iboard:latest .
# 可选 --build-arg VITE_API_BASE=https://api.example.com 让前端走绝对地址
# CI 传入 --build-arg PREBUILT_DIST=true 复用 CI 已构建的 dist/ (跳过镜像内前端构建)
# 运行: docker run -d --name iboard -p 3001:3001 -e DATABASE_URL=... iboard:latest
# 需要 BuildKit (cache mount 语法; Docker 23+ 默认开启)
# ===============================================================
FROM node:22-alpine AS builder
WORKDIR /app
# 国内构建时可换源
RUN npm config set registry https://registry.npmmirror.com
# 先拷贝 package 文件, 利用 Docker 缓存
COPY package*.json ./
COPY prisma ./prisma/
COPY prisma.config.ts ./
COPY generated ./generated/
# 装全量依赖 (dev 也装, 后面 prisma generate / 本地构建前端要用)
# cache mount 复用 npm 下载缓存, 不进镜像层
RUN --mount=type=cache,target=/root/.npm npm ci
# 拷贝源码
COPY . .
# 构建 Prisma 客户端
RUN --mount=type=cache,target=/root/.npm npx prisma generate
# 构建前端 (输出到 dist/)
# PREBUILT_DIST=true: CI 已在镜像外构建过 dist/ (供 OTA 打包), 直接复用, 跳过镜像内构建
ARG PREBUILT_DIST=false
# build 时把 VITE_API_BASE 写进 bundle, 默认空 = 走相对路径 /api (同源 Express)
ARG VITE_API_BASE=
ENV VITE_API_BASE=$VITE_API_BASE
RUN if [ "$PREBUILT_DIST" = "true" ]; then \
test -f dist/index.html || { echo "PREBUILT_DIST=true 但 dist/ 缺失"; exit 1; }; \
else \
npm run build; \
fi
# 运行时阶段
FROM node:22-alpine
WORKDIR /app
# 阿里源加速
RUN npm config set registry https://registry.npmmirror.com
# 只装运行时依赖 (better-sqlite3 是 @powersync/node 的 devOptional peer, 需同时 omit optional 才能跳过)
COPY package*.json ./
RUN --mount=type=cache,target=/root/.npm npm ci --omit=dev --omit=optional
# 拷贝构建产物
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/server ./server
COPY --from=builder /app/generated ./generated
COPY --from=builder /app/prisma ./prisma
COPY --from=builder /app/prisma.config.ts ./prisma.config.ts
COPY --from=builder /app/scripts ./scripts
COPY --from=builder /app/content ./content
COPY --from=builder /app/public ./public
# OTA 升级包: CI (docker-build.yml) 生成的 ota-package/, 随镜像发布
# 本地构建时只有占位文件 (ota-package/README.md), server 找不到 latest.json 则 OTA 返回"无更新"
COPY ota-package ./ota-package
ENV OTA_PACKAGE_DIR=/app/ota-package
# OTA bundle 下载用的绝对 base (manifest.url 必须是绝对 URL, 插件不拼 base, 相对路径会导致下载 0% 失败)
# 生产为 http 明文 (无 https), 客户端 tauri.conf.json 的 hotswap.require_https 已设为 false 放行 http
ENV OTA_PUBLIC_BASE=https://iboard.honor3.com
# 健康检查 (端口跟随 PORT 环境变量, 0.2.1 部署为 13001)
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget -qO- http://127.0.0.1:${PORT:-3001}/api/health || exit 1
EXPOSE 3001
ENV NODE_ENV=production
ENV PORT=3001
# Node 22 默认未开 strip-types, 显式开启以便加载 generated/prisma/*.ts
CMD ["node", "--experimental-strip-types", "--no-warnings", "server/index.js"]
+316
View File
@@ -0,0 +1,316 @@
# iBoard
跨平台个人看板 (Tauri + React + Express + MariaDB)。用于收集和整理经济数据,但是以策略为主体重新组织。
- 宏观:新闻 / 经济文章(本地),以及外接 ifinance 模块的分类(经济数据 / 策略研究 / 看板,运行时动态挂载为 tab)
- 数据与策略由外接 ifinance 模块承担(静态包,见 `../ifinance/readme.md`)
- 报警并入「监控」;油价模块已下线
## 宏观
宏观页固定「新闻 / 经济文章」两个本地 tab,并动态挂载外接 ifinance `info.json` 的 `categories[]` 作为额外 tab:
- 新闻:每日夜间 3 点通过妙想 MCP 获取国内外最新经济政治新闻,按影响大小排序 10 条,存入数据库;列表按时间倒序无限滚动。
- 经济文章:由 `content/articles/*.md` 承载的长文。
- 经济数据 / 策略研究 / …:交给外接 ifinance 模块,宿主只读取 `info.json` 并用 iframe 打开条目页(纯前端直连,见 `ifinance/readme.md` §9)。iBoard 本地不再保存指标 / 策略数据。
## 监控
监控分为主机监控 / 服务监控 / 环境监控 / 报警四个 tab。主机监控对接 komari:<https://komari.honor3.com/>;报警 tab 展示跨源数据差异与定时任务异常,按时间倒序。
---
## 分支规则
### 分支类型与职责
| 分支 | 职责 | 部署 | 合并方向 |
| --- | --- | --- | --- |
| `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/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` 并推送后才会部署。
### 日常操作规则(工作目录约定)
- 主工作目录(`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`)**:
```bash
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](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`](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 优先命中覆盖包
```
### 发布说明与提交号(2026-10-25 起)
- CI(docker-build.yml)把**触发构建的 commit message**(标题+正文,去空行,截断 400 字)作为 `notes` 写入 latest.json;
- commit sha 经 `--commit` 参数单独写入 `commit` 字段;
- "我的"页:发布说明展示 commit message,「提交」行展示短 sha。
### 首次部署
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 "修复登录按钮" --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/便携包、也不必重装。
```bash
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 二进制。
+11
View File
@@ -0,0 +1,11 @@
# 复制为 .env (服务器上 chmod 600, 已 gitignore)。
# POWERSYNC_JWT_KEY_B64 = base64url(POWERSYNC_JWT_SECRET 的 UTF-8 字节)
# 生成: printf %s "$POWERSYNC_JWT_SECRET" | openssl base64 -A | tr '+/' '-_' | tr -d '='
PG_PASSWORD=change-me
PS_STORAGE_PASSWORD=change-me
JWT_SECRET_021=change-me
POWERSYNC_JWT_SECRET=change-me
POWERSYNC_JWT_KEY_B64=change-me
POWERSYNC_URL=https://bh.vps.honor3.com:13443
OTA_PUBLIC_BASE=http://bh.vps.honor3.com:13001
+61
View File
@@ -0,0 +1,61 @@
# 0.2.1 独立验证部署 (baihu /app/iboard-0.2.1)
与现网 `/app/iboard`(3001/宿主 MySQL) 完全隔离:本栈使用独立 PG 容器、端口 13443/13001/13002,不写原库。
**浏览器入口: `https://bh.vps.honor3.com:13443`**(自签证书,首次访问需在浏览器"高级→继续访问"接受一次;PowerSync 网页端必须运行在安全上下文,http://13001 入口仅供接口调试,任务模块会报 Navigator locks 错误)。
正式启用需替换受信证书:把 `bh.pem/bh.key` 换成受信证书后 `docker compose restart tls-front`(例如 Let's Encrypt DNS-01,80/443 已被 gateway 占用无法 HTTP-01)。
## 栈
| 服务 | 说明 |
| --- | --- |
| `pg` | postgres:16-alpine,`wal_level=logical`(供 PowerSync 逻辑复制),数据在 `./data/pg`,**不对外暴露端口** |
| `app` | 本仓库 Dockerfile 构建,13001 对外;启动时先 `prisma migrate deploy`;`DISABLE_CRON=1` 关闭定时任务 |
| `powersync` | journeyapps/powersync-service,13002 对外;源库=pg,bucket storage=同实例 PG 的 powersync schema |
## 首次部署
```bash
# 服务器上 (源码已上传到 /app/iboard-0.2.1)
cd /app/iboard-0.2.1/deploy/0.2.1
# 1. 生成 .env (参照 .env.example; POWERSYNC_JWT_KEY_B64 由 secret 派生)
umask 077
PG_PASSWORD=$(openssl rand -hex 16)
PS_STORAGE_PASSWORD=$(openssl rand -hex 16)
JWT_SECRET_021=$(openssl rand -hex 32)
POWERSYNC_JWT_SECRET=$(openssl rand -hex 32)
POWERSYNC_JWT_KEY_B64=$(printf %s "$POWERSYNC_JWT_SECRET" | openssl base64 -A | tr '+/' '-_' | tr -d '=')
cat > .env <<EOF
PG_PASSWORD=$PG_PASSWORD
PS_STORAGE_PASSWORD=$PS_STORAGE_PASSWORD
JWT_SECRET_021=$JWT_SECRET_021
POWERSYNC_JWT_SECRET=$POWERSYNC_JWT_SECRET
POWERSYNC_JWT_KEY_B64=$POWERSYNC_JWT_KEY_B64
POWERSYNC_URL=http://bh.vps.honor3.com:13002
OTA_PUBLIC_BASE=http://bh.vps.honor3.com:13001
EOF
# 2. 构建并启动
docker compose build app
docker compose up -d
# 3. 验证
docker compose ps
curl -s http://127.0.0.1:13001/api/health
docker compose logs powersync --tail 50 # 应看到连接源库/建 publication/sync config 部署成功
```
## 日常更新
```bash
cd /app/iboard-0.2.1
git archive 方式重新上传源码 tar 后解包覆盖 (data/.env 不受影响)
cd deploy/0.2.1 && docker compose build app && docker compose up -d app
```
## 注意
- `./data/pg` 为 0.2.1 独立数据卷;`./.env` 为密钥,两者均已 gitignore,更新源码时不会覆盖 `.env`(tar 解包不产生它)。
- PowerSync 的 publication / replication slot 由 service 自动管理(账号 iboard021 为容器内 superuser)。
- 同步规则改动:编辑 `powersync/sync-config.yaml` 后 `docker compose restart powersync`。
+84
View File
@@ -0,0 +1,84 @@
name: iboard-021
# 0.2.1 独立验证栈: PG (逻辑复制) + PowerSync + App, 与现网 /app/iboard 完全隔离
# 见 docs/V0.2/离线编辑模块/0.2.1实施方案.md; 端口 13001 (app) / 13002 (powersync), PG 不对外
services:
pg:
image: postgres:16-alpine
restart: unless-stopped
command: ["postgres", "-c", "wal_level=logical"]
environment:
POSTGRES_USER: iboard021
POSTGRES_PASSWORD: ${PG_PASSWORD:?PG_PASSWORD missing}
POSTGRES_DB: iboard021
# initdb 脚本用: 创建 powersync bucket storage 用户
PS_STORAGE_PASSWORD: ${PS_STORAGE_PASSWORD:?PS_STORAGE_PASSWORD missing}
volumes:
- ./data/pg:/var/lib/postgresql/data
- ./initdb:/docker-entrypoint-initdb.d:ro
healthcheck:
test: ["CMD-SHELL", "pg_isready -U iboard021 -d iboard021"]
interval: 5s
timeout: 5s
retries: 12
app:
build:
context: ../..
dockerfile: Dockerfile
restart: unless-stopped
depends_on:
pg:
condition: service_healthy
environment:
DATABASE_URL: postgresql://iboard021:${PG_PASSWORD}@pg:5432/iboard021
PORT: "13001"
JWT_SECRET: ${JWT_SECRET_021:?JWT_SECRET_021 missing}
POWERSYNC_JWT_SECRET: ${POWERSYNC_JWT_SECRET:?POWERSYNC_JWT_SECRET missing}
POWERSYNC_URL: ${POWERSYNC_URL:-http://bh.vps.honor3.com:13002}
OTA_PUBLIC_BASE: ${OTA_PUBLIC_BASE:-http://bh.vps.honor3.com:13001}
# 0.2.1 验证部署关闭定时任务 (新闻/油价/米家等), 避免重复外呼
DISABLE_CRON: "1"
command: ["sh", "-c", "npx prisma migrate deploy && node --experimental-strip-types --no-warnings server/index.js"]
ports:
- "13001:13001"
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:13001/api/health || exit 1"]
interval: 30s
timeout: 5s
retries: 5
start_period: 20s
# TLS 前置 (自签证书): PowerSync 网页端必须在安全上下文运行 (navigator.locks),
# 因此浏览器入口统一走 https://bh.vps.honor3.com:13443 (证书接受一次即可, app+sync 同源同证书)
tls-front:
image: nginx:alpine
restart: unless-stopped
depends_on:
- app
- powersync
volumes:
- ./tls/nginx.conf:/etc/nginx/conf.d/default.conf:ro
- ./data/tls:/etc/nginx/certs:ro
ports:
- "13443:443"
powersync:
image: journeyapps/powersync-service:latest
restart: unless-stopped
depends_on:
pg:
condition: service_healthy
command: ["start", "-r", "unified"]
volumes:
- ./powersync/service.yaml:/config/service.yaml:ro
- ./powersync/sync-config.yaml:/config/sync-config.yaml:ro
environment:
POWERSYNC_CONFIG_PATH: /config/service.yaml
# service.yaml 内 !env PS_* 引用 (整值替换; URI 在此组装)
PS_SOURCE_URI: postgresql://iboard021:${PG_PASSWORD}@pg:5432/iboard021?sslmode=disable
PS_STORAGE_URI: postgresql://powersync_storage:${PS_STORAGE_PASSWORD}@pg:5432/iboard021?sslmode=disable
PS_JWT_KEY_B64: ${POWERSYNC_JWT_KEY_B64:?POWERSYNC_JWT_KEY_B64 missing}
ports:
- "13002:8080"
+13
View File
@@ -0,0 +1,13 @@
#!/bin/bash
# 一次性 ETL: 清空 0.2.1 库 -> 从现网 MariaDB 只读快照导入
# 现网连接串从 iboard 容器环境变量读取 (不回显), 源库仅 SELECT
set -e
cd /app/iboard-0.2.1/deploy/0.2.1
SRC=$(docker inspect iboard --format '{{range .Config.Env}}{{println .}}{{end}}' | grep '^DATABASE_URL=' | cut -d= -f2-)
[ -n "$SRC" ] || { echo '未取到现网 DATABASE_URL'; exit 1; }
echo '--- truncate 0.2.1 库 ---'
docker compose exec -T pg psql -U iboard021 -d iboard021 -c "TRUNCATE TABLE \"User\",\"Project\",\"ProjectMember\",\"BoardColumn\",\"Task\",\"TaskMember\",\"TaskTag\",\"Habit\",\"CheckIn\",\"CheckList\",\"CheckListItem\",\"CheckListTemplate\",\"Article\",\"Strategy\",\"Indicator\",\"IndicatorPoint\",\"News\",\"Alert\",\"ServerNode\",\"ServerNodeHistory\",\"ServiceCheck\",\"Econ_SowInventory\",\"ScraperCache\",\"OilPriceHistory\",\"MihomeAccount\",\"MihomeHome\",\"MihomeDevice\",\"MihomeEnvSample\",\"MihomeEnvHour\" RESTART IDENTITY CASCADE;"
echo '--- etl run ---'
docker compose exec -T -e SRC_DATABASE_URL="$SRC" app node scripts/etl-mariadb-to-pg.mjs
@@ -0,0 +1,18 @@
#!/bin/bash
# 一次性初始化 (仅首次建库时执行): 创建 powersync bucket storage 专用用户
# PG >= 14 允许源库与 bucket storage 同实例 (powersync schema 由 service 自建)
set -e
: "${PS_STORAGE_PASSWORD:?PS_STORAGE_PASSWORD missing}"
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
DO \$\$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'powersync_storage') THEN
CREATE USER powersync_storage WITH PASSWORD '$PS_STORAGE_PASSWORD';
END IF;
END
\$\$;
GRANT CREATE ON DATABASE $POSTGRES_DB TO powersync_storage;
EOSQL
echo "[initdb] powersync_storage user ready."
+10
View File
@@ -0,0 +1,10 @@
-- 一次性初始化: PowerSync 逻辑复制所需 publication (服务不自建, 见 PSYNC_S1141)
-- 仅发布任务域 + User; 新增同步表时需 ALTER PUBLICATION powersync ADD TABLE ...
CREATE PUBLICATION powersync FOR TABLE
"Project",
"ProjectMember",
"BoardColumn",
"Task",
"TaskMember",
"TaskTag",
"User";
+41
View File
@@ -0,0 +1,41 @@
# PowerSync Service 主配置 (0.2.1 验证部署)
# 参考: https://docs.powersync.com/configuration/powersync-service/self-hosted-instances
# 注意: !env 是 YAML 整值标签, 只能整个值替换变量, 不能内嵌在 URI 字符串中;
# 完整连接串由 docker-compose 以 PS_* 环境变量注入。
replication:
connections:
- type: postgresql
# 源库: 本栈 pg 容器 (0.2.1 独立实例), 复制走 iboard021 账号
uri: !env PS_SOURCE_URI
sslmode: disable
# bucket storage: 同实例 PG (16 >= 14 允许同库不同 schema, service 自建 powersync schema)
storage:
type: postgresql
uri: !env PS_STORAGE_URI
sslmode: disable
port: 8080
sync_config:
path: /config/sync-config.yaml
# 客户端鉴权: app 后端 /api/powersync/token 签发 HS256 JWT (aud=powersync)
# oct 密钥 k = base64url(POWERSYNC_JWT_SECRET)
client_auth:
jwks:
keys:
- kty: oct
k: !env PS_JWT_KEY_B64
alg: HS256
audience:
- powersync
telemetry:
disable_telemetry_sharing: true
system:
logging:
level: info
format: text
+25
View File
@@ -0,0 +1,25 @@
# Sync Streams (edition 3): 任务模块 6 表 + User (展示用)
# Sequential ID Mapping (见 docs/V0.2/离线编辑模块/0.2.1实施方案.md §3):
# - 客户端主键 = uuid 列 (离线新建行同步回填后 id 稳定); int 自增 id 不下发
# - 外键输出 uuid 镜像列, 别名回 projectId/columnId/parentId/taskId (客户端字段名不变)
# - 引号保留 PG PascalCase 标识; 可见性按 ProjectMember 过滤
config:
edition: 3
streams:
iboard_users:
auto_subscribe: true
query: SELECT CAST("id" AS TEXT) AS "id", "name", "email" FROM "User"
iboard_task_data:
auto_subscribe: true
with:
my_projects: SELECT "projectId" FROM "ProjectMember" WHERE CAST("userId" AS TEXT) = auth.user_id()
queries:
- SELECT "uuid" AS "id", "uuid", "code", "name", CAST("ownerId" AS TEXT) AS "ownerId", "isPersonal", "archived", "notes", "taskKinds", "icon", "autoArchive", "createdAt", "updatedAt" FROM "Project" WHERE "id" IN my_projects
- SELECT "uuid" AS "id", "uuid", "projectUuid" AS "projectId", CAST("userId" AS TEXT) AS "userId", "permission", "createdAt" FROM "ProjectMember" WHERE "projectId" IN my_projects
- SELECT "uuid" AS "id", "uuid", "projectUuid" AS "projectId", "name", "sortOrder", "boundStatus", "retention", "createdAt", "updatedAt" FROM "BoardColumn" WHERE "projectId" IN my_projects
- SELECT "uuid" AS "id", "uuid", "projectUuid" AS "projectId", "columnUuid" AS "columnId", "parentUuid" AS "parentId", "title", "description", "kind", "markDate", "status", "doneAt", CAST("assigneeId" AS TEXT) AS "assigneeId", CAST("creatorId" AS TEXT) AS "creatorId", "extras", "sortOrder", "number", "archived", "archivedAt", "archiveSource", "createdAt", "updatedAt" FROM "Task" WHERE "projectId" IN my_projects
- SELECT "uuid" AS "id", "uuid", "taskUuid" AS "taskId", CAST("userId" AS TEXT) AS "userId" FROM "TaskMember" WHERE "taskId" IN (SELECT "id" FROM "Task" WHERE "projectId" IN my_projects)
- SELECT "uuid" AS "id", "uuid", "taskUuid" AS "taskId", "name" FROM "TaskTag" WHERE "taskId" IN (SELECT "id" FROM "Task" WHERE "projectId" IN my_projects)
+37
View File
@@ -0,0 +1,37 @@
# 0.2.1 TLS 前置: 单端口 13443, 按路径分流 (自签证书, 见 README)
# PowerSync 页面必须在安全上下文运行 (navigator.locks 仅 HTTPS/localhost), 故 app 与 sync 统一走 TLS
server {
listen 443 ssl;
server_name bh.vps.honor3.com;
ssl_certificate /etc/nginx/certs/bh.pem;
ssl_certificate_key /etc/nginx/certs/bh.key;
# PowerSync (HTTP 流式 + WebSocket 兜底)
location /sync/ {
proxy_pass http://powersync:8080;
proxy_http_version 1.1;
proxy_set_header Connection '';
proxy_buffering off;
proxy_read_timeout 3600s;
}
location /socket/ {
proxy_pass http://powersync:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 3600s;
}
location /health {
proxy_pass http://powersync:8080;
}
# 应用 (静态前端 + API)
location / {
proxy_pass http://app:13001;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
}
}
+9
View File
@@ -0,0 +1,9 @@
# 生产环境变量 (deploy/prod/.env, 权限 600, 绝不入库)
# POWERSYNC_JWT_KEY_B64 = base64url(POWERSYNC_JWT_SECRET 的 UTF-8 字节)
# printf %s "$POWERSYNC_JWT_SECRET" | openssl base64 -A | tr '+/' '-_' | tr -d '='
PG_PASSWORD=change-me
PS_STORAGE_PASSWORD=change-me
JWT_SECRET=change-me
POWERSYNC_JWT_SECRET=change-me
POWERSYNC_JWT_KEY_B64=change-me
PUBLIC_URL=https://iboard.honor3.com
+59
View File
@@ -0,0 +1,59 @@
# iBoard 0.2.1 生产部署 (单端口, 无独立同步端口)
```
pg (内网) ← app (3001 对外, /sync 反代) → powersync (内网)
```
对外只有 **3001** 一个端口,域名 `https://iboard.honor3.com` 由 NPM 反代到这里;
同步服务不再单独暴露端口(客户端拿到的 `POWERSYNC_URL` 是域名本身,请求经 app 的
`/sync`、`/socket` 反代进入内网 powersync)。
## 首次部署顺序 (关键: publication 要在建表之后)
```bash
# 0) 目录: /app/iboard-prod (源码已上传), 镜像 iboard:latest 已构建
cd /app/iboard-prod/deploy/prod
cp .env.example .env && chmod 600 .env # 填真实密钥
mkdir -p data/pg data/cache
# 1) 先只起数据库
docker compose --env-file .env up -d pg
# 2) 跑迁移建表 (一次性)
docker run --rm --network iboard-prod_default \
-e DATABASE_URL="postgresql://iboard:<PG_PASSWORD>@pg:5432/iboard" \
iboard:latest npx prisma migrate deploy
# 3) 建逻辑复制 publication (表已存在)
docker compose --env-file .env exec -T pg \
psql -U iboard -d iboard -f /dev/stdin < sql/publication.sql
# 4) 起应用与同步服务
docker compose --env-file .env up -d app powersync
# 5) 验证
curl -s http://127.0.0.1:3001/api/health
docker compose logs --tail 20 powersync # 应看到 replication 激活
```
## 数据迁移 (从现网 MySQL 快照)
```bash
# 只读源库; 目标必须是空库 (有数据会跳过, ETL_FORCE=1 强制)
docker run --rm --network iboard-prod_default \
-e DATABASE_URL="postgresql://iboard:<PG_PASSWORD>@pg:5432/iboard" \
-e SRC_DATABASE_URL="mysql://<user>:<pass>@<host>:3306/iboard" \
iboard:latest node scripts/etl-mariadb-to-pg.mjs
```
迁移后需要重启 powersync 让它重新快照:
```bash
docker compose --env-file .env restart powersync
```
## 版本与回滚
- 新版本: 构建新镜像 → `docker compose up -d app` (只重建 app, 数据卷不动)
- 回滚: 用上一次的镜像标签重新 `up -d app`
- 数据: `data/pg` 持久化; 变更前建议 `docker compose exec -T pg pg_dump -U iboard iboard > backup.sql`
+82
View File
@@ -0,0 +1,82 @@
name: iboard-prod
# iBoard 0.2.1 生产栈 (单端口对外)
# - app : 3001 对外 (唯一入口), 内部把 /sync 反代到 powersync
# - pg : PostgreSQL 16, 仅容器内网 (逻辑复制供同步服务消费)
# - powersync : 同步服务, 仅容器内网 (不对外暴露端口)
#
# 与旧的生产方式区别: 不再依赖宿主 MySQL, 数据库与同步服务都在栈内;
# 对外只有 3001 一个端口, 域名 https://iboard.honor3.com (NPM 反代) 即可。
#
# 用法 (baihu:/app/iboard-prod):
# cp deploy/prod/.env.example deploy/prod/.env # 填密钥
# docker compose --env-file deploy/prod/.env -f deploy/prod/docker-compose.yml up -d
services:
pg:
image: postgres:16-alpine
restart: unless-stopped
command: ["postgres", "-c", "wal_level=logical"]
environment:
POSTGRES_USER: iboard
POSTGRES_PASSWORD: ${PG_PASSWORD:?PG_PASSWORD missing}
POSTGRES_DB: iboard
PS_STORAGE_PASSWORD: ${PS_STORAGE_PASSWORD:?PS_STORAGE_PASSWORD missing}
volumes:
- ./data/pg:/var/lib/postgresql/data
- ./initdb:/docker-entrypoint-initdb.d:ro
healthcheck:
test: ["CMD-SHELL", "pg_isready -U iboard -d iboard"]
interval: 5s
timeout: 5s
retries: 12
app:
image: iboard:latest
build:
context: ../..
dockerfile: Dockerfile
restart: unless-stopped
depends_on:
pg:
condition: service_healthy
environment:
DATABASE_URL: postgresql://iboard:${PG_PASSWORD}@pg:5432/iboard
PORT: "3001"
JWT_SECRET: ${JWT_SECRET:?JWT_SECRET missing}
POWERSYNC_JWT_SECRET: ${POWERSYNC_JWT_SECRET:?POWERSYNC_JWT_SECRET missing}
# 客户端拿到的同步地址 = 同域 (经由 app 的 /sync 反代)
POWERSYNC_URL: ${PUBLIC_URL:-https://iboard.honor3.com}
# app 内部转发同步请求到内网 powersync
POWERSYNC_UPSTREAM: http://powersync:8080
OTA_PUBLIC_BASE: ${PUBLIC_URL:-https://iboard.honor3.com}
OTA_PACKAGE_DIR: /app/ota-package
MIHOME_CACHE_DIR: /app/cache
volumes:
- ./data/cache:/app/cache
# OTA 热更包 (CI 构建产物, 由流水线写入宿主 data/ota)
- ./data/ota:/app/ota-package
ports:
- "${HOST_PORT:-3001}:3001"
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:3001/api/health || exit 1"]
interval: 30s
timeout: 5s
retries: 5
start_period: 20s
powersync:
image: journeyapps/powersync-service:latest
restart: unless-stopped
depends_on:
pg:
condition: service_healthy
command: ["start", "-r", "unified"]
volumes:
- ./powersync/service.yaml:/config/service.yaml:ro
- ./powersync/sync-config.yaml:/config/sync-config.yaml:ro
environment:
POWERSYNC_CONFIG_PATH: /config/service.yaml
PS_SOURCE_URI: postgresql://iboard:${PG_PASSWORD}@pg:5432/iboard?sslmode=disable
PS_STORAGE_URI: postgresql://powersync_storage:${PS_STORAGE_PASSWORD}@pg:5432/iboard?sslmode=disable
PS_JWT_KEY_B64: ${POWERSYNC_JWT_KEY_B64:?POWERSYNC_JWT_KEY_B64 missing}
@@ -0,0 +1,17 @@
#!/bin/bash
# 生产 PG 初始化: PowerSync bucket storage 专用用户
set -e
: "${PS_STORAGE_PASSWORD:?PS_STORAGE_PASSWORD missing}"
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
DO \$\$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'powersync_storage') THEN
CREATE USER powersync_storage WITH PASSWORD '$PS_STORAGE_PASSWORD';
END IF;
END
\$\$;
GRANT CREATE ON DATABASE $POSTGRES_DB TO powersync_storage;
EOSQL
echo '[initdb] powersync_storage ready.'
+41
View File
@@ -0,0 +1,41 @@
# PowerSync Service 主配置 (0.2.1 验证部署)
# 参考: https://docs.powersync.com/configuration/powersync-service/self-hosted-instances
# 注意: !env 是 YAML 整值标签, 只能整个值替换变量, 不能内嵌在 URI 字符串中;
# 完整连接串由 docker-compose 以 PS_* 环境变量注入。
replication:
connections:
- type: postgresql
# 源库: 本栈 pg 容器 (0.2.1 独立实例), 复制走 iboard021 账号
uri: !env PS_SOURCE_URI
sslmode: disable
# bucket storage: 同实例 PG (16 >= 14 允许同库不同 schema, service 自建 powersync schema)
storage:
type: postgresql
uri: !env PS_STORAGE_URI
sslmode: disable
port: 8080
sync_config:
path: /config/sync-config.yaml
# 客户端鉴权: app 后端 /api/powersync/token 签发 HS256 JWT (aud=powersync)
# oct 密钥 k = base64url(POWERSYNC_JWT_SECRET)
client_auth:
jwks:
keys:
- kty: oct
k: !env PS_JWT_KEY_B64
alg: HS256
audience:
- powersync
telemetry:
disable_telemetry_sharing: true
system:
logging:
level: info
format: text
+25
View File
@@ -0,0 +1,25 @@
# Sync Streams (edition 3): 任务模块 6 表 + User (展示用)
# Sequential ID Mapping (见 docs/V0.2/离线编辑模块/0.2.1实施方案.md §3):
# - 客户端主键 = uuid 列 (离线新建行同步回填后 id 稳定); int 自增 id 不下发
# - 外键输出 uuid 镜像列, 别名回 projectId/columnId/parentId/taskId (客户端字段名不变)
# - 引号保留 PG PascalCase 标识; 可见性按 ProjectMember 过滤
config:
edition: 3
streams:
iboard_users:
auto_subscribe: true
query: SELECT CAST("id" AS TEXT) AS "id", "name", "email" FROM "User"
iboard_task_data:
auto_subscribe: true
with:
my_projects: SELECT "projectId" FROM "ProjectMember" WHERE CAST("userId" AS TEXT) = auth.user_id()
queries:
- SELECT "uuid" AS "id", "uuid", "code", "name", CAST("ownerId" AS TEXT) AS "ownerId", "isPersonal", "archived", "notes", "taskKinds", "icon", "autoArchive", "createdAt", "updatedAt" FROM "Project" WHERE "id" IN my_projects
- SELECT "uuid" AS "id", "uuid", "projectUuid" AS "projectId", CAST("userId" AS TEXT) AS "userId", "permission", "createdAt" FROM "ProjectMember" WHERE "projectId" IN my_projects
- SELECT "uuid" AS "id", "uuid", "projectUuid" AS "projectId", "name", "sortOrder", "boundStatus", "retention", "createdAt", "updatedAt" FROM "BoardColumn" WHERE "projectId" IN my_projects
- SELECT "uuid" AS "id", "uuid", "projectUuid" AS "projectId", "columnUuid" AS "columnId", "parentUuid" AS "parentId", "title", "description", "kind", "markDate", "status", "doneAt", CAST("assigneeId" AS TEXT) AS "assigneeId", CAST("creatorId" AS TEXT) AS "creatorId", "extras", "sortOrder", "number", "archived", "archivedAt", "archiveSource", "createdAt", "updatedAt" FROM "Task" WHERE "projectId" IN my_projects
- SELECT "uuid" AS "id", "uuid", "taskUuid" AS "taskId", CAST("userId" AS TEXT) AS "userId" FROM "TaskMember" WHERE "taskId" IN (SELECT "id" FROM "Task" WHERE "projectId" IN my_projects)
- SELECT "uuid" AS "id", "uuid", "taskUuid" AS "taskId", "name" FROM "TaskTag" WHERE "taskId" IN (SELECT "id" FROM "Task" WHERE "projectId" IN my_projects)
+11
View File
@@ -0,0 +1,11 @@
-- 生产 PG: PowerSync 逻辑复制 publication
-- 必须在 prisma migrate deploy 建表之后执行 (initdb 阶段表还不存在, 故不放 initdb 目录)。
-- 幂等: 已存在则跳过。
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_publication WHERE pubname = 'powersync') THEN
CREATE PUBLICATION powersync FOR TABLE
"Project", "ProjectMember", "BoardColumn", "Task", "TaskMember", "TaskTag", "User";
END IF;
END
$$;
+82
View File
@@ -0,0 +1,82 @@
-- iBoard 0.2.1: 外键 uuid 镜像列触发器
-- 由 0.2.1 验证库导出; 服务端写入时自动维护 projectUuid/columnUuid/parentUuid/taskUuid,
-- 同步流据此把镜像列以 projectId/columnId/parentId/taskId 的别名下发给客户端。
CREATE OR REPLACE FUNCTION public.func_boardcolumn_uuid_map()
RETURNS trigger
LANGUAGE plpgsql
AS $function$
BEGIN
SELECT "uuid" INTO NEW."projectUuid" FROM "Project" WHERE "id" = NEW."projectId";
IF NOT FOUND THEN RAISE EXCEPTION 'Project % 不存在', NEW."projectId"; END IF;
RETURN NEW;
END;
$function$
;
CREATE OR REPLACE FUNCTION public.func_projectmember_uuid_map()
RETURNS trigger
LANGUAGE plpgsql
AS $function$
BEGIN
SELECT "uuid" INTO NEW."projectUuid" FROM "Project" WHERE "id" = NEW."projectId";
IF NOT FOUND THEN RAISE EXCEPTION 'Project % 不存在', NEW."projectId"; END IF;
RETURN NEW;
END;
$function$
;
CREATE OR REPLACE FUNCTION public.func_task_uuid_map()
RETURNS trigger
LANGUAGE plpgsql
AS $function$
BEGIN
SELECT "uuid" INTO NEW."projectUuid" FROM "Project" WHERE "id" = NEW."projectId";
IF NOT FOUND THEN RAISE EXCEPTION 'Project % 不存在', NEW."projectId"; END IF;
SELECT "uuid" INTO NEW."columnUuid" FROM "BoardColumn" WHERE "id" = NEW."columnId";
IF NOT FOUND THEN RAISE EXCEPTION 'BoardColumn % 不存在', NEW."columnId"; END IF;
IF NEW."parentId" IS NULL THEN
NEW."parentUuid" := NULL;
ELSE
SELECT "uuid" INTO NEW."parentUuid" FROM "Task" WHERE "id" = NEW."parentId";
IF NOT FOUND THEN RAISE EXCEPTION 'Task % 不存在', NEW."parentId"; END IF;
END IF;
RETURN NEW;
END;
$function$
;
CREATE OR REPLACE FUNCTION public.func_taskmember_uuid_map()
RETURNS trigger
LANGUAGE plpgsql
AS $function$
BEGIN
SELECT "uuid" INTO NEW."taskUuid" FROM "Task" WHERE "id" = NEW."taskId";
IF NOT FOUND THEN RAISE EXCEPTION 'Task % 不存在', NEW."taskId"; END IF;
RETURN NEW;
END;
$function$
;
CREATE OR REPLACE FUNCTION public.func_tasktag_uuid_map()
RETURNS trigger
LANGUAGE plpgsql
AS $function$
BEGIN
SELECT "uuid" INTO NEW."taskUuid" FROM "Task" WHERE "id" = NEW."taskId";
IF NOT FOUND THEN RAISE EXCEPTION 'Task % 不存在', NEW."taskId"; END IF;
RETURN NEW;
END;
$function$
;
CREATE TRIGGER trg_boardcolumn_uuid_map BEFORE INSERT OR UPDATE ON public."BoardColumn" FOR EACH ROW EXECUTE FUNCTION func_boardcolumn_uuid_map();
CREATE TRIGGER trg_projectmember_uuid_map BEFORE INSERT OR UPDATE ON public."ProjectMember" FOR EACH ROW EXECUTE FUNCTION func_projectmember_uuid_map();
CREATE TRIGGER trg_task_uuid_map BEFORE INSERT OR UPDATE ON public."Task" FOR EACH ROW EXECUTE FUNCTION func_task_uuid_map();
CREATE TRIGGER trg_taskmember_uuid_map BEFORE INSERT OR UPDATE ON public."TaskMember" FOR EACH ROW EXECUTE FUNCTION func_taskmember_uuid_map();
CREATE TRIGGER trg_tasktag_uuid_map BEFORE INSERT OR UPDATE ON public."TaskTag" FOR EACH ROW EXECUTE FUNCTION func_tasktag_uuid_map();
+379
View File
@@ -0,0 +1,379 @@
# 0.2.1 发布任务 — 目标与现状
> 最后更新:2026-10-27 08:20
> 分支:`dev-0.2` 与 `master` 均已发布(`a1c267b`)
> 状态:**全部完成** —— 生产已切 0.2.1 单端口架构,master 发布 CI 通过,Android APK 与 Windows 安装包均已产出
## 零、执行结果速览(2026-10-27 已完成)
| 项目 | 结果 |
| --- | --- |
| 生产地址 | `https://iboard.honor3.com` → baihu **3001**(NPM + 有效证书) |
| 生产架构 | `iboard-prod-app-1`(3001 对外) + `iboard-prod-pg-1`(内网) + `iboard-prod-powersync-1`(内网) |
| 端口 | **只剩 3001**;13001 / 13002 / 13443 全部下线 |
| 数据迁移 | MySQL → PG 完成:Project 3、Task 25、BoardColumn 12、User 1、CheckIn 17、News 19、IndicatorPoint 245、Mihome 等全部迁入 |
| 旧生产 | 容器 `iboard` 已 **stop(未删除)**,`docker start iboard` 可秒级回滚 |
| 登录态 | `JWT_SECRET` 沿用 `iboard`,现有用户无需重新登录 |
| 米家凭证 | `/app/iboard/cache` → `deploy/prod/data/cache`(`auth.txt` + `apizero/`) |
| E2E 验证 | 在线:项目卡片 + `ADMIN-16`/`ADMIN-18` 编号;断网:徽标出现、任务页数据完整、无 fail fetch;恢复:徽标消失 |
| master 发布 | 已推 `master`(`a1c267b`);CI `build-image` **10/10 步成功**(版本校验 / 前端 / OTA 签名包 / 镜像 / 部署步按设计跳过) |
| Android | `android-1.0` CI **16/16 步成功**,APK 20MB(versionName 0.2.1)已发布到 Gitea packages |
| Windows | 本地 `npm run build:win` 成功:NSIS 6.9MB + 便携 zip 7.9MB |
| OTA 热更 | **已打通**:`https://iboard.honor3.com/api/ota/check/0` 返回 `0.2.1-ota`、`min_binary_version: 0.2.1`、签名与下载地址齐全 |
### 迁移过程中踩到并修掉的坑
1. `prisma/migrations/` 是 **MySQL 语法**(反引号),PG 无法 `migrate deploy` → 改用 `prisma db push --accept-data-loss` 按 schema 建表。
2. `db push` 首次因参数不被识别而输出帮助(表结构停在旧 schema,缺 `autoArchive`)→ 去掉错误参数重跑,列已补齐。
3. 新库缺 **uuid 镜像触发器**(`projectUuid`/`columnUuid`/`parentUuid`/`taskUuid`)→ 从 0.2.1 验证库导出 5 个函数 + 5 个触发器,落库并入库 `deploy/prod/sql/uuid-triggers.sql`。
4. bash 把 `$$` 展开成 PID 导致 TRUNCATE 脚本报错 → 改用 SQL 文件传入。
5. baihu 上 `docker build` 缺 buildx → 改用 `docker compose build`。
6. **OTA 包此前完全没有**:手工构建的镜像内 `/app/ota-package` 只有 README(`ota-package/` 被 `.gitignore` 排除,CI 部署段又跳过),线上 OTA 接口返回空 → 已给 app 容器加 `data/ota` 挂载,并新增 CI 步骤用 `docker cp` 经 bind-mount 容器写入宿主(CI 在容器内无法直接写宿主文件)。
7. CI 首次写入路径多写了一层 `data/ota`(与 compose 的 `deploy/prod/data/ota` 不一致)→ 路径已对齐。
8. `OTA_MIN_BINARY` 由 `0.1.0` 提升到 **`0.2.1`**,防止旧外壳拉到新域名前端后被 CSP 拦截白屏。
---
## 一、任务目标(用户原始要求)
1. 域名 `iboard.honor3.com` 已指向 3000 端口、HTTPS 证书已配好 → 客户端全面切到该域名。
2. **Windows 外壳 + 离线编辑功能正式合并到 `dev-0.2`**,完整版本号 **0.2.1**。
3. 确保 **Android** 功能正常(通过 CI 编译验证,不在本地装 Android 工具链)。
4. 确保 **CI/CD** 正常。
5. Windows **本地打包正常**即可。
6. `dev-0.2` 更新完成后 **通过 `master` 分支发布**。
7. 追加要求:**不要再有 13002 这类独立端口**,所有版本合并成一套。
---
## 二、已完成
### 2.1 分支合并
| 内容 | 结果 |
| --- | --- |
| `dev-0.2.1`(离线编辑 + 项目编号 + PG/PowerSync)→ `dev-0.2` | 已合并 `e689e44` |
| `dev-windows-0.1`(Windows 外壳:托盘/悬浮球/吸附/自启动)→ `dev-0.2` | 已按路径迁入 `eef2969` |
| `dev-0.2` → `android-1.0`(触发 APK 构建) | 已合并 `4958359` |
| 冲突处理 | `projects.js`/`tasks.js`/`api/task.js` 手工合并,**两边功能都保留**(`autoArchive` 归档 + `code` 编号) |
| 归档功能补齐到本地库 | `src/api/task.js` 新增 `archive`/`unarchive`/`archivedList`;`schema.js` 加 `archived/archivedAt/archiveSource/autoArchive`;`sync-config.yaml` 与 upload 处理器同步 |
| Prisma 客户端补齐 | dev-0.2 原来缺任务模块 6 个模型,已重新 generate `fc23919` |
### 2.2 版本与域名
- 版本统一 **0.2.1**:`package.json` / `src-tauri/tauri.conf.json` / `Cargo.toml` / `Cargo.lock`(`scripts/ci/check-version.mjs` 通过)
- 域名切 `https://iboard.honor3.com`:
- `src/api/client.js` 注释、`.env.shell`、`scripts/wsl/build-apk.sh`、`.gitea/workflows/android-build.yml`、`.gitea/workflows/docker-build.yml`、`Dockerfile`(`OTA_PUBLIC_BASE`)、`tauri.conf.json`(OTA endpoint + CSP)
- 旧域名 `bh.vps.honor3.com:3001` 仅保留在 CSP 白名单里做滚动兼容
### 2.3 单端口架构(去掉 13002)
| 改动 | 位置 |
| --- | --- |
| Express 增加 `/sync`、`/socket` 同域反代到内网 powersync | `server/powersync-proxy.js` + `server/index.js`(挂在 `express.json()` 之前,支持长连接) |
| 生产编排:app(3001 对外) + pg(内网) + powersync(内网) | `deploy/prod/docker-compose.yml` |
| 生产部署脚本(幂等:迁移 → publication → 起栈 → 健康检查) | `scripts/deploy/prod.sh` |
| PG 初始化(powersync_storage 用户) | `deploy/prod/initdb/01-powersync-storage.sh` |
| 逻辑复制 publication(**建表后**执行) | `deploy/prod/sql/publication.sql` |
| 部署文档 | `deploy/prod/README.md`、`.env.example` |
| CI 部署段改为调用宿主脚本,未配 PG secrets 时自动跳过 | `.gitea/workflows/docker-build.yml` |
### 2.4 Windows 本地打包 — 通过 ✅
`npm run build:win` 全流程成功(本机需 Node 20.19+;已用便携 Node 20.18.1 之外的 Node 24 验证过 vite 构建):
| 产物 | 路径 | 大小 |
| --- | --- | --- |
| 安装包 | `artifacts/windows/iBoard_0.2.1_x64-setup.exe` | 6.9 MB |
| 便携版 zip | `artifacts/windows/iBoard_0.2.1_x64-portable.zip` | 7.9 MB |
外壳能力(继承 dev-windows-0.1):托盘、悬浮球吸附、自动隐藏、单实例、自启动、窗口尺寸单值配置
(`%APPDATA%\com.iboard.app\shell-config.json` 的 `window_width`,高度按 405:720 锁定推导)。
离线能力(继承 0.2.1):GET 响应本地缓存、同步凭证持久化(离线不清库)、左上角「离线」徽标。
### 2.5 Android CI — 通过 ✅
- 触发:`android-1.0` 分支 push(`4ded8e3`)
- 结果:**16 个步骤全部 success**,`Build release APK` + `Publish APK to Gitea packages` 成功
- 产物:`app-universal-release.apk`(20 MB),versionName = **0.2.1**
已发布到 `http://git.honor3.com/api/packages/Fullstack/generic/iboard/1.0-4ded8e30/`
- 首次失败原因:runner 宿主机(baihu)磁盘 **95%** → `No space left on device`;
清理悬空镜像 + 构建缓存后释放 11.5 GB,重跑通过。**磁盘现已 60%(39 GB 可用)**
### 2.6 CI/CD 现状
| Workflow | 触发 | 状态 |
| --- | --- | --- |
| `docker-build.yml` | `master` push | 改造完成,未在 master 上验证(部署段会因缺 secrets 跳过) |
| `android-build.yml` | `android-*` push | **已验证通过** |
| `ota-build.yml` / `sync-registry.yml` | 参照原逻辑 | 未改动 |
---
## 三、当前卡点(生产切换未执行)
生产镜像 **已构建成功**:`iboard:latest` = `db6b27b851b8`(924 MB,2026-10-26 22:53)
baihu 上当前跑着两套:
| 容器 | 用途 | 端口 |
| --- | --- | --- |
| `iboard`(旧) | **现网生产**,MySQL(宿主 3306) | 3001 |
| `iboard-021-app-1` 等 4 个 | 0.2.1 验证栈 | 13001 / 13002 / 13443 |
| (未启动)`iboard-prod` | **新生产栈**(PG + PowerSync + app) | 3001(计划) |
`/app/iboard-prod` 已上传源码、`deploy/prod/.env` 已生成(6 个密钥,600 权限)。
**剩余步骤(约 4 步)**:
1. 起新栈(先用 `HOST_PORT=13011` 灰度,不动现网)
2. 数据迁移:现网 MySQL → 新 PG(`scripts/etl-mariadb-to-pg.mjs`,只读源库)
3. 验证:健康检查、登录、任务列表、离线编辑(断网后仍可读)、同步回流
4. 切换:停 `iboard` 旧容器 → 新栈改绑 3001 → 域名 `iboard.honor3.com` 生效 → 下线验证栈(13001/13002/13443)
然后 `dev-0.2` → `master` 合并推送,触发 CI 发布。
---
## 四、需要确认的信息(阻塞项)
1. **Gitea Actions Secrets 是否要配 PG 密钥?**
需配:`PG_PASSWORD`、`PS_STORAGE_PASSWORD`、`JWT_SECRET`、`POWERSYNC_JWT_SECRET`、`POWERSYNC_JWT_KEY_B64`。
不配的话:master 的 CI 只构建镜像不部署,生产靠手工执行 `scripts/deploy/prod.sh`(当前默认路径)。
2. **生产数据迁移的时间窗口**:迁移需短暂停写(现网 MySQL 只读快照)。是否可以挑一个低峰时间、期间 (~10 分钟) 站点只读?
3. **现网 MySQL 里的米家凭证/缓存**:`/app/iboard/cache`(`auth.txt` 等)是否要一并搬到新栈的 `deploy/prod/data/cache`?
4. **用户密码**:迁移只搬数据,`JWT_SECRET` 若换新,所有人需重新登录;是否沿用现网 `JWT_SECRET`(`iboard`)以保持登录态?**建议换新**(当前值过弱)。
5. **域名端口说明**:`iboard.honor3.com` 实际反代到 baihu **3001**(NPM 配置:`114.134.187.170:3001`,SSL 强制)。
你提到"已转向 3000 端口"——请确认指的是 `honor3.com` 根域(→ homepage)而非 `iboard` 子域,
否则需要调整 NPM 的 forward_port。
---
## 五、关键路径与命令速查
```bash
# 仓库
C:\Users\Rabbit\Documents\workbetch\iboard # dev-0.2 主工作区
C:\Users\Rabbit\Documents\workbetch\iboard-dev-0.2.1 # 研究分支 worktree
C:\Users\Rabbit\Documents\workbetch\iboard-dev-windows # Windows 外壳 worktree
# 服务器
ssh root@baihu # 生产 + CI runner(baihu 上跑 act_runner,注册名 baishe)
ssh root@baishe # 反代(NPM)、Gitea、homepage
# 生产目录
/app/iboard-prod/deploy/prod/{docker-compose.yml,.env,data/,powersync/,sql/,initdb/}
# 常用
docker compose -f /app/iboard-prod/deploy/prod/docker-compose.yml --env-file /app/iboard-prod/deploy/prod/.env ps
bash /app/iboard-prod/scripts/deploy/prod.sh
curl -s https://iboard.honor3.com/api/health
```
## 五之二、后续动作执行步骤(已确认方案,按序执行)
### 决策确认(2026-10-27)
| 项 | 决定 |
| --- | --- |
| PG 密钥 | **不进 Gitea Secrets**,直接写在服务器 `/app/iboard-prod/deploy/prod/.env`(600) |
| 迁移时机 | 立即执行 |
| 米家凭证 | 保留,`/app/iboard/cache` 搬到新栈 `deploy/prod/data/cache` |
| `JWT_SECRET` | 沿用现网值(`iboard`),保持现有登录态 |
| 域名 | `iboard.honor3.com` → baihu **3001**(NPM 已配好,SSL 强制) |
### 步骤 A:准备(不改动现网)
```bash
# A1. 沿用现网 JWT_SECRET (其余密钥保持已生成的随机值)
cd /app/iboard-prod/deploy/prod
sed -i 's/^JWT_SECRET=.*/JWT_SECRET=iboard/' .env
# A2. 搬米家缓存 (auth.txt 等)
mkdir -p data/cache
rsync -a /app/iboard/cache/ data/cache/
# A3. 保留旧镜像用于回滚
docker tag iboard:latest iboard:mysql-final # 需在重新构建前执行; 若已覆盖见"回滚预案"
```
### 步骤 B:起灰度栈(不动现网 3001)
```bash
cd /app/iboard-prod/deploy/prod
HOST_PORT=13011 docker compose --project-name iboard-prod \
--env-file .env -f docker-compose.yml up -d pg
# 迁移建表
docker run --rm --network iboard-prod_default \
-e DATABASE_URL="postgresql://iboard:$(grep ^PG_PASSWORD .env | cut -d= -f2)@pg:5432/iboard" \
iboard:latest npx prisma migrate deploy
# 建 publication (必须在建表之后)
docker compose --project-name iboard-prod --env-file .env -f docker-compose.yml exec -T pg \
psql -U iboard -d iboard -v ON_ERROR_STOP=1 -f /dev/stdin < sql/publication.sql
# 起 app + powersync (只有 13011 对外)
HOST_PORT=13011 docker compose --project-name iboard-prod \
--env-file .env -f docker-compose.yml up -d app powersync
# 验收
curl -s http://127.0.0.1:13011/api/health # {"ok":true,...}
ss -tlnp | grep -E ':(13011|5432|8080)\b' # 只应看到 13011
```
### 步骤 C:数据迁移(MySQL → PG)
```bash
docker run --rm --network iboard-prod_default \
-e DATABASE_URL="postgresql://iboard:<PG_PASSWORD>@pg:5432/iboard" \
-e SRC_DATABASE_URL="mysql://root:<MYSQL_PASSWORD>@<host>:3306/iboard" \
iboard:latest node scripts/etl-mariadb-to-pg.mjs
# 迁移后重启同步服务重建快照
docker compose --project-name iboard-prod --env-file .env -f docker-compose.yml restart powersync
```
**验收**:项目 3 个(ADMIN / IBOARD / P3F921)、任务 ~22 条、用户 1 个(admin@iboard.local);
`docker compose logs powersync` 显示 replication 已激活且无报错。
### 步骤 D:功能验证(13011)
- 登录 `admin@iboard.local`
- 任务页:项目卡片、任务编号(如 `ADMIN-16`)
- **离线验证**:断网后列表仍可读、左上角出现「离线」徽标,恢复网络自动同步回流
- 监控 / 宏观 / 打卡页有数据
### 步骤 E:切换生产
```bash
# E1. 停旧容器 (MySQL 版)
docker rm -vf iboard || true
# E2. 新栈改绑 3001
cd /app/iboard-prod/deploy/prod
HOST_PORT=3001 docker compose --project-name iboard-prod \
--env-file .env -f docker-compose.yml up -d app
curl -s https://iboard.honor3.com/api/health # 经 NPM + 证书
# E3. 下线 0.2.1 验证栈 (释放 13001/13002/13443)
cd /app/iboard-0.2.1/deploy/0.2.1 && docker compose down
```
**验收**:`https://iboard.honor3.com` 可登录、任务正常、离线可编辑;
`ss -tlnp` 只剩 3001(外加仅供本机的 3306)。
### 步骤 F:发布
```bash
git checkout master && git merge dev-0.2 --no-edit
git push origin master # 触发 CI: 构建镜像 + OTA
```
CI 未配 PG secrets → 只构建不部署;生产已由步骤 B~E 手工就绪。
### 回滚预案
| 场景 | 动作 |
| --- | --- |
| 新栈起不来 | 旧容器未停,现网不受影响,回步骤 B 排查 |
| 迁移数据不对 | 清空 `deploy/prod/data/pg` 后重跑 ETL |
| 切换后严重故障 | **`docker start iboard`**(旧容器仍在,见 6.2 警告:旧镜像已被 prune,不能重建) |
## 六、遗留清单与清理建议(2026-10-27 实测)
### 6.1 本机(Windows)
| 路径 | 占用 | 建议 | 说明 |
| --- | --- | --- | --- |
| `workbetch/iboard`(主 worktree) | 3.81 GB | **保留** | 含 `node_modules` 0.49 GB + `src-tauri/target` 2.56 GB |
| `workbetch/iboard-dev-0.2.1` | 0.53 GB | **可删** | 内容已全部并入 `dev-0.2` |
| `workbetch/iboard-dev-windows` | 8.88 GB | **可删** | 同上,最大占用(含 Rust 编译产物) |
| `iboard/src-tauri/target` | 2.56 GB | 可选 `cargo clean` | 删后下次打包需重编译(约 8 分钟) |
| `iboard/artifacts/windows` | 32 MB | 保留最新安装包 | 0.2.1 exe/zip |
| `%TEMP%\node-portable` | 78 MB | **保留** | 本机无系统 Node/npm,`npm run build:win` 依赖它 |
| `%TEMP%\esbuild-*`(4 个) | 8 MB | 可删 | 构建残留 |
| `%TEMP%\iboard-e2e`、`iboard-shell-offline` | < 1 MB | 可删 | 验证截图 |
| `%TEMP%\edge-e2e-profile` | ~100 MB | 可删 | 无头浏览器测试配置 |
```bash
# 删除 worktree 与已合并分支(内容已在 dev-0.2)
git worktree remove ../iboard-dev-0.2.1
git worktree remove ../iboard-dev-windows
git branch -d dev-0.2.1 dev-windows-0.1
git branch -D tmp-offline-edit
```
### 6.2 服务器 baihu
| 路径/对象 | 占用 | 建议 | 说明 |
| --- | --- | --- | --- |
| `/app/iboard-prod` | 72 MB | **保留** | 当前生产 |
| `/app/iboard` | 24 KB | 暂留 → 可删 | 旧生产只剩 `cache/`(`auth.txt` 已复制到新栈)+ `start.sh` |
| `/app/iboard-0.2.1` | 71 MB | **可删** | 验证栈已 down;其中 `deploy/0.2.1/data` 67 MB 是废弃 PG 数据 |
| `/app/cicd` | 36 KB | **保留** | act_runner 配置 |
| 容器 `iboard`(Exited) | — | **暂留** | 当前唯一回滚手段(见下方警告) |
| 镜像 `iboard-021-app:latest` | 924 MB | **可删** | 验证栈镜像 |
| 镜像 `iboard:latest` | 934 MB | 保留 | CI 构建产物 |
| 悬空镜像 + 构建缓存 | 18.7 GB + 2.4 GB | 可清 | `docker image prune -f && docker builder prune -af` |
> **⚠ 回滚能力警告(实测更正)**
> 原计划保留的 `iboard:mysql-final` 旧镜像**已被 `docker image prune` 清除** —— 旧镜像失去 tag 后成为悬空镜像被回收。
> 现在唯一回滚途径是重启仍存在的旧容器 `iboard`(镜像层随容器保留):
>
> ```bash
> docker stop iboard-prod-app-1
> docker start iboard # 旧 MySQL 版立即恢复 3001
> ```
>
> **在再次执行 `docker image prune` 前,务必先确认旧容器仍在**;容器一旦删除,MySQL 版镜像只能从 git 历史重新构建。
### 6.3 代码与其他
- 临时测试脚本(`scripts/_prod-e2e.mjs`、`_browser-test.mjs`、`_shell-offline-test.mjs`)含真实口令,已加入 `.gitignore`(`scripts/_*.mjs`),**不要入库**
- `0.2.1` 验证库里的测试任务(如 `浏览器验证任务`)已按 MySQL 现状一并迁入 PG,如需清理直接在 PG 删除
- `prisma/migrations/` 仍是 MySQL 语法,**新环境建表必须用 `prisma db push --accept-data-loss`**(已写入 `deploy/prod/README.md`)
- `JWT_SECRET` 仍为弱值(`iboard`),建议择机换新(换新需全员重新登录)
- `node_modules` 在多个 Node 版本间切换过,遇 esbuild 权限报错时清空重装
## 七、外壳升级策略:必须整包升级,不能只靠 OTA
### 结论
| 平台 | 结论 | 关键原因 |
| --- | --- | --- |
| Windows 外壳(0.1.0) | **必须重装 exe** | ① CSP 白名单编译进原生二进制,0.1.0 不含 `https://iboard.honor3.com`;② 旧外壳 OTA 检查地址指向 `13001`,**该端口已下线**,连检查都失败 |
| Android(0.1.0 APK) | **必须换装 APK** | CSP 同样是原生配置;旧 APK 的 OTA 端点(3001)虽然有效,但拉到 0.2.1 前端会被 CSP 拦截 → 白屏 |
### 为什么 OTA 不够
`tauri-plugin-hotswap` 的 OTA 包内容只有 `bundle.tar.gz`(即 `dist/` 前端资源),**不触碰原生层**:
| 配置项 | 位置 | OTA 能否修改 |
| --- | --- | --- |
| CSP 白名单(`connect-src`) | `tauri.conf.json` → 编译期 `generate_context!` 注入二进制 | ❌ |
| OTA 检查 endpoint | 同上 | ❌ |
| 窗口尺寸/最小尺寸 | 同上(另可被 `shell-config.json` 覆盖) | ❌ |
| 托盘 / 悬浮球 / 自启动 | Rust `src-tauri/src/shell/*` | ❌ |
| 页面 JS/CSS/WASM(含离线编辑、徽标、编号展示) | `dist/` | ✅ |
因此旧外壳无法通过 OTA 获得访问新域名的能力。
### 已采取的保护措施
CI 的 `OTA_MIN_BINARY` 已从 `0.1.0` 提升到 **`0.2.1`**:
- 旧外壳(0.1.0 < 0.2.1)会被 hotswap 判定为不兼容 → **拒绝接收新前端**
- 旧外壳继续运行旧前端,仍访问 `http://bh.vps.honor3.com:3001`(该地址现指向新栈,`/api` 完全兼容)
- 结果:**旧外壳不会白屏**,只是缺少离线编辑、项目编号等 0.2.1 新能力
### 升级后
- 新外壳版本 0.2.1,CSP 含 `https://iboard.honor3.com`,OTA 恢复正常(后续纯前端改动可继续走 OTA)
- Windows 窗口尺寸通过 `%APPDATA%\com.iboard.app\shell-config.json` 的 `window_width` 单值配置
@@ -0,0 +1,399 @@
# iBoard V0.2 任务模块 功能设计
| 项 | 内容 |
| --- | --- |
| 版本 | V0.2(对应分支 `dev-0.2`,应用版本 `0.2.0`) |
| 文档状态 | **设计基线**(待评审) |
| 上游文档 | `任务模块需求文档.md`(**149 条需求点**,编号 `T-*`) |
| 配套文档 | `任务模块设计文档-框架设计.md`、`任务模块数据库设计文档.md`、`任务模块界面设计.html` |
| 对应关系 | **本文与需求点一一对应**:每条需求点均给出「设计落点(功能/组件/接口/表字段)」与「实现要点」 |
> **阅读方式**
> 1. 本文按需求文档的模块码组织(`VW` / `COL` / `BD` / `FL` / `TK` / `AS` / `ST` / `PJ` / `PM` / `CP` / `UI` / `DT` / `NF` / `EX`)。
> 2. 每条需求点一行:**编号 → 设计落点 → 实现要点**。设计落点中的组件名、接口路径、表字段均与《任务模块数据库设计文档》《任务模块界面设计》一致。
> 3. 文末 §15 给出**覆盖自检表**,确保 149 条无遗漏。
---
## 0. 通用约定
### 0.1 契约命名
| 类别 | 约定 | 示例 |
| --- | --- | --- |
| 状态枚举 | `todo` / `doing` / `done` | 未开始 / 执行中 / 完成 |
| 列 | `BoardColumn` 实体,按 `sortOrder` 升序 | 默认 `Plan/Doing/Action/Done` |
| 保留选项 | `never` / `today` / `3d` / `week` / `month` / `forever` | 永不显示 / 当天 / 3 天 / 本周 / 本月 / 永久显示 |
| 视图 | `list` / `board` | 设备端记忆 |
| 设备端键 | `iboard_task_prefs` | `{ lastProjectId, viewMode: { [projectId]: 'list'\|'board' } }` |
### 0.2 接口总览
| 方法 | 路径 | 用途 |
| --- | --- | --- |
| GET | `/api/projects` | 我参与的项目(含个人默认项目、归档标记) |
| POST | `/api/projects` | 新建普通项目(同时初始化 4 个默认列) |
| GET | `/api/projects/:id` | 项目详情(含成员、列配置) |
| PUT | `/api/projects/:id` | 改名 / 归档 |
| DELETE | `/api/projects/:id` | 删除项目(个人项目 400) |
| POST | `/api/projects/:id/members` | 加成员(个人项目 400) |
| DELETE | `/api/projects/:id/members/:uid` | 移成员 / 自己退出 |
| GET/POST | `/api/projects/:id/columns` | 列列表 / 新增列 |
| PUT | `/api/columns/:id` | 改名 / 改绑定 / 改保留选项 |
| DELETE | `/api/columns/:id` | 删列(需 `moveToColumnId`,最后列 400) |
| PUT | `/api/projects/:id/columns/order` | 列排序 |
| GET | `/api/projects/:id/tasks` | 任务列表(含子任务与列信息) |
| POST | `/api/projects/:id/tasks` | 新建任务 / 子任务 |
| GET | `/api/tasks/:id` | 任务详情 |
| PUT | `/api/tasks/:id` | 改状态 / 换列 / 改字段(触发裁决) |
| DELETE | `/api/tasks/:id` | 删除任务(级联子任务) |
| POST | `/api/tasks/:id/copy` | 跨项目拷贝 |
---
## 1. 视图体系(T-VW)
| 编号 | 设计落点 | 实现要点 |
| --- | --- | --- |
| T-VW-01 | `ProjectView.jsx` 内 `viewMode` 分支渲染 `ListView` / `BoardView` | 两视图共用 `TaskStore` 的同一份任务数据,仅渲染方式不同 |
| T-VW-02 | `SwipeTabs`/分段控件切换 `viewMode` | 纯本地状态切换,不触发路由跳转、不重新拉取数据(满足"无整页刷新") |
| T-VW-03 | `prefs.js` → `viewMode[projectId]` | 按项目 id 分键存储,A/B 项目互不影响 |
| T-VW-04 | `ProjectView` 挂载时读 `prefs.getViewMode(projectId)` | 有值用之;无值走 T-VW-05 默认 |
| T-VW-05 | 默认值 `'list'` | 新项目首次进入为列表 |
| T-VW-06 | `localStorage.iboard_task_prefs` | **不落库、不同步**;换设备不继承(与 T-FL-15 的项目级配置区分) |
| T-VW-07 | `prefs.getLastProjectId()` / `setLastProjectId()` | 进入 `/task` 时读取;无记录或记录的项目已不可见 ⇒ 回落个人默认项目 |
| T-VW-08 | `TaskPage` 首屏上部 `RecentProjects` 条 | 取最近访问的 3 个项目(按设备端访问时间倒序),末尾「更多项目」入口 → `/task/list`(完整项目列表 `ProjectList`,从「更多」进入) |
| T-VW-09 | `TaskPage` 首屏下部直接内嵌 `ProjectView` | 当前项目 = 上次访问项目(无记录 ⇒ 个人默认项目);点首屏项目卡片切换当前项目,下部整体切换,不跳转页面 |
| T-VW-10 | `prefs.js` 增 `recentOrder[]`(设备端) | 长按卡片拖动排序并持久化;无手动记录时按最近访问时间自动排列;项目不可见时从记录剔除 |
> **首屏结构调整(T-VW-08/09)**:`TaskPage`(`/task`)不再以项目列表为首屏,改为「最近项目条 + 当前项目 `ProjectView`」;完整项目列表迁移至 `/task/list`。旧路由 `/task/:pid` 保留(打开指定项目),供深链使用。
> **实现细节**:`lastProjectId` 指向的项目若已被退出/删除,视为"无记录",静默回落到个人默认项目,不弹错。
---
## 2. 看板列管理(T-COL)
| 编号 | 设计落点 | 实现要点 |
| --- | --- | --- |
| T-COL-01 | `POST /api/projects` 内部初始化 4 列 | 事务内创建 `Plan(todo, sort 0)`、`Doing(doing,1)`、`Action(null,2)`、`Done(done,3)` |
| T-COL-02 | `ColumnSettings` → `POST /api/projects/:id/columns` | `sortOrder = max+1` 追加末尾;`boundStatus=null`、`retention='forever'`(B-18) |
| T-COL-03 | `PUT /api/columns/:id` | 改 `name`;前端 `TaskStore` 整体替换列列表后重渲染 |
| T-COL-04 | `DELETE /api/columns/:id?moveToColumnId=` | 二次确认弹窗;见 T-EX-16 |
| T-COL-05 | `PUT /api/projects/:id/columns/order` | 传完整 `[columnId…]` 数组,服务端按索引重写 `sortOrder` |
| T-COL-06 | 服务端校验总量 | `1 <= count <= 10`;达上限拒绝新增(T-EX-19)、仅剩 1 列拒绝删除(T-EX-17) |
| T-COL-07 | `BoardColumn.name` 校验 | trim 后非空、长度 ≤ 12;`@@unique([projectId,name])` 兜底重名 409 |
| T-COL-08 | 列删除即绑定消失 | 绑定存在列记录上,列删则解除,无需额外清理 |
| T-COL-09 | `requireProjectMember` | 项目级配置,任一成员可改;不影响其他项目 |
| T-COL-10 | `ListView` 不分组但仍显示列信息 | 任务行内以小标签显示所属列名(可关闭),归属始终正确 |
---
## 3. 看板与列-状态绑定(T-BD)
> 联动的**唯一实现**在 `server/task-domain/resolveStatus.js`,前端只做乐观更新。判定矩阵见《框架设计》§2.3。
| 编号 | 设计落点 | 实现要点 |
| --- | --- | --- |
| T-BD-01 | `BoardView` 按 `columns` 数组渲染列 | 列来自项目配置,可增删改排序 |
| T-BD-02 | `ColumnSettings` 每列一组「绑定开关 + 状态下拉」 | 写入 `BoardColumn.boundStatus`(`null`/`todo`/`doing`/`done`) |
| T-BD-03 | 无唯一约束限制多列绑定同状态 | 允许 `done` 与 `action` 同为 `done` |
| T-BD-04 | `onColumnChange()` | 目标列 `boundStatus != null` ⇒ `status = boundStatus` |
| T-BD-05 | `onStatusChange()` | 状态变更后若当前列已匹配则不动作,否则走归位 |
| T-BD-06 | 归位取列序首个匹配 | `columns` 按 `sortOrder` 排序后 `find()` 首个 |
| T-BD-07 | **只看目标列** | 目标列不绑定 ⇒ 保留原状态;**无"拖出回退"规则** |
| T-BD-08 | 不绑定列完全正交 | 拖入/拖出、改状态均不改该列以外语义 |
| T-BD-14 | `resolveColumn(task, newStatus)` | 当前列 `boundStatus !== newStatus` ⇒ 触发归位 |
| T-BD-15 | 按当前 `sortOrder` 取首个 | 用户调序后归位目标随之改变 |
| T-BD-16 | 无匹配返回 `null` | `null` ⇒ 不改 `columnId`,状态照常写库 |
| T-BD-09 | 初始化默认值 | `Plan=todo`、`Doing=doing`、`Done=done`、`Action=null` |
| T-BD-10 | 项目级配置 | 任一成员可改;不同项目独立 |
| T-BD-11 | 配置变更不回溯 | 保存配置时**不触发**任何任务更新语句 |
| T-BD-12 | 保存后返回最新列列表 | 前端整体替换 + 重算可见集合,无需刷新 |
| T-BD-13 | `resolveStatus()` 首行判 `parentId` | 子任务直接跳过全部联动 |
---
## 4. 已完成任务显示控制(T-FL)
| 编号 | 设计落点 | 实现要点 |
| --- | --- | --- |
| T-FL-01 | `BoardColumn.retention` | 每列独立字段 |
| T-FL-02 | `ColumnSettings` 下拉 6 项 | 固定枚举,无自由输入 |
| T-FL-03 | `retention='forever'` | 判定恒为可见 |
| T-FL-04 | `retention='never'` | 已完成任务恒不可见 |
| T-FL-05 | `retention='today'` | `doneAt >= 今天 00:00` |
| T-FL-06 | `retention='3d'` | `doneAt >= 今天-2天 00:00` |
| T-FL-07 | `retention='week'` | `doneAt >= 本周一 00:00` |
| T-FL-08 | `retention='month'` | `doneAt >= 本月1日 00:00` |
| T-FL-09 | `task-domain/retention.js` + 前端定时重算 | 超出窗口自动隐藏,无需操作 |
| T-FL-10 | 判定字段 `Task.doneAt` | 与当前列无关;取消完成时 `doneAt=null` |
| T-FL-11 | 前端可见性统一走同一函数 | 列表与看板共用 `isTaskVisible(task, column)`;列表不分组但归属不变 |
| T-FL-12 | 判定前置 `status !== 'done'` | 未完成任务恒可见 |
| T-FL-13 | 纯展示层过滤 | 不修改任何字段;接口返回全量,前端过滤(或服务端按需过滤但不改数据) |
| T-FL-14 | 列计数取过滤后集合长度 | 列头计数与卡片数同源,杜绝不一致 |
| T-FL-15 | 存在 `BoardColumn` 上 | 项目级共享,非个人偏好(对比 T-VW-06 设备端) |
> **实现细节**:保留判定在前后端各有一份实现(服务端用于分页/统计,前端用于即时过滤),但**必须共用同一口径常量**,跨度边界由 `date.js` 单测覆盖。
---
## 5. 任务字段体系(T-TK)
| 编号 | 设计落点 | 实现要点 |
| --- | --- | --- |
| T-TK-01 | `Task` 表 | 字段:`title`/`description`/`projectId`/`columnId`/`status`/`assigneeId`/`creatorId`/`doneAt`/`sortOrder` + 关联成员/标签/扩展 |
| T-TK-02 | `title` 非空校验 | 其余字段允许空;trim 后为空 ⇒ 400 |
| T-TK-03 | `status` 枚举三值 | DB 层用 `VARCHAR(8)` + 应用层校验(避免 ENUM 改值困难) |
| T-TK-04 | **已移除 UI**(2026-10-24):`ExtrasCard` 组件删除;信息记录由项目信息块(T-PJ-13)承载 |
| T-TK-05 | **随 T-TK-04 移除**;`extras` 字段在 DB 保留不再使用 |
| T-TK-06 | **随 T-TK-04 移除** |
| T-TK-07 | **随 T-TK-04 移除** |
| T-TK-08 | `Task.assigneeId` 单值 | 重复设置以最后一次为准 |
| T-TK-09 | 新建时服务端默认赋值 | 未显式传 `assigneeId` ⇒ 取 `req.user.id`(B-10) |
| T-TK-10 | `assigneeId` 允许 `null` | 显式传 `null` 即清空,UI 显示「未指派」 |
| T-TK-11 | `TaskMember` 关联表 | 多对多,`@@unique([taskId,userId])` 去重 |
| T-TK-12 | `TaskTag` 表 + 卡片标签渲染 | 多标签,展示于卡片/条目 |
| T-TK-13 | 服务端维护 | `creatorId`/`createdAt` 不可改;`updatedAt` 自动刷新 |
| T-TK-14 | 标题 30 字上限 + 快速添加截断 | 服务端 create/update 校验 `title ≤ 30`;前端 `quickAdd.js#splitTitle`:≤30 原样,超长在 30 字内(≥8 字)找最后一个分隔符截断,前段为标题、**完整原文**写入 `description`;快速添加输入框 `maxLength=200` |
| T-TK-15 | `Task.markDate DATETIME(3) NULL`(迁移 20261025120000) | create/update 接受 `YYYY-MM-DD`(空/null 清除),完整日期存储;详情页 DatePicker(precision=day)选择/清除;卡片展示 `MM-dd` 小标签(`date-tag`) |
| T-TK-16 | `quickAdd.js#parseTimePrefix` | 前缀词表:今天/明天/后天/本周/下周/本月/下月(须后跟分隔符或结束);今天=当日,明天/后天=+1/+2,本周=本周日、下周=下周日(周一为一周开始),本月/下月=当月/下月最后一天;命中后前缀移出标题并写入 `markDate` |
---
## 6. 负责人 & 成员(T-AS)
| 编号 | 设计落点 | 实现要点 |
| --- | --- | --- |
| T-AS-01 | `TaskCard` 负责人区 | 字号 13px/600 + 头像,位于标题下方主信息位 |
| T-AS-02 | `TaskCard` 成员区 | 字号 11px/400、色 `#999`,位于卡片底部 |
| T-AS-03 | `UserPicker` 数据源 = 项目成员 | 只列 `ProjectMember` 中的用户 |
| T-AS-04 | 同上(多选模式) | 不可选项目外用户 |
| T-AS-05 | `assigneeId === null` 时渲染「未指派」 | 占位样式弱化,可点击再次指派 |
| T-AS-06 | 渲染时 `LEFT JOIN ProjectMember` 判定 | 非成员 ⇒ 显示「已退出」灰标;见 T-EX-06 |
| T-AS-07 | 权限模型 | 无"负责人可编辑"概念,所有成员全读写(T-PM-02) |
---
## 7. 子任务体系(T-ST)
| 编号 | 设计落点 | 实现要点 |
| --- | --- | --- |
| T-ST-01 | `Task.parentId` 自关联 | 创建子任务时校验:父任务 `parentId IS NULL`,否则 400 |
| T-ST-02 | 一致性校验 | 子任务 `projectId` 必须等于父任务 `projectId`,否则 400(T-EX-12) |
| T-ST-03 | 状态独立 | 父/子状态字段各自独立,无级联更新 |
| T-ST-04 | `SubtaskRow` 缩进 | 左侧 28px 缩进 + 层级线 |
| T-ST-05 | 折叠/展开按钮 | 组件内存状态,折叠时不渲染子项 |
| T-ST-06 | `TaskCard` 内嵌 `SubtaskRow` 列表 | 简洁单行:复选框 + 标题,最多显示 3 条 + 「查看全部」 |
| T-ST-07 | 卡片内直接勾选 | 勾选调 `PUT /api/tasks/:id {status}`;完整编辑走 `TaskDetail` 弹窗 |
| T-ST-08 | 子任务跳过绑定 | 见 T-BD-13;父任务完成时子任务状态不变 |
| T-ST-09 | `SubtaskRow` 带 `StatusDot` | 三色小圆点:灰=未开始 / 蓝=执行中 / 绿=完成 |
| T-ST-10 | 看板只渲染顶层任务 | 看板查询 `WHERE parentId IS NULL` |
| T-ST-11 | `Task.parentId` 级联删除 | `onDelete: Cascade`(T-EX-07) |
| T-ST-12 | `useLongPressDrag` 落点为目标顶层任务 | 设置 `parentId` |
| T-ST-13 | 目标为子任务 ⇒ 拒绝 | Toast「仅支持一级子任务」 |
| T-ST-14 | 被拖任务有子任务 ⇒ 拒绝 | 避免二级嵌套 |
| T-ST-15 | 保持原列与状态 | 仅更新 `parentId`,不触发状态裁决 |
---
## 8. 项目体系(T-PJ)
| 编号 | 设计落点 | 实现要点 |
| --- | --- | --- |
| T-PJ-01 | 用户首次访问自动创建 | 按 `isPersonal=true` 且 `ownerId=userId` 幂等创建 |
| T-PJ-02 | `DELETE /api/projects/:id` 校验 `isPersonal` | 为真 ⇒ 400(T-EX-09) |
| T-PJ-03 | `POST .../members` 校验 `isPersonal` | 为真 ⇒ 400 |
| T-PJ-04 | `name` 初始 `${user.name}的任务` | 允许 `PUT` 改名,`isPersonal` 不变 |
| T-PJ-05 | `POST /api/projects` | 创建者自动写入 `ProjectMember` |
| T-PJ-06 | 成员接口 | 增/删成员,即时影响可见性 |
| T-PJ-07 | `DELETE .../members/:uid` 允许 `uid == ownerId` | 创建人退出后不再是成员 ⇒ 不可见 |
| T-PJ-08 | 删除最后成员不删项目 | 仅删 `ProjectMember`(B-11);见 T-EX-11 |
| T-PJ-09 | `Project.archived` | 列表分「进行中 / 已归档」 |
| T-PJ-10 | 列表查询按成员关系过滤 | `WHERE EXISTS ProjectMember(userId)` |
| T-PJ-11 | 新组件 `ProjectManage`(项目管理页) | 分区承载:成员(复用 `ProjectMembers` 逻辑)、列设置(复用 `ColumnSettings`)、项目信息;`ProjectView` 头部仅保留「项目管理」一个入口 |
| T-PJ-12 | `ProjectManage` 的「项目信息」分区 | 改名(`PUT /api/projects/:id {name}`)、归档/取消归档(`{archived}`)、删除(个人项目隐藏入口)、退出(仅创建人可见);删除与退出均有二次确认 |
| T-PJ-13 | `Project.notes` Json + `ProjectManage` 的「信息块」分区 | 存 `[{title, content}]`(≤50 块,标题 ≤50 字、内容 ≤20000 字);`PUT {notes}` 整体替换;底部弹层独立编辑(标题输入 + 多行 textarea + 删除),键盘弹出时复用 `useKeyboardInset` 抬升 |
---
## 9. 项目权限与入口(T-PM)
| 编号 | 设计落点 | 实现要点 |
| --- | --- | --- |
| T-PM-01 | `requireProjectMember` 中间件 | 非成员 403;列表查询天然过滤 |
| T-PM-02 | 无角色区分 | 所有成员对任务/列/成员均有全读写 |
| T-PM-03 | `ProjectMember.permission` 字段 | 本期恒为 `member`,预留分级扩展位 |
| T-PM-04 | 归档接口无权限限制 | 任意成员可归档(`PUT /api/projects/:id {archived}`) |
| T-PM-05 | 复用 `authRequired` | 未登录 401 |
| T-PM-06 | `BottomNav.TABS` 插入 `{ key:'/task', label:'任务' }` | 位于 `打卡` 之后;`HomePath` 仍为 `/checkin` |
| T-PM-07 | `prefs.lastProjectId` | 设备端;无记录 ⇒ 个人默认项目 |
---
## 10. 跨项目拷贝(T-CP)
| 编号 | 设计落点 | 实现要点 |
| --- | --- | --- |
| T-CP-01 | `POST /api/tasks/:id/copy` | 复制而非移动,源任务零改动 |
| T-CP-02 | `copyTask()` 字段映射 | 标题/描述/状态/列/扩展/标签/成员 全量复制 |
| T-CP-03 | 递归复制子任务 | 只复制一层(子任务无更深层级) |
| T-CP-04 | `creatorId = req.user.id` | 覆盖源创建人 |
| T-CP-05 | 无任何关联字段 | 拷贝后两条独立记录 |
| T-CP-06 | 校验目标项目成员身份 | 非成员 403 |
| T-CP-07 | 人员交集过滤 | 保留属于目标项目成员者,返回 `dropped` 名单(T-EX-08) |
| T-CP-08 | 子任务 `projectId` 同步改写 | 与父任务一致 |
| T-CP-09 | 状态与列沿用不纠正 | 见 T-EX-14 |
---
## 11. UI 交互规范(T-UI)
| 编号 | 设计落点 | 实现要点 |
| --- | --- | --- |
| T-UI-01 | 列表条目结构 / 快速新增 / 子任务折叠 | 参考 Todoist:底部输入框回车即建 |
| T-UI-02 | `TaskCard` 精简版 | 仅标题、状态点、负责人、子任务摘要;无边框装饰 |
| T-UI-03 | 单 `TaskStore` | 任一视图修改写入 store,另一视图读取同一数据 |
| T-UI-04 | 视图切换纯本地 | 不重新请求,不重载页面 |
| T-UI-05 | 复用 `.card` 与安全区变量 | 375~430px 无横向滚动;底部预留 TabBar 高度 |
| T-UI-06 | 勾选即时变色 | 乐观更新,先改 UI 再发请求 |
| T-UI-07 | `useLongPressDrag` | 长按 300ms 拖拽 + 「⋯」菜单「移动到」兜底 |
| T-UI-08 | `EmptyState` 组件 | 无项目 / 无任务 / 无子任务三档空态 + CTA |
| T-UI-09 | `LoadingState` 骨架屏 | 首屏与切换视图 |
| T-UI-10 | `ErrorState` 组件 | 展示错误 + 重试按钮 |
| T-UI-11 | 弹层容器统一 `padding-bottom: calc(env(safe-area-inset-bottom) + 20px)` | 底部按钮(删除等)与屏幕底边保持安全距离;小屏下弹层 `max-height` + 内部滚动,底部按钮始终完整可见 |
---
## 12. 数据需求(T-DT)
| 编号 | 设计落点 | 实现要点 |
| --- | --- | --- |
| T-DT-01 | `Project` / `ProjectMember` | 见《数据库设计文档》 |
| T-DT-02 | `BoardColumn.boundStatus` | 每列独立字段,允许多列同值 |
| T-DT-03 | `BoardColumn.retention` | 六档枚举字符串 |
| T-DT-04 | `Task` | 含 `parentId` 自关联、`assigneeId`、`status`、`columnId`、`extras` |
| T-DT-05 | `TaskMember` / `TaskTag` | 关联表;`extras` 为 JSON |
| T-DT-06 | 设备端 | 不落库 |
| T-DT-07 | 索引 | `(projectId, columnId)`、`(projectId, status)`、`(parentId)` |
| T-DT-08 | 迁移文件 | `prisma migrate deploy` 流程 |
| T-DT-09 | `Project.archived` / 任务无物理删 | 归档保留历史 |
---
## 13. 非功能需求(T-NF)
| 编号 | 设计落点 | 实现要点 |
| --- | --- | --- |
| T-NF-01 | 单请求聚合 + 乐观更新 | 见《框架设计》§4.2 |
| T-NF-02 | 不依赖桌面专有 API | 触控事件统一 `touch` / `pointer` 兼容 |
| T-NF-03 | `src/pages/task/*` + `server/routes/*.js` | 与既有模块结构一致 |
| T-NF-04 | 路由挂载于 `server/index.js` | 沿用既有组织方式 |
| T-NF-05 | 千级任务可用 | 索引 + 分页/虚拟滚动 |
---
## 14. 异常与边界(T-EX)
| 编号 | 设计落点 | 实现要点 |
| --- | --- | --- |
| T-EX-01 | 前端 500ms 防抖 + 服务端幂等判定 | 相同终态请求直接返回当前任务 |
| T-EX-02 | 乐观更新快照回滚 | 失败恢复快照 + Toast + 重试按钮 |
| T-EX-03 | 列配置改为不绑定 | 已有任务保持不动 |
| T-EX-04 | 状态与列不匹配 | 走 T-BD-14~16 归位 |
| T-EX-05 | 拖到不绑定列 | 状态保留,不回退 |
| T-EX-06 | 非成员渲染「已退出」 | 见 T-AS-06 |
| T-EX-07 | 级联删除子任务 | 确认弹窗提示条数 |
| T-EX-08 | 拷贝丢弃人员 | 返回 `dropped` 并提示 |
| T-EX-09 | 个人项目两个保护接口 | 400 + 明确文案 |
| T-EX-10 | 列空态 | 展示该列空态,不报错 |
| T-EX-11 | 无成员项目 | 静默不可见,无特判报错 |
| T-EX-12 | 子任务跨项目 | 服务端 400 |
| T-EX-13 | 拖拽降级 | 保持原列与状态 |
| T-EX-14 | 拷贝状态/列无法对应 | 沿用源值,不纠正 |
| T-EX-15 | 拖到自身或后代 | 拒绝 + Toast |
| T-EX-16 | 删除有任务的列 | 二次确认 + 迁移到指定列(默认相邻)+ 重新裁决 |
| T-EX-17 | 删除最后一列 | 400「至少保留一列」 |
| T-EX-18 | 跨周期当场消失 | 定时重算 + 退场动画 + 重排 |
| T-EX-19 | 达到列数上限 | 拒绝新增 + 提示 |
---
## 15. 任务归档(T-ARC)
> 归档与 T-FL 显示控制的分层:**隐藏 = 展示过滤(可逆、不落库)**;**归档 = 移出主工作集(落库、可恢复)**。
> 自动归档的倒计时起点是任务**被隐藏的时刻**(由 `doneAt + 列保留窗口` 推导,不新增 hiddenAt 字段)。
### 15.1 数据
| 落点 | 说明 |
| --- | --- |
| `Project.autoArchive VARCHAR(8) NOT NULL DEFAULT 'never'` | 自动归档档位:`3d` / `1w` / `1m` / `never`;项目级共享(同 T-FL-12 口径) |
| `Task.archived BOOLEAN NOT NULL DEFAULT false` | 归档标记;归档**不迁移表**,主查询统一追加 `archived = false` |
| `Task.archivedAt DATETIME(3) NULL` | 归档时刻(查询过滤与排序键) |
| `Task.archiveSource VARCHAR(8) NULL` | `'manual'` / `'auto'`,溯源用 |
| 索引 | `idx_task_project_archived (projectId, archived, archivedAt)`;`idx_task_project_status_archived (projectId, status, archived)` |
| 迁移(待实施) | `20261026000000_task_archive`:三列 + 两索引(MySQL DDL 见数据库设计文档 §3.1/§3.4) |
### 15.2 API
| 接口 | 说明 |
| --- | --- |
| `POST /api/tasks/:id/archive` | 手动归档:父任务连带全部后代置 `archived/archivedAt/archiveSource='manual'`;幂等(已归档直接返回当前态) |
| `POST /api/tasks/:id/unarchive` | 恢复:父任务连带后代清除归档三字段;原列因 `ON DELETE RESTRICT` 必存在,状态保持不变 |
| `GET /api/projects/:id/tasks/archived?keyword&kind&columnId&from&to&cursor&limit` | 归档查询:`keyword` 标题 LIKE、`kind`、`columnId`、`from/to`(归档日期,含边界);`limit` 默认 20、≤100;游标 = `archivedAt+id` 复合降序,返回 `{ items, nextCursor }`,`nextCursor` 为空即到底 |
| `PUT /api/projects/:id` | 增接受 `autoArchive`,枚举校验(非法 400) |
| 既有查询 | `GET /projects/:id/tasks`、统计、列计数等全部追加 `archived: false`;`taskCount`(projectSummary)同口径 |
### 15.3 自动归档执行
- **归档到期判定**:`due = 隐藏时刻 + autoArchive 档位`;隐藏时刻 = `doneAt + 列保留窗口`(复用 `retentionStart` 口径,含 `today/3d/week/month`;`forever` 列任务永不隐藏、不进入自动归档)。
- **执行时机(双保险)**:
1. **读时惰性**:加载项目任务时对该项目先执行一轮到期归档(保证用户看到的数据与判定一致,刚到期的任务当场消失);
2. **每小时 cron 兜底**:扫描全部 `autoArchive != 'never'` 项目的到期 done 任务批量归档(`archiveSource='auto'`)。
- **幂等**:`archived = false` 且 `status = 'done'` 且 `doneAt` 非空才进入判定;重复执行无副作用。
- **批量写**:cron 用 `updateMany`(按到期集合),读时惰性用单项目范围 `updateMany`。
### 15.4 前端
| 落点 | 说明 |
| --- | --- |
| 任务 `⋯` 菜单 | 新增「归档」(`Dialog.confirm`,含子任务时提示连带条数);成功后列表/看板乐观移除 |
| 项目管理 | 「归档设置」分区:`Selector` 四档(3 天后/一周后/一月后/永不),保存即生效 |
| 项目视图头部 | 项目栏新增「已归档」入口 → 全屏弹层:过滤条(**关键词 / 看板列 / 归档时段**;时段含「本周 / 本月 / 本季度」快捷项与自定义区间;条件编辑后点「筛选」统一提交,另有「重置」;**不提供类型过滤**)+ 已应用条件标签(可单独撤销)与命中条数 + 分页「加载更多」+ 行内「恢复 / 删除」 |
| 主视图 | 列表/看板无需感知归档(数据源已排除);恢复后经 `load()` 回到原位 |
### 15.5 边界
| 场景 | 处理 |
| --- | --- |
| 归档父任务时新增/恢复子任务 | 子任务随父判定:父 archived 则子不单独出现在主列表(查询按父排除) |
| 自动归档期间用户正在编辑该任务 | 详情弹层保存正常(归档不锁写);保存后任务仍在归档集 |
| 恢复后任务再次到期隐藏 | 重新进入自动归档倒计时(`doneAt` 未变则可能立即再次到期,属预期) |
| 「永不」档位 | 不做任何自动归档;手动归档不受影响 |
| 拷贝任务 | 拷贝件不继承归档态(`copyTask` 不复制 archived 三字段) |
---
## 16. 覆盖自检表
| 模块 | 编号范围 | 条数 | 本文对应章节 |
| --- | --- | --- | --- |
| 视图体系 | T-VW-01 ~ 10 | 10 | §1 |
| 看板列管理 | T-COL-01 ~ 10 | 10 | §2 |
| 列-状态绑定 | T-BD-01 ~ 16 | 16 | §3 |
| 已完成显示控制 | T-FL-01 ~ 15 | 15 | §4 |
| 任务字段体系 | T-TK-01 ~ 13 | 13 | §5 |
| 负责人 & 成员 | T-AS-01 ~ 07 | 7 | §6 |
| 子任务体系 | T-ST-01 ~ 15 | 15 | §7 |
| 项目体系 | T-PJ-01 ~ 13 | 13 | §8 |
| 项目权限与入口 | T-PM-01 ~ 07 | 7 | §9 |
| 跨项目拷贝 | T-CP-01 ~ 09 | 9 | §10 |
| UI 交互规范 | T-UI-01 ~ 11 | 11 | §11 |
| 数据需求 | T-DT-01 ~ 09 | 9 | §12 |
| 非功能需求 | T-NF-01 ~ 05 | 5 | §13 |
| 异常与边界 | T-EX-01 ~ 19 | 19 | §14 |
| 任务归档 | T-ARC-01 ~ 08 | 8 | §15 |
| **合计** | — | **157** | — |
> 需求文档共 **157 条**需求点,本文逐条给出设计落点,覆盖率 **157/157**。(其中 `T-BD-14~16`、`T-COL-*`、`T-EX-18~19`、`T-VW-08~10`、`T-PJ-11/12`、`T-UI-11`、`T-ARC-*` 为评审后新增,编号不连续,非笔误。)
@@ -0,0 +1,467 @@
# iBoard V0.2 任务模块 数据库设计文档
| 项 | 内容 |
| --- | --- |
| 版本 | V0.2(对应分支 `dev-0.2`,应用版本 `0.2.0`) |
| 文档状态 | **设计基线**(待评审,未落库) |
| 数据库 | MariaDB / MySQL 8(Prisma `provider = "mysql"`) |
| 字符集 | `utf8mb4 / utf8mb4_unicode_ci` |
| 时区约定 | 应用层统一 `Asia/Shanghai`,库内 `DATETIME` 存 UTC |
| 相关文件 | `prisma/schema.prisma`、`prisma/migrations/20261024xxxxxx_add_task_module/migration.sql` |
| 关联文档 | `任务模块需求文档.md`、`任务模块设计文档-框架设计.md`、`任务模块功能设计.md`、`任务模块界面设计.html` |
---
## 1. 概述
任务模块新增 **6 张表**,并把既有 `User` 表补上反向关系(不改 `User` 结构):
| 表 | 作用 | 归属 |
| --- | --- | --- |
| `Project` | 项目(隔离与权限单位) | 项目域 |
| `ProjectMember` | 项目成员(权限关系) | 项目域 |
| `BoardColumn` | 看板列(名称 / 顺序 / 状态绑定 / 保留选项) | 看板域 |
| `Task` | 任务与子任务(自关联,一层) | 任务域 |
| `TaskMember` | 任务成员(多人标记) | 任务域 |
| `TaskTag` | 任务标签 | 任务域 |
### 1.1 设计原则
1. **列是实体,不是枚举。** 看板列允许增删改与排序(T-COL),若仍用 `plan/doing/action/done` 枚举,则"改名"无从表达、"排序"只能堆在 JSON 里、"删除列"会留下悬空字符串。因此独立建 `BoardColumn` 表,`Task` 用 `columnId` 外键引用。
2. **状态与列双落库,互不推导。** `Task.status`(三值)与 `Task.columnId` 是两个正交维度;未绑定的列(默认 `Action`)必须允许二者不一致(T-BD-08、T-BD-16),任何"由状态反推列"或"由列反推状态"的写法都会丢失信息。联动只在写接口中按项目配置即时裁决。
3. **配置不下沉到任务。** 绑定与保留选项只存在 `BoardColumn` 上,任务不冗余任何配置副本——改配置立即对所有任务生效,且不需要回填。
4. **按项目隔离。** 所有业务表都可通过 `projectId` 归集,权限校验统一查 `ProjectMember`。
5. **软删除优先。** 项目用 `archived`;任务删除为级联物理删除(T-ST-11),但删除前有二次确认。
6. **迁移只增不改。** 新增需求一律新迁移文件,不修改已执行的迁移。
---
## 2. 概念模型(ER)
```
┌────────────────┐
│ User │ (既有表, 仅补反向关系)
└───┬────────┬───┘
│ │
┌──────────▼──┐ ┌──▼──────────────┐
│ProjectMember│ │ TaskMember │
│(权限关系) │ │ (任务成员) │
└──────┬──────┘ └──▲──────────────┘
│ * │ *
│ │
┌───▼──────┐ ┌─┴──────┐
│ Project │1─*│ Task │*─1┌────────────┐
└───┬──────┘ └───┬────┘ │ BoardColumn│
│ * │ * └────────────┘
┌────▼───────┐ │ ▲
│BoardColumn │1────*┘ │
└────────────┘ (columnId) │
│
┌────────┐ │
│TaskTag │*──────────────────┘
└────────┘
Task 自关联: parentId ── 子任务(一层, 仅顶层任务可作父)
```
基数说明:
| 关系 | 基数 | 级联 |
| --- | --- | --- |
| User → ProjectMember | 1 : N | Cascade |
| Project → ProjectMember | 1 : N | Cascade |
| Project → BoardColumn | 1 : N | Cascade |
| Project → Task | 1 : N | Cascade |
| BoardColumn → Task | 1 : N | **RESTRICT**(有任务的列不可直接删) |
| Task → Task(parent) | 1 : N | Cascade |
| Task → TaskMember | 1 : N | Cascade |
| Task → TaskTag | 1 : N | Cascade |
| User → Task(assignee / creator) | 1 : N | SetNull / Cascade(见下) |
> `BoardColumn → Task` 使用 `RESTRICT` 而非 `Cascade`:防止误删列时静默丢任务。删除列必须先在其接口内把任务迁移走(事务内改 `columnId`),迁移完成后再删列。
---
## 3. 表结构
### 3.1 `Project` — 项目
| 字段 | 类型 | 空 | 默认 | 说明 |
| --- | --- | --- | --- | --- |
| `id` | `INT` PK AI | 否 | — | 主键 |
| `name` | `VARCHAR(50)` | 否 | — | 项目名 |
| `ownerId` | `INT` | 否 | — | 创建人(可退出项目,故与成员表分离) |
| `isPersonal` | `BOOLEAN` | 否 | `false` | 个人默认项目:不可删、不可加成员(T-PJ-02/03) |
| `archived` | `BOOLEAN` | 否 | `false` | 归档(T-PJ-09) |
| `notes` | `JSON` | 否 | `[]` | 项目信息块 `[{title, content}]`(T-PJ-13) |
| `taskKinds` | `JSON` | **是** | `NULL` | 任务类型标签列表;`NULL` 时服务端回落默认四类(T-TK-14) |
| `icon` | `TEXT` | **是** | `NULL` | 项目图标 data URL(客户端裁剪 96x96 JPEG) |
| `autoArchive` | `VARCHAR(8)` | 否 | `'never'` | 自动归档档位:`3d`/`1w`/`1m`/`never`(T-ARC-02) |
| `createdAt` | `DATETIME(3)` | 否 | `now()` | — |
| `updatedAt` | `DATETIME(3)` | 否 | — | `@updatedAt` |
索引:
| 名称 | 字段 | 用途 |
| --- | --- | --- |
| `idx_project_owner` | `(ownerId)` | 查"我的项目" |
| `idx_project_archived` | `(archived)` | 归档分区 |
**约束说明**
- `isPersonal` 唯一性由应用层保证(每个用户至多一个):`UNIQUE(ownerId, isPersonal)` 不可用(普通项目也是 `false`),改为在创建时查询判重。
### 3.2 `ProjectMember` — 项目成员
| 字段 | 类型 | 空 | 默认 | 说明 |
| --- | --- | --- | --- | --- |
| `id` | `INT` PK AI | 否 | — | — |
| `projectId` | `INT` | 否 | — | FK → `Project.id` `ON DELETE CASCADE` |
| `userId` | `INT` | 否 | — | FK → `User.id` `ON DELETE CASCADE` |
| `permission` | `VARCHAR(16)` | 否 | `'member'` | 预留权限分级(T-PM-03),本期恒为 `member` |
| `createdAt` | `DATETIME(3)` | 否 | `now()` | 加入时间 |
索引:
| 名称 | 字段 | 唯一 | 用途 |
| --- | --- | --- | --- |
| `uk_member_project_user` | `(projectId, userId)` | ✔ | 防重复加入;权限查询 |
| `idx_member_user` | `(userId)` | — | "我参与的项目" |
> **注意**:视图记忆与上次项目**不在此表**。视图记忆存设备端(T-VW-06、T-DT-06),早期草案把它放在 `ProjectMember` 上,已在评审中否决。
### 3.3 `BoardColumn` — 看板列
**本模块新增的核心实体**,承载三件事:显示顺序、状态绑定、已完成保留选项。
| 字段 | 类型 | 空 | 默认 | 说明 |
| --- | --- | --- | --- | --- |
| `id` | `INT` PK AI | 否 | — | — |
| `projectId` | `INT` | 否 | — | FK → `Project.id` `ON DELETE CASCADE` |
| `name` | `VARCHAR(20)` | 否 | — | 列名,项目内唯一(T-COL-07) |
| `sortOrder` | `INT` | 否 | `0` | 显示顺序;**决定自动归位取"第一个匹配列"**(T-BD-15) |
| `boundStatus` | `VARCHAR(8)` | **是** | `NULL` | `NULL`=不绑定;或 `todo`/`doing`/`done`(T-BD-02) |
| `retention` | `VARCHAR(8)` | 否 | `'forever'` | `never`/`today`/`3d`/`week`/`month`/`forever`(T-FL-02) |
| `createdAt` | `DATETIME(3)` | 否 | `now()` | — |
| `updatedAt` | `DATETIME(3)` | 否 | — | — |
索引:
| 名称 | 字段 | 唯一 | 用途 |
| --- | --- | --- | --- |
| `uk_column_project_name` | `(projectId, name)` | ✔ | 列名不重名(T-COL-07) |
| `idx_column_project_sort` | `(projectId, sortOrder)` | — | 看板渲染与归位查找 |
**默认数据**(新建项目时由应用层在事务内写入,T-COL-01、T-BD-09):
| sortOrder | name | boundStatus | retention |
| --- | --- | --- | --- |
| 0 | `Plan` | `todo` | `forever` |
| 1 | `Doing` | `doing` | `forever` |
| 2 | `Action` | `NULL` | `forever` |
| 3 | `Done` | `done` | `forever` |
> **为什么保留选项默认为 `forever` 而不是 `never`**:默认应"看得见",避免用户误以为数据丢失;隐藏是主动行为。
**约束与校验**
| 规则 | 实现层 |
| --- | --- |
| 列数 1~10(上限待确认,T-COL-06) | 应用层:新增/删除前计数校验 |
| 至少保留 1 列(T-EX-17) | 应用层:删除前 count > 1 |
| 删列必须指定迁移目标(T-EX-16) | 应用层:接口必须带 `moveToColumnId` |
| `boundStatus` 取值合法 | 应用层白名单校验 |
| `retention` 取值合法 | 应用层白名单校验 |
### 3.4 `Task` — 任务 / 子任务
| 字段 | 类型 | 空 | 默认 | 说明 |
| --- | --- | --- | --- | --- |
| `id` | `INT` PK AI | 否 | — | — |
| `projectId` | `INT` | 否 | — | FK → `Project.id` `ON DELETE CASCADE` |
| `columnId` | `INT` | 否 | — | FK → `BoardColumn.id` `ON DELETE RESTRICT`(T-BD-01) |
| `parentId` | `INT` | **是** | `NULL` | 非空 = 一级子任务;FK → `Task.id` `ON DELETE CASCADE`(T-ST-01/11) |
| `title` | `VARCHAR(200)` | 否 | — | 唯一必填(T-TK-02);应用层限 30 字(T-TK-14) |
| `description` | `TEXT` | 否 | `''` | 快速添加超长文本的余文并入此处(T-TK-14) |
| `kind` | `VARCHAR(20)` | **是** | `NULL` | 任务类型标签,取值见 `Project.taskKinds` |
| `markDate` | `DATETIME(3)` | **是** | `NULL` | 时间标记,完整日期存储、展示 `MM-dd`(T-TK-15;迁移 20261025120000) |
| `status` | `VARCHAR(8)` | 否 | `'todo'` | `todo`/`doing`/`done`(T-TK-03) |
| `doneAt` | `DATETIME(3)` | **是** | `NULL` | 完成时刻;**保留选项判定依据**(T-FL-10) |
| `assigneeId` | `INT` | **是** | `NULL` | 负责人(单人);新建默认创建者(T-TK-09) |
| `creatorId` | `INT` | 否 | — | 创建人;拷贝时为操作者(T-CP-04) |
| `extras` | `JSON` | 否 | `[]` | 扩展信息卡片 `[{label,value,url?}]`(T-TK-04/05) |
| `sortOrder` | `INT` | 否 | `0` | 列内/列表内排序 |
| `archived` | `BOOLEAN` | 否 | `false` | 归档标记;主查询一律 `archived = false`(T-ARC-01/03) |
| `archivedAt` | `DATETIME(3)` | **是** | `NULL` | 归档时刻;归档查询的过滤/排序键(T-ARC-04) |
| `archiveSource` | `VARCHAR(8)` | **是** | `NULL` | `'manual'` / `'auto'` 溯源(T-ARC-01/02) |
| `createdAt` | `DATETIME(3)` | 否 | `now()` | — |
| `updatedAt` | `DATETIME(3)` | 否 | — | — |
外键与索引:
| 名称 | 字段 | 类型 | 说明 |
| --- | --- | --- | --- |
| `fk_task_project` | `(projectId)` | Cascade | 删项目连带删任务 |
| `fk_task_column` | `(columnId)` | **Restrict** | 有任务的列不可直接删(见 §2 说明) |
| `fk_task_parent` | `(parentId)` | Cascade | 删父任务连带删子任务(T-EX-07) |
| `fk_task_assignee` | `(assigneeId)` | SetNull | 用户删除时清空负责人 |
| `fk_task_creator` | `(creatorId)` | Cascade | 用户删除时连带删其创建的任务* |
| `idx_task_project_column` | `(projectId, columnId)` | — | 看板按列查询 |
| `idx_task_project_status` | `(projectId, status)` | — | 列表/统计 |
| `idx_task_parent` | `(parentId)` | — | 取子任务 |
| `idx_task_done_at` | `(projectId, doneAt)` | — | 保留窗口计算 |
| `idx_task_project_archived` | `(projectId, archived, archivedAt)` | — | 已归档查询(T-ARC-04) |
| `idx_task_project_status_arch` | `(projectId, status, archived)` | — | 主列表/看板排除归档(T-ARC-03) |
> \* `creatorId` 用 Cascade 会丢失"他人创建的任务"的历史;更稳妥的做法是 `Restrict` 并禁止删除有任务的用户。**建议改为 `Restrict`**,与既有 `User` 删除行为保持一致(本期系统无删除用户入口)。
**校验规则**
| 规则 | 位置 |
| --- | --- |
| `title` trim 后非空且 ≤ 200 | 应用层 |
| `parentId` 非空时,其父任务 `parentId` 必须为空(禁止二级,T-ST-01) | 应用层 |
| 子任务 `projectId` == 父任务 `projectId`(T-EX-12) | 应用层 + 事务内校验 |
| `parentId` 不得指向自身或其后代(T-EX-15) | 应用层 |
| `status='done'` 时写 `doneAt=now()`;退出 `done` 时置 `NULL` | 应用层(领域统一裁决) |
| 归档/恢复父任务连带全部后代,同事务内更新 `archived`/`archivedAt`/`archiveSource`(T-ARC-01/05) | 应用层 |
| `autoArchive` 枚举 `3d`/`1w`/`1m`/`never`,非法 400(T-ARC-02) | 应用层 |
| `assigneeId` 必须是 `ProjectMember` 中的用户(T-AS-03) | 应用层 |
### 3.5 `TaskMember` — 任务成员
| 字段 | 类型 | 空 | 默认 | 说明 |
| --- | --- | --- | --- | --- |
| `id` | `INT` PK AI | 否 | — | — |
| `taskId` | `INT` | 否 | — | FK → `Task.id` `ON DELETE CASCADE` |
| `userId` | `INT` | 否 | — | FK → `User.id` `ON DELETE CASCADE` |
索引:`uk_task_member (taskId, userId)` 唯一。
### 3.6 `TaskTag` — 任务标签
| 字段 | 类型 | 空 | 默认 | 说明 |
| --- | --- | --- | --- | --- |
| `id` | `INT` PK AI | 否 | — | — |
| `taskId` | `INT` | 否 | — | FK → `Task.id` `ON DELETE CASCADE` |
| `name` | `VARCHAR(20)` | 否 | — | 标签名 |
索引:`idx_tag_task (taskId)`。
---
## 4. Prisma Schema(草案)
```prisma
model Project {
id Int @id @default(autoincrement())
name String @db.VarChar(50)
ownerId Int
isPersonal Boolean @default(false)
archived Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
owner User @relation("ProjectOwner", fields: [ownerId], references: [id])
members ProjectMember[]
columns BoardColumn[]
tasks Task[]
@@index([ownerId])
@@index([archived])
@@map("Project")
}
model ProjectMember {
id Int @id @default(autoincrement())
projectId Int
userId Int
permission String @default("member") @db.VarChar(16)
createdAt DateTime @default(now())
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([projectId, userId])
@@index([userId])
@@map("ProjectMember")
}
model BoardColumn {
id Int @id @default(autoincrement())
projectId Int
name String @db.VarChar(20)
sortOrder Int @default(0)
boundStatus String? @db.VarChar(8) // null | todo | doing | done
retention String @default("forever") @db.VarChar(8)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
tasks Task[]
@@unique([projectId, name])
@@index([projectId, sortOrder])
@@map("BoardColumn")
}
model Task {
id Int @id @default(autoincrement())
projectId Int
columnId Int
parentId Int?
title String @db.VarChar(200)
description String @default("") @db.Text
status String @default("todo") @db.VarChar(8)
doneAt DateTime?
assigneeId Int?
creatorId Int
extras Json @default("[]")
sortOrder Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
column BoardColumn @relation(fields: [columnId], references: [id], onDelete: Restrict)
parent Task? @relation("Subtask", fields: [parentId], references: [id], onDelete: Cascade)
children Task[] @relation("Subtask")
assignee User? @relation("TaskAssignee", fields: [assigneeId], references: [id], onDelete: SetNull)
creator User @relation("TaskCreator", fields: [creatorId], references: [id], onDelete: Restrict)
members TaskMember[]
tags TaskTag[]
@@index([projectId, columnId])
@@index([projectId, status])
@@index([parentId])
@@index([projectId, doneAt])
@@map("Task")
}
model TaskMember {
id Int @id @default(autoincrement())
taskId Int
userId Int
task Task @relation(fields: [taskId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([taskId, userId])
@@map("TaskMember")
}
model TaskTag {
id Int @id @default(autoincrement())
taskId Int
name String @db.VarChar(20)
task Task @relation(fields: [taskId], references: [id], onDelete: Cascade)
@@index([taskId])
@@map("TaskTag")
}
```
`User` 需补反向关系(不改字段):
```prisma
model User {
// …既有字段…
ownedProjects Project[] @relation("ProjectOwner")
projectMembers ProjectMember[]
assignedTasks Task[] @relation("TaskAssignee")
createdTasks Task[] @relation("TaskCreator")
taskMembers TaskMember[]
}
```
---
## 5. 关键查询
| 场景 | 查询 |
| --- | --- |
| 我的项目 | `SELECT p.* FROM Project p JOIN ProjectMember m ON m.projectId=p.id WHERE m.userId=? AND NOT p.archived` |
| 项目详情 + 列 | `SELECT * FROM BoardColumn WHERE projectId=? ORDER BY sortOrder` |
| 看板任务(含子任务) | 顶层:`WHERE projectId=? AND parentId IS NULL`;子任务:`WHERE parentId IN (...)` 一次取回后在内存分组(避免 N+1) |
| 权限校验 | `SELECT 1 FROM ProjectMember WHERE projectId=? AND userId=?` |
| 保留窗口过滤 | 服务端可选:`status <> 'done' OR (doneAt >= ?)`;前端另有一份同口径实现 |
| 自动归位选列 | 内存中对已加载的 `columns`(按 sortOrder)`find(c => c.boundStatus === status)` |
| 已归档查询(T-ARC-04) | `WHERE projectId=? AND archived=1 [AND kind=?] [AND columnId=?] [AND archivedAt BETWEEN ? AND ?] [AND title LIKE ?] ORDER BY archivedAt DESC, id DESC LIMIT ?+1`;多取 1 条判定 `nextCursor` |
**防 N+1**:一次请求返回 `{ project, columns, members, tasks }`,子任务与父任务同批返回,由前端按 `parentId` 分组。
---
## 6. 迁移与初始化
### 6.1 迁移文件
`prisma/migrations/20261024xxxxxx_add_task_module/migration.sql`:
1. 建 `Project`、`ProjectMember`、`BoardColumn`、`Task`、`TaskMember`、`TaskTag` 六张表;
2. 建外键(注意 `Task.columnId` 为 `RESTRICT`);
3. 建唯一索引与普通索引;
4. **不做数据回填**(本模块为新功能,无历史数据)。
后续增量迁移:
| 迁移 | 内容 | 状态 |
| --- | --- | --- |
| `20261022120000_add_checkin_checklist` | 打卡/清单模块 | 已上线 |
| `20261025030000_task_kind` | `Project.taskKinds` / `Task.kind` | 已上线 |
| `20261025120000_task_mark_date` | `Task.markDate` | 已上线 |
| `20261025130000_project_icon` | `Project.icon` | 已上线 |
| `20261026000000_task_archive` | 任务归档三字段 + 两索引(T-ARC,DDL 见 §3.1/§3.4 与功能设计 §15) | **待实施** |
### 6.2 个人默认项目初始化
```sql
-- 应用层逻辑(伪 SQL),首次访问 /api/projects 时执行
INSERT INTO Project (name, ownerId, isPersonal, createdAt, updatedAt)
SELECT CONCAT(u.name, '的任务'), u.id, 1, NOW(3), NOW(3)
FROM User u
WHERE u.id = ?
AND NOT EXISTS (SELECT 1 FROM Project p WHERE p.ownerId = u.id AND p.isPersonal = 1);
-- 随后为该项目的 4 个默认列插入 BoardColumn(同事务)
```
### 6.3 新建普通项目
同事务内:建 `Project`(`isPersonal=0`)→ 写 `ProjectMember(owner)` → 写 4 个默认 `BoardColumn`。**任一步失败整体回滚**,避免出现"无列项目"。
---
## 7. 容量与性能估算
| 项 | 估算 | 依据 |
| --- | --- | --- |
| 单项目任务 | 千级 / 年 | T-NF-05 |
| 单项目列 | ≤ 10 | T-COL-06 |
| `Task` 行宽 | ~400 B + JSON | 含 `extras` |
| 单项目年增量 | ≈ 400 KB | 1k 任务 × 400B |
| 索引开销 | 约表大小 30% | 4 个二级索引 |
**无需分表**;按 `projectId` 的查询均可走索引。
---
## 8. 与既有模块的关系
| 项 | 说明 |
| --- | --- |
| `User` 表 | 仅补反向关系,**不改结构**,不影响打卡/宏观/监控模块 |
| 打卡模块(`Habit`/`CheckIn`) | 完全独立,同为"按用户隔离";任务模块改为"按项目隔离" |
| 迁移顺序 | 在 `20261022120000_add_checkin_checklist` 之后追加,互不依赖 |
| 时区 | 沿用 Asia/Shanghai 应用层口径;`doneAt` 用 `DATETIME(3)` 存 UTC |
---
## 9. 未决事项
| # | 事项 | 当前处理 |
| --- | --- | --- |
| 1 | 列数上限是否 10 | 按 10 设计(T-COL-06 标注"待确认"),若定其他值只改常量 |
| 2 | `Task.creatorId` 外键行为 | 草案建议 `Restrict`(见 §3.4 注),需与"用户删除策略"一并确认 |
| 3 | 孤儿项目(无成员)的维护 | 本期不处理(T-PJ-08),由后台流程清理;数据库层保留数据 |
| 4 | `extras` 条目数量上限 | 建议 ≤ 20,未在需求中约束,设计预留 |
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,451 @@
# iBoard V0.2 任务模块 设计文档 — 框架设计
| 项 | 内容 |
| --- | --- |
| 版本 | V0.2(对应分支 `dev-0.2`,应用版本 `0.2.0`) |
| 文档状态 | **设计基线**(待评审;不含实现代码) |
| 上游文档 | `任务模块需求文档.md`(需求编号 `T-*` 原文见该文档,本文按编号逐条对应) |
| 配套文档 | `任务模块功能设计.md`(功能设计,需求点一一对应)、`任务模块数据库设计文档.md`、`任务模块界面设计.html` |
| 技术栈 | Tauri 2 + React 18 + antd-mobile + React Router v6 + Express 4 + Prisma / MariaDB |
| 设计基线 | 移动端优先,视口 420×820,375~430px 自适应;时区统一 `Asia/Shanghai` |
> **编号约定**:本文多处出现需求编号(如 `T-BD-14`),其完整描述与验收要点以《任务模块需求文档》为准。每个模块的「需求点 → 设计落点」对照见《任务模块功能设计》。
---
## 1. 总体设计
### 1.1 目标与范围
在既有 iBoard 之上新增**任务模块**,并把它作为**顶级 Tab 插入「打卡」之后**(顺序:打卡 → **任务** → 宏观 → 监控 → 我,默认首页仍为打卡,T-PM-06)。
模块解决的问题与核心隐喻:
| 视图 / 概念 | 解决的问题 | 核心隐喻 |
| --- | --- | --- |
| **列表视图** | 快速记录、逐条推进 | Todoist 式条目 + 一级子任务缩进 |
| **看板视图** | 多人分工下的阶段流转 | 列(阶段)× 卡片(任务),长按拖拽换列 |
| **项目** | 数据与权限的隔离单位 | 项目即"工作区",成员可见即可读写 |
| **列-状态绑定** | 让"拖动列"与"改状态"两个动作不必重复做 | 列是**空间**,状态是**属性**,绑定是二者之间的**可选桥** |
| **列保留选项** | 已完成任务不淹没在列表里 | 每列自己的"保质期" |
**设计的关键取舍**:把「看板列」与「任务状态」设计成**两个独立维度**,再用**项目级、逐列的绑定配置**把它们关联起来。这样既支持"拖动即改状态"的一体化体验,也支持"这一列只是分类、不进状态机"的轻量用法——这正是需求中 `Action` 默认不绑定状态的意图。
### 1.2 架构总览
```
┌──────────────────── 前端 (React + antd-mobile,打包进 Tauri / OTA) ────────────────────┐
│ App.jsx 路由 │
│ └ /task ──► TaskPage (首屏: 上=RecentProjects 条, 下=当前项目 ProjectView) │
│ /task/list ──► ProjectList (完整项目列表, 从「更多项目」进入) │
│ /task/:pid ──► TaskPage (打开指定项目, 供深链) │
│ ProjectView ──┬── ListView (列表 + 子任务缩进折叠) │
│ ├── BoardView (看板 + 长按拖拽) │
│ └── ProjectManage (项目管理: 成员 / 列设置 / 项目信息) │
│ 组件: TaskCard / SubtaskRow / StatusDot / ExtrasCard / UserPicker / RecentProjects │
│ 状态: TaskStore (项目内单例) / ReorderableList / useLongPressDrag │
│ 工具: src/pages/task/date.js (与后端同口径的自然日边界) │
│ src/pages/task/prefs.js (设备端: 视图记忆 / 上次项目 / 最近项目手动排序) │
└──────────────────────────────┬─────────────────────────────────────────────────────────┘
│ HTTPS + JWT (Authorization: Bearer)
┌──────────────────────────────▼─────────────────────────────────────────────────────────┐
│ Express │
│ /api/projects/* ─► routes/projects.js ─┐ │
│ /api/tasks/* ─► routes/tasks.js ─┤─► middleware.authRequired │
│ /api/columns/* ─► routes/columns.js ─┤─► requireProjectMember (项目成员校验) │
│ └─► task-domain/ ← 【领域逻辑唯一来源】 │
│ resolveStatus() 列/状态联动裁决 │
│ resolveColumn() 自动归位 │
│ retentionWindow() 保留窗口 │
│ copyTask() 跨项目拷贝 │
└──────────────────────────────┬─────────────────────────────────────────────────────────┘
▼
MariaDB: Project / ProjectMember / BoardColumn / Task / TaskMember / TaskTag
```
### 1.3 分层与职责
| 层 | 位置 | 职责 |
| --- | --- | --- |
| 页面 | `src/pages/task/*.jsx` | 视图编排、本地状态、乐观更新与回滚 |
| 组件 | `src/components/task/*.jsx` | 纯展示 + 局部交互(卡片、列、子任务行、状态点、扩展卡片) |
| 设备端偏好 | `src/pages/task/prefs.js` | `viewMode` / `lastProjectId`(**仅设备端**,T-VW-06、T-VW-07) |
| 日期工具 | `src/pages/task/date.js` | 自然日边界计算,**与后端 `task-domain/date.js` 同口径,改一处必须同步另一处** |
| API 封装 | `src/api/task.js` | 统一走既有 `api()`(自带 token / 错误解析) |
| 路由 | `server/routes/tasks.js`、`projects.js`、`columns.js` | 参数校验、鉴权、组装展示态 |
| 领域逻辑 | `server/task-domain/*.js` | **列-状态裁决、自动归位、保留窗口、拷贝**(唯一口径) |
| 持久化 | Prisma Client | 表操作 |
> **与打卡模块的区别**:打卡的周期口径只有 `period.js` 一个文件;任务模块的领域规则(三条)彼此有依赖(归位依赖列序、保留依赖完成时间与列配置),统一收敛到 `task-domain/` 目录,**前后端不得各自实现一遍裁决逻辑**——前端只做乐观更新,最终以服务端返回为准。
### 1.4 目录结构(本次新增/修改)
```
prisma/
schema.prisma # +6 个 model,User 加反向关系
migrations/20261024xxxxxx_add_task_module/ # 新建表 + 外键 + 索引
server/
task-domain/
date.js # 自然日边界(今天/本周/本月)
resolveStatus.js # 列→状态、状态→列、自动归位
retention.js # 列保留窗口 → 可见性
copyTask.js # 跨项目深拷贝
routes/
projects.js # /api/projects/*
columns.js # /api/columns/*
tasks.js # /api/tasks/*
index.js # 挂载三个路由
src/
api/task.js # projectApi / columnApi / taskApi
pages/task/
date.js # 与后端同口径
prefs.js # 设备端偏好(视图记忆/上次项目/最近项目手动排序)
TaskPage.jsx # 首屏容器: RecentProjects 条 + 当前项目 ProjectView
ProjectList.jsx # 完整项目列表(/task/list, 从「更多项目」进入)
ProjectView.jsx # 列表/看板容器
ListView.jsx # 列表视图
BoardView.jsx # 看板视图
ColumnSettings.jsx # 列管理 + 绑定 + 保留选项(由项目管理进入)
TaskDetail.jsx # 任务详情(含扩展卡片 / 子任务;底部安全距离)
ProjectMembers.jsx # 成员管理(由项目管理进入)
ProjectManage.jsx # 项目管理: 成员 / 列设置 / 项目信息 三区
components/task/
TaskCard.jsx SubtaskRow.jsx StatusDot.jsx
ExtrasCard.jsx UserPicker.jsx useLongPressDrag.js
RecentProjects.jsx # 首屏最近项目条(卡片可长按排序)
components/BottomNav.jsx # Tab 增「任务」,位于打卡之后
components/TabIcon.jsx # 增 task 图标
App.jsx # +/task、/task/:pid
styles/globals.css # .task-* 样式段
docs/V0.2/任务模块/ # 需求 / 框架 / 功能 / 数据库 / 界面
```
### 1.5 数据流总览
```
用户操作 ──► 前端乐观更新(本地 store 立即变)
│
▼
PUT /api/tasks/:id { columnId?, status?, done? }
│
▼
requireProjectMember(非成员 403)
│
▼
resolveStatus() ── 读项目 columnBindings(列序 + 绑定)
│ ① 目标列绑定状态 ⇒ status = boundStatus
│ ② 目标列不绑定 ⇒ 保留原 status
│ ③ 状态变了且当前列不绑定该状态 ⇒ 自动归位(列序首个匹配)
│ ④ 无匹配列 ⇒ 列不动
▼
写库(status + columnId 同时落库)
│
▼
返回最新任务 ──► 前端以响应覆盖本地乐观值(不一致则纠正)
```
---
## 2. 领域规则设计(重点)
> 本模块所有"难"的地方都集中在**列与状态的双向联动**上。下面把规则收敛为一组**无歧义的判定函数**,前后端都以此为准。
### 2.1 两个正交维度的定义
| 维度 | 字段 | 取值 | 谁决定 |
| --- | --- | --- | --- |
| **空间** | `Task.columnId` | 指向项目内某个 `BoardColumn` | 用户拖拽 / 自动归位 |
| **属性** | `Task.status` | `todo`(未开始)/ `doing`(执行中)/ `done`(完成) | 用户勾选 / 列绑定联动 |
**二者独立落库**,不做互相推导(见《数据库设计文档》§2)。原因:
1. 未绑定的列(如默认 `Action`)要求二者**完全正交**,任何推导都会丢失信息;
2. 同一状态可被多列绑定(T-BD-03),状态无法反推唯一列;
3. 保留选项依赖"完成时间"而非列,推导会引入隐式状态。
### 2.2 三种联动场景与判定函数
设项目列列表为 `C = [c₁, c₂, …, cₙ]`(按 `sortOrder` 升序),每列 `c.boundStatus ∈ {todo, doing, done, null}`。
**场景 A · 拖拽换列(`T-BD-04/07/08`)**
```
function onColumnChange(task, targetColumnId):
c = columnById(targetColumnId)
if c.boundStatus != null:
task.status = c.boundStatus # 列决定状态
else:
# 目标列不绑定 ⇒ 保留原状态(T-BD-07、T-EX-05)
pass
task.columnId = targetColumnId
```
> **注意**:这里**不存在「从某列拖出即回退状态」的规则**。是否改状态只看**目标列**。这是评审中明确修正过的口径(原设计曾写"拖出回退为未开始",已废弃)。
**场景 B · 改状态(勾选/选择器,`T-BD-14/15/16`)**
```
function onStatusChange(task, newStatus):
task.status = newStatus
# 当前列是否已经"匹配"这个状态?
if task.column.boundStatus == newStatus:
return # 已匹配,列不动
# 不匹配 ⇒ 自动归位,按列序取第一个绑定 newStatus 的列(T-BD-15)
target = first(c in C where c.boundStatus == newStatus)
if target != null:
task.columnId = target.id
else:
# 无匹配列 ⇒ 不移动,保留原列(T-BD-16)
pass
```
**场景 C · 子任务(`T-BD-13`)**
```
if task.parentId != null:
# 子任务完全跳过上述两种联动
persist(status, columnId) as-is
```
### 2.3 判定矩阵(可转写为单测)
以默认配置 `plan→todo, doing→doing, action→null, done→done` 为例:
| # | 初始(列 / 状态) | 操作 | 结果(列 / 状态) | 覆盖需求 |
| --- | --- | --- | --- | --- |
| 1 | plan / todo | 拖到 doing | doing / doing | T-BD-04 |
| 2 | plan / todo | 拖到 action(不绑定) | action / **todo**(保留) | T-BD-07、T-BD-08 |
| 3 | action / todo | 改为 done | **done** / done(自动归位) | T-BD-14、T-BD-15 |
| 4 | action / todo | 改为 doing | doing / doing | T-BD-14 |
| 5 | done / done | 拖到 action | action / **done**(保留) | T-BD-07、T-EX-05 |
| 6 | done / done | 改为 doing | doing / doing | T-BD-14 |
| 7 | plan / todo | 改为 todo | plan / todo(已匹配,不动) | T-BD-14 |
| 8 | 全部列不绑定 done,任务在 action | 改为 done | action / done(**无匹配不移动**) | T-BD-16 |
| 9 | 父任务 done,子任务在 action | 改父任务状态 | 子任务完全不受影响 | T-BD-13 |
| 10 | 两列都绑定 done(done、action) | 改为 done | 列序靠前者(done) | T-BD-06、T-BD-15 |
> 第 8 行是评审中特别确认的:**没有「已完成统一归到 Done 列」的强制规则**,那只是默认配置下的自然效果。用户取消全部绑定后,任务就停在原列。
### 2.4 列配置变更的语义(`T-BD-11`、`T-COL-08`)
**核心原则:配置变更只影响后续操作,不批量改写历史数据。**
| 变更 | 处理 |
| --- | --- |
| 列 A 由「绑定 done」改为「不绑定」 | 已在 A 中且状态为 done 的任务**保持不动**(列与状态都保留),见 T-EX-03 |
| 列 A 绑定状态由 doing 改为 done | 已在 A 中的任务状态**不回写**;后续拖入 A 的任务才按 done 处理(T-BD-11) |
| 删除列 A | 二次确认 → 列内任务迁移到指定列(默认相邻列)→ 迁移后**按目标列重新裁决状态**;A 的绑定解除(T-COL-08、T-EX-16) |
| 新增列 | 取默认值 `retention=forever`、`boundStatus=null`,**不继承相邻列**(B-18) |
> 为什么删除列要"重新裁决"而改绑定不裁决:删除是**数据搬家**(任务必须有个列),搬家后必须与目标列的语义自洽;而改绑定只是改了"未来的规则",不该动已有数据。
### 2.5 空归属与一致性保障
| 风险 | 保障 |
| --- | --- |
| 删除列后任务 `columnId` 悬空 | 迁移在同事务内完成;`Task.columnId` 为 `NOT NULL`,外键 `ON DELETE RESTRICT` 从库层面阻止误删 |
| 列被删后 `boundStatus` 残留 | `BoardColumn` 是实体,列删则绑定自然消失,无残留(优于早期 JSON 方案) |
| 并发拖拽同一任务 | 服务端以「读-判定-写」在同一事务内完成;前端防抖 + 版本号(可选) |
| 状态改了但归位列不存在 | 判定函数显式处理「无匹配列」分支(T-BD-16),不抛异常 |
---
## 3. 重点流程设计
### 3.1 看板拖拽(长按 → 跨列 → 落到子任务)
这是本模块交互最复杂的一条链路,同时涉及**换列**与**降级为子任务**两种语义。
```
长按 300ms ─► 卡片抬起(缩小 + 浮层跟随手指)
│
├─ 拖到【另一列空白区】
│ └─ 松手 → onColumnChange(任务, 目标列)
│ → 按 §2.2 场景 A 裁决
│ → 乐观更新本地列与状态 → PATCH(失败回滚 + Toast)
│
├─ 拖到【另一列某卡片上】
│ └─ 该卡片高亮为"落点"
│ 松手 → 若落点是顶层任务且被拖任务无子任务 ⇒ 降为子任务
│ ├─ 是子任务 ⇒ 拒绝(T-ST-13)
│ ├─ 被拖任务有子任务 ⇒ 拒绝(T-ST-14)
│ └─ 拖到自己/后代 ⇒ 拒绝(T-EX-15)
│
└─ 拖到【原列】→ 视为排序(本期仅更新 sortOrder)
```
**难点与对策**:
| 难点 | 对策 |
| --- | --- |
| 移动端长按与滚动冲突 | 长按 300ms 后才进入拖拽态;进入后 `touch-action:none` 并锁定滚动容器 |
| 看板横向滚动 vs 拖拽换列 | 拖拽时容器允许横向自动滚动(边缘 24px 触发),松手位置决定列 |
| 落点判定(列 vs 卡片) | 用 `elementFromPoint` 命中 `[data-column-id]` / `[data-task-id]`,卡片优先 |
| 误操作 | 拒绝场景统一 Toast 说明原因;松手动画回弹原位 |
| 降为子任务后卡片消失 | 子任务不出独立卡片(T-ST-10),故从其原列移除,父任务卡片内出现该子任务行(T-ST-06) |
> 兜底:无法拖拽时(如长按被系统手势占用),卡片「⋯」菜单提供「移动到列 / 移动到子任务」的等价操作(T-UI-07)。
### 3.2 保留选项的可见性计算与"当场消失"
**窗口计算**(后端 `task-domain/retention.js`,自然日口径):
```
function retentionStart(option, now):
today = startOfDay(now) # Asia/Shanghai 00:00
switch option:
never => 返回"不可见"
forever => 返回"无下界"(全可见)
today => today
3d => today - 2 天 # 含今天共 3 天
week => 本周一 00:00
month => 本月 1 日 00:00
可见性判定:task.status == done
? (doneAt != null && doneAt >= retentionStart(该任务所属列的 option))
: true # 未完成任务始终可见(T-FL-12)
```
**判定依据**:`Task.doneAt`(完成时刻),**与任务当前所在列无关**(T-FL-10);归属哪一列决定用哪一列的选项(T-FL-11)。
**前端"当场消失"**(T-EX-18):
```
每分钟(以及跨日/跨周/跨月边界整点)执行一次:
重新计算当前可见集合
对"由可见变不可见"的任务卡片:
加 .is-leaving(透明 + 高度收缩,约 240ms)
动画结束移除 DOM
其余卡片通过布局动画平滑重排(不做全局刷新)
```
> 也允许退化为"刷新时计算"——需求明确接受该实现方式(§5 决策 23)。跨周期边界用**定时器 + 页面可见性变化(`visibilitychange`)**双保险。
### 3.3 跨项目拷贝
拷贝是**深拷贝**,且要处理目标项目成员差异(T-CP-07)与绑定差异(T-CP-09)。
```
POST /api/tasks/:id/copy { projectId: 目标项目 }
│
├─ 校验:源任务可读、目标是本人成员(T-CP-06)
├─ 事务内:
│ newTask = create({
│ ...全部业务字段(标题/描述/状态/列/扩展卡片/标签)
│ creatorId = 当前操作者 ← T-CP-04
│ projectId = 目标项目
│ columnId = 目标项目中"列名相同"的列,找不到则取第一列
│ })
│ assigneeId = 目标项目成员?保留 : 置 null ← T-CP-07
│ taskMembers = 过滤出目标项目成员中的交集 ← T-CP-07
│ for 每个子任务:同法 create(parentId = newTask.id) ← T-CP-03、T-CP-08
│ 状态与列不强行纠正(T-EX-14)
└─ 返回 { task, dropped: { assignee, members[] } } ← 提示被丢弃的人员
```
**新旧任务完全独立**(T-CP-05):拷贝后无任何关联字段,是两条独立记录。
### 3.4 个人默认项目的创建与保护
```
创建时机:用户首次访问 /api/projects(或注册时)→ 不存在 isPersonal 项目则创建
name = `${user.name}的任务`(T-PJ-04,可改名)
保护规则(服务端强制,前端不提供入口):
DELETE /api/projects/:id → isPersonal ⇒ 400(T-PJ-02、T-EX-09)
POST /api/projects/:id/members → isPersonal ⇒ 400(T-PJ-03、T-EX-09)
PUT /api/projects/:id → 允许改名(T-PJ-04)
```
### 3.5 成员清空后的项目
删除最后一名成员时**不删除项目**,只删 `ProjectMember` 记录(B-11、T-PJ-08):
- 项目成为"无成员项目",任何人都不可见(T-PJ-10);
- 前端不做特判、不报错(T-EX-11);
- 数据保留,由后台维护流程后续处理(不在本模块范围,§1.5)。
> 副作用:曾为该项目任务的负责人 / 成员会显示为「已退出」(T-EX-06),因为其 `userId` 已不是项目成员。
---
## 4. 状态管理与性能
### 4.1 前端状态
| 状态 | 存放 | 生命周期 |
| --- | --- | --- |
| 当前项目、任务列表、列配置 | `TaskStore`(项目内单例) | 进入项目时加载,切换项目时替换 |
| 视图(list/board) | **设备端** `localStorage.iboard_task_prefs` | 按 `projectId` 分键(T-VW-03/06) |
| 上次访问项目 | **设备端** 同上 | 单个 key(T-VW-07) |
| 折叠状态(子任务) | 内存(组件内) | 会话内,不持久化 |
| 弹窗 / 拖拽态 | 组件内 | 操作结束即清 |
### 4.2 性能设计
| 场景 | 目标 | 手段 |
| --- | --- | --- |
| 首屏加载(T-NF-01) | < 1.5s | 任务与列配置**一次请求返回**;子任务随父任务同批返回 |
| 交互响应(T-NF-01) | < 300ms | 乐观更新,不等接口返回 |
| 千级任务(T-NF-05) | 可用 | 索引覆盖(`projectId+columnId`、`projectId+status`);列表虚拟化(按需) |
| 保留计算 | 不额外请求 | 前端本地按 `doneAt` + 列选项实时过滤,切换档位零请求 |
| 看板拖拽 | 60fps | 拖拽期只用 transform,不改布局;落下后才重排 |
### 4.3 一致性与容错
- **乐观更新 + 回滚**:失败时恢复快照并 Toast(T-EX-02)。
- **幂等**:拖拽/勾选 500ms 内重复提交合并(T-EX-01)。
- **配置变更即时生效**:保存列配置后服务端返回最新列列表,前端整体替换并重算可见集合,无需刷新(T-BD-12)。
---
## 5. 风险与权衡
| # | 风险 / 权衡 | 说明与对策 |
| --- | --- | --- |
| 1 | **列与状态双维度带来的认知成本** | 默认配置下二者一一对应(除 Action),用户"不懂也不会错";只有主动改配置时才需要理解绑定 |
| 2 | **自动归位可能让用户"找不到"任务** | 任务从当前列跳走。对策:归位后该卡片带一次高亮动画;提供 Toast「已移至 <列名>」 |
| 3 | **保留选项与"未绑定完成的已完任务"** | 状态为 done 但所有列都不绑定 done 时,任务停在不绑定列,其可见性按该列选项判定(T-FL-11)——语义自洽但需要文案说明 |
| 4 | **列可增删导致看板横向过宽** | 上限暂定 10 列(T-COL-06,**待确认**);超出时看板横向滚动 + 列头悬浮 |
| 5 | **子任务状态与父任务不一致的展示** | 卡片内嵌子任务带**小状态标记**(T-ST-09),避免"父已完成但子未做"的信息丢失 |
| 6 | **跨项目拷贝丢弃人员** | 返回被丢弃的名单并在前端提示,避免静默丢数据(T-EX-08) |
| 7 | **无成员项目的数据残留** | 本期不处理(T-PJ-08),需后台维护;在数据库设计文档中标注为"孤儿项目" |
| 8 | **前后端日期口径不一致** | `date.js` 双份实现是既有项目的做法;本期继续沿用并在两处加注释互指,写单测覆盖边界(周日 23:59 / 周一 00:00 / 月末) |
---
## 6. 测试与验证要点
| 类别 | 要点 |
| --- | --- |
| **领域规则单测** | §2.3 判定矩阵 10 条全覆盖;`retentionStart()` 六档边界(含周日/月末跨点) |
| **接口测试** | 非项目成员访问 403;个人默认项目的两个保护接口返回 400;拷贝后源任务不变 |
| **交互验证** | 拖拽换列 / 降为子任务 / 拒绝场景 Toast;保留档位切换任务当场消失并重排 |
| **兼容性** | Android WebView + 移动浏览器(T-NF-02);375 / 390 / 430 三档视口 |
| **两视图一致** | 列表与看板可见条数一致(T-FL-11、T-FL-14);任一处修改另一处即时可见(T-UI-03) |
---
## 7. 重点设计(2026-10-24 二轮评审)
### 7.1 任务拖动方案(重做)
旧版(手指跟随浮层 + 落点命中即转子任务)在真机上不可靠:浮层遮挡视线、touch 事件与滚动冲突、拖到卡片上误触转子任务。重做后的方案收敛为「**一个可靠手势 + 一个可靠菜单**」:
| 场景 | 交互 | 实现 |
| --- | --- | --- |
| 看板换列 | 长按 400ms 激活(激活前移动 >10px 视为滚动,自动取消)→ 卡片**原位抬起**(不跟随手指、无浮层、无任务名)→ 指针划过列高亮 → 松手落列 | `useColumnDrag`:Pointer Events + `setPointerCapture`(触摸/鼠标统一、捕获后不丢事件、天然吞掉 pointercancel);`elementFromPoint` 命中 `[data-column-id]`;指针靠近容器边缘时 rAF 自动横滚,可拖到屏幕外的列 |
| 列表移动 | 长按 500ms → 弹「移动到列」ActionSheet(当前列打勾) | `TaskRow` 长按计时 → `ProjectView.openMoveMenu`;列表是纵向滚动流,不做自由拖动 |
| 转为子任务 | 不走拖拽(已移除「拖到卡片上」路径,误触率高) | 详情页「层级」区:选父任务转为子任务 / 一键转为顶层 |
关键细节:卡片 `touch-action: pan-y`(纵向滚动留给浏览器,水平位移给拖动);激活与提交都以同一元素的 pointer 事件为准;`pointercancel` 与 `pointerup` 同样清理状态。换列后的状态仍由服务端按目标列绑定统一裁决(§2.2 不变)。
### 7.2 键盘遮挡统一方案
多个底部弹层(任务详情、信息块编辑、人员选择、拷贝)都出现过"输入框被输入法遮挡"。逐个组件接键盘逻辑不可维护,统一为**一个全局信号 + 一个 CSS 类**:
1. **信号源**:`App` 级唯一监听器 `useKeyboardInsetWatcher`,同时监听 `visualViewport` 的 resize/scroll 与 `window.resize`(配合 `index.html` 的 `interactive-widget=resizes-content`,Chromium 系 WebView 键盘弹出会整体收缩布局视口);以"见过的最大视口高"为基线计算键盘插入量,写入 CSS 变量 **`--kb-inset`**(>90px 才视为键盘,过滤地址栏抖动)。
2. **消费端**:底部弹层统一挂 **`.kb-safe`** 类 —— `padding-bottom: calc(安全区 + 20px + var(--kb-inset))`、`max-height` 用 `dvh` 随视口收缩并内部滚动。组件不再各自 import 键盘 hook、不再写内联 padding。
3. **兜底**:长输入区(子任务输入、信息块 textarea)聚焦 300ms 后 `scrollIntoView({block:'center'})`,双保险。
新增弹层只要挂 `.kb-safe` 即自动获得键盘避让,无需再处理。
@@ -0,0 +1,605 @@
# iBoard V0.2 任务模块 需求文档 — 轻量任务 / 项目管理
> 版本:V0.2 · 模块:任务(Task) · 日期:2026-10-24
> 适用范围:iBoard APP 内的任务模块(沿用现有项目框架、路由体系与登录态实现)
> 关联文档:《设计文档》《数据库设计文档》《界面设计》——接口清单在**设计阶段**重新设计,本文档不含接口定义
---
## 1. 总体需求
### 1.1 一句话描述
在 iBoard 现有 APP 内新增一个**轻量级的项目与任务管理模块**:界面交互参考 Todoist,以**项目**为隔离单位组织任务,提供**列表 / 看板**双视图,并允许**每个项目独立配置看板列与任务状态的对应关系、以及每列已完成任务的保留选项**,用最小认知成本覆盖「个人琐事记录」与「多人协作推进」两类场景。
**「轻量」的含义**:不做需求管理、工时、甘特图、工作流引擎、审批流;字段克制、结构最多两层(任务 + 一级子任务)、权限不做细分角色。目标是用最少的配置让用户"打开就能记、记完就能推进"。
### 1.2 核心概念
| 概念 | 定义 |
| --- | --- |
| **项目(Project)** | 任务归属的隔离单位,**也是权限单位**。用户只能看到自己是成员的项目;项目内成员权限目前均等 |
| **个人默认项目(Personal Project)** | 每个用户自动拥有且仅有一个的私有项目。**不可删除、不可添加其他成员**,名称以用户名开头且可自定义,用于个人琐碎事务 |
| **普通协作项目(Shared Project)** | 用户新建的项目,可增删成员、可归档 |
| **项目成员(Member)** | 用户与项目的多对多关联。成员的**存在**决定权限,「负责人 / 任务成员」字段只决定**展示** |
| **任务(Task)** | 一条事务记录。归属唯一项目;含基础字段 + 内置可折叠的**扩展信息卡片**;特例:子任务 |
| **一级子任务(Subtask)** | 挂在父任务下的任务。**最多一层**,不可再嵌套;必须与父任务同项目;完成状态与父任务**相互独立** |
| **负责人(Assignee)** | 任务上的**单人**标记,字号更大、视觉突出。**新建任务时默认填入创建者本人**,可清空 |
| **任务成员(Task Members)** | 任务上的**多人**标记,字号更小、弱化展示,位于卡片 / 条目下方 |
| **扩展信息卡片** | 任务内置的可折叠信息区,承载**任意名称 / 值 / 链接**的扩展信息(来源不限、字段不固定),**默认收起、全部非必填** |
| **看板列(Column)** | 项目级可配置的列,**默认 4 列** `Plan → Doing → Action → Done`;**可增删改、可调整顺序**,数量不固定 |
| **任务状态(Status)** | 任务的处理阶段,取值为 `未开始` / `执行中` / `完成`。**与看板列是两个独立概念**,二者的对应关系由项目配置决定 |
| **列-状态绑定(Column Status Binding)** | 项目级配置:**每一列独立**选择"是否绑定任务状态"以及"绑定到哪个状态"。绑定后列与状态双向联动;不绑定时二者完全独立 |
| **已完成但未绑定状态** | 任务状态为「完成」、但其所在看板列未绑定到「完成」的情形——此时列保持不变 |
| **看板列保留选项(Column Retention)** | 项目级配置:**每一列独立**设置已完成任务的保留时长,取值为「永不显示」/「当天」/「3 天」/「本周」/「本月」/「永久显示」六档,用于控制该列中已完成任务的可见性 |
| **视图记忆** | 设备端按项目独立保存"上次选择的视图(列表 / 看板)",进入项目自动恢复 |
| **上下文** | 用户当前所处的视图位置(某个项目 / 某个父任务的子任务列表 / 某个列表分区),用于解释"本次修改"相对哪些任务生效 |
### 1.3 完整功能
1. **视图体系**:列表视图、看板视图,可随时互相切换,同源数据、实时双向同步;每个项目独立记忆上次使用的视图。列表只是不按列分组,**任务归属的看板列与看板视图完全一致**。**首屏**上部为「最近项目」(3 个卡片 + 更多入口,卡片可手动排序),下部直接展示**当前项目**的任务管理视图,不必先进项目列表。
2. **看板与任务状态的绑定配置**:
- 看板列**默认 4 列** `Plan → Doing → Action → Done`;列**可增删改、可调整顺序**,数量不固定。
- **每一列独立**选择是否绑定任务状态,并可选择绑定到 `未开始 / 执行中 / 完成` 中的任一个;**允许多列绑定同一状态**。
- **列决定状态**:任务拖入某列时,该列绑定了状态则状态随之改变;该列未绑定则**保留原状态**。
- **状态与列不匹配时自动归位**:状态改变后若当前所在列未绑定该状态,则按**当前列顺序**移入第一个绑定该状态的列;**没有匹配列就不移动**,保留当前位置。
- **默认**:`Plan` 绑定「未开始」、`Doing` 绑定「执行中」、`Done` 绑定「完成」、`Action` **不绑定**。
- 配置是**项目级**的,项目内任意成员均可修改,不同项目互不影响。
- **子任务不受列-状态绑定约束**,其状态可与父任务不一致。
3. **已完成任务显示控制**:**按看板列独立配置保留选项**——`永不显示` / `当天` / `3 天` / `本周` / `本月` / `永久显示` 六选一;超出保留选项的已完成任务在该列中自动隐藏。列表视图按其所属列的配置执行同样的显示控制。
4. **任务字段体系**:基础字段(标题必填,负责人默认本人)+ 内置可折叠**扩展信息卡片**(默认收起、全非必填);负责人(单人)与任务成员(多人)均为信息标记。
5. **负责人 & 成员展示规则**:负责人大字号突出、成员小字号弱化并置于下方;二者只能从**当前项目成员**中选择;不改变项目统一读写权限。
6. **子任务体系**:仅一级、必须同项目、父子完成状态互不联动;列表视图支持缩进与折叠展开;**看板卡片内以简洁形式嵌套展示子任务并可直接勾选**,完整详情在卡片弹窗内编辑;支持**长按拖拽把任务降为另一个任务的一级子任务**。
7. **项目体系**:个人默认项目(不可删、不可加成员、名称以用户名开头且可改名);普通协作项目(可增删成员);创建人可主动退出;成员清空后静默保留,交后台维护。成员管理、列设置与项目信息(改名 / 归档 / 删除 / 退出)统一收进「**项目管理**」入口。
8. **项目权限**:以项目为隔离单位,只能看到自己是成员的项目;项目内成员权限均等、全读写;预留权限分级入口但不实现细分角色;任意成员均可归档项目。
9. **跨项目拷贝**:是**拷贝**不是移动;完整复制全部字段、扩展信息、子任务、负责人、成员、状态、看板列、标签;新任务创建人为执行拷贝者;拷贝后新旧任务完全独立。
10. **UI 交互规范**:参考 Todoist 的列表交互与子任务折叠风格;看板卡片轻量化;列表与看板同源、切换无刷新、数据一致;具备空状态 / 加载状态 / 数据异常状态。
11. **模块入口**:任务模块作为**顶级 Tab** 接入底部导航,位于**「打卡」之后**(顺序:打卡 → 任务 → 宏观 → 监控 → 我)。
### 1.4 典型用法
**场景 A · 个人琐事(个人默认项目)**
用户打开模块 → 进入自己的个人默认项目(如「张三的琐事」)→ 列表视图输入标题回车新增,**负责人自动是本人**,也可清空 → 需要推进时切到看板,把任务从 `Plan` 拖到 `Doing`。
因个人项目**不能加成员**,负责人 / 成员的可选范围只有自己,无需任何权限配置。
**场景 B · 多人协作与列-状态绑定**
用户新建普通项目「APP 改版」并加入 3 名同事。默认配置下:任务在 `Plan`=未开始、`Doing`=执行中、`Done`=完成;`Action` 不绑定状态,任务放进去后状态保持不变。
成员 A 把任务拖入 `Done` ⇒ **状态自动变为「完成」**;成员 B 在列表视图把某任务状态改为「完成」⇒ 任务**自动出现在 `Done` 列**。
若任务停留在 `Action`(不绑定)里被改成「完成」,因当前列与状态不匹配,系统**自动归位**到第一个绑定「完成」的列(默认即 `Done`);若该项目把「完成」的绑定全部取消,则**无匹配列,任务留在 `Action` 不动**。若把 `Action` 也设为绑定「完成」,则按列序仍先进 `Done`——这是**项目级**配置,不影响其他项目。
**场景 C · 扩展信息卡片**
新增任务「登录接口返回 500」→ 展开默认收起的**扩展信息卡片** → 按需添加任意条目,如「仓库 = gitlab/app」「分支 = fix/login-500」或一条相关链接(全部非必填,不填不影响任务本身)。
**场景 D · 跨项目复用**
把「APP 改版」里的某任务跨项目拷贝到「APP 二期」→ 生成一条**全新任务**,字段、扩展信息、子任务、负责人、成员、状态、看板列、标签全部复制,创建人记为执行拷贝的用户;此后两任务互不影响。
**场景 E · 按列控制已完成任务显示**
某项目的 `Done` 列已完成任务过多影响阅读 → 把该列保留选项设为「当天」→ 更早的已完成任务自动隐藏;同时把 `Action` 列设为「永不显示」,该列不再展示已完成任务;`Plan` 列保持「永久显示」不受影响。列表视图同样按各任务所属列的配置隐藏它们,与看板一致。切到另一个项目时,该项目各列配置保持自己的设置(互不干扰)。
**场景 F · 拖拽调整层级**
列表视图中长按任务卡片,向上拖动到另一个任务上并停留 → 该任务成为目标任务的**一级子任务**(缩进显示)。若目标任务本身已是子任务,或被拖任务已带子任务,则拒绝并给出提示(仅支持一级)。降为子任务后,它原有的看板列与状态保持不变。
**场景 G · 看板卡片内的子任务**
看板中某任务卡片下方以简洁样式嵌套显示其子任务 → 直接在卡片内勾选子任务完成,**父任务状态不受影响**,子任务状态也不受该列绑定规则约束;每个嵌套子任务带**自身状态小标记**,便于识别与父任务不一致的状态;需要编辑子任务标题或负责人时,点开卡片弹窗操作。
**场景 H · 从底部 Tab 进入(首屏)**
底部导航顺序为「打卡 → **任务** → 宏观 → 监控 → 我」→ 点击「任务」进入首屏:**上部**是最近使用的 3 个项目卡片 + 「更多项目」入口(点卡片即切换当前项目,长按卡片可调整顺序);**下部**直接是当前项目(上次访问的,记录在设备端;无记录则回到个人默认项目)的任务管理页,无需先进项目列表。点「更多项目」才进入完整的项目列表页。
**场景 I · 项目管理**
在项目页头部点「项目管理」→ 弹出统一管理页,内含三个分区:**成员**(查看 / 添加 / 移除)、**列设置**(列管理 / 状态绑定 / 保留选项)、**项目信息**(改名 / 归档 / 删除 / 退出,个人默认项目不出现删除与退出)。
### 1.5 不在本期范围(明确排除)
- 多层子任务(仅一级)。
- 角色 / 权限分级(只预留入口,本期全部成员权限均等)。
- 任务评论、@提及、附件上传、提醒通知、日历同步。
- 列内拖拽排序的精细规则(本期为简单位置值)。
- 甘特图、工时、报表统计。
- 项目成员清空后的数据清理(由后台维护,前端不做特殊处理)。
- 独立的 WebView / 登录态 / 返回键适配(**沿用现有项目框架**,不单独提出要求)。
---
## 2. 术语与概念定义
| 术语 | 定义 |
| --- | --- |
| **项目(Project)** | 任务与权限的隔离单位;用户只能看到自己为成员的项目 |
| **个人默认项目** | 每个用户唯一、自动创建、不可删除、不可添加成员的私有项目;`isPersonal = true` |
| **普通协作项目** | 可增删成员、可归档的项目;`isPersonal = false` |
| **项目成员(Member)** | `ProjectMember` 记录,决定**读写权限** |
| **负责人(Assignee)** | 任务上的单人标记(须为项目成员),决定列表 / 卡片视觉上的突出位置;新建时默认为创建者,可清空 |
| **任务成员(TaskMember)** | 任务上的多成员标记(须为项目成员),弱化展示,可为空 |
| **任务状态(status)** | `todo`(未开始)/ `doing`(执行中)/ `done`(完成),三值枚举 |
| **看板列(column)** | 项目级可配置的列实体,默认 `Plan` / `Doing` / `Action` / `Done` 四列;可增删改、可排序,数量不固定 |
| **列-状态绑定(columnBindings)** | 项目级配置:每列独立声明「是否绑定」及「绑定到哪个状态」;默认 plan→todo、doing→doing、done→done、action→不绑定 |
| **已完成但未绑定状态** | 状态为 `done` 但所在列未绑定到 `done`;此时拖拽/勾选只改状态不改列(见 T-EX-04) |
| **看板列保留选项(columnRetention)** | 项目级配置:每列独立设置已完成任务的保留时长,`never` 永不显示 / `today` 当天 / `3d` 3 天 / `week` 本周 / `month` 本月 / `forever` 永久显示 |
| **视图记忆(viewMode)** | `list` / `board`,**按项目独立记忆、保存在设备端** |
| **扩展信息卡片** | 任务的折叠信息区,存任意 `[{label, value, url?}]` 条目,默认收起、全部非必填 |
| **一级子任务** | `parentId` 非空的任务;其父任务必须为顶层任务,且同项目 |
| **上下文** | 本次操作所处位置(项目 / 父任务 / 列表分区),用于界定"从该列拖出"等相对语义的作用范围 |
| **拷贝(Copy)** | 跨项目生成新任务并复制全部内容;原任务不受影响 |
---
## 3. 需求点
**编号规则**:`T-模块-序号`。前缀 `T` 表示本模块(Task / 任务)。模块:
| 模块码 | 模块名 | 对应 §1.3 条目 |
| --- | --- | --- |
| `VW` | 视图体系 | 第 1 条 |
| `COL` | 看板列管理 | 第 2 条 |
| `BD` | 看板与列-状态绑定 | 第 2 条 |
| `FL` | 已完成任务显示控制(按列保留选项) | 第 3 条 |
| `TK` | 任务字段体系(含扩展信息卡片) | 第 4 条 |
| `AS` | 负责人与成员 | 第 5 条 |
| `ST` | 子任务体系 | 第 6 条 |
| `PJ` | 项目体系 | 第 7 条 |
| `PM` | 项目权限与模块入口 | 第 8 条、第 11 条 |
| `CP` | 跨项目拷贝 | 第 9 条 |
| `UI` | UI 交互规范 | 第 10 条 |
| `DT` | 数据需求 | — |
| `NF` | 非功能需求 | — |
| `EX` | 异常与边界 | — |
**优先级**:`P0` = 本期必须有;`P1` = 本期应该有;`P2` = 有则更好(可延后)。
**验收要点写法**:统一采用「给什么输入 / 做什么操作 → 期望得到什么可观察结果」,可直接转写为测试用例。
### 3.1 视图体系(T-VW)
| 编号 | 需求描述 | 验收要点 | 优先级 |
| --- | --- | --- | --- |
| T-VW-01 | 项目内提供**列表视图**与**看板视图**两种呈现方式 | 同一项目的同一批任务,两种视图均能完整呈现,条数一致 | P0 |
| T-VW-02 | 可在列表 / 看板之间**互相切换**,切换过程**无整页刷新** | 切换不触发页面重载、不出现白屏,切换后滚动位置或回到顶部行为明确 | P0 |
| T-VW-03 | **视图记忆按单个项目独立保存**:A 项目选列表、B 项目选看板互不影响 | 项目 A 切到看板 → 进入项目 B → B 仍为 B 上次选择的视图;回到 A → 仍为看板 | P0 |
| T-VW-04 | 进入项目时**自动恢复**该项目上次选择的视图 | 退出模块后重新进入某项目,视图与上次退出时一致 | P0 |
| T-VW-05 | 从未选择过视图的新项目,**默认使用列表视图** | 新建项目首次进入为列表视图 | P1 |
| T-VW-06 | 视图记忆保存在**设备端**,不要求跨设备同步;本机跨会话保持 | 杀进程重进某项目,视图仍为上次选择;换设备登录不要求一致 | P0 |
| T-VW-07 | **上次访问的项目**记录在**设备端**;无记录时**一律回到个人默认项目** | 杀进程重进落在上次项目;首次使用或记录失效则进入个人默认项目 | P0 |
| T-VW-08 | 任务**首屏上部**展示**最近使用的 3 个项目**卡片,末尾放「更多项目」入口;点击「更多项目」进入完整项目列表页 | 最近项目按访问时间倒序取前 3;点卡片切换当前项目并高亮;点「更多」进入项目列表 | P0 |
| T-VW-09 | 任务**首屏下部**直接展示**当前选中项目**的任务管理视图(列表 / 看板,随该项目视图记忆);当前项目默认为上次访问的项目,无记录时为个人默认项目 | 进入任务 Tab 即见当前项目的任务,**无需先进项目列表**;切换首屏项目卡片后下部随之切换 | P0 |
| T-VW-10 | 首屏**最近项目卡片支持手动排序**(长按拖动调整顺序),排序结果存**设备端**;未手动排序时按最近使用时间自动排列 | 拖动后顺序持久化,杀进程重进保持;新近访问但未手动排序的项目不出现在固定卡位之外 | P1 |
### 3.2 看板列管理(T-COL)
> 看板列是**项目级可配置实体**:默认 4 列,允许增删改与排序。列与「状态绑定」「保留选项」是三条正交的配置。
| 编号 | 需求描述 | 验收要点 | 优先级 |
| --- | --- | --- | --- |
| T-COL-01 | 新建项目**默认生成 4 列**:`Plan` / `Doing` / `Action` / `Done`,顺序即此 | 新项目看板渲染 4 列且名称、顺序与默认一致 | P0 |
| T-COL-02 | 支持**新增列**(自定义名称),新列默认追加在末尾 | 新增后看板多一列,位于最后一列之后,可正常拖入任务 | P0 |
| T-COL-03 | 支持**重命名列** | 改名后看板、列设置、任务归属处同步显示新名称 | P0 |
| T-COL-04 | 支持**删除列**;删除前需二次确认,并明确其中任务的处理方式 | 按 T-EX-16 约定迁移任务,不产生无归属任务 | P0 |
| T-COL-05 | 支持**调整列顺序**(拖动排序),顺序即看板显示顺序 | 拖动后顺序持久化,重新进入保持新顺序 | P0 |
| T-COL-06 | 列数量**不固定**,约束上下限:**至少 1 列、至多 10 列**(上限待确认) | 可增到多于 4 列;删到仅剩 1 列时禁止再删;达 10 列时禁止新增并提示 | P0 |
| T-COL-07 | 列名需**非空**且有长度上限,同一项目内**不允许重名** | 提交空名 / 重名时拒绝保存并提示 | P0 |
| T-COL-08 | 删除列时,若该列绑定了状态,绑定关系随之解除 | 删除绑定「完成」的列后,该状态不再有该列可归位 | P1 |
| T-COL-09 | 列配置属**项目级**,项目内任意成员可修改,不影响其他项目 | 成员 A 改列名,成员 B 同步可见;其他项目列不受影响 | P0 |
| T-COL-10 | 列表视图不展示列分组,但任务仍归属某一列(列可被增删后依然成立) | 删除/新增列后,列表视图中任务的列归属正确 | P0 |
### 3.3 看板与列-状态绑定(T-BD)
| 编号 | 需求描述 | 验收要点 | 优先级 |
| --- | --- | --- | --- |
| T-BD-01 | 看板以**列**为单位组织任务,列由项目配置(默认 4 列,支持增删改与排序,详见 T-COL) | 看板按项目当前列配置渲染;调整列后任务归属随之正确呈现 | P0 |
| T-BD-02 | **每一列独立**配置是否绑定任务状态;绑定时可选择绑定到 `未开始` / `执行中` / `完成` 中任一个 | 每列都有独立的绑定开关与状态选择器;各列配置独立保存 | P0 |
| T-BD-03 | 允许**多列绑定到同一个状态**(如 `Done` 与 `Action` 都绑定「完成」) | 同一项目配置两列均绑定「完成」后,两列均能触发完成联动 | P0 |
| T-BD-04 | **绑定列 → 拖入即改状态**:任务落入绑定列时,任务状态改为该列所绑定的状态 | 把「未开始」任务拖入绑定「执行中」的列 → 状态变「执行中」;拖入绑定「完成」的列 → 状态变「完成」 | P0 |
| T-BD-05 | **状态 → 自动移入列**:任务状态改为某状态时,任务自动移入绑定该状态的列 | 列表视图把状态改为「完成」→ 切到看板,任务位于绑定「完成」的列 | P0 |
| T-BD-06 | 同一状态被多列绑定时,自动移入**按当前列顺序取第一个**匹配列,规则确定且可预期 | 两列均绑定「完成」时,任务状态改为「完成」后仅出现在列序靠前的那个绑定列,不重复出现 | P0 |
| T-BD-07 | **列决定状态,不看来源列**:拖动任务时,一律以「**目标列**的绑定配置」决定任务状态——目标列绑定了状态,则状态改为所绑定状态;**目标列未绑定,则任务保留原状态** | ①从绑定「完成」的列拖到绑定「执行中」的列 → 状态变「执行中」;②从任意列拖到**未绑定**的 `Action` → 状态保持不变 | P0 |
| T-BD-08 | **不绑定列**与任务状态**完全独立**:拖入、拖出该列以及在其他位置改变状态,均不改变该列归属以外的行为 | 把「执行中」任务拖入未绑定的 `Action` → 状态仍为「执行中」,仅列变化 | P0 |
| T-BD-14 | **状态与所在列不匹配时自动归位**:任务状态改变后,若其**当前所在列未绑定该状态**,则自动查找绑定该状态的列并将其移入 | 在未绑定的 `Action` 中把任务改为「完成」→ 任务自动移入绑定「完成」的列 | P0 |
| T-BD-15 | 自动归位时**按当前列的实际显示顺序取第一个匹配列**(列顺序可被用户调整) | 若两列都绑定「完成」,状态改为「完成」后移入排在前面那一列 | P0 |
| T-BD-16 | **无匹配列时任务不移动**,保留在原列(状态照常改变) | 取消所有列对「完成」的绑定后,把任务改为「完成」→ 状态为完成,列保持不变 | P0 |
| T-BD-09 | **默认配置**:`Plan` 绑定「未开始」、`Doing` 绑定「执行中」、`Done` 绑定「完成」、`Action` 不绑定 | 新建项目首次打开看板配置,四列取值与默认完全一致 | P0 |
| T-BD-10 | 绑定配置是**项目级**的,项目内**任意成员**均可修改,不同项目互不影响 | 成员 A 修改项目一配置成功;项目二配置不受影响;成员 B 也能修改 | P0 |
| T-BD-11 | 修改绑定配置**不批量改写**既有任务的状态与列,仅对之后的操作生效 | 把 `Action` 由不绑定改为绑定「完成」后,已在 `Action` 的任务状态保持原样 | P1 |
| T-BD-12 | 绑定配置变更后,看板与列表**即时一致**,无需手动刷新 | 保存配置后,看板列徽标与完成态立即反映新规则 | P1 |
| T-BD-13 | **子任务不受看板列-状态绑定影响**:子任务状态不因所在列而改写,也不触发「状态 → 移入列」 | 父任务在绑定「完成」的列,其子任务状态仍可为「执行中」且不被改写;修改子任务状态不使其离开当前列 | P0 |
### 3.4 已完成任务显示控制(T-FL)
> 本节的「保留选项」**以看板列为单位**配置,取代原先的「项目整体 5 档过滤」。
| 编号 | 需求描述 | 验收要点 | 优先级 |
| --- | --- | --- | --- |
| T-FL-01 | **每一列独立**配置已完成任务的保留选项,配置项属于项目级 | 各列(含新增列)都有独立配置入口,修改一列不影响其他列 | P0 |
| T-FL-02 | 保留选项为**固定 6 档**:`永不显示` / `当天` / `3 天` / `本周` / `本月` / `永久显示` | 下拉仅此六项,不提供自由输入天数 | P0 |
| T-FL-03 | 「永久显示」:该列已完成任务**始终展示**,不受时间限制 | 设为永久显示后,历史任意时间完成的任务均可见 | P0 |
| T-FL-04 | 「永不显示」:该列已完成任务**始终不展示** | 设为永不显示后,该列无已完成任务卡片 | P0 |
| T-FL-05 | 「当天」:仅展示**完成于今天(自然日)**的已完成任务 | 今天完成的可见;昨天完成的次日即不显示 | P0 |
| T-FL-06 | 「3 天」:展示**完成于最近 3 个自然日(含今天)**的已完成任务 | 今天、昨天、前天完成可见,更早不显示 | P0 |
| T-FL-07 | 「本周」:展示**完成于本周(周一 00:00 起,至今天)**的已完成任务 | 周一起算;下周一进来看,上周完成的任务不再显示 | P0 |
| T-FL-08 | 「本月」:展示**完成于本月(当月 1 日起,至今天)**的已完成任务 | 当月 1 日起算;进入次月后,上月完成的任务不再显示 | P0 |
| T-FL-09 | 超出所选档位范围的已完成任务**自动隐藏**(无需手动操作) | 某列设为「当天」,次日进入该列,昨日完成的任务自动消失 | P0 |
| T-FL-10 | 保留判定以「该任务的**完成时间**」为准,与任务当前所在列无关 | 任务完成后再移动列,可见性仍按其完成时间与新列档位判定 | P0 |
| T-FL-11 | **列表视图与看板视图行为一致**:列表只是不按看板列分组展示,任务归属哪一列仍是哪一列,显示控制同样按**其所属列**的配置执行 | 看板中某列已隐藏的已完成任务,在列表视图中同样不出现;两视图可见条数一致 | P0 |
| T-FL-12 | 该配置仅影响**已完成任务**;未完成任务始终展示,不受保留选项影响 | 未完成任务在任何保留选项配置下均可见 | P0 |
| T-FL-13 | 显示控制为**展示层过滤**,不改变任务数据与状态 | 修改保留选项后,任务数据、状态、计数口径不发生变化 | P0 |
| T-FL-14 | 过滤后每列的计数与展示条数一致 | 列头显示 N 条时,该列实际可见卡片同为 N 条 | P1 |
| T-FL-15 | 保留选项配置在**项目内所有成员间共享**(属项目配置,非个人偏好) | 成员 A 修改后,成员 B 进入即按新配置显示 | P0 |
### 3.5 任务字段体系(T-TK)
| 编号 | 需求描述 | 验收要点 | 优先级 |
| --- | --- | --- | --- |
| T-TK-01 | 任务包含**基础字段**:标题(必填)、描述、所属项目、看板列、任务状态、负责人、任务成员、标签、创建人、创建时间、更新时间 | 新建任务后上述字段均可正确保存与读取 | P0 |
| T-TK-02 | 标题为**唯一必填**字段,其余字段均可为空 | 仅填标题即可保存成功;仅清空标题时拒绝保存并提示 | P0 |
| T-TK-03 | 任务状态为三值枚举:`未开始` / `执行中` / `完成` | 任一任务的状态只能取这三个值之一,不存在第四种取值 | P0 |
| T-TK-04 | ~~内置扩展信息卡片~~ **已移除**(2026-10-24 评审):相关信息记录改由**项目信息块**承载(T-PJ-13) | — | 已取消 |
| T-TK-05 | ~~扩展信息条目~~ **随 T-TK-04 一并移除** | — | 已取消 |
| T-TK-06 | ~~扩展信息卡片折叠~~ **随 T-TK-04 一并移除** | — | 已取消 |
| T-TK-07 | ~~扩展信息非必填~~ **随 T-TK-04 一并移除** | — | 已取消 |
| T-TK-08 | 每条任务有**负责人(单人)** | 任务最多只有一个负责人,重复设置时以最后一次为准 | P0 |
| T-TK-09 | 新建任务时**负责人默认填入创建者本人** | 新建任务后未手动修改时,负责人即为创建者 | P0 |
| T-TK-10 | 负责人**允许清空为空** | 清空后任务可正常保存,展示为「未指派」 | P0 |
| T-TK-11 | 每条任务有**任务成员(多人)** | 可设置 0 ~ 多个成员,成员不重复 | P0 |
| T-TK-12 | 任务支持**标签** | 可添加多个标签,展示于条目 / 卡片 | P1 |
| T-TK-13 | 任务的创建人、创建时间、更新时间由系统维护,用户不可直接编辑 | 编辑任务不改变创建人与创建时间;任何修改刷新更新时间 | P1 |
| T-TK-14 | 任务标题收紧至**30 个汉字**;快速添加输入框允许输入 200 字,超过 30 字时自动在 30 字以内的**分隔符(空格/标点等)**处截断,前段作为标题,整体文本并入描述 | 输入长文本创建后:标题 ≤30 字且不在词中间切断;描述包含完整原文余文;直接编辑标题超 30 字时拒绝保存并提示 | P0 |
| T-TK-15 | 任务可选**时间标记**(完整日期存储),设置了时间标记的任务在列表行与看板卡片上以 `MM-dd` 小标签展示 | 详情页可选择/清除时间标记;设置后卡片出现 MM-dd 标签,清除后消失 | P1 |
| T-TK-16 | 快速添加输入框支持**时间词前缀**:以 今天/明天/后天/本周/下周/本月/下月 开头(后跟分隔符)时,自动为任务设置对应时间标记并把该前缀从标题中移除 | 输入「明天 交周报」→ 创建任务标题为「交周报」、时间标记为明天的日期;无匹配前缀时行为不变 | P1 |
### 3.6 负责人 & 成员 UI 展示规则(T-AS)
| 编号 | 需求描述 | 验收要点 | 优先级 |
| --- | --- | --- | --- |
| T-AS-01 | **负责人**字号更大、视觉突出,位于条目 / 卡片的主要信息区 | 同名负责人与成员并排时,负责人字号明显更大且位于主位 | P0 |
| T-AS-02 | **任务成员**字号更小、弱化展示(低对比色),位于卡片 / 条目**下方** | 成员信息渲染在负责人 / 标题之下,视觉层次弱于负责人 | P0 |
| T-AS-03 | 负责人只能从**当前项目的成员**中选择 | 选择器只列出本项目成员;项目外用户无法被选中 | P0 |
| T-AS-04 | 任务成员只能从**当前项目的成员**中选择 | 选择器只列出本项目成员,不提供项目外用户 | P0 |
| T-AS-05 | 负责人为空(已清空)时展示「未指派」占位,正常渲染 | 清空负责人后条目 / 卡片显示「未指派」,可再次指派 | P0 |
| T-AS-06 | 人员被移出项目后,其负责人 / 成员标记按约定处理且不报错 | 见 T-EX-06 | P1 |
| T-AS-07 | 负责人、任务成员**仅为信息标记**,不改变项目统一读写权限 | 仅为负责人或仅为成员的用户,对项目读写能力与其他成员完全相同 | P0 |
### 3.7 子任务体系(T-ST)
| 编号 | 需求描述 | 验收要点 | 优先级 |
| --- | --- | --- | --- |
| T-ST-01 | **仅支持一级子任务**,不支持多层嵌套 | 子任务上无「添加子任务」入口;后端拒绝以子任务为父任务创建任务 | P0 |
| T-ST-02 | 子任务**必须与父任务归属同一项目** | 尝试将子任务置于其他项目时被拒绝(含跨项目拷贝场景) | P0 |
| T-ST-03 | 父任务与子任务的**完成状态相互独立**、不自动联动 | 子任务全部完成,父任务状态不变;父任务完成,子任务状态不变 | P0 |
| T-ST-04 | **列表视图**支持子任务**缩进**展示 | 子任务在父任务下方缩进渲染,层级一眼可辨 | P0 |
| T-ST-05 | **列表视图**支持子任务**折叠 / 展开** | 折叠后子任务不显示且不占位;展开后恢复;折叠状态可感知 | P0 |
| T-ST-06 | **看板卡片内嵌套展示子任务**:含子任务的卡片中,子任务以**简洁形式嵌套**显示在任务卡片内 | 卡片内可见子任务列表(如紧凑单行:复选框 + 标题),样式轻量、不喧宾夺主 | P0 |
| T-ST-07 | 嵌套子任务可在**卡片内直接勾选完成**;完整详情(编辑标题 / 负责人 / 扩展信息等)在卡片弹窗内查看编辑 | 点击子任务复选框即切换其状态;点击卡片打开弹窗可编辑子任务全部字段 | P0 |
| T-ST-08 | **子任务状态与父任务状态可以不一致**,且子任务**不受看板列-状态绑定约束** | 父任务在 `Done`(绑定「完成」)时,其子任务可为「未开始」且不被改写 | P0 |
| T-ST-09 | 卡片内嵌套的子任务**显示自身状态小标记**(区分「未开始 / 执行中 / 完成」),以便识别与其父任务不一致的状态 | 父任务为完成、子任务为执行中时,卡片内可见子任务带「执行中」小标记 | P0 |
| T-ST-10 | 子任务不可作为看板独立卡片出现,只在其父任务卡片内嵌套 | 看板上一个任务只占一张卡,子任务不会单独成卡 | P1 |
| T-ST-11 | 删除父任务时**级联删除**其子任务 | 删除父任务后子任务一并删除,不产生孤儿数据 | P0 |
| T-ST-12 | **支持长按拖拽把任务降为另一个任务的一级子任务** | 列表视图长按任务 A 拖到任务 B 上释放 → A 成为 B 的子任务并缩进显示 | P0 |
| T-ST-13 | 拖拽成为子任务时,若目标任务**本身已是子任务**,则拒绝操作并提示 | 把任务拖到某个子任务上 → 操作被拒绝,给出「仅支持一级子任务」提示,数据不变 | P0 |
| T-ST-14 | 拖拽成为子任务时,若被拖任务是**有子任务的父任务**,则拒绝并提示(父任务不可整体挂到别的任务下) | 拖动含子任务的任务到其他任务上 → 拒绝,避免形成二级嵌套 | P0 |
| T-ST-15 | 拖拽成为子任务时,子任务的**看板列与状态保持自身原值**,仅改变父子归属 | 按 T-EX-13 约定处理,保证子任务与父任务同项目且层级合法 | P1 |
### 3.8 项目体系(T-PJ)
| 编号 | 需求描述 | 验收要点 | 优先级 |
| --- | --- | --- | --- |
| T-PJ-01 | 每个用户**自动拥有且仅有一个个人默认项目** | 新用户首次进入模块即已存在个人项目;不可再创建第二个 | P0 |
| T-PJ-02 | 个人默认项目**不可删除** | 无删除入口;直接调用删除接口被拒绝 | P0 |
| T-PJ-03 | 个人默认项目**不能添加其他成员** | 成员管理入口不存在或不可用;调用添加成员接口被拒绝 | P0 |
| T-PJ-04 | 个人默认项目名称**以用户名开头**,且**整体名称可自定义修改** | 初始名称以用户名开头;改名后修改内容生效,仍保持「个人项目」性质 | P0 |
| T-PJ-05 | 支持**新建普通协作项目** | 可创建多个普通项目,创建者为成员 | P0 |
| T-PJ-06 | 普通项目**可以增删成员** | 添加 / 移除成员后,成员列表与可见性即时变化 | P0 |
| T-PJ-07 | **项目创建人可以主动退出项目** | 创建人退出后不再是成员,该项目不再对其可见 | P0 |
| T-PJ-08 | 项目**成员清空后静默保留**,交后台后续维护,前端不做特殊处理 | 成员全部退出后,前端不弹特殊提示、不出现错误;不可见 | P0 |
| T-PJ-09 | 项目可**归档**,归档项目与进行中项目区分展示 | 归档后从进行中列表移出,可在归档区找回 | P1 |
| T-PJ-10 | 项目列表按用户可见范围返回,仅含自己为成员的项目 | 非成员项目不出现在列表中 | P0 |
| T-PJ-11 | 项目的**成员管理、成员新增、列设置**统一收进「**项目管理**」入口;项目页头部不再散置多个功能入口 | 项目管理内含 成员 / 列设置 / 项目信息 三个分区;头部仅保留「项目管理」一个入口 | P0 |
| T-PJ-12 | 项目管理内提供**项目信息管理**:改名(T-PJ-04)、归档 / 取消归档(T-PJ-09)、删除项目(T-PJ-02,个人默认项目禁用)、退出项目(T-PJ-07,创建人可退) | 各操作在项目管理中可用且遵循既有约束;个人项目不出现删除与退出入口 | P0 |
| T-PJ-13 | 项目支持**信息块**:可自由添加任意多个长文本信息块(标题 + 多行内容,如 Readme / 项目说明),每块**独立编辑与删除**;属项目级数据,全体成员可见可编辑 | 可增删改任意块;内容支持多行长文本;所有成员看到同一份信息块 | P0 |
### 3.9 项目权限(T-PM)
| 编号 | 需求描述 | 验收要点 | 优先级 |
| --- | --- | --- | --- |
| T-PM-01 | 权限**以项目为隔离单位**:用户只能看到自己是成员的项目及其任务 | 非成员访问某项目接口返回 403 / 404,列表不出现该项目 | P0 |
| T-PM-02 | 项目内**所有成员权限均等**,具备**全读写**能力 | 任意成员均可新增 / 编辑 / 删除任务、管理成员、修改列-状态绑定与保留选项配置 | P0 |
| T-PM-03 | 预留**权限分级入口**,本期**不实现**细分角色 | 数据结构或接口层保留扩展位;界面无角色分配功能 | P1 |
| T-PM-04 | 项目内**所有成员都可以归档项目** | 任意成员执行归档均成功 | P0 |
| T-PM-05 | 复用现有项目框架的登录态与鉴权,未登录访问被拒 | 无有效登录态访问返回 401 | P0 |
| T-PM-06 | 任务模块作为**顶级 Tab** 接入底部导航,位置在**「打卡」之后** | 底部 Tab 顺序为「打卡 → 任务 → 宏观 → 监控 → 我」;点击该 Tab 进入任务模块 | P0 |
| T-PM-07 | 进入任务模块后**默认落在上次访问的项目**(记录在**设备端**);**无记录时一律回到个人默认项目** | 首次进入 / 记录失效 → 个人默认项目;再次进入 → 上次离开时的项目;换设备不继承 | P0 |
### 3.10 跨项目拷贝(T-CP)
| 编号 | 需求描述 | 验收要点 | 优先级 |
| --- | --- | --- | --- |
| T-CP-01 | 跨项目操作为**拷贝**,不是移动:源任务保留在原项目 | 拷贝后源任务仍存在于原项目且内容不变 | P0 |
| T-CP-02 | 拷贝时**完整复制全部字段** | 标题、描述、负责人、成员、标签、任务状态、看板列、扩展信息卡片逐项比对一致 | P0 |
| T-CP-03 | 拷贝时**完整复制子任务** | 子任务条数、标题、状态、列归属均与源任务一致 | P0 |
| T-CP-04 | 拷贝后**新任务的创建人**为本次执行拷贝操作的用户 | 新任务创建人 = 操作者,非源任务创建人 | P0 |
| T-CP-05 | 拷贝完成后**新旧任务互相独立**,后续修改互不影响 | 修改新任务标题 / 状态,源任务不变;反之亦然 | P0 |
| T-CP-06 | 拷贝目标必须是操作者**有权限**的项目 | 目标项目非本人成员时拒绝并提示 | P0 |
| T-CP-07 | 拷贝时负责人 / 成员若不属目标项目成员,按约定处理 | 按 T-EX-08 约定处理,不产生「项目外人」脏数据 | P1 |
| T-CP-08 | 拷贝包含子任务时,子任务与父任务**同属目标项目** | 拷贝后子任务项目归属与父任务一致 | P0 |
| T-CP-09 | 拷贝时若目标任务项目的**列-状态绑定规则不同**,按 T-EX-14 约定处理 | 状态与列不出现互相矛盾的组合 | P1 |
### 3.11 UI 交互规范(T-UI)
| 编号 | 需求描述 | 验收要点 | 优先级 |
| --- | --- | --- | --- |
| T-UI-01 | 整体界面、列表交互、子任务折叠风格**参考 Todoist** | 列表条目结构、快速新增、勾选完成、子任务折叠的交互与 Todoist 同类操作一致 | P0 |
| T-UI-02 | **看板卡片轻量化**,样式简洁 | 卡片仅承载必要信息,无多余装饰;信息层级清晰 | P0 |
| T-UI-03 | 列表与看板**同源数据**,修改**实时双向同步** | 列表改标题 / 状态 → 切到看板立即可见;看板改 → 切到列表立即可见 | P0 |
| T-UI-04 | 视图切换**无刷新**、数据一致 | 切换过程中不发起整页加载,数据不发生丢失或错位 | P0 |
| T-UI-05 | 移动端适配:复用现有样式体系与底部导航,安全区不遮挡内容 | 375~430px 视口无横向滚动、无内容被遮挡 | P0 |
| T-UI-06 | 勾选完成有即时反馈(视觉状态变化),不阻塞后续操作 | 点击勾选后立即变化,无需等待接口返回才能继续操作 | P1 |
| T-UI-07 | 看板拖拽在移动端支持**长按拖拽**,并提供卡片「移动到」快捷项作为兜底 | 长按可拖起卡片;无法拖拽的场景可用「移动到」菜单完成同效果 | P0 |
| T-UI-08 | 具备**空状态**展示(无项目 / 无任务 / 无子任务等) | 各层级无数据时展示空态与引导入口,不出现空白页 | P0 |
| T-UI-09 | 具备**加载状态**展示 | 首屏与切换视图时展示加载态,不出现闪烁或错乱 | P0 |
| T-UI-10 | 具备**数据异常状态**展示,可重试 | 接口失败时展示异常态与重试入口,重试成功后恢复 | P0 |
| T-UI-11 | 弹层(任务详情等)**底部按钮与屏幕底边保持安全距离**,不被安全区 / 手势条遮挡 | 底部留白 ≥ 安全区高度 + 12px;小屏下弹层内容可滚动,删除等底部按钮始终完整可见可点 | P0 |
### 3.12 数据需求(T-DT)
| 编号 | 需求描述 | 验收要点 | 优先级 |
| --- | --- | --- | --- |
| T-DT-01 | 新增 Prisma 模型:`Project`(含 `isPersonal`、列-状态绑定配置、列保留选项配置、`archived` 等)、`ProjectMember` | 字段见附录 A | P0 |
| T-DT-02 | 列-状态绑定配置**按列存储**,每列独立保存「是否绑定 + 绑定状态」,支持多列指向同一状态 | 任意数量列的配置可独立读写;两列绑定同一状态可正确落库 | P0 |
| T-DT-03 | 列保留选项配置**按列存储**,取值覆盖六档:`never` / `today` / `3d` / `week` / `month` / `forever` | 三档取值均可落库并正确读回 | P0 |
| T-DT-04 | 新增 Prisma 模型:`Task`(含 `parentId` 自关联、`assigneeId`、`status`、看板列、扩展信息卡片、`sortOrder`) | 字段见附录 A | P0 |
| T-DT-05 | 新增 Prisma 模型:`TaskMember`(任务成员关联)、扩展信息卡片结构、标签结构 | 字段见附录 A | P0 |
| T-DT-06 | 视图记忆按**设备端**存储,不要求落库 | 换设备登录不要求视图一致 | P0 |
| T-DT-07 | 建立必要索引:任务按项目查询、按父子关系查询、按状态查询 | 列表 / 看板 / 显示控制查询均走索引 | P0 |
| T-DT-08 | 提供迁移 SQL 并纳入 `prisma migrate deploy` 流程 | 与现有迁移流程一致 | P0 |
| T-DT-09 | 归档采用软删除语义,历史数据保留 | 归档不物理删除任务与项目数据 | P1 |
### 3.13 非功能需求(T-NF)
| 编号 | 需求描述 | 验收要点 | 优先级 |
| --- | --- | --- | --- |
| T-NF-01 | 项目内任务列表首屏加载 < 1.5s(正常网络),交互响应 < 300ms | 真机可感知流畅 | P0 |
| T-NF-02 | 兼容当前 APP 的 WebView 运行环境与移动端浏览器 | 真机验证通过 | P0 |
| T-NF-03 | 前端代码组织遵循现有约定:`src/pages/task/*` + `src/components/*` + `server/routes/task*.js` | 结构一致 | P0 |
| T-NF-04 | 路由与接口挂载**沿用现有项目框架**的组织方式 | 与现有模块的组织方式一致 | P0 |
| T-NF-05 | 数据量按「单项目千级任务 / 年」设计,无需分表 | 千级任务下列表与看板可用 | P1 |
### 3.14 异常与边界(T-EX)
| 编号 | 需求描述 | 处理约定 | 优先级 |
| --- | --- | --- | --- |
| T-EX-01 | 重复点击勾选 / 重复拖拽 | 前端防抖 + 后端幂等,状态最终一致 | P0 |
| T-EX-02 | 接口失败 / 断网 | 乐观更新回滚 + 提示,可重试 | P0 |
| T-EX-03 | 绑定配置被改为「不绑定」时,已在该列的已完成任务 | 保持当前列与状态不变,等待用户后续操作 | P0 |
| T-EX-04 | 任务状态与所在列绑定不一致(如在不绑定的 `Action` 中改为「完成」) | 触发自动归位:移入第一个绑定该状态的列;**无匹配列则不移动**,保留原列(见 T-BD-14~16) | P0 |
| T-EX-05 | 任务被拖到**未绑定状态**的列(如 `Action`),其原状态如何处理 | 原状态**保持不变**,不因离开原列而回退(不存在「拖出即回退」规则) | P0 |
| T-EX-06 | 项目成员被移除,其仍担任某任务负责人 / 成员 | 保留历史标记,展示为「已退出」样式;不报错、不清空 | P1 |
| T-EX-07 | 删除父任务 | 级联删除其子任务,确认弹窗提示「将一并删除 N 条子任务」 | P0 |
| T-EX-08 | 跨项目拷贝时负责人 / 成员不属于目标项目 | 仅保留属于目标项目成员者,其余丢弃并在结果中提示 | P1 |
| T-EX-09 | 个人默认项目尝试添加成员 / 尝试删除 | 接口返回明确错误,前端不提供入口 | P0 |
| T-EX-10 | 某列设为「当天」等档位且该列无符合条件的已完成任务 | 展示该列空态,不报错 | P0 |
| T-EX-11 | 项目成员清空后的项目 | 静默保留,前端不报错、不特殊提示 | P0 |
| T-EX-12 | 子任务所属项目与父任务不一致(绕过前端直接调用) | 后端拒绝并返回错误 | P0 |
| T-EX-13 | 拖拽成为子任务时,子任务的**看板列与状态**如何取值 | 保持自身原值不变,仅改变父子归属 | P1 |
| T-EX-14 | 跨项目拷贝时,源任务的状态 / 列在目标项目绑定规则下无对应关系 | 沿用源值落库,不强行纠正;由后续用户操作自然修正 | P1 |
| T-EX-15 | 拖拽将任务放到自身或其后代上 | 拒绝操作并提示,避免形成环形父子关系 | P0 |
| T-EX-16 | 删除仍有任务的列 | 二次确认后,该列任务迁移至**相邻列**(或由用户指定目标列),不产生无归属任务 | P0 |
| T-EX-17 | 删除唯一剩余的列 | **拒绝操作**并提示「至少保留一列」 | P0 |
| T-EX-19 | 新增列时已达列数上限 | 拒绝新增并提示已达上限,不产生空列 | P1 |
| T-EX-18 | 已完成任务跨周期(周一零点 / 月初零点)后应隐藏 | **当场消失**并带移除动画,其余任务平滑重排,不留空白 | P1 |
### 3.15 任务归档(T-ARC)
> 与「已完成显示控制(T-FL)」的关系:T-FL 决定任务**看不看得见**(可逆的展示过滤),
> 归档决定任务**在不在主工作集**(数据层面的收纳)。隐藏是自动归档的前置条件,归档移出主列表,
> 可通过「已归档查询」恢复。
| 编号 | 需求描述 | 验收要点 | 优先级 |
| --- | --- | --- | --- |
| T-ARC-01 | **手动归档**:任务 `⋯` 菜单增加「归档」;任意未归档任务(不限状态)均可归档;父任务归档时**连带全部子任务**一并归档;操作需二次确认(提示连带条数) | 归档后任务从列表/看板消失;子任务同步消失;确认弹窗条数正确 | P0 |
| T-ARC-02 | **自动归档**:项目级配置「**隐藏的任务**多久归档」,可选 `3 天后 / 一周后 / 一月后 / 永不`(默认永不);已隐藏(T-FL 保留窗口过期)的 done 任务到期后由系统自动归档 | 配置保存后生效;隐藏任务到期被自动归档;「永不」时永不自动归档;未隐藏的任务不受影响 | P1 |
| T-ARC-03 | **归档排除**:已归档任务从任务列表、看板、列计数、项目任务数统计中排除 | 归档后各视图与计数立即减少;恢复后回到原位 | P0 |
| T-ARC-04 | **已归档查询**:独立入口查看本项目已归档任务;支持关键词(标题)、任务类型、看板列、归档时间范围过滤;**游标分页**支撑大量数据(千级) | 过滤条件生效;分页可连续加载到底;千级数据下查询流畅 | P0 |
| T-ARC-05 | **恢复**:已归档任务可取消归档,回到原看板列(列被删除的场景因外键 `ON DELETE RESTRICT` 不存在),状态保持不变;父任务恢复连带子任务 | 恢复后任务与子任务重新出现在列表/看板原列 | P0 |
| T-ARC-06 | **彻底删除**:已归档任务可物理删除(含子任务),二次确认 | 删除后不再出现在已归档查询 | P1 |
| T-ARC-07 | **配置入口**:项目管理新增「归档设置」分区,展示与修改自动归档档位 | 修改后即时保存并对全项目生效 | P1 |
| T-ARC-08 | **权限口径**:项目成员均可归档/恢复/删除归档任务(权限均等口径同 T-PM-04) | 与其他写操作权限一致,无额外限制 | P1 |
---
## 4. 优先级汇总(本期交付范围)
- **P0(必须)**:T-VW-01~04、T-VW-06~09;T-COL-01~07、T-COL-09、T-COL-10;T-BD-01~10、13~16;T-FL-01~13、15;T-TK-01~11;T-AS-01~05、07;T-ST-01~09、11~14;T-PJ-01~08、10~13;T-PM-01、02、04~07;T-CP-01~06、08;T-UI-01~05、07~11;T-DT-01~08;T-NF-01~04;T-EX-01~05、07、09~12、15~17;T-ARC-01、03、04、05。
- **P1(应该)**:T-VW-05、10;T-COL-08;T-BD-11、12;T-FL-14;T-TK-12、13;T-AS-06;T-ST-10、15;T-PJ-09;T-PM-03;T-CP-07、09;T-UI-06;T-DT-09;T-NF-05;T-EX-06、08、13、14、18、19;T-ARC-02、06、07、08。
---
## 5. 已决策问题(评审记录)
以下问题已在 2026-10-24 评审中确认,作为实现依据:
| # | 问题 | 决策 |
| --- | --- | --- |
| 1 | 列-状态绑定是单列还是多列? | **每列独立配置**,允许**多列绑定同一状态**;默认 plan→未开始、doing→执行中、done→完成、action→不绑定(T-BD-02、03、09) |
| 2 | 保留档位的时间边界按自然日还是滚动小时? | **自然日**(Asia/Shanghai,含今天 00:00 起);本周按周一起算、本月按 1 日起算 |
| 3 | 视图记忆 / 显示配置存在设备端还是服务端? | 视图记忆**设备端**、不跨设备;保留选项**项目级共享**(T-VW-06、T-FL-12、T-DT-06) |
| 4 | 删除父任务时子任务如何处理? | **级联删除**,弹窗提示条数(T-EX-07) |
| 5 | 已完成任务在看板中的呈现? | 由**该列的保留选项**控制显示与隐藏(T-FL-01~10) |
| 6 | 看板拖拽的移动端交互? | **长按拖拽** + 「移动到」兜底;**允许拖拽降为子任务**(T-UI-07、T-ST-12) |
| 7 | 项目成员清空后是否需要本模块的管理入口? | **本期不承担**,交后台;前端静默(T-PJ-08) |
| 8 | 扩展信息组的具体字段清单? | **不固定字段**,改为**任意扩展信息卡片**(T-TK-04、05) |
| 9 | 已完成显示策略的粒度? | 改为**按看板列独立的保留选项**(T-FL-01、02) |
| 10 | 是否单独要求 WebView / 登录态 / 返回键? | **不单列**,沿用现有项目框架(T-NF-04、T-PM-05) |
| 11 | 同一状态被多列绑定,状态变化时进哪一列? | **按当前列顺序取第一个匹配**(T-BD-06、15、B-3) |
| 12 | **从绑定列拖出**时状态是否回退? | **不设「拖出即回退」规则**。只看**目标列**:目标列绑定了状态就按目标列改,未绑定则**保留原状态**(T-BD-07、B-4) |
| 13 | 列表视图如何处理「按列」的显示控制? | 列表只是**不按列分组**,任务归属仍是原看板列,行为与看板**完全一致**(T-FL-11、B-6) |
| 14 | 看板卡片如何展示子任务? | **在卡片内嵌套简洁展示**,可直接勾选;完整详情在卡片弹窗(T-ST-06、07) |
| 15 | 子任务是否受列-状态绑定影响? | **不受**。子任务状态可与父任务不一致,拖入列不改写其状态(T-BD-13、T-ST-08、B-7) |
| 16 | 父任务能否整体挂到别的任务下? | **不能**。含子任务的父任务不可被拖拽为子任务(T-ST-14) |
| 17 | 保留选项的取值范围? | 固定 **6 档**:`永不显示` / `当天` / `3 天` / `本周` / `本月` / `永久显示`,不做自由输入(T-FL-02、B-5) |
| 18 | 任务状态与所在列绑定不一致时怎么处理? | **触发自动归位**:查找绑定该状态的列并移入,按列序取第一个;**无匹配列则不移动**(T-BD-14~16、B-4b) |
| 19 | 卡片内嵌套的子任务是否显示自身状态? | **显示小状态标记**,以区分与父任务不一致的状态(T-ST-09) |
| 20 | 任务模块入口放在哪里? | 作为**顶级 Tab**,位置在**「打卡」之后**(T-PM-06) |
| 21 | 5 个底部 Tab 是否影响既有布局与默认首页? | **暂无障碍**;默认首页**仍为打卡**(T-PM-06) |
| 22 | 任务状态为「完成」但所有列都未绑定「完成」时,列表视图按哪一列过滤? | **保留在当前列**,按该列保留选项过滤。**不存在「已完成统一用 Done 列」的强制规则**,那只是默认配置下的自然效果,用户可完全自定义(T-FL-11) |
| 23 | 「本周 / 本月」跨周跨月时是否要求实时隐藏? | **当场消失并带移除动画,其余任务平滑重排**;以刷新方式实现也可接受(T-EX-18) |
| 24 | 「上次访问的项目」记录在哪?无记录时如何? | 记录在**设备端**;**无记录一律回个人默认项目**(T-VW-07、T-PM-07) |
| 25 | 看板列是否固定 4 列? | **非固定**:默认 4 列,支持增删改与排序,数量可变,至少保留 1 列(T-COL-01~07) |
| 26 | 是否允许删除最后一个列? | **不允许**。删除唯一剩余列时拒绝并提示,项目至少保留 1 列(T-COL-06、T-EX-17) |
| 27 | 新增列的默认设置? | 按**默认值**:保留选项 `永久显示`、不绑定状态,**不继承**相邻列设置(T-COL-02、B-18) |
| 28 | 看板列数量上限? | 暂定**上限 10 列**(`待确认`,见下) |
| 29 | 任务首屏结构? | **上部** = 最近使用的 3 个项目卡片 + 「更多项目」入口;**下部** = 当前选中项目的任务管理视图,默认上次访问的项目(T-VW-08/09) |
| 30 | 首屏「任务卡片可以排序」的指代? | 指**最近项目卡片**可长按拖动排序,结果存设备端;任务卡片的列内排序维持既有口径(T-VW-10) |
| 31 | 成员 / 列设置入口的位置? | 统一收进「**项目管理**」入口(含成员、列设置、项目信息三区),项目页头部不再散置(T-PJ-11) |
| 32 | 项目信息管理包含什么? | 改名、归档 / 取消归档、删除(个人项目禁用)、退出(创建人可退),入口在项目管理内(T-PJ-12) |
| 33 | 任务详情底部按钮遮挡? | 弹层底部预留**安全区 + 12px** 距离;小屏下弹层可滚动,底部按钮完整可见(T-UI-11) |
| 34 | 项目相关长文本信息记录在哪? | 项目管理内新增**信息块**分区:任意多个 {标题, 长文本} 块,独立编辑,属项目级数据(T-PJ-13) |
| 35 | 任务级「扩展信息」的去留? | **移除 UI**(2026-10-24 评审):任务不再带扩展信息,项目相关长文本由项目信息块(T-PJ-13)承载;DB 字段保留不用 |
**仍待评审确认(影响实现口径):**
| # | 问题 | 说明 |
| --- | --- | --- |
| A | 列数量上限是否按 10 列? | 现按「至少 1 列、至多 10 列」写入(T-COL-06)。若无需上限,可去掉上限约束 |
---
## 附录 A:核心实体草案(供设计阶段参考,非最终表结构)
```prisma
// 项目
model Project {
id Int @id @default(autoincrement())
name String
ownerId Int // 创建人 (可退出项目, 故与成员表分离)
isPersonal Boolean @default(false) // true = 个人默认项目: 不可删、不可加成员
archived Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
members ProjectMember[]
columns BoardColumn[]
tasks Task[]
@@index([archived])
}
// 看板列 (项目级实体: 默认 4 列, 可增删改与排序)
model BoardColumn {
id Int @id @default(autoincrement())
projectId Int
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
name String // 列名, 项目内唯一
sortOrder Int @default(0) // 显示顺序, 决定自动归位时的"第一个匹配列"
boundStatus String? // null = 不绑定; todo | doing | done
retention String @default("forever") // never | today | 3d | week | month | forever
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
tasks Task[]
@@unique([projectId, name])
@@index([projectId, sortOrder])
}
// 项目成员 (权限单位: 在表内即全读写)
model ProjectMember {
id Int @id @default(autoincrement())
projectId Int
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
userId Int
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
permission String @default("member") // 预留权限分级入口, 本期恒为 member
createdAt DateTime @default(now())
@@unique([projectId, userId])
@@index([userId])
}
// 任务 / 子任务 (parentId 为空 = 顶层任务)
model Task {
id Int @id @default(autoincrement())
projectId Int
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
parentId Int? // 非空 = 一级子任务 (最多一层)
parent Task? @relation("Subtask", fields: [parentId], references: [id], onDelete: Cascade)
children Task[] @relation("Subtask")
title String
description String @default("")
status String @default("todo") // todo | doing | done
columnId Int // 所属看板列 (BoardColumn), 列可增删改故用 id 而非固定名
column BoardColumn @relation(fields: [columnId], references: [id])
assigneeId Int? // 负责人 (可空; 新建默认 = 创建者)
assignee User? @relation("Assignee", fields: [assigneeId], references: [id], onDelete: SetNull)
creatorId Int // 创建人 (跨项目拷贝时 = 操作者)
creator User @relation("Creator", fields: [creatorId], references: [id])
// 扩展信息卡片: 任意条目, 默认收起
extras Json @default("[]") // [{ label: "仓库", value: "gitlab/app", url: "" }]
sortOrder Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
members TaskMember[]
tags TaskTag[]
@@index([projectId, columnId])
@@index([projectId, status])
@@index([parentId])
}
// 任务成员 (多人, 须为项目成员)
model TaskMember {
id Int @id @default(autoincrement())
taskId Int
task Task @relation(fields: [taskId], references: [id], onDelete: Cascade)
userId Int
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([taskId, userId])
}
// 任务标签
model TaskTag {
id Int @id @default(autoincrement())
taskId Int
task Task @relation(fields: [taskId], references: [id], onDelete: Cascade)
name String
@@index([taskId])
}
```
> 说明:
> 1. `status` 与 `columnId` **两个字段都落库**(而非互相推导),因为未绑定的列需要二者彼此独立;联动仅在项目配置了绑定的列/状态之间发生,由服务端统一裁决。
> 2. 保留选项的判定依赖「在该列的完成时间」,建议在任务上记录完成时间或按列迁移时间计算,具体口径在设计阶段确定。
> 3. 视图记忆存设备端(localStorage),不落库。
---
## 附录 B:实现约定(开发期补充,用于消除口径歧义)
| # | 约定 | 说明 |
| --- | --- | --- |
| B-1 | **看板列** | 列是项目级实体(`BoardColumn`),含名称与顺序;默认 4 列 `Plan/Doing/Action/Done`;支持增删改与排序,至少保留 1 列 |
| B-2 | **列-状态绑定** | `columnBindings` 为「列 → 状态或 null」映射;null 表示不绑定;**允许多列指向同一状态**;状态枚举 `todo` / `doing` / `done` |
| B-3 | **多列绑定同一状态时的目标列** | 移入列时按**当前列顺序**取**第一个**绑定该状态的列(列顺序可被用户调整) |
| B-4 | **列决定状态** | 拖动任务时只看**目标列**:目标列绑定了状态 ⇒ 状态 = 所绑定状态(含从任意列拖入);目标列未绑定 ⇒ 状态保持原值。**不存在「从某列拖出即回退」的规则** |
| B-4b | **状态与列不一致时的自动归位** | 状态改变后,若当前所在列未绑定该状态 ⇒ 按列序取第一个绑定该状态的列并移入;**无匹配列则不移动**,状态照常改变 |
| B-5 | **列保留选项** | `columnRetention` 为「列 → 选项」映射,选项为固定 6 档:`never`(永不显示)/ `today`(当天)/ `3d`(3 天)/ `week`(本周)/ `month`(本月)/ `forever`(永久显示) |
| B-5b | **各档位口径** | 均按 Asia/Shanghai 自然日:`today` = 今天;`3d` = 含今天在内的最近 3 天;`week` = 本周一 00:00 起;`month` = 本月 1 日 00:00 起;判定依据为任务的**完成时间** |
| B-6 | **显示控制作用域** | 列表视图与看板视图**同样**按任务**当前所属列**的保留选项执行过滤;列表只是不按列分组,归属不变;未完成任务不受影响 |
| B-7 | **子任务与绑定无关** | 子任务状态**不受**看板列-状态绑定影响:拖入列不改写子任务状态,修改子任务状态也不触发「移入列」 |
| B-8 | **子任务约束** | `parentId` 非空时:父任务 `parentId` 必须为空(禁止二级);`projectId` 必须与父任务一致;禁止指向自身或后代 |
| B-9 | **拖拽降为子任务** | 长按拖动任务 A 到任务 B 上释放 ⇒ A.parentId = B.id;要求 B 为顶层任务且 A 本身无子任务,否则拒绝 |
| B-10 | **负责人默认值** | 新建任务时若未显式指定,`assigneeId` 默认取创建者的 userId;显式传 null 表示清空 |
| B-11 | **个人默认项目** | `isPersonal = true`;服务端拒绝「添加成员」「删除项目」;名称初始为 `{用户名}的任务`,可改名 |
| B-12 | **成员清空** | 移除最后一名成员时不删除 `Project`,仅删除 `ProjectMember` 记录;前端不特判 |
| B-13 | **跨项目拷贝** | 新建任务(新 id)+ 复制全部字段、扩展卡片与子任务;`creatorId` = 操作者;负责人 / 成员按 T-EX-08 过滤;状态与列按 T-EX-14 沿用 |
| B-14 | **权限校验** | 所有项目 / 任务接口均先校验「请求者为该项目成员」,非成员返回 403 |
| B-15 | **偏好存储** | 视图记忆与「上次访问的项目」存设备端;列定义、列-状态绑定、列保留选项属项目配置,存服务端、项目内共享 |
| B-16 | **看板列实体** | 列为 `BoardColumn`(name / sortOrder / boundStatus / retention);默认 4 列由新建项目时初始化;至少保留 1 列;列名项目内唯一 |
| B-17 | **删除列的迁移** | 删除列前需二次确认;列内任务迁移至指定列(默认相邻列),迁移后任务状态按目标列绑定重新裁决;**唯一剩余列不可删除** |
| B-18 | **新增列默认值** | 新增列取默认值:`retention = forever`、`boundStatus = null`,**不继承**相邻列配置;`sortOrder` 追加到末尾 |
| B-19 | **列数上限** | 至少 1 列、至多 10 列(上限待确认) |
@@ -0,0 +1,627 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>已归档任务管理 · 界面重设计(原型待评审)</title>
<style>
:root{
--bg:#f0f2f5; --page:#fff; --card:#fff;
--text:#333; --muted:#666; --weak:#999; --line:#eee;
--primary:#1677ff; --danger:#ff3141; --success:#00b578;
--nav-h:45px; --tab-h:72px;
}
*{box-sizing:border-box;margin:0;padding:0}
body{
background:var(--bg); color:#1f2328; font-size:14px; line-height:1.6;
font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','PingFang SC','Hiragino Sans GB','Microsoft YaHei',Roboto,sans-serif;
-webkit-font-smoothing:antialiased;
}
.wrap{max-width:1400px;margin:0 auto;padding:36px 24px 72px}
.doc-head{border-bottom:1px solid #e3e6ea;padding-bottom:20px;margin-bottom:28px}
.doc-head h1{font-size:26px;font-weight:700;letter-spacing:.5px}
.doc-head p{color:var(--muted);font-size:13px;margin-top:6px}
.meta{display:flex;flex-wrap:wrap;gap:8px;margin-top:14px}
.meta span{font-size:12px;background:#fff;border:1px solid #e3e6ea;border-radius:999px;padding:3px 12px;color:var(--muted)}
h2{font-size:19px;font-weight:700;margin:40px 0 6px;display:flex;align-items:center;gap:10px}
h2::before{content:"";width:4px;height:17px;background:var(--primary);border-radius:2px}
h2 .sub{font-size:12px;font-weight:400;color:var(--weak)}
p.desc{color:var(--muted);font-size:13px;margin-bottom:16px}
/* ---------- 改动要点 ---------- */
.changes{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:14px}
.chg{background:#fff;border:1px solid #e6e8eb;border-radius:12px;padding:14px 16px}
.chg .k{font-size:11px;color:var(--primary);font-weight:700;letter-spacing:.6px}
.chg h4{font-size:14px;font-weight:700;margin:5px 0 6px}
.chg p{font-size:12.5px;color:var(--muted);line-height:1.65}
.chg .old{color:var(--danger);font-size:12px;text-decoration:line-through}
/* ---------- 手机框 ---------- */
.stage{display:flex;gap:34px;flex-wrap:wrap;align-items:flex-start;margin-top:18px}
.phone{
width:400px;height:812px;background:var(--page);position:relative;overflow:hidden;
border:1px solid #d8dde3;border-radius:22px;box-shadow:0 10px 24px rgba(15,23,42,.08);
display:flex;flex-direction:column;
}
.phone::after{content:"";position:absolute;top:8px;left:50%;transform:translateX(-50%);width:112px;height:19px;background:#111827;border-radius:12px;opacity:.9;z-index:5}
.phone.sm{width:340px;height:700px}
.phone .nav{
height:var(--nav-h);margin-top:26px;flex-shrink:0;display:flex;align-items:center;gap:10px;
padding:0 14px;background:#fff;border-bottom:1px solid var(--line);
}
.phone .nav .back{font-size:20px;color:#666;line-height:1;cursor:pointer}
.phone .nav .title{font-size:16px;font-weight:700}
.phone .nav .proj{margin-left:auto;font-size:11px;color:var(--weak);background:#f5f6f8;border-radius:999px;padding:2px 9px;max-width:130px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.phone .body{flex:1;min-height:0;overflow-y:auto;background:#f5f5f5;padding-bottom:18px}
/* ---------- 筛选卡 ---------- */
.fcard{background:#fff;margin:10px 12px 0;border-radius:12px;padding:4px 14px 12px;box-shadow:0 1px 2px rgba(0,0,0,.04)}
.frow{display:flex;align-items:center;gap:10px;padding:10px 0;border-bottom:1px solid #f4f5f7}
.frow:last-of-type{border-bottom:0}
.frow > .lbl{width:56px;flex-shrink:0;font-size:12.5px;font-weight:600;color:var(--text)}
.frow > .ctl{flex:1;min-width:0}
/* 标题输入框前的提示 */
.kwbox{display:flex;align-items:center;gap:7px;background:#f7f8fa;border:1px solid #eceef1;border-radius:8px;padding:6px 9px}
.kwbox:focus-within{border-color:var(--primary);background:#fff}
.kwbox .ph{font-size:11.5px;color:var(--weak);flex-shrink:0}
.kwbox input{flex:1;min-width:0;border:0;background:transparent;font-size:13px;outline:none;color:var(--text)}
.kwbox input::placeholder{color:#c2c6cc}
.kwbox .clr{font-size:12px;color:#c2c6cc;cursor:pointer;flex-shrink:0}
.chips{display:flex;flex-wrap:wrap;gap:6px}
.chip{
font-size:12px;padding:4px 11px;border-radius:999px;border:1px solid #e6e8eb;
background:#fafbfc;color:var(--muted);cursor:pointer;white-space:nowrap;transition:.15s;
}
.chip:hover{border-color:#c9d3e0}
.chip.on{background:#eaf3ff;border-color:var(--primary);color:var(--primary);font-weight:600}
.chip .x{margin-left:5px;color:#9aa4b2}
.range{display:flex;align-items:center;gap:7px;margin-top:8px}
.range .dp{font-size:12px;padding:4px 10px;border:1px dashed #d9dee5;border-radius:7px;color:var(--weak);cursor:pointer;background:#fff}
.range .dp.on{color:var(--text);border-style:solid;border-color:#d9dee5}
.range .sep{color:#c2c6cc;font-size:12px}
.factions{display:flex;gap:9px;padding-top:12px}
.btn{flex:1;height:36px;border-radius:9px;border:0;font-size:13.5px;font-weight:600;cursor:pointer;transition:.15s}
.btn.ghost{background:#f4f5f7;color:var(--muted);flex:0 0 84px}
.btn.primary{background:var(--primary);color:#fff;box-shadow:0 2px 8px rgba(22,119,255,.26)}
.btn.primary:active{transform:translateY(1px)}
.btn .badge{display:inline-block;min-width:16px;height:16px;line-height:16px;border-radius:8px;background:rgba(255,255,255,.28);font-size:10.5px;margin-left:5px;padding:0 4px}
/* ---------- 已应用条件 ---------- */
.applied{display:flex;flex-wrap:wrap;gap:6px;align-items:center;padding:11px 14px 3px}
.applied .tag{font-size:11.5px;background:#eaf3ff;color:var(--primary);border-radius:6px;padding:2px 7px;display:flex;align-items:center;gap:5px}
.applied .tag b{font-weight:600}
.applied .tag .x{cursor:pointer;opacity:.6}
.applied .cnt{margin-left:auto;font-size:11.5px;color:var(--weak)}
/* ---------- 列表 ---------- */
.list{padding:8px 12px 0;display:flex;flex-direction:column;gap:8px}
.item{background:#fff;border-radius:10px;padding:11px 12px;box-shadow:0 1px 2px rgba(0,0,0,.04)}
.item .top{display:flex;gap:8px;align-items:flex-start}
.item .ttl{flex:1;min-width:0;font-size:13.5px;font-weight:600;line-height:1.42;word-break:break-word}
.item .num{font-size:10px;color:#9aa4b2;background:#f5f6f8;border-radius:4px;padding:1px 5px;flex-shrink:0;margin-top:1px}
.item .meta{display:flex;gap:7px;align-items:center;flex-wrap:wrap;margin-top:7px;font-size:10.5px;color:var(--weak)}
.item .meta .col{color:var(--muted)}
.item .meta .dot{width:3px;height:3px;border-radius:50%;background:#d5d9e0}
.item .acts{display:flex;gap:7px;justify-content:flex-end;margin-top:9px;padding-top:9px;border-top:1px dashed #f0f1f3}
.mini{font-size:11.5px;padding:3px 12px;border-radius:999px;border:1px solid var(--primary);color:var(--primary);background:#fff;cursor:pointer;white-space:nowrap}
.mini.danger{border-color:#ffd4d7;color:var(--danger)}
.kind{font-size:10px;line-height:1;font-weight:600;color:#fff;border-radius:4px;padding:3px 5px}
.empty{text-align:center;padding:56px 20px;color:#b8bcc4;font-size:13px}
.empty .ico{font-size:34px;opacity:.35;margin-bottom:10px}
.more{display:block;width:calc(100% - 24px);margin:12px auto 0;height:34px;border-radius:9px;border:1px solid #e6e8eb;background:#fff;color:var(--muted);font-size:12.5px;cursor:pointer}
.toast{
position:absolute;left:50%;bottom:56px;transform:translateX(-50%) translateY(8px);
background:rgba(0,0,0,.78);color:#fff;font-size:12.5px;padding:8px 16px;border-radius:8px;
opacity:0;transition:.22s;z-index:9;pointer-events:none;white-space:nowrap;
}
.toast.show{opacity:1;transform:translateX(-50%) translateY(0)}
.caption{margin-top:12px;background:#fff;border:1px solid #e6e8eb;border-radius:10px;padding:11px 14px;width:400px}
.caption b{font-size:13px;display:block;margin-bottom:4px}
.caption span{font-size:12px;color:var(--muted);line-height:1.65}
.notes{flex:1;min-width:300px;display:flex;flex-direction:column;gap:12px}
.note{background:#fff;border:1px solid #e6e8eb;border-radius:12px;padding:14px 16px}
.note h4{font-size:13px;font-weight:700;margin-bottom:7px;display:flex;align-items:center;gap:7px}
.note h4 i{width:18px;height:18px;border-radius:50%;background:#eaf3ff;color:var(--primary);font-size:11px;font-style:normal;display:flex;align-items:center;justify-content:center;font-weight:700}
.note ul{list-style:none;font-size:12.5px;color:var(--muted);line-height:1.85}
.note ul li::before{content:"·";color:#c2c6cc;margin-right:6px}
.note code{background:#f5f6f8;border-radius:4px;padding:1px 5px;font-size:11.5px;color:#c7254e}
table{width:100%;border-collapse:collapse;background:#fff;border-radius:12px;overflow:hidden;border:1px solid #e6e8eb;font-size:12.5px}
th,td{padding:10px 14px;text-align:left;border-bottom:1px solid #f0f1f3;vertical-align:top}
th{background:#fafbfc;font-size:12px;color:var(--muted);font-weight:600}
tr:last-child td{border-bottom:0}
td .no{color:var(--danger)}
td .yes{color:var(--success);font-weight:600}
.variants{display:flex;gap:26px;flex-wrap:wrap;margin-top:16px}
</style>
</head>
<body>
<div class="wrap">
<div class="doc-head">
<h1>已归档任务管理 · 界面重设计</h1>
<p>对应需求 T-ARC-04/05/06。本文件是<strong>评审用原型</strong>,不含业务代码改动;确认后再落地。</p>
<div class="meta">
<span>原型 · 待评审</span>
<span>2026-10-28</span>
<span>可交互:点预设 / 筛选 / 恢复</span>
<span>范围:任务模块 › 项目 › 已归档</span>
</div>
</div>
<h2>改动要点 <span class="sub">对照你提出的 4 点</span></h2>
<div class="changes">
<div class="chg">
<div class="k">01</div>
<h4>去掉「类型」过滤</h4>
<p>现版本的「全部类型」下拉整体移除。任务类型是弱维度,归档场景几乎不按类型找东西,反而占掉一横行高。</p>
<p class="old">全部类型 ▾</p>
</div>
<div class="chg">
<div class="k">02</div>
<h4>输入框前加提示</h4>
<p>关键词输入框前加固定标签「标题」,输入框内用灰字占位「输入关键词」——标签常驻,占位随输入消失,不会出现"不知道搜什么"。</p>
<p class="old">搜索任务标题(无标签)</p>
</div>
<div class="chg">
<div class="k">03</div>
<h4>归档时间快捷选择</h4>
<p>新增 <b>全部 / 本周 / 本月 / 本季度</b> 四个一键预设,选完自动算出起止日期并显示;需要精确区间时再选「自定义」。</p>
<p class="old">开始日期 ~ 结束日期(纯手点)</p>
</div>
<div class="chg">
<div class="k">04</div>
<h4>补上「筛选」按钮</h4>
<p>现版本改任何条件都立即查询、没有确认动作。改为:改条件 → 点<b>筛选</b>才生效,按钮上显示生效条件数;另给「重置」。</p>
<p class="old">无按钮(自动即时查询)</p>
</div>
</div>
<h2>主界面 <span class="sub">可交互 · 点一下试试</span></h2>
<div class="stage">
<div>
<div class="phone" id="phone">
<div class="nav">
<span class="back">‹</span>
<span class="title">已归档任务</span>
<span class="proj">Admin的任务</span>
</div>
<div class="body">
<!-- 筛选卡 -->
<div class="fcard">
<div class="frow">
<span class="lbl">标题</span>
<span class="ctl">
<span class="kwbox">
<input id="kw" placeholder="输入关键词" />
<span class="clr" id="kwClear" style="display:none">✕</span>
</span>
</span>
</div>
<div class="frow">
<span class="lbl">看板列</span>
<span class="ctl">
<span class="chips" id="colChips">
<span class="chip on" data-col="">全部</span>
<span class="chip" data-col="Plan">Plan</span>
<span class="chip" data-col="Doing">Doing</span>
<span class="chip" data-col="Done">Done</span>
</span>
</span>
</div>
<div class="frow">
<span class="lbl">归档时间</span>
<span class="ctl">
<span class="chips" id="timeChips">
<span class="chip on" data-t="all">全部</span>
<span class="chip" data-t="week">本周</span>
<span class="chip" data-t="month">本月</span>
<span class="chip" data-t="quarter">本季度</span>
<span class="chip" data-t="custom">自定义</span>
</span>
<span class="range" id="rangeRow" style="display:none">
<span class="dp" id="dpFrom">开始日期</span>
<span class="sep">~</span>
<span class="dp" id="dpTo">结束日期</span>
</span>
<div id="rangeHint" style="font-size:11px;color:#9aa4b2;margin-top:6px"></div>
</span>
</div>
<div class="factions">
<button class="btn ghost" id="resetBtn">重置</button>
<button class="btn primary" id="filterBtn">筛选</button>
</div>
</div>
<!-- 已应用条件 -->
<div class="applied" id="appliedBar" style="display:none"></div>
<!-- 列表 -->
<div class="list" id="list"></div>
<button class="more" id="moreBtn">加载更多</button>
</div>
<div class="toast" id="toast"></div>
</div>
<div class="caption">
<b>主界面</b>
<span>筛选卡固定三行:标题 / 看板列 / 归档时间,外加操作行。改动不再即时生效,点「筛选」统一提交。</span>
</div>
</div>
<div class="notes">
<div class="note">
<h4><i>1</i>为什么改成"点按钮才筛选"</h4>
<ul>
<li>现在每改一个条件就重查一次,连点三个条件就发三次请求</li>
<li>输入关键词时每敲一个字都可能触发(失焦也查),列表会闪</li>
<li>改成显式提交后,用户能先把条件填完整,再一次性看结果</li>
<li>按钮上带数字徽标,一眼知道当前生效了几个条件</li>
</ul>
</div>
<div class="note">
<h4><i>2</i>快捷时间怎么算</h4>
<ul>
<li><b>本周</b>:周一 00:00 ~ 今天(不跨到未来)</li>
<li><b>本月</b>:1 号 00:00 ~ 今天</li>
<li><b>本季度</b>:当季首月 1 号 00:00 ~ 今天</li>
<li>选完在下方灰字直接显示实际区间,避免"以为选的是整周"</li>
<li>「自定义」才展开两个日期选择器,默认收起</li>
</ul>
</div>
<div class="note">
<h4><i>3</i>已应用条件可单独撤销</h4>
<ul>
<li>筛选后在列表上方显示条件标签,每个带 <code>×</code> 可单独去掉</li>
<li>右侧显示命中条数,避免"筛完不知道有没有筛到"</li>
<li>无结果时给明确空状态,而不是一片留白</li>
</ul>
</div>
<div class="note">
<h4><i>4</i>列表行重排</h4>
<ul>
<li>标题独占一行(原来和类型/列/时间挤在下面)</li>
<li>元信息收成一行:<code>#编号</code> · 看板列 · 归档于 日期</li>
<li>「恢复 / 删除」从右侧竖排改到底部右侧横排,减少每行高度</li>
<li>类型标签保留展示(只是不再作为筛选项)</li>
</ul>
</div>
</div>
</div>
<h2>状态变体</h2>
<div class="variants">
<div>
<div class="phone sm">
<div class="nav"><span class="back">‹</span><span class="title">已归档任务</span></div>
<div class="body">
<div class="fcard">
<div class="frow"><span class="lbl">标题</span><span class="ctl"><span class="kwbox"><input placeholder="输入关键词" /></span></span></div>
<div class="frow"><span class="lbl">看板列</span><span class="ctl"><span class="chips"><span class="chip on">全部</span><span class="chip">Plan</span><span class="chip">Done</span></span></span></div>
<div class="frow"><span class="lbl">归档时间</span><span class="ctl">
<span class="chips"><span class="chip">全部</span><span class="chip on">本周</span><span class="chip">本月</span><span class="chip">本季度</span><span class="chip">自定义</span></span>
<div style="font-size:11px;color:#9aa4b2;margin-top:6px">2026-10-26 ~ 2026-10-28</div>
</span></div>
<div class="factions"><button class="btn ghost">重置</button><button class="btn primary">筛选<span class="badge">2</span></button></div>
</div>
<div class="applied">
<span class="tag"><b>本周</b><span class="x">×</span></span>
<span class="tag"><b>Done</b><span class="x">×</span></span>
<span class="cnt">共 3 条</span>
</div>
<div class="list">
<div class="item">
<div class="top"><div class="ttl">任务详情页滚动优化</div><span class="num">#22</span></div>
<div class="meta"><span class="kind" style="background:#1677ff">优化</span><span class="col">Done</span><span class="dot"></span><span>归档于 10-27</span></div>
<div class="acts"><span class="mini">恢复</span><span class="mini danger">删除</span></div>
</div>
<div class="item">
<div class="top"><div class="ttl">桌面端。</div><span class="num">#5</span></div>
<div class="meta"><span class="kind" style="background:#fa8c16">需求</span><span class="col">Done</span><span class="dot"></span><span>归档于 10-26</span></div>
<div class="acts"><span class="mini">恢复</span><span class="mini danger">删除</span></div>
</div>
</div>
</div>
</div>
<div class="caption"><b>已应用条件 + 命中条数</b><span>条件可逐个撤销;时间预设自动显示实际区间。</span></div>
</div>
<div>
<div class="phone sm">
<div class="nav"><span class="back">‹</span><span class="title">已归档任务</span></div>
<div class="body">
<div class="fcard">
<div class="frow"><span class="lbl">标题</span><span class="ctl"><span class="kwbox"><input value="日报" /><span class="clr">✕</span></span></span></div>
<div class="frow"><span class="lbl">看板列</span><span class="ctl"><span class="chips"><span class="chip on">全部</span><span class="chip">Done</span></span></span></div>
<div class="frow"><span class="lbl">归档时间</span><span class="ctl">
<span class="chips"><span class="chip">全部</span><span class="chip">本周</span><span class="chip">本月</span><span class="chip">本季度</span><span class="chip on">自定义</span></span>
<span class="range"><span class="dp on">2026-09-01</span><span class="sep">~</span><span class="dp on">2026-09-30</span></span>
</span></div>
<div class="factions"><button class="btn ghost">重置</button><button class="btn primary">筛选<span class="badge">3</span></button></div>
</div>
<div class="applied">
<span class="tag"><b>标题: 日报</b><span class="x">×</span></span>
<span class="tag"><b>09-01~09-30</b><span class="x">×</span></span>
<span class="cnt">共 0 条</span>
</div>
<div class="empty"><div class="ico">🗂</div>没有符合条件的归档任务<br /><span style="font-size:11.5px;color:#c2c6cc">试试放宽时间范围或清空关键词</span></div>
</div>
</div>
<div class="caption"><b>自定义区间 + 空结果</b><span>选「自定义」才展开日期;空结果给可操作的建议文案。</span></div>
</div>
<div>
<div class="phone sm">
<div class="nav"><span class="back">‹</span><span class="title">已归档任务</span></div>
<div class="body">
<div class="fcard">
<div class="frow"><span class="lbl">标题</span><span class="ctl"><span class="kwbox"><input placeholder="输入关键词" /></span></span></div>
<div class="frow"><span class="lbl">看板列</span><span class="ctl"><span class="chips"><span class="chip on">全部</span><span class="chip">Plan</span><span class="chip">Doing</span><span class="chip">Done</span></span></span></div>
<div class="frow"><span class="lbl">归档时间</span><span class="ctl"><span class="chips"><span class="chip on">全部</span><span class="chip">本周</span><span class="chip">本月</span><span class="chip">本季度</span><span class="chip">自定义</span></span></span></div>
<div class="factions"><button class="btn ghost">重置</button><button class="btn primary">筛选</button></div>
</div>
<div class="empty"><div class="ico">📭</div>这个项目还没有归档任务<br /><span style="font-size:11.5px;color:#c2c6cc">任务完成后可在 ⋯ 菜单里归档</span></div>
</div>
</div>
<div class="caption"><b>初始空状态</b><span>一个都没归档时,告诉用户归档入口在哪。</span></div>
</div>
</div>
<h2>与现版本对照</h2>
<table>
<thead>
<tr><th style="width:130px">项</th><th style="width:38%">现在</th><th>建议</th></tr>
</thead>
<tbody>
<tr><td>类型过滤</td><td class="no">「全部类型」下拉 + 独占一行</td><td class="yes">移除</td></tr>
<tr><td>关键词</td><td class="no">无标签,占位「搜索任务标题」</td><td class="yes">左侧常驻标签「标题」+ 占位「输入关键词」</td></tr>
<tr><td>归档时间</td><td class="no">两个日期按钮,无快捷项</td><td class="yes">全部 / 本周 / 本月 / 本季度 / 自定义</td></tr>
<tr><td>查询触发</td><td class="no">改条件即时查询,无按钮</td><td class="yes">「重置 / 筛选」,按钮带生效条件数</td></tr>
<tr><td>条件可见性</td><td class="no">条件散在控件里,看不出生效了什么</td><td class="yes">列表上方条件标签 + 命中条数,可单独撤销</td></tr>
<tr><td>列表行</td><td class="no">标题与元信息挤在一起,右侧竖排两按钮</td><td class="yes">标题独占行;元信息一行;按钮移到底部右侧</td></tr>
<tr><td>空状态</td><td class="no">一行灰字「没有符合条件的归档任务」</td><td class="yes">区分「还没归档过」与「筛选无结果」,给出下一步建议</td></tr>
</tbody>
</table>
<h2>待你确认 <span class="sub">确认后我再改代码</span></h2>
<div class="notes" style="max-width:820px">
<div class="note">
<ul>
<li>「筛选」改为显式提交,是否符合你的使用习惯?(也可以保留"改预设即时生效、只有关键词需要点按钮")</li>
<li>看板列是否也保留?还是只留 标题 + 归档时间 两个条件更清爽?</li>
<li>列表行的「恢复 / 删除」放底部右侧可以吗?还是希望点行进入详情/预览?</li>
<li>快捷时间是否需要再加「近 7 天 / 近 30 天」?</li>
</ul>
</div>
</div>
</div>
<script>
(function(){
// ---- 原型状态 ----
const state = { kw:'', col:'', time:'all', from:'', to:'', applied:{ kw:'', col:'', time:'all', from:'', to:'' } }
const MOCK = [
{ id:1, title:'任务详情页滚动优化', num:'#22', kind:'优化', kindColor:'#1677ff', col:'Done', at:'2026-10-27' },
{ id:2, title:'桌面端。', num:'#5', kind:'需求', kindColor:'#fa8c16', col:'Done', at:'2026-10-26' },
{ id:3, title:'任务详情中切换看板列是提示不存在。', num:'#23', kind:'Bug', kindColor:'#ff3141', col:'Done', at:'2026-10-25' },
{ id:4, title:'windows 电脑加入监控', num:'#24', kind:'需求', kindColor:'#fa8c16', col:'Plan', at:'2026-10-21' },
{ id:5, title:'补日报', num:'#28', kind:'需求', kindColor:'#fa8c16', col:'Doing', at:'2026-10-12' },
]
let rows = MOCK.slice()
const $ = (id) => document.getElementById(id)
const toastEl = $('toast')
let toastTimer = null
function toast(msg){
toastEl.textContent = msg
toastEl.classList.add('show')
clearTimeout(toastTimer)
toastTimer = setTimeout(() => toastEl.classList.remove('show'), 1600)
}
// ---- 快捷时间区间 ----
function pad(n){ return String(n).padStart(2,'0') }
function fmt(d){ return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())}` }
function presetRange(t){
const now = new Date()
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate())
if (t === 'week') {
const dow = (today.getDay() + 6) % 7 // 周一=0
const mon = new Date(today); mon.setDate(today.getDate() - dow)
return { from: fmt(mon), to: fmt(today) }
}
if (t === 'month') return { from: fmt(new Date(today.getFullYear(), today.getMonth(), 1)), to: fmt(today) }
if (t === 'quarter') {
const qm = Math.floor(today.getMonth() / 3) * 3
return { from: fmt(new Date(today.getFullYear(), qm, 1)), to: fmt(today) }
}
return { from:'', to:'' }
}
function rangeLabel(t){
if (t === 'custom') return ''
const r = presetRange(t)
return r.from ? `${r.from} ~ ${r.to}` : ''
}
// ---- 渲染 ----
function render(){
// 时间行
const custom = state.time === 'custom'
$('rangeRow').style.display = custom ? 'flex' : 'none'
const hint = rangeLabel(state.time)
$('rangeHint').textContent = hint
$('dpFrom').textContent = state.from || '开始日期'
$('dpTo').textContent = state.to || '结束日期'
$('dpFrom').classList.toggle('on', !!state.from)
$('dpTo').classList.toggle('on', !!state.to)
$('kwClear').style.display = state.kw ? 'block' : 'none'
// 生效条件数
const n = countApplied()
const btn = $('filterBtn')
btn.innerHTML = n ? `筛选<span class="badge">${n}</span>` : '筛选'
// 已应用条
const bar = $('appliedBar')
const tags = []
const a = state.applied
if (a.kw) tags.push(`<span class="tag"><b>标题: ${a.kw}</b><span class="x" data-clear="kw">×</span></span>`)
if (a.col) tags.push(`<span class="tag"><b>看板列: ${a.col}</b><span class="x" data-clear="col">×</span></span>`)
if (a.time === 'custom') {
if (a.from || a.to) tags.push(`<span class="tag"><b>${a.from || '…'} ~ ${a.to || '…'}</b><span class="x" data-clear="time">×</span></span>`)
} else if (a.time !== 'all') {
const name = { week:'本周', month:'本月', quarter:'本季度' }[a.time]
tags.push(`<span class="tag"><b>${name}</b><span class="x" data-clear="time">×</span></span>`)
}
if (tags.length) {
bar.style.display = 'flex'
bar.innerHTML = tags.join('') + `<span class="cnt">共 ${rows.length} 条</span>`
} else {
bar.style.display = 'none'
}
// 列表
const list = $('list')
if (!rows.length) {
const never = !tags.length
list.innerHTML = never
? `<div class="empty"><div class="ico">📭</div>这个项目还没有归档任务<br /><span style="font-size:11.5px;color:#c2c6cc">任务完成后可在 ⋯ 菜单里归档</span></div>`
: `<div class="empty"><div class="ico">🗂</div>没有符合条件的归档任务<br /><span style="font-size:11.5px;color:#c2c6cc">试试放宽时间范围或清空关键词</span></div>`
$('moreBtn').style.display = 'none'
} else {
$('moreBtn').style.display = 'block'
list.innerHTML = rows.map(r => `
<div class="item" data-id="${r.id}">
<div class="top"><div class="ttl">${r.title}</div><span class="num">${r.num}</span></div>
<div class="meta">
<span class="kind" style="background:${r.kindColor}">${r.kind}</span>
<span class="col">${r.col}</span><span class="dot"></span><span>归档于 ${r.at}</span>
</div>
<div class="acts"><span class="mini" data-act="restore">恢复</span><span class="mini danger" data-act="remove">删除</span></div>
</div>`).join('')
}
}
function countApplied(){
const a = state.applied
let n = 0
if (a.kw) n++
if (a.col) n++
if (a.time === 'custom') { if (a.from || a.to) n++ } else if (a.time !== 'all') n++
return n
}
function applyFilter(){
state.applied = { kw: state.kw, col: state.col, time: state.time, from: state.from, to: state.to }
const a = state.applied
rows = MOCK.filter(r => {
if (a.kw && !r.title.includes(a.kw)) return false
if (a.col && r.col !== a.col) return false
if (a.time !== 'custom' && a.time !== 'all') {
const rg = presetRange(a.time)
if (rg.from && (r.at < rg.from || r.at > rg.to)) return false
}
if (a.time === 'custom') {
if (a.from && r.at < a.from) return false
if (a.to && r.at > a.to) return false
}
return true
})
render()
toast(countApplied() ? `已应用 ${countApplied()} 个条件 · ${rows.length} 条` : `显示全部 ${rows.length} 条`)
}
// ---- 事件 ----
$('kw').addEventListener('input', (e) => { state.kw = e.target.value.trim(); $('kwClear').style.display = state.kw ? 'block' : 'none' })
$('kw').addEventListener('keydown', (e) => { if (e.key === 'Enter') applyFilter() })
$('kwClear').addEventListener('click', () => { state.kw = ''; $('kw').value = ''; render() })
document.querySelectorAll('#colChips .chip').forEach(el => {
el.addEventListener('click', () => {
document.querySelectorAll('#colChips .chip').forEach(x => x.classList.remove('on'))
el.classList.add('on')
state.col = el.dataset.col || ''
})
})
document.querySelectorAll('#timeChips .chip').forEach(el => {
el.addEventListener('click', () => {
document.querySelectorAll('#timeChips .chip').forEach(x => x.classList.remove('on'))
el.classList.add('on')
state.time = el.dataset.t
if (state.time !== 'custom') { state.from = ''; state.to = '' }
render()
})
})
$('dpFrom').addEventListener('click', () => {
const v = prompt('开始日期 (YYYY-MM-DD)', state.from || '2026-10-01')
if (v !== null) { state.from = v.trim(); render() }
})
$('dpTo').addEventListener('click', () => {
const v = prompt('结束日期 (YYYY-MM-DD)', state.to || '2026-10-28')
if (v !== null) { state.to = v.trim(); render() }
})
$('filterBtn').addEventListener('click', applyFilter)
$('resetBtn').addEventListener('click', () => {
state.kw = ''; state.col = ''; state.time = 'all'; state.from = ''; state.to = ''
state.applied = { kw:'', col:'', time:'all', from:'', to:'' }
$('kw').value = ''
document.querySelectorAll('#colChips .chip').forEach((x, i) => x.classList.toggle('on', i === 0))
document.querySelectorAll('#timeChips .chip').forEach((x, i) => x.classList.toggle('on', i === 0))
rows = MOCK.slice()
render()
toast('已重置')
})
$('appliedBar').addEventListener('click', (e) => {
const x = e.target.closest('.x'); if (!x) return
const k = x.dataset.clear
if (k === 'kw') state.applied.kw = ''
if (k === 'col') state.applied.col = ''
if (k === 'time') { state.applied.time = 'all'; state.applied.from = ''; state.applied.to = '' }
const a = state.applied
rows = MOCK.filter(r => {
if (a.kw && !r.title.includes(a.kw)) return false
if (a.col && r.col !== a.col) return false
return true
})
render()
toast('已移除该条件')
})
$('list').addEventListener('click', (e) => {
const btn = e.target.closest('[data-act]'); if (!btn) return
const item = btn.closest('.item')
const id = Number(item.dataset.id)
const row = rows.find(r => r.id === id)
if (btn.dataset.act === 'restore') {
toast(`已恢复「${row.title}」`)
} else {
toast(`已删除「${row.title}」(原型演示,不实际删除)`)
}
item.style.opacity = '.35'
item.style.pointerEvents = 'none'
})
$('moreBtn').addEventListener('click', () => toast('加载更多(原型演示)'))
render()
})()
</script>
</body>
</html>
@@ -0,0 +1,108 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>设计稿 · 视图切换移入项目栏 (T-VW-02/03 调整)</title>
<style>
:root { --primary:#1677ff; --text:#333; --muted:#999; --border:#eee; }
* { box-sizing: border-box; margin: 0; padding: 0; font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif; }
body { background:#eceef1; padding: 24px; display:flex; gap:28px; flex-wrap:wrap; }
.phone { width:375px; background:#f5f5f5; border-radius:18px; overflow:hidden; box-shadow:0 8px 30px rgba(0,0,0,.15); }
.phone h3 { font-size:13px; color:#556; padding:10px 14px 0; font-weight:600; }
.phone .cap { font-size:11px; color:#889; padding:2px 14px 10px; }
.bar { display:flex; align-items:center; gap:8px; padding:10px 12px; background:#fff; border-bottom:1px solid #f0f0f0; }
.pname { font-size:15px; font-weight:700; color:var(--text); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; min-width:0; }
/* 紧凑分段切换: 高 26px, 内边距收窄, 选中项白底浮起 */
.vseg { display:flex; background:#f2f3f5; border-radius:8px; padding:2px; flex-shrink:0; }
.vseg div { font-size:12px; line-height:1; padding:5px 11px; border-radius:6px; color:var(--muted); cursor:pointer; white-space:nowrap; }
.vseg div.on { background:#fff; color:var(--text); font-weight:600; box-shadow:0 1px 2px rgba(0,0,0,.08); }
.mini { font-size:11px; color:var(--primary); background:#f0f5ff; border-radius:6px; padding:5px 8px; white-space:nowrap; flex-shrink:0; cursor:pointer; }
.mini.solid { background:var(--primary); color:#fff; }
.mini.ghost { background:#fff; border:1px solid #e3e6ea; color:#666; }
.bar-note { font-size:10px; color:#b0b6bf; padding:6px 12px; background:#fafbfc; border-bottom:1px dashed #e5e8ec; }
.demo-body { padding:12px; }
.card { background:#fff; border-radius:9px; padding:10px 12px; margin-bottom:7px; font-size:13px; color:#444; box-shadow:0 1px 2px rgba(0,0,0,.04); display:flex; gap:8px; align-items:center; }
.card .dot { width:8px; height:8px; border-radius:50%; background:#c9ced6; flex-shrink:0; }
.cols { display:flex; gap:8px; }
.col { flex:1; background:#fff; border-radius:9px; padding:8px; }
.col b { font-size:11px; color:#556; }
.col .ccard { background:#f6f7f9; border-radius:6px; font-size:11px; color:#555; padding:6px; margin-top:6px; }
.anno { font-size:11px; color:#7a828e; padding:8px 14px 14px; line-height:1.7; background:#fafbfc; }
.anno b { color:#43506b; }
</style>
</head>
<body>
<div class="phone">
<h3>状态 1 · 列表视图(默认)</h3>
<div class="cap">切换段控放在项目名右侧, 与名称同一行</div>
<div class="bar">
<div class="pname">我的看板项目</div>
<div class="vseg"><div class="on">列表</div><div>看板</div></div>
<div class="mini">已归档</div>
<div class="mini ghost">管理</div>
<div class="mini solid">+ 新增</div>
</div>
<div class="bar-note">「项目管理」按钮文案收窄为「管理」, 与「已归档」同为次级样式, 突出右侧主操作</div>
<div class="demo-body">
<div class="card"><span class="dot"></span>梳理 V0.2 归档需求</div>
<div class="card"><span class="dot"></span>OTA 发布说明样式修复</div>
<div class="card"><span class="dot"></span>看板列滚动条位置调整</div>
</div>
<div class="anno">
<b>规格</b><br/>
· 分段控件高 26px, 选中项白底浮起(复用现有 .seg 体系收窄版)<br/>
· 项目名 flex:1 + 单行省略, 最短保留 4 字符<br/>
· 段控始终紧贴名称右侧, 距离 8px
</div>
</div>
<div class="phone">
<h3>状态 2 · 看板视图</h3>
<div class="cap">点「看板」后段控选中项滑换, 其余布局不变</div>
<div class="bar">
<div class="pname">我的看板项目</div>
<div class="vseg"><div>列表</div><div class="on">看板</div></div>
<div class="mini">已归档</div>
<div class="mini ghost">管理</div>
<div class="mini solid">+ 新增</div>
</div>
<div class="demo-body">
<div class="cols">
<div class="col"><b>Plan · 2</b><div class="ccard">梳理归档需求</div><div class="ccard">OTA 说明修复</div></div>
<div class="col"><b>Doing · 1</b><div class="ccard">滚动条调整</div></div>
<div class="col"><b>Done · 1</b><div class="ccard">版本方案落地</div></div>
</div>
</div>
<div class="anno">
<b>交互</b><br/>
· 点按即切换并持久化(设备端记忆, T-VW-02/03 不变)<br/>
· 切换不引发项目栏高度变化, 下方视图平滑重排<br/>
· 长按/拖动手势不受影响(段控独立控件, 不在卡片上)
</div>
</div>
<div class="phone">
<h3>状态 3 · 长项目名</h3>
<div class="cap">名称被压缩时单行省略, 段控与按钮永不换行/收缩</div>
<div class="bar">
<div class="pname">经济数据整理与策略研究-2026年第一季度重点项目</div>
<div class="vseg"><div class="on">列表</div><div>看板</div></div>
<div class="mini">已归档</div>
<div class="mini ghost">管理</div>
<div class="mini solid">+ 新增</div>
</div>
<div class="demo-body">
<div class="card"><span class="dot"></span>完整项目名在详情/管理页仍可见</div>
</div>
<div class="anno">
<b>宽度预算 (375px)</b><br/>
· 左右内边距 24 + 段控 90 + 已归档 48 + 管理 42 + 新增 54 + 间距 32 ≈ 290<br/>
· 名称可用 ≈ 85px(约 6 个汉字)起, 不足部分省略<br/>
· 若仍嫌拥挤, 备选:「已归档」「管理」收进 ⋯ 溢出菜单(二期可选, 本稿未采用)
</div>
</div>
</body>
</html>
@@ -0,0 +1,103 @@
# 0.2.1 实施方案:PostgreSQL 迁移 + 任务模块离线编辑 (PowerSync)
| 项 | 内容 |
| --- | --- |
| 分支 | `dev-0.2.1`(worktree `iboard-dev-0.2.1`,独立临时分支,与 master / dev-0.2 隔离) |
| 日期 | 2026-10-25 |
| 依据 | [PowerSync可行性研究报告](./PowerSync可行性研究报告.md),按其 5.2.2 路线执行 |
| 目标 | 在现网应用与数据不受影响的前提下,验证任务模块离线编辑 + 联网后自动双向同步;数据库由 MariaDB 迁移至 PostgreSQL |
## 1. 已确认决策(2026-10-25 与需求方对齐)
| 决策点 | 结论 |
| --- | --- |
| 技术路线 | PowerSync 全链路(自部署 powersync-service) |
| 离线范围 | 仅任务模块(Project / ProjectMember / BoardColumn / Task / TaskMember / TaskTag);打卡、清单本阶段不纳入;新闻/指标/监控等时序域仅做普通缓存 |
| 客户端 | 先 Web / Windows 外壳(WebView2)跑通全链路;Android 真机后续验证 |
| 编号 | 项目短前缀由用户自拟(2~8 位,全局唯一);任务号 = 项目内自增整数,展示 `前缀-序号`;编号由服务端统一分配,离线创建的任务在同步定号前显示临时标识 |
| 初始数据 | 从现网 MariaDB 只读快照导出并转换导入新 PG;0.2.1 期间产生的新数据不回写现网 |
| 端口 | Web/API 13001、PowerSync WebSocket 13002、PG 仅容器内网(调试绑 127.0.0.1:15432) |
| 部署 | 源码上传 baihu `/app/iboard-0.2.1`,服务器上 `docker compose build`;不使用原 CI/CD |
| 现网保护 | `/app/iboard`、宿主 3306 MySQL、3001 端口、原 CI/CD 一律不使用(快照导出仅 SELECT) |
| 磁盘 | 允许在 baihu 执行 docker 悬空镜像/构建缓存的安全清理(磁盘仅剩 9.9G) |
## 2. 目标架构
```
[Web / Tauri WebView2]
React + @powersync/react
本地 WASM SQLite (OPFS 持久化)
│ 离线: 直接读写本地库; 写操作进入 upload queue
│ 联网: 批量 POST 上行到 Express API (按 uuid 幂等 upsert)
▼
[iboard-0.2.1 Express :13001] ── Prisma (adapter-pg) ──┐
+ /api/powersync/token (为登录用户签发同步 JWT) ▼
[PostgreSQL 容器] (wal_level=logical)
▲ 逻辑复制 (publication + replication slot)
[powersync-service :13002] ── Sync Rules: 任务域 6 表 + User 只读引用, 按 ProjectMember 过滤
```
- 写路径:客户端本地写 → upload queue → 上行 API → Prisma 写 PG → 逻辑复制捕获 → 广播各端;
- 读路径:任务模块页面全部改读本地 SQLite(@powersync/react 响应式查询),其余页面维持 axios REST;
- 认证:复用现有 jsonwebtoken 体系,新增 `/api/powersync/token` 签发 PowerSync 校验的 JWT。
## 3. 数据模型改造(原则:只增不改)
采用报告 3.2 中的「新增 uuid 列 + 应用层幂等 upsert」选项,不做全表主键 UUID 化:外键关系全部保留,存量数据迁移风险最小。
- 任务域 6 表各加 `uuid varchar(36) unique`(客户端创建时生成;存量行导入时回填);
- `Project` 加 `code varchar(8) unique`(用户自拟短前缀;存量项目导入时回填自动码,后续可改);
- `Task` 加 `number int`,`@@unique([projectId, number])`(服务端按项目 max+1 分配;离线创建的任务上行时定号,本地未定号行显示临时标识);
- 主键仍为自增 Int,业务外键不动;客户端上行一律以 uuid 引用父子行,服务端解析为内部 id;
- Prisma datasource: `mysql` → `postgresql`;本分支重建基线迁移(旧 MySQL 迁移文件移除,历史保留在 git)。
### 3.1 Sequential ID Mapping(落地时按官方模式补充)
按 PowerSync 官方 [Sequential ID Mapping](https://docs.powersync.com/client-sdks/advanced/sequential-id-mapping) 模式实施,解决"离线新建行同步回填后本地 id 变化"问题:
- 任务域 6 表再加**外键 uuid 镜像列**(`Task.projectUuid/columnUuid/parentUuid`、`BoardColumn.projectUuid`、`ProjectMember.projectUuid`、`TaskMember/TaskTag.taskUuid`),由 **PG 触发器**在写入时按 int 外键自动回填(迁移 `20261026010000_uuid_fk_mapping`),库级 NOT NULL;
- Sync Streams 以 **`uuid` 作为客户端主键**(`SELECT "uuid" AS "id"`),镜像列别名输出回 `projectId/columnId/parentId/taskId` —— 客户端 schema 与页面代码**零改动**;
- 服务端 upload 处理器按 uuid 解析父行(`findUnique({ where: { uuid } })`),解析后再写 int 外键 → 触发器回填镜像,两侧闭环一致;
- 镜像列在 Prisma 模型中声明为可空(触发器保证库级非空),避免 create 校验拦截。
## 4. 同步与冲突
- Sync Rules 仅下发任务域 6 表 + `User`(展示名称/头像用),按 `ProjectMember.userId = auth.parameters.userId` 过滤项目可见性;时序/监控/米家等表不纳入;
- 上行:`POST /api/powersync/upload` 批处理客户端 CRUD 操作,按 uuid 幂等 upsert,更新时以服务端时间戳做 LWW 仲裁;
- 冲突:字段级 LWW 为主;`BoardColumn.sortOrder` / `Task.sortOrder` 由服务端重排仲裁;并发唯一冲突(项目前缀、看板列名)返回明确错误,客户端提示重试;
- `Task.number` / `Project.code` 由服务端唯一分配,多端并发下天然无冲突。
## 5. 部署拓扑(baihu `/app/iboard-0.2.1`)
docker compose 三服务:
| 服务 | 镜像 | 端口 | 说明 |
| --- | --- | --- | --- |
| `pg-021` | postgres:16-alpine | 仅容器内网 | `wal_level=logical`;数据卷 `./data/pg` |
| `powersync-021` | powersync 官方镜像 | 13002 对外 | 连接 PG 逻辑复制 + WebSocket 下发 |
| `app-021` | 本仓库 Dockerfile 构建 | 13001 对外 | `OTA_PUBLIC_BASE=http://bh.vps.honor3:13001` |
- 环境变量由 compose 注入:`DATABASE_URL`(指向 pg-021)、`JWT_SECRET`(0.2.1 独立新设)、`PORT=13001`;
- 0.2.1 容器内 cron/米家调度预计无凭证或产生重复外呼,部署时以环境开关关闭(实现于 M4)。
## 6. 数据迁移(一次性)
1. 在 baihu 上用仓库自带 `mariadb` 驱动以只读 SQL 从宿主 MySQL(3306) 导出全表 JSON(仅 SELECT);
2. 转换:回填 uuid / `Project.code`(按名称自动生成,可后改)/ `Task.number`(按项目内 createdAt 顺序);时间字段转 PG 时间戳;
3. PG 侧先 `prisma migrate deploy` 建基线表,再执行 ETL 写入;
4. 校验:逐表行数比对 + 抽样字段比对,输出核对报告。
## 7. 里程碑
1. **M1** schema/适配层切 PG(本提交):schema 字段、db.js 适配器、基线迁移重建、本地 `prisma generate` 通过;
2. **M2** 服务端:powersync token 端点、upload 幂等上行端点、任务域路由幂等化与编号分配逻辑;
3. **M3** 前端:任务模块数据层切 PowerSync(本地库 + useQuery + 上传队列),桌面端联调;
4. **M4** compose 编排 + ETL 导数 + baihu 部署,端口/现网隔离验证;
5. **M5** 离线-联网闭环验证(断网编辑 → 恢复自动同步 → 多端一致),Android 真机评估。
## 8. 开放问题 / 风险
- powersync-service 的元数据存储(bucket storage)对 MongoDB 的依赖需在部署阶段核实;若当前版本不支持用 PG 存元数据,则加一个 mongo 容器;
- Android System WebView 的 OPFS/WASM SQLite 兼容性(M5 评估,先桌面);
- 项目前缀全局唯一在多端并发下的冲突提示体验(上行返回结构化错误码);
- 0.2.1 验证期数据与现网彻底隔离,不回迁;若验证后需并回主线,另行决策迁移口径。
@@ -0,0 +1,127 @@
# 基于 PowerSync 的离线编辑可行性研究报告
| 项 | 内容 |
| --- | --- |
| 版本 | V0.2 |
| 日期 | 2026-10-25 |
| 评估对象 | PowerSync(powersync.com,开源 self-hosted + 云服务) |
| 评估目标 | 项目离线操作(本地编辑)+ 联网后自动同步 |
| 结论(TL;DR) | **技术上可行,但落地成本高、风险中高。不建议全量接入;若确需离线,建议仅对「打卡 / 清单 / 任务」等用户编辑域做局部接入,且需要先解决 MariaDB 兼容与主键策略两个前置问题。** |
---
## 1. 项目现状约束(评估输入)
结合代码库(`package.json`、`prisma/schema.prisma`、`server/index.js`)梳理:
1. **前端**:React 18 SPA(antd-mobile),通过 axios 调 `/api/*` REST 接口;`src/api`、`src/hooks`、`src/pages` 均以"请求 → state → 渲染"模式组织,无任何本地持久化数据层。
2. **桌面/移动端**:Tauri 2(Windows WebView2 桌面 + Android System WebView),意味着前端运行在 WebView 内,浏览器类 SDK 是候选。
3. **后端**:Express + Prisma,数据库为 **MariaDB / MySQL**(`provider = "mysql"`)。
4. **认证**:自签 JWT(jsonwebtoken),登录接口在 `/api/auth`。
5. **写路径**:全部业务写操作走 REST(打卡、清单、任务模块);同时存在大量**服务端定时写入**(cron:新闻、指标、监控、米家采样等,见 `server/cron.js`)。
6. **数据模型**:约 25 张表,主键全部为 `Int @id @default(autoincrement())` 自增整数,多表带复合唯一约束(如 `CheckIn @@unique([habitId, date, seq])`)。
7. **数据体量特征**:两类数据并存——
- **用户编辑域**(Habit / CheckIn / CheckList* / 任务模块 10 张表):冲突敏感、离线价值高;
- **服务端只读时序域**(News / Indicator / ServerNodeHistory / MihomeEnv* 等):append-only、量大、离线价值中等(缓存即可满足)。
---
## 2. PowerSync 是什么、怎么工作
- 架构:客户端本地 **SQLite**(Web 端为 WASM SQLite,存 OPFS/IndexedDB)↔ **PowerSync Service**(self-hosted 开源组件或云服务)↔ 源数据库。服务端通过 **逻辑复制/变更流** 捕获源库写入,按 **Sync Rules** 定义的下推子集同步到每个客户端的本地 SQLite。
- 写路径(官方推荐模式):客户端离线写本地 SQLite → 进入 **upload queue** → 联网后由前端把变更批量 POST 到**现有后端 API** → 后端用原有方式(本项目即 Prisma)写库 → 变更流捕获 → 广播回各端。
- 认证:客户端持后端签发的 JWT 连接同步服务;PowerSync 只做校验,不签发(可复用现有 jsonwebtoken 体系)。
- 服务端直写的表(cron 产生)也能被变更流捕获并下推,不需要改造写入方。
参考:[PowerSync Service(GitHub)](https://github.com/powersync-ja/powersync-service)、[源数据库支持](https://docs.powersync.com/configuration/source-db/setup)。
---
## 3. 逐项可行性分析
### 3.1 数据库兼容性 —— ⚠️ 主要风险点
- PowerSync 对源库支持:**PostgreSQL(GA)**、MongoDB、SQL Server;**MySQL 仍处于 Beta**(基于 binlog 复制,见 [MySQL Beta 公告](https://releases.powersync.com/announcements/mysql-backend-database-support-has-reached-beta)、[Alpha 公告](https://releases.powersync.com/announcements/announcing-mysql-as-a-source-database-alpha))。
- 本项目用的是 **MariaDB**。MariaDB 的 binlog 格式与 MySQL 8 有差异(GTID、行事件细节),PowerSync MySQL 插件(@powersync/mysql-zongji 系)对 MariaDB **没有官方兼容承诺,需要实测验证**,很可能踩坑。
- **结论**:要么实测 MariaDB + MySQL-beta 通路(风险高),要么把数据库迁到 PostgreSQL(PowerSync 最成熟路径,Prisma 迁移成本可控但涉及全部表和线上数据)。这是"能不能用"的第一道门槛。
### 3.2 数据模型适配 —— 改造成本大
- PowerSync Sync Rules 要求同步表有稳定的行标识,**强烈建议 UUID 文本主键**;而本项目全部是自增整数主键,且客户端离线创建记录时无法预知自增 id。
- 离线创建的通用解法:主键改为客户端生成的 UUID(`char(36)`),或新增 `uuid` 列 + 应用层幂等 upsert。对用户编辑域的 10 余张表意味着**一次不小的 schema 迁移 + 全部相关路由改造**(迁移原则"只增不改"仍可遵守,但外键列类型变更工作量大)。
- `DateTime`、`Json` 类型可映射(客户端以 ISO 文本/JSON 文本存储),但 `BigInt`(监控表)在 JS 端需按文本处理,注意精度。
- 复合唯一约束(`CheckIn(habitId,date,seq)`、`BoardColumn(projectId,name)` 等)在离线多端并发下会冲突,需在 upload 处理端路由里做服务端仲裁(如 seq 重排、重名加后缀),现有路由大多不具备幂等/仲裁语义。
### 3.3 写路径(离线编辑 + 上行同步)—— 模式契合,但需逐路由改造
- PowerSync 的 upload-queue → 后端 API 模式与现有 Express 架构**天然契合**:不需要后端暴露直接写库的通口,各业务路由补成"接收客户端 UUID + 幂等 upsert"即可。
- 打卡/清单/任务的路由数量约 3 个 router 文件(checkin / checklists / projects / tasks),逐个改造为幂等是可控的。
- 服务端 cron 写入的表(新闻/指标/监控/米家)无需改造写入方,只需在 Sync Rules 里定义是否下推。
### 3.4 读路径与前端数据层 —— 全面前重写
这是**工作量最大**的部分:
- 现有 `src/api` + axios + useEffect 模式必须替换为 PowerSync 本地 SQLite 查询 + `@powersync/react` 的响应式 hooks(useQuery 等),否则离线读不到数据。
- 涉及 `src/pages` 下全部用户编辑域页面(checkin、task),以及若要离线浏览还需覆盖 news/macro/monitor 的只读缓存页。
- 工作量量级:相当于把前端数据层重构一遍;可与"离线只读缓存(HTTP 缓存)"分层实施以降低风险。
### 3.5 客户端运行环境 —— 大体可行,Android 需验证
- **浏览器 / Tauri 桌面(WebView2)**:PowerSync Web SDK(WASM SQLite + OPFS)在 Chromium 系 WebView 已被官方支持,可行。
- **Tauri Android(System WebView)**:WASM SQLite 性能与 OPFS 在部分国产/旧版 WebView 上存在兼容性风险,需真机验证;备选方案是封装原生 SQLite(官方 Capacitor 插件思路可借鉴,但 Tauri 无官方插件,需自研 Rust 命令桥接,成本额外增加)。
- 时序大表(ServerNodeHistory 每分钟一条、MihomeEnv*)不建议纳入同步——Sync Rules 需做时间窗过滤(如仅近 7 天),否则客户端存储和同步流量都会失控。
### 3.6 认证与部署 —— 可行
- JWT 可复用:新增一个 `/api/powersync/token` 端点为已登录用户签发 PowerSync 接受的 token,改造小。
- 部署:self-hosted 需在现有 Express 之外多跑一个 powersync-service 进程(含 Mongo 存储元数据或云实例),并暴露 WebSocket 端口;或使用 PowerSync Cloud。运维面增加一个组件。
### 3.7 冲突解决
- PowerSync 本身不做自动合并,只提供上传队列与冲突回调;对"最后写入胜出"友好的字段(清单勾选、任务标题/状态)默认策略够用;对 `CheckIn.seq`、`BoardColumn.sortOrder` 这类"位置/序号型"数据需要自定义仲裁逻辑(上传端或同步流中处理),设计上可解,但要专门做。
---
## 4. 风险与成本汇总
| # | 风险/成本项 | 等级 | 说明 |
| --- | --- | --- | --- |
| 1 | MariaDB 兼容性未验证,MySQL 支持仅 Beta | 高 | 很可能需要迁移 PostgreSQL |
| 2 | 前端数据层全面重构(axios → 本地 SQLite 查询) | 高 | 涉及所有相关页面 |
| 3 | 用户编辑域主键整数自增 → UUID 迁移 | 中高 | schema + 路由幂等改造 |
| 4 | 复合唯一约束的离线冲突仲裁 | 中 | CheckIn.seq、列名、排序等 |
| 5 | Android WebView WASM SQLite/OPFS 兼容 | 中 | 需真机验证 |
| 6 | 新增 self-hosted 组件的运维 | 中 | 多一个常驻服务 |
| 7 | 时序大表同步范围控制 | 低 | Sync Rules 时间窗过滤即可 |
对照收益:真正需要离线**编辑**的只有打卡 / 清单 / 任务三个域(数据量小、单用户为主、冲突场景有限);其余域离线**只读缓存**即可满足。
---
## 5. 结论与建议
### 5.1 结论
**可行,但不建议以"全项目接入 PowerSync"的方式落地。** PowerSync 的同步模型与项目的 Express API 写路径契合、认证可复用、桌面端可行;但 MariaDB/MySQL-beta 兼容、整数自增主键、前端数据层全面重构三座大山使总成本明显高于当前阶段的收益。
### 5.2 建议(按优先序)
1. **首选(低成本方案)**:不引入 PowerSync,为用户编辑域实现轻量离线队列——前端把写请求落到 localStorage/IndexedDB 待发队列(含客户端生成的 UUID),联网后重放到现有 REST API(路由补幂等 upsert);只读页用 HTTP 缓存 + SW。可覆盖约 80% 的离线诉求,改动局限在 `src/api` 层与少量路由。
2. **若决定采用 PowerSync**:
- 前置 1:源库迁移 PostgreSQL(避开 MySQL Beta + MariaDB 兼容雷区);
- 前置 2:仅将用户编辑域(Habit/CheckIn/CheckList*/任务模块)纳入 Sync Rules,时序与监控表排除或做时间窗过滤;
- 前置 3:这些表的主键/唯一标识改造为客户端可生成的 UUID;
- 分阶段:先桌面端(WebView2)验证全链路,再攻 Android WebView 兼容性;
- 冲突策略:字段级 LWW 为主,seq/sortOrder 类服务端仲裁。
3. **不建议**:为离线只读域(新闻/指标/监控)上 PowerSync——用简单缓存即可,同步引擎属于过度设计。
---
## 6. 参考资料
- [PowerSync Service(GitHub,self-hosted 开源)](https://github.com/powersync-ja/powersync-service)
- [PowerSync 源数据库配置文档](https://docs.powersync.com/configuration/source-db/setup)
- [MySQL 后端支持 Beta 公告](https://releases.powersync.com/announcements/mysql-backend-database-support-has-reached-beta)
- [MySQL 后端支持 Alpha 公告](https://releases.powersync.com/announcements/announcing-mysql-as-a-source-database-alpha)
- 本项目:`prisma/schema.prisma`、`server/index.js`、`docs/V0.2/数据库设计文档.md`、`docs/V0.2/任务模块/任务模块数据库设计文档.md`
@@ -0,0 +1,312 @@
# 任务模块「即时同步」方案
> 状态:**方案设计,未实施**(本文只描述做法,不含代码改动)
> 范围:任务模块(Project / BoardColumn / Task / TaskMember / TaskTag)
> 相关文档:[0.2.1 实施方案](./0.2.1实施方案.md)、[PowerSync 可行性研究报告](./PowerSync可行性研究报告.md)
> 编写时间:2026-10-27
---
## 1. 问题与现状
### 1.1 现象
多端(Windows 外壳 / Android / Web)同时打开同一项目时:
- A 端点完成或改状态,**B 端不会自动更新**;
- 必须**切到别的页面再切回来**(组件重新挂载触发重新查询)才能看到;
- 看板分组(按 `columnId`)与详情状态(按 `status`)也会出现一段时间的显示不一致。
### 1.2 根因:不是"同步没到",而是"到了没人通知界面"
PowerSync 的下行链路本身是**推送式**的(服务端 PG 逻辑复制 → sync service → WebSocket →
客户端写入本地 SQLite),延迟通常在亚秒级。问题出在**消费端**:
| 层 | 现状 | 结论 |
| --- | --- | --- |
| 传输层 | `PowerSyncProvider` 已在 `src/App.jsx` 挂载,连接正常 | ✅ 正常 |
| 数据层 | 变化已写入本地 SQLite | ✅ 正常 |
| **UI 层** | **全部是命令式读取**:`db().getAll(...)`,读完就不再看 | ❌ **断点在这** |
具体地:
- `src/api/task.js` 的 `projectApi.list/detail`、`taskApi.list` 都是一次性 `getAll()`,
且在 JS 里做二次装配(子任务、成员、标签、列),**没有任何订阅**;
- `src/lib/powersync/react.jsx` 虽然 re-export 了 `useQuery/useStatus`,
但**全项目一次都没用过**(`grep useQuery src` 只命中该文件的导出语句);
- `src/pages/task/TaskPage.jsx`、`ProjectView.jsx` 只在 `useEffect(..., [load])` 里加载一次;
- 唯一的刷新机制是 `src/hooks/useRefreshOnVisible.js`(回前台/窗口聚焦时重查),
而且**任务模块这两个页面根本没有用它**(只有打卡 `MergedView/ListView` 与宏观各页在用)。
所以「切面板能刷新」只是因为它**重新挂载组件 → 重新执行一次 `load()`**。
### 1.3 现状刷新机制盘点
| 页面 | 首次加载 | 回前台刷新 | 数据变化实时反映 |
| --- | --- | --- | --- |
| `TaskPage` | `useEffect` | ❌ 无 | ❌ |
| `ProjectView` | `useEffect` | ❌ 无 | ❌(仅本地操作后手动 `load()`) |
| `TaskDetail` | 打开时传入快照 | — | ❌(快照可能过期) |
| `MergedView`(打卡) | `useEffect` | ✅ | ❌(且打卡走 REST,非 PowerSync) |
---
## 2. 目标与非目标
### 2.1 目标
1. **跨端即时**:一端修改,其它端在 **≤1s** 内自动呈现(不含网络中断场景);
2. **不打断操作**:正在输入的详情表单、正在拖拽的卡片**不被远端变更打断**;
3. **离线可用**:断网期间照常读写本地库,恢复后自动补齐并刷新;
4. **改动可控**:优先复用现有装配逻辑(`taskApi` 的 shape),不做大规模重写;
5. **可回滚**:通过开关或逐步替换,出问题能退回"手动刷新"。
### 2.2 非目标
- 不做**多人协同编辑**(同一字段并发写仍按 LWW 后到覆盖);
- 不解决打卡/清单模块的实时性(它们走 REST,见 §7);
- 不引入 WebSocket 之外的额外实时通道(如 SSE)。
---
## 3. 方案选型
| 方案 | 做法 | 优点 | 缺点 | 结论 |
| --- | --- | --- | --- | --- |
| **A. 轮询** | 定时(如 3s)重跑 `load()` | 改动最小 | 无谓查询、电量/流量、仍不"即时" | ❌ |
| **B. 全量响应式** | 把 `taskApi.list` 整体改成一条大 SQL + `useQuery` | 最"正统",细粒度 | 现有装配(子任务/成员/标签)要在 SQL 里重写;改动面大、回归风险高 | ⏳ 后续 |
| **C. 变更信号 + 重算**(推荐) | 用 `db.watch()` 订阅一个**轻量"信号查询"**,信号变化时重跑现有 `load()` | 复用现有装配逻辑;改动集中且小;天然合并多次变更 | 每次重算一次列表(数据量小,可接受) | ✅ **P0 采用** |
| **D. 表级广播** | 监听"某表有变更"事件触发重查 | 实现最简单 | 无法区分项目/范围,易全页面重查 | 作为 C 的退化实现 |
**结论**:P0 用方案 C;跑稳后再评估把高频视图(看板)迁到 B 的增量查询。
---
## 4. 推荐方案详设(方案 C)
### 4.1 核心思路
```
本地 SQLite ←(PowerSync 下行写入)— 任一端/服务端变更
│
└─▶ db.watch(<信号查询>) ── 变化 ──▶ 节流合并 ──▶ 重跑 load()
└─▶ setState 渲染
```
**"信号查询"** 是一段**廉价但能覆盖所有相关表**的 SQL:只要任务域的任一相关行变化,
其结果就会变;它不承担业务语义,只负责触发。
### 4.2 订阅范围:用 SDK 的表级过滤(已实施)
> 初版设计是手写一段"信号 SQL"(count / sum / max(updatedAt) 汇总),
> 实施时发现 `db.watch()` 原生支持 **`tables` 表名过滤** 与 **`throttleMs` 节流**,
> 由 SDK 判断"结果是否真的变了",比自己做汇总更可靠,因此改为表级过滤。
`CommonPowerSyncDatabase.watch(sql, params, handler, options)` 的 `options`:
| 选项 | 用途 |
| --- | --- |
| `tables: string[]` | 只关心这些表的变更,SDK 内部按表失效 |
| `throttleMs` | 两次回调之间的最小间隔,天然合并同一批同步写入 |
| `signal: AbortSignal` | 卸载时取消订阅 |
订阅内容(`src/lib/powersync/useDbSignal.js`):
```js
db.watch('SELECT 1 AS s', [], { onResult, onError }, {
tables: ['Task', 'BoardColumn', 'TaskMember', 'TaskTag'],
throttleMs: 250,
signal: ac.signal,
})
```
查询本身(`SELECT 1`)没有业务含义,只借 watch 的变更通知能力。表名集合定义在
`src/api/task.js`:
| 常量 | 内容 | 用于 |
| --- | --- | --- |
| `TASK_TABLES` | `Task, BoardColumn, TaskMember, TaskTag` | 项目内任务列表 / 看板 |
| `PROJECT_TABLES` | `Project, ProjectMember` | 项目列表、成员 |
之所以不需要 `max(updatedAt)`:表级过滤关心的是"行有没有被写过",
而不是"结果集内容是否变化",所以改标题、改状态这类不增删的写入同样会触发。
### 4.3 UI 层接入
提供两个小工具,避免每个页面各写一套:
| 工具 | 位置(建议) | 作用 |
| --- | --- | --- |
| `useDbSignal(sql, params, onChange)` | `src/lib/powersync/useDbSignal.js`(新增) | 封装 `db.watch()` + 节流 + 卸载清理 |
| `useTaskListSignal(projectId, onChange)` | `src/api/task.js` 或同目录(新增) | 拼好上述信号 SQL,页面只关心"变了要干嘛" |
接入示意(`ProjectView`):
```jsx
// 现状
useEffect(() => { setTasks(null); load() }, [load])
// 改造后(示意)
useEffect(() => { setTasks(null); load() }, [load])
useDbSignal(TASK_LIST_SIGNAL_SQL, [projectId, projectId, projectId, projectId, projectId, projectId], () => {
if (dragActiveRef.current) return // 见 §4.4
load()
})
```
`TaskPage` 同理,信号为项目表。
### 4.4 变更合并与"不打断"策略
这是本方案的关键,否则会出现「打字打到一半表单被重置」「拖到一半卡片跳走」。
| 场景 | 策略 |
| --- | --- |
| 连续多次变更(一次同步写多行) | `useDbSignal` 内部 **150~300ms 防抖**,合并成一次 `load()` |
| 看板/列表**拖拽进行中** | 用 `dragActiveRef` 跳过本次刷新;拖拽结束后补一次 `load()` |
| 任务**详情页打开中** | 不自动刷新表单;改为监听该任务是否变化,变化时在详情顶部显示「**该任务已在他端更新,点击刷新**」 |
| 正在编辑标题/描述 | 同上传:不覆盖输入框,只提示 |
| 本地乐观更新进行中(`updateTask` 未完成) | 以本地 patch 为准,`load()` 延后到请求结束(现有 `updateTask` 已有 snapshot 回滚,保留) |
### 4.5 同步状态指示
用已导出的 `useStatus()` 在任务页顶部显示一个极轻量的状态点:
| 状态 | 展示 |
| --- | --- |
| `connected && dataFlowStatus.connected` | 不显示(无干扰) |
| 正在下载/上传 | 细小的同步图标 |
| 离线 / 连接失败 | 「离线,改动将在联网后同步」 |
> 说明:当前外壳里 PowerSync 会打 `Multiple tab support is not enabled` 的提示,
> 属预期(已在 0.2.1 关闭多标签以避免 Windows reload 后本地库挂起),不影响本方案。
### 4.6 离线与重连
- 断网:`db.watch()` 仍监听**本地库**,本地写入照常触发刷新(体验一致);
- 恢复:PowerSync 自动重连并回放,写入本地库后信号触发刷新,**无需额外逻辑**;
- 应用回到前台:保留 `useRefreshOnVisible` 作为**兜底**(WebView 可能挂起渲染线程),
与 `db.watch()` 并存不冲突(两者都只是调 `load()`)。
---
## 5. 改造清单(按文件)
| 文件 | 改动 | 状态 |
| --- | --- | --- |
| `src/lib/powersync/useDbSignal.js` | **新增**:`db.watch` + 表过滤 + 节流 + 卸载清理 | ✅ P0 已完成 |
| `src/api/task.js` | 新增 `TASK_TABLES` / `PROJECT_TABLES` 常量 | ✅ P0 |
| `src/pages/task/ProjectView.jsx` | 订阅任务域;拖拽中延后刷新;回前台兜底 | ✅ P0 |
| `src/pages/task/TaskPage.jsx` | 订阅项目域;回前台兜底 | ✅ P0 |
| `src/pages/task/BoardView.jsx` / `ListView.jsx` | 上报 `onDragActiveChange` | ✅ P0 |
| `src/pages/task/TaskDetail.jsx` | 「已在他端更新」提示(不覆盖表单) | ⏳ P1 |
| 同步状态指示 | 用 `useStatus()` 显示离线/同步中 | ⏳ P1 |
**不需要改动**:`server/`、`sync-config.yaml`、数据库结构。
---
## 6. 分期与验收
### 6.1 分期
| 阶段 | 内容 | 预估 |
| --- | --- | --- |
| **P0** | `useDbSignal` + `ProjectView`/`TaskPage` 接入 + 拖拽保护 | 0.5~1 天 |
| **P1** | 详情页「已在他端更新」提示 + 同步状态指示 | 0.5 天 |
| **P2** | 看板改增量查询(方案 B);打卡/清单的实时性另议 | 按需 |
### 6.2 验收标准(可测)
1. **跨端延迟**:A 端改状态/拖列/改标题,B 端自动更新。实测 **约 1~2s**(数据进本地库约 1s,渲染 +0.3s,见 §7.0);
2. **不打断**:B 端正在输入标题时,A 端改同任务描述 → B 端输入框内容**不被清空**,仅出现提示;
3. **拖拽安全**:B 端拖拽中收到远端变更 → 卡片**不跳位**,松手后列表更新;
4. **离线**:断网期间本地操作正常,恢复后自动补齐并刷新;
5. **回滚**:`localStorage['iboard_realtime'] = '0'` 后重启应用,退回"手动刷新",无副作用;
6. **性能**:单项目 100 条任务、连续 20 次远端写入,页面无卡顿、无重复渲染风暴(可用 Performance 面板核对)。
### 6.3 度量
- 在 `load()` 内打点(可临时 `console.time`),统计重算耗时与触发次数;
- 目标:一次远端变更只触发 **1 次** 重算;100 条任务时单次重算 < 60ms。
---
## 7. 已知边界与后续
### 7.0 实测:端到端约 1~2s(达标)
P0 落地后在真实 Windows 外壳上实测,服务端直接改库模拟另一端:
| 项 | 结果 |
| --- | --- |
| 是否还需要切页面 | **不需要** —— 服务端改完,页面在无任何操作的情况下自动更新 ✅ |
| 端到端延迟 | **约 1~2s**(数据进本地库约 1s,渲染再 +0.3s)✅ |
**测量方法(关键:早期结论曾被测量方式误导两次,务必按此方法复测)**
测量必须在**页面内**自计时,不能用 CDP 轮询 DOM:
```js
// 页面内: 50ms 轮询本地库 + MutationObserver 观察 DOM, 各自记录 performance.now()
const P = { startedAt: performance.now(), dbAt: null, domAt: null }
setInterval(async () => {
if (P.dbAt !== null) return
const row = await window.__psdb.get('SELECT title FROM Task WHERE number = ?', [16])
if (row && row.title.includes(MARK)) P.dbAt = performance.now() - P.startedAt
}, 50)
new MutationObserver(check).observe(document.body, { childList: true, subtree: true })
```
一次干净测量的结果(已重载页面、无残留定时器):
| 度量 | 数值 |
| --- | --- |
| `psql` 命令总耗时(ssh + docker exec + UPDATE) | 2138ms |
| 数据出现在客户端本地库(相对计时起点) | 2251ms → **≈ 命令返回后 113ms** |
| 界面更新(相对计时起点) | 2580ms |
| **同步数据到达 → 渲染完成** | **329ms** |
结论:**服务端改库到界面呈现约 1~2s(含 SSH/psql 开销在内的保守上界),其中 UI 渲染只占 ~0.33s**,
`useDbSignal` 的表级 watch 工作正常,无需改 `useQuery`。
**两次被推翻的中间结论(留档,避免重复踩坑)**
| 曾经的判断 | 为何是错的 |
| --- | --- |
| ① "nginx `proxy_buffering` 导致按批下发" | 加了 `proxy_buffering off` 等配置并热重载(`nginx -T` 确认生效),延迟毫无变化。**该配置已回滚**(`advanced_config` 清空、生效 conf 还原、nginx 已重载)—— 保留它反而会让 OTA 大包下载失去缓冲保护 |
| ② "同步数据早在服务端发出,客户端 `db.watch`/渲染滞后 15~30s" | 错在测量:用 CDP 反复读 `document.body.innerText`(长列表页上开销很大)轮询,且把 `ssh + docker exec` 的 ~6s 开销算进了延迟。改为页面内自计时后,实测只有 ~1~2s |
**服务端侧的旁证**(与上述一致):powersync 日志 `checkpoint_complete` 与改库同一秒完成;
`stream_ms` 达 42 分钟、`close_reason: client closing stream`,长连接稳定,非频繁重连。
| 项 | 说明 |
| --- | --- |
| 打卡 / 清单 | 走 REST(`src/api/checkin.js`),不经过本地库,**本项目方案不覆盖**;如需实时,可复用现有 `useRefreshOnVisible` + 定时轮询,或后续把这些模块也迁到 PowerSync |
| 并发写冲突 | 仍为 LWW(`updatedAt` 后到覆盖);本文不引入人工合并 |
| 排序并发 | `taskApi.reorder` 会整批重写 `sortOrder`,两端同时拖拽可能相互覆盖;建议 P2 再引入"版本号/操作序列" |
| 大列表 | 方案 C 每次全量重算,任务数极大(>1000)时需转方案 B 的增量查询 |
| WebView 后台节流 | 应用在后台时 `watch` 回调可能被延后,回前台由 `useRefreshOnVisible` 兜底 |
---
## 8. 附:关键 API 备忘
| API | 来源 | 用途 |
| --- | --- | --- |
| `db.watch(sql, params, options)` | `@powersync/common` `CommonPowerSyncDatabase` | 订阅查询结果,返回 `AsyncIterable` |
| `useQuery(sql, params, options)` | `@powersync/react` | React 版订阅;`options.rowComparator` 可做**增量**更新(P2 用) |
| `useStatus()` | `@powersync/react` | 连接/同步状态 |
| `getPowersyncDb()` | `src/lib/powersync/db.js` | 本地库单例(当前唯一入口) |
> 注:`src/lib/powersync/react.jsx` 已 re-export `useQuery/useStatus/usePowerSync`,
> 本次改造**无需新增依赖**。
---
## 9. 结论
当前"必须切面板才刷新"**不是同步没到,而是 UI 从未订阅本地库变化**。
采用「**变更信号 + 复用现有装配重算**」(方案 C)可在改动面很小的情况下获得亚秒级跨端同步,
并把"不打断编辑/拖拽"作为硬性要求一并纳入。P0 约半天到一天即可上线验证。
+30 -20
View File
@@ -27,21 +27,6 @@ export type User = Prisma.UserModel
*
*/
export type Article = Prisma.ArticleModel
/**
* Model Strategy
*
*/
export type Strategy = Prisma.StrategyModel
/**
* Model Indicator
*
*/
export type Indicator = Prisma.IndicatorModel
/**
* Model IndicatorPoint
*
*/
export type IndicatorPoint = Prisma.IndicatorPointModel
/**
* Model News
*
@@ -77,11 +62,6 @@ export type Econ_SowInventory = Prisma.Econ_SowInventoryModel
*
*/
export type ScraperCache = Prisma.ScraperCacheModel
/**
* Model OilPriceHistory
*
*/
export type OilPriceHistory = Prisma.OilPriceHistoryModel
/**
* Model MihomeAccount
*
@@ -132,3 +112,33 @@ export type CheckListItem = Prisma.CheckListItemModel
*
*/
export type CheckListTemplate = Prisma.CheckListTemplateModel
/**
* Model Project
*
*/
export type Project = Prisma.ProjectModel
/**
* Model ProjectMember
*
*/
export type ProjectMember = Prisma.ProjectMemberModel
/**
* Model BoardColumn
*
*/
export type BoardColumn = Prisma.BoardColumnModel
/**
* Model Task
*
*/
export type Task = Prisma.TaskModel
/**
* Model TaskMember
*
*/
export type TaskMember = Prisma.TaskMemberModel
/**
* Model TaskTag
*
*/
export type TaskTag = Prisma.TaskTagModel
+30 -20
View File
@@ -51,21 +51,6 @@ export type User = Prisma.UserModel
*
*/
export type Article = Prisma.ArticleModel
/**
* Model Strategy
*
*/
export type Strategy = Prisma.StrategyModel
/**
* Model Indicator
*
*/
export type Indicator = Prisma.IndicatorModel
/**
* Model IndicatorPoint
*
*/
export type IndicatorPoint = Prisma.IndicatorPointModel
/**
* Model News
*
@@ -101,11 +86,6 @@ export type Econ_SowInventory = Prisma.Econ_SowInventoryModel
*
*/
export type ScraperCache = Prisma.ScraperCacheModel
/**
* Model OilPriceHistory
*
*/
export type OilPriceHistory = Prisma.OilPriceHistoryModel
/**
* Model MihomeAccount
*
@@ -156,3 +136,33 @@ export type CheckListItem = Prisma.CheckListItemModel
*
*/
export type CheckListTemplate = Prisma.CheckListTemplateModel
/**
* Model Project
*
*/
export type Project = Prisma.ProjectModel
/**
* Model ProjectMember
*
*/
export type ProjectMember = Prisma.ProjectMemberModel
/**
* Model BoardColumn
*
*/
export type BoardColumn = Prisma.BoardColumnModel
/**
* Model Task
*
*/
export type Task = Prisma.TaskModel
/**
* Model TaskMember
*
*/
export type TaskMember = Prisma.TaskMemberModel
/**
* Model TaskTag
*
*/
export type TaskTag = Prisma.TaskTagModel
+248 -252
View File
@@ -16,8 +16,8 @@ import type * as Prisma from "./internal/prismaNamespace.ts"
export type IntFilter<$PrismaModel = never> = {
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
in?: number[]
notIn?: number[]
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
@@ -27,8 +27,8 @@ export type IntFilter<$PrismaModel = never> = {
export type StringFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
in?: string[]
notIn?: string[]
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
@@ -36,14 +36,14 @@ export type StringFilter<$PrismaModel = never> = {
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
search?: string
mode?: Prisma.QueryMode
not?: Prisma.NestedStringFilter<$PrismaModel> | string
}
export type DateTimeFilter<$PrismaModel = never> = {
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
in?: Date[] | string[]
notIn?: Date[] | string[]
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
@@ -53,8 +53,8 @@ export type DateTimeFilter<$PrismaModel = never> = {
export type IntWithAggregatesFilter<$PrismaModel = never> = {
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
in?: number[]
notIn?: number[]
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
@@ -69,8 +69,8 @@ export type IntWithAggregatesFilter<$PrismaModel = never> = {
export type StringWithAggregatesFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
in?: string[]
notIn?: string[]
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
@@ -78,7 +78,7 @@ export type StringWithAggregatesFilter<$PrismaModel = never> = {
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
search?: string
mode?: Prisma.QueryMode
not?: Prisma.NestedStringWithAggregatesFilter<$PrismaModel> | string
_count?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedStringFilter<$PrismaModel>
@@ -87,8 +87,8 @@ export type StringWithAggregatesFilter<$PrismaModel = never> = {
export type DateTimeWithAggregatesFilter<$PrismaModel = never> = {
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
in?: Date[] | string[]
notIn?: Date[] | string[]
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
@@ -99,74 +99,48 @@ export type DateTimeWithAggregatesFilter<$PrismaModel = never> = {
_max?: Prisma.NestedDateTimeFilter<$PrismaModel>
}
export type JsonFilter<$PrismaModel = never> =
| Prisma.PatchUndefined<
Prisma.Either<Required<JsonFilterBase<$PrismaModel>>, Exclude<keyof Required<JsonFilterBase<$PrismaModel>>, 'path'>>,
Required<JsonFilterBase<$PrismaModel>>
>
| Prisma.OptionalFlat<Omit<Required<JsonFilterBase<$PrismaModel>>, 'path'>>
export type JsonFilterBase<$PrismaModel = never> = {
equals?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
path?: string
mode?: Prisma.QueryMode | Prisma.EnumQueryModeFieldRefInput<$PrismaModel>
string_contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
string_starts_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
string_ends_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
array_starts_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
array_ends_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
array_contains?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
lt?: runtime.InputJsonValue
lte?: runtime.InputJsonValue
gt?: runtime.InputJsonValue
gte?: runtime.InputJsonValue
not?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
export type StringNullableFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
mode?: Prisma.QueryMode
not?: Prisma.NestedStringNullableFilter<$PrismaModel> | string | null
}
export type JsonWithAggregatesFilter<$PrismaModel = never> =
| Prisma.PatchUndefined<
Prisma.Either<Required<JsonWithAggregatesFilterBase<$PrismaModel>>, Exclude<keyof Required<JsonWithAggregatesFilterBase<$PrismaModel>>, 'path'>>,
Required<JsonWithAggregatesFilterBase<$PrismaModel>>
>
| Prisma.OptionalFlat<Omit<Required<JsonWithAggregatesFilterBase<$PrismaModel>>, 'path'>>
export type JsonWithAggregatesFilterBase<$PrismaModel = never> = {
equals?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
path?: string
mode?: Prisma.QueryMode | Prisma.EnumQueryModeFieldRefInput<$PrismaModel>
string_contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
string_starts_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
string_ends_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
array_starts_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
array_ends_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
array_contains?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
lt?: runtime.InputJsonValue
lte?: runtime.InputJsonValue
gt?: runtime.InputJsonValue
gte?: runtime.InputJsonValue
not?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
_count?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedJsonFilter<$PrismaModel>
_max?: Prisma.NestedJsonFilter<$PrismaModel>
export type SortOrderInput = {
sort: Prisma.SortOrder
nulls?: Prisma.NullsOrder
}
export type BoolFilter<$PrismaModel = never> = {
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>
not?: Prisma.NestedBoolFilter<$PrismaModel> | boolean
}
export type BoolWithAggregatesFilter<$PrismaModel = never> = {
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>
not?: Prisma.NestedBoolWithAggregatesFilter<$PrismaModel> | boolean
_count?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedBoolFilter<$PrismaModel>
_max?: Prisma.NestedBoolFilter<$PrismaModel>
export type StringNullableWithAggregatesFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
mode?: Prisma.QueryMode
not?: Prisma.NestedStringNullableWithAggregatesFilter<$PrismaModel> | string | null
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
_min?: Prisma.NestedStringNullableFilter<$PrismaModel>
_max?: Prisma.NestedStringNullableFilter<$PrismaModel>
}
export type FloatFilter<$PrismaModel = never> = {
equals?: number | Prisma.FloatFieldRefInput<$PrismaModel>
in?: number[]
notIn?: number[]
in?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel>
notIn?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel>
lt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
lte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
gt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
@@ -176,8 +150,8 @@ export type FloatFilter<$PrismaModel = never> = {
export type FloatWithAggregatesFilter<$PrismaModel = never> = {
equals?: number | Prisma.FloatFieldRefInput<$PrismaModel>
in?: number[]
notIn?: number[]
in?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel>
notIn?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel>
lt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
lte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
gt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
@@ -190,48 +164,10 @@ export type FloatWithAggregatesFilter<$PrismaModel = never> = {
_max?: Prisma.NestedFloatFilter<$PrismaModel>
}
export type StringNullableFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
in?: string[] | null
notIn?: string[] | null
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
search?: string
not?: Prisma.NestedStringNullableFilter<$PrismaModel> | string | null
}
export type SortOrderInput = {
sort: Prisma.SortOrder
nulls?: Prisma.NullsOrder
}
export type StringNullableWithAggregatesFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
in?: string[] | null
notIn?: string[] | null
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
search?: string
not?: Prisma.NestedStringNullableWithAggregatesFilter<$PrismaModel> | string | null
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
_min?: Prisma.NestedStringNullableFilter<$PrismaModel>
_max?: Prisma.NestedStringNullableFilter<$PrismaModel>
}
export type BigIntFilter<$PrismaModel = never> = {
equals?: bigint | number | Prisma.BigIntFieldRefInput<$PrismaModel>
in?: bigint[] | number[]
notIn?: bigint[] | number[]
in?: bigint[] | number[] | Prisma.ListBigIntFieldRefInput<$PrismaModel>
notIn?: bigint[] | number[] | Prisma.ListBigIntFieldRefInput<$PrismaModel>
lt?: bigint | number | Prisma.BigIntFieldRefInput<$PrismaModel>
lte?: bigint | number | Prisma.BigIntFieldRefInput<$PrismaModel>
gt?: bigint | number | Prisma.BigIntFieldRefInput<$PrismaModel>
@@ -241,8 +177,8 @@ export type BigIntFilter<$PrismaModel = never> = {
export type BigIntWithAggregatesFilter<$PrismaModel = never> = {
equals?: bigint | number | Prisma.BigIntFieldRefInput<$PrismaModel>
in?: bigint[] | number[]
notIn?: bigint[] | number[]
in?: bigint[] | number[] | Prisma.ListBigIntFieldRefInput<$PrismaModel>
notIn?: bigint[] | number[] | Prisma.ListBigIntFieldRefInput<$PrismaModel>
lt?: bigint | number | Prisma.BigIntFieldRefInput<$PrismaModel>
lte?: bigint | number | Prisma.BigIntFieldRefInput<$PrismaModel>
gt?: bigint | number | Prisma.BigIntFieldRefInput<$PrismaModel>
@@ -264,7 +200,7 @@ export type JsonNullableFilter<$PrismaModel = never> =
export type JsonNullableFilterBase<$PrismaModel = never> = {
equals?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
path?: string
path?: string[]
mode?: Prisma.QueryMode | Prisma.EnumQueryModeFieldRefInput<$PrismaModel>
string_contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
string_starts_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
@@ -272,10 +208,10 @@ export type JsonNullableFilterBase<$PrismaModel = never> = {
array_starts_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
array_ends_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
array_contains?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
lt?: runtime.InputJsonValue
lte?: runtime.InputJsonValue
gt?: runtime.InputJsonValue
gte?: runtime.InputJsonValue
lt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
lte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
gt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
gte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
not?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
}
@@ -288,7 +224,7 @@ export type JsonNullableWithAggregatesFilter<$PrismaModel = never> =
export type JsonNullableWithAggregatesFilterBase<$PrismaModel = never> = {
equals?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
path?: string
path?: string[]
mode?: Prisma.QueryMode | Prisma.EnumQueryModeFieldRefInput<$PrismaModel>
string_contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
string_starts_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
@@ -296,10 +232,10 @@ export type JsonNullableWithAggregatesFilterBase<$PrismaModel = never> = {
array_starts_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
array_ends_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
array_contains?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
lt?: runtime.InputJsonValue
lte?: runtime.InputJsonValue
gt?: runtime.InputJsonValue
gte?: runtime.InputJsonValue
lt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
lte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
gt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
gte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
not?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
_min?: Prisma.NestedJsonNullableFilter<$PrismaModel>
@@ -308,8 +244,8 @@ export type JsonNullableWithAggregatesFilterBase<$PrismaModel = never> = {
export type IntNullableFilter<$PrismaModel = never> = {
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null
in?: number[] | null
notIn?: number[] | null
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
@@ -319,8 +255,8 @@ export type IntNullableFilter<$PrismaModel = never> = {
export type DateTimeNullableFilter<$PrismaModel = never> = {
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null
in?: Date[] | string[] | null
notIn?: Date[] | string[] | null
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
@@ -328,10 +264,15 @@ export type DateTimeNullableFilter<$PrismaModel = never> = {
not?: Prisma.NestedDateTimeNullableFilter<$PrismaModel> | Date | string | null
}
export type BoolFilter<$PrismaModel = never> = {
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>
not?: Prisma.NestedBoolFilter<$PrismaModel> | boolean
}
export type IntNullableWithAggregatesFilter<$PrismaModel = never> = {
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null
in?: number[] | null
notIn?: number[] | null
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
@@ -346,8 +287,8 @@ export type IntNullableWithAggregatesFilter<$PrismaModel = never> = {
export type DateTimeNullableWithAggregatesFilter<$PrismaModel = never> = {
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null
in?: Date[] | string[] | null
notIn?: Date[] | string[] | null
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
@@ -358,10 +299,18 @@ export type DateTimeNullableWithAggregatesFilter<$PrismaModel = never> = {
_max?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>
}
export type BoolWithAggregatesFilter<$PrismaModel = never> = {
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>
not?: Prisma.NestedBoolWithAggregatesFilter<$PrismaModel> | boolean
_count?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedBoolFilter<$PrismaModel>
_max?: Prisma.NestedBoolFilter<$PrismaModel>
}
export type FloatNullableFilter<$PrismaModel = never> = {
equals?: number | Prisma.FloatFieldRefInput<$PrismaModel> | null
in?: number[] | null
notIn?: number[] | null
in?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel> | null
notIn?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel> | null
lt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
lte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
gt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
@@ -371,8 +320,8 @@ export type FloatNullableFilter<$PrismaModel = never> = {
export type FloatNullableWithAggregatesFilter<$PrismaModel = never> = {
equals?: number | Prisma.FloatFieldRefInput<$PrismaModel> | null
in?: number[] | null
notIn?: number[] | null
in?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel> | null
notIn?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel> | null
lt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
lte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
gt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
@@ -385,10 +334,61 @@ export type FloatNullableWithAggregatesFilter<$PrismaModel = never> = {
_max?: Prisma.NestedFloatNullableFilter<$PrismaModel>
}
export type JsonFilter<$PrismaModel = never> =
| Prisma.PatchUndefined<
Prisma.Either<Required<JsonFilterBase<$PrismaModel>>, Exclude<keyof Required<JsonFilterBase<$PrismaModel>>, 'path'>>,
Required<JsonFilterBase<$PrismaModel>>
>
| Prisma.OptionalFlat<Omit<Required<JsonFilterBase<$PrismaModel>>, 'path'>>
export type JsonFilterBase<$PrismaModel = never> = {
equals?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
path?: string[]
mode?: Prisma.QueryMode | Prisma.EnumQueryModeFieldRefInput<$PrismaModel>
string_contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
string_starts_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
string_ends_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
array_starts_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
array_ends_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
array_contains?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
lt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
lte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
gt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
gte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
not?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
}
export type JsonWithAggregatesFilter<$PrismaModel = never> =
| Prisma.PatchUndefined<
Prisma.Either<Required<JsonWithAggregatesFilterBase<$PrismaModel>>, Exclude<keyof Required<JsonWithAggregatesFilterBase<$PrismaModel>>, 'path'>>,
Required<JsonWithAggregatesFilterBase<$PrismaModel>>
>
| Prisma.OptionalFlat<Omit<Required<JsonWithAggregatesFilterBase<$PrismaModel>>, 'path'>>
export type JsonWithAggregatesFilterBase<$PrismaModel = never> = {
equals?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
path?: string[]
mode?: Prisma.QueryMode | Prisma.EnumQueryModeFieldRefInput<$PrismaModel>
string_contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
string_starts_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
string_ends_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
array_starts_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
array_ends_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
array_contains?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
lt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
lte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
gt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
gte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
not?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
_count?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedJsonFilter<$PrismaModel>
_max?: Prisma.NestedJsonFilter<$PrismaModel>
}
export type NestedIntFilter<$PrismaModel = never> = {
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
in?: number[]
notIn?: number[]
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
@@ -398,8 +398,8 @@ export type NestedIntFilter<$PrismaModel = never> = {
export type NestedStringFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
in?: string[]
notIn?: string[]
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
@@ -407,14 +407,13 @@ export type NestedStringFilter<$PrismaModel = never> = {
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
search?: string
not?: Prisma.NestedStringFilter<$PrismaModel> | string
}
export type NestedDateTimeFilter<$PrismaModel = never> = {
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
in?: Date[] | string[]
notIn?: Date[] | string[]
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
@@ -424,8 +423,8 @@ export type NestedDateTimeFilter<$PrismaModel = never> = {
export type NestedIntWithAggregatesFilter<$PrismaModel = never> = {
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
in?: number[]
notIn?: number[]
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
@@ -440,8 +439,8 @@ export type NestedIntWithAggregatesFilter<$PrismaModel = never> = {
export type NestedFloatFilter<$PrismaModel = never> = {
equals?: number | Prisma.FloatFieldRefInput<$PrismaModel>
in?: number[]
notIn?: number[]
in?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel>
notIn?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel>
lt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
lte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
gt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
@@ -451,8 +450,8 @@ export type NestedFloatFilter<$PrismaModel = never> = {
export type NestedStringWithAggregatesFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
in?: string[]
notIn?: string[]
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
@@ -460,7 +459,6 @@ export type NestedStringWithAggregatesFilter<$PrismaModel = never> = {
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
search?: string
not?: Prisma.NestedStringWithAggregatesFilter<$PrismaModel> | string
_count?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedStringFilter<$PrismaModel>
@@ -469,8 +467,8 @@ export type NestedStringWithAggregatesFilter<$PrismaModel = never> = {
export type NestedDateTimeWithAggregatesFilter<$PrismaModel = never> = {
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
in?: Date[] | string[]
notIn?: Date[] | string[]
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
@@ -481,47 +479,52 @@ export type NestedDateTimeWithAggregatesFilter<$PrismaModel = never> = {
_max?: Prisma.NestedDateTimeFilter<$PrismaModel>
}
export type NestedJsonFilter<$PrismaModel = never> =
| Prisma.PatchUndefined<
Prisma.Either<Required<NestedJsonFilterBase<$PrismaModel>>, Exclude<keyof Required<NestedJsonFilterBase<$PrismaModel>>, 'path'>>,
Required<NestedJsonFilterBase<$PrismaModel>>
>
| Prisma.OptionalFlat<Omit<Required<NestedJsonFilterBase<$PrismaModel>>, 'path'>>
export type NestedJsonFilterBase<$PrismaModel = never> = {
equals?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
path?: string
mode?: Prisma.QueryMode | Prisma.EnumQueryModeFieldRefInput<$PrismaModel>
string_contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
string_starts_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
string_ends_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
array_starts_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
array_ends_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
array_contains?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
lt?: runtime.InputJsonValue
lte?: runtime.InputJsonValue
gt?: runtime.InputJsonValue
gte?: runtime.InputJsonValue
not?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
export type NestedStringNullableFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
not?: Prisma.NestedStringNullableFilter<$PrismaModel> | string | null
}
export type NestedBoolFilter<$PrismaModel = never> = {
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>
not?: Prisma.NestedBoolFilter<$PrismaModel> | boolean
export type NestedStringNullableWithAggregatesFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
not?: Prisma.NestedStringNullableWithAggregatesFilter<$PrismaModel> | string | null
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
_min?: Prisma.NestedStringNullableFilter<$PrismaModel>
_max?: Prisma.NestedStringNullableFilter<$PrismaModel>
}
export type NestedBoolWithAggregatesFilter<$PrismaModel = never> = {
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>
not?: Prisma.NestedBoolWithAggregatesFilter<$PrismaModel> | boolean
_count?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedBoolFilter<$PrismaModel>
_max?: Prisma.NestedBoolFilter<$PrismaModel>
export type NestedIntNullableFilter<$PrismaModel = never> = {
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
not?: Prisma.NestedIntNullableFilter<$PrismaModel> | number | null
}
export type NestedFloatWithAggregatesFilter<$PrismaModel = never> = {
equals?: number | Prisma.FloatFieldRefInput<$PrismaModel>
in?: number[]
notIn?: number[]
in?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel>
notIn?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel>
lt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
lte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
gt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
@@ -534,54 +537,10 @@ export type NestedFloatWithAggregatesFilter<$PrismaModel = never> = {
_max?: Prisma.NestedFloatFilter<$PrismaModel>
}
export type NestedStringNullableFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
in?: string[] | null
notIn?: string[] | null
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
search?: string
not?: Prisma.NestedStringNullableFilter<$PrismaModel> | string | null
}
export type NestedStringNullableWithAggregatesFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
in?: string[] | null
notIn?: string[] | null
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
search?: string
not?: Prisma.NestedStringNullableWithAggregatesFilter<$PrismaModel> | string | null
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
_min?: Prisma.NestedStringNullableFilter<$PrismaModel>
_max?: Prisma.NestedStringNullableFilter<$PrismaModel>
}
export type NestedIntNullableFilter<$PrismaModel = never> = {
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null
in?: number[] | null
notIn?: number[] | null
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
not?: Prisma.NestedIntNullableFilter<$PrismaModel> | number | null
}
export type NestedBigIntFilter<$PrismaModel = never> = {
equals?: bigint | number | Prisma.BigIntFieldRefInput<$PrismaModel>
in?: bigint[] | number[]
notIn?: bigint[] | number[]
in?: bigint[] | number[] | Prisma.ListBigIntFieldRefInput<$PrismaModel>
notIn?: bigint[] | number[] | Prisma.ListBigIntFieldRefInput<$PrismaModel>
lt?: bigint | number | Prisma.BigIntFieldRefInput<$PrismaModel>
lte?: bigint | number | Prisma.BigIntFieldRefInput<$PrismaModel>
gt?: bigint | number | Prisma.BigIntFieldRefInput<$PrismaModel>
@@ -591,8 +550,8 @@ export type NestedBigIntFilter<$PrismaModel = never> = {
export type NestedBigIntWithAggregatesFilter<$PrismaModel = never> = {
equals?: bigint | number | Prisma.BigIntFieldRefInput<$PrismaModel>
in?: bigint[] | number[]
notIn?: bigint[] | number[]
in?: bigint[] | number[] | Prisma.ListBigIntFieldRefInput<$PrismaModel>
notIn?: bigint[] | number[] | Prisma.ListBigIntFieldRefInput<$PrismaModel>
lt?: bigint | number | Prisma.BigIntFieldRefInput<$PrismaModel>
lte?: bigint | number | Prisma.BigIntFieldRefInput<$PrismaModel>
gt?: bigint | number | Prisma.BigIntFieldRefInput<$PrismaModel>
@@ -614,7 +573,7 @@ export type NestedJsonNullableFilter<$PrismaModel = never> =
export type NestedJsonNullableFilterBase<$PrismaModel = never> = {
equals?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
path?: string
path?: string[]
mode?: Prisma.QueryMode | Prisma.EnumQueryModeFieldRefInput<$PrismaModel>
string_contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
string_starts_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
@@ -622,17 +581,17 @@ export type NestedJsonNullableFilterBase<$PrismaModel = never> = {
array_starts_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
array_ends_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
array_contains?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
lt?: runtime.InputJsonValue
lte?: runtime.InputJsonValue
gt?: runtime.InputJsonValue
gte?: runtime.InputJsonValue
lt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
lte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
gt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
gte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
not?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
}
export type NestedDateTimeNullableFilter<$PrismaModel = never> = {
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null
in?: Date[] | string[] | null
notIn?: Date[] | string[] | null
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
@@ -640,10 +599,15 @@ export type NestedDateTimeNullableFilter<$PrismaModel = never> = {
not?: Prisma.NestedDateTimeNullableFilter<$PrismaModel> | Date | string | null
}
export type NestedBoolFilter<$PrismaModel = never> = {
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>
not?: Prisma.NestedBoolFilter<$PrismaModel> | boolean
}
export type NestedIntNullableWithAggregatesFilter<$PrismaModel = never> = {
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null
in?: number[] | null
notIn?: number[] | null
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
@@ -658,8 +622,8 @@ export type NestedIntNullableWithAggregatesFilter<$PrismaModel = never> = {
export type NestedFloatNullableFilter<$PrismaModel = never> = {
equals?: number | Prisma.FloatFieldRefInput<$PrismaModel> | null
in?: number[] | null
notIn?: number[] | null
in?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel> | null
notIn?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel> | null
lt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
lte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
gt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
@@ -669,8 +633,8 @@ export type NestedFloatNullableFilter<$PrismaModel = never> = {
export type NestedDateTimeNullableWithAggregatesFilter<$PrismaModel = never> = {
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null
in?: Date[] | string[] | null
notIn?: Date[] | string[] | null
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
@@ -681,10 +645,18 @@ export type NestedDateTimeNullableWithAggregatesFilter<$PrismaModel = never> = {
_max?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>
}
export type NestedBoolWithAggregatesFilter<$PrismaModel = never> = {
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>
not?: Prisma.NestedBoolWithAggregatesFilter<$PrismaModel> | boolean
_count?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedBoolFilter<$PrismaModel>
_max?: Prisma.NestedBoolFilter<$PrismaModel>
}
export type NestedFloatNullableWithAggregatesFilter<$PrismaModel = never> = {
equals?: number | Prisma.FloatFieldRefInput<$PrismaModel> | null
in?: number[] | null
notIn?: number[] | null
in?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel> | null
notIn?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel> | null
lt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
lte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
gt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
@@ -697,4 +669,28 @@ export type NestedFloatNullableWithAggregatesFilter<$PrismaModel = never> = {
_max?: Prisma.NestedFloatNullableFilter<$PrismaModel>
}
export type NestedJsonFilter<$PrismaModel = never> =
| Prisma.PatchUndefined<
Prisma.Either<Required<NestedJsonFilterBase<$PrismaModel>>, Exclude<keyof Required<NestedJsonFilterBase<$PrismaModel>>, 'path'>>,
Required<NestedJsonFilterBase<$PrismaModel>>
>
| Prisma.OptionalFlat<Omit<Required<NestedJsonFilterBase<$PrismaModel>>, 'path'>>
export type NestedJsonFilterBase<$PrismaModel = never> = {
equals?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
path?: string[]
mode?: Prisma.QueryMode | Prisma.EnumQueryModeFieldRefInput<$PrismaModel>
string_contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
string_starts_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
string_ends_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
array_starts_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
array_ends_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
array_contains?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
lt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
lte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
gt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
gte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
not?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
}
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
@@ -53,9 +53,6 @@ export const AnyNull = runtime.AnyNull
export const ModelName = {
User: 'User',
Article: 'Article',
Strategy: 'Strategy',
Indicator: 'Indicator',
IndicatorPoint: 'IndicatorPoint',
News: 'News',
Alert: 'Alert',
ServerNode: 'ServerNode',
@@ -63,7 +60,6 @@ export const ModelName = {
ServiceCheck: 'ServiceCheck',
Econ_SowInventory: 'Econ_SowInventory',
ScraperCache: 'ScraperCache',
OilPriceHistory: 'OilPriceHistory',
MihomeAccount: 'MihomeAccount',
MihomeHome: 'MihomeHome',
MihomeDevice: 'MihomeDevice',
@@ -73,7 +69,13 @@ export const ModelName = {
CheckIn: 'CheckIn',
CheckList: 'CheckList',
CheckListItem: 'CheckListItem',
CheckListTemplate: 'CheckListTemplate'
CheckListTemplate: 'CheckListTemplate',
Project: 'Project',
ProjectMember: 'ProjectMember',
BoardColumn: 'BoardColumn',
Task: 'Task',
TaskMember: 'TaskMember',
TaskTag: 'TaskTag'
} as const
export type ModelName = (typeof ModelName)[keyof typeof ModelName]
@@ -117,46 +119,6 @@ export const ArticleScalarFieldEnum = {
export type ArticleScalarFieldEnum = (typeof ArticleScalarFieldEnum)[keyof typeof ArticleScalarFieldEnum]
export const StrategyScalarFieldEnum = {
id: 'id',
name: 'name',
description: 'description',
data: 'data',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
} as const
export type StrategyScalarFieldEnum = (typeof StrategyScalarFieldEnum)[keyof typeof StrategyScalarFieldEnum]
export const IndicatorScalarFieldEnum = {
id: 'id',
name: 'name',
groupName: 'groupName',
title: 'title',
unit: 'unit',
source: 'source',
frequency: 'frequency',
hidden: 'hidden',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
} as const
export type IndicatorScalarFieldEnum = (typeof IndicatorScalarFieldEnum)[keyof typeof IndicatorScalarFieldEnum]
export const IndicatorPointScalarFieldEnum = {
id: 'id',
indicatorId: 'indicatorId',
period: 'period',
value: 'value',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
} as const
export type IndicatorPointScalarFieldEnum = (typeof IndicatorPointScalarFieldEnum)[keyof typeof IndicatorPointScalarFieldEnum]
export const NewsScalarFieldEnum = {
id: 'id',
title: 'title',
@@ -276,19 +238,6 @@ export const ScraperCacheScalarFieldEnum = {
export type ScraperCacheScalarFieldEnum = (typeof ScraperCacheScalarFieldEnum)[keyof typeof ScraperCacheScalarFieldEnum]
export const OilPriceHistoryScalarFieldEnum = {
id: 'id',
date: 'date',
province: 'province',
fuel: 'fuel',
price: 'price',
source: 'source',
createdAt: 'createdAt'
} as const
export type OilPriceHistoryScalarFieldEnum = (typeof OilPriceHistoryScalarFieldEnum)[keyof typeof OilPriceHistoryScalarFieldEnum]
export const MihomeAccountScalarFieldEnum = {
id: 'id',
key: 'key',
@@ -443,6 +392,106 @@ export const CheckListTemplateScalarFieldEnum = {
export type CheckListTemplateScalarFieldEnum = (typeof CheckListTemplateScalarFieldEnum)[keyof typeof CheckListTemplateScalarFieldEnum]
export const ProjectScalarFieldEnum = {
id: 'id',
uuid: 'uuid',
code: 'code',
name: 'name',
ownerId: 'ownerId',
isPersonal: 'isPersonal',
archived: 'archived',
notes: 'notes',
taskKinds: 'taskKinds',
icon: 'icon',
autoArchive: 'autoArchive',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
} as const
export type ProjectScalarFieldEnum = (typeof ProjectScalarFieldEnum)[keyof typeof ProjectScalarFieldEnum]
export const ProjectMemberScalarFieldEnum = {
id: 'id',
uuid: 'uuid',
projectUuid: 'projectUuid',
projectId: 'projectId',
userId: 'userId',
permission: 'permission',
createdAt: 'createdAt'
} as const
export type ProjectMemberScalarFieldEnum = (typeof ProjectMemberScalarFieldEnum)[keyof typeof ProjectMemberScalarFieldEnum]
export const BoardColumnScalarFieldEnum = {
id: 'id',
uuid: 'uuid',
projectUuid: 'projectUuid',
projectId: 'projectId',
name: 'name',
sortOrder: 'sortOrder',
boundStatus: 'boundStatus',
retention: 'retention',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
} as const
export type BoardColumnScalarFieldEnum = (typeof BoardColumnScalarFieldEnum)[keyof typeof BoardColumnScalarFieldEnum]
export const TaskScalarFieldEnum = {
id: 'id',
uuid: 'uuid',
projectUuid: 'projectUuid',
columnUuid: 'columnUuid',
parentUuid: 'parentUuid',
projectId: 'projectId',
columnId: 'columnId',
parentId: 'parentId',
title: 'title',
description: 'description',
kind: 'kind',
markDate: 'markDate',
status: 'status',
doneAt: 'doneAt',
assigneeId: 'assigneeId',
creatorId: 'creatorId',
extras: 'extras',
sortOrder: 'sortOrder',
archived: 'archived',
archivedAt: 'archivedAt',
archiveSource: 'archiveSource',
createdAt: 'createdAt',
updatedAt: 'updatedAt',
number: 'number'
} as const
export type TaskScalarFieldEnum = (typeof TaskScalarFieldEnum)[keyof typeof TaskScalarFieldEnum]
export const TaskMemberScalarFieldEnum = {
id: 'id',
uuid: 'uuid',
taskUuid: 'taskUuid',
taskId: 'taskId',
userId: 'userId'
} as const
export type TaskMemberScalarFieldEnum = (typeof TaskMemberScalarFieldEnum)[keyof typeof TaskMemberScalarFieldEnum]
export const TaskTagScalarFieldEnum = {
id: 'id',
uuid: 'uuid',
taskUuid: 'taskUuid',
taskId: 'taskId',
name: 'name'
} as const
export type TaskTagScalarFieldEnum = (typeof TaskTagScalarFieldEnum)[keyof typeof TaskTagScalarFieldEnum]
export const SortOrder = {
asc: 'asc',
desc: 'desc'
@@ -451,13 +500,6 @@ export const SortOrder = {
export type SortOrder = (typeof SortOrder)[keyof typeof SortOrder]
export const JsonNullValueInput = {
JsonNull: JsonNull
} as const
export type JsonNullValueInput = (typeof JsonNullValueInput)[keyof typeof JsonNullValueInput]
export const NullableJsonNullValueInput = {
DbNull: DbNull,
JsonNull: JsonNull
@@ -466,32 +508,11 @@ export const NullableJsonNullValueInput = {
export type NullableJsonNullValueInput = (typeof NullableJsonNullValueInput)[keyof typeof NullableJsonNullValueInput]
export const UserOrderByRelevanceFieldEnum = {
name: 'name',
email: 'email',
password: 'password'
export const JsonNullValueInput = {
JsonNull: JsonNull
} as const
export type UserOrderByRelevanceFieldEnum = (typeof UserOrderByRelevanceFieldEnum)[keyof typeof UserOrderByRelevanceFieldEnum]
export const ArticleOrderByRelevanceFieldEnum = {
title: 'title',
slug: 'slug',
content: 'content',
date: 'date'
} as const
export type ArticleOrderByRelevanceFieldEnum = (typeof ArticleOrderByRelevanceFieldEnum)[keyof typeof ArticleOrderByRelevanceFieldEnum]
export const JsonNullValueFilter = {
DbNull: DbNull,
JsonNull: JsonNull,
AnyNull: AnyNull
} as const
export type JsonNullValueFilter = (typeof JsonNullValueFilter)[keyof typeof JsonNullValueFilter]
export type JsonNullValueInput = (typeof JsonNullValueInput)[keyof typeof JsonNullValueInput]
export const QueryMode = {
@@ -502,33 +523,6 @@ export const QueryMode = {
export type QueryMode = (typeof QueryMode)[keyof typeof QueryMode]
export const StrategyOrderByRelevanceFieldEnum = {
name: 'name',
description: 'description'
} as const
export type StrategyOrderByRelevanceFieldEnum = (typeof StrategyOrderByRelevanceFieldEnum)[keyof typeof StrategyOrderByRelevanceFieldEnum]
export const IndicatorOrderByRelevanceFieldEnum = {
name: 'name',
groupName: 'groupName',
title: 'title',
unit: 'unit',
source: 'source',
frequency: 'frequency'
} as const
export type IndicatorOrderByRelevanceFieldEnum = (typeof IndicatorOrderByRelevanceFieldEnum)[keyof typeof IndicatorOrderByRelevanceFieldEnum]
export const IndicatorPointOrderByRelevanceFieldEnum = {
period: 'period'
} as const
export type IndicatorPointOrderByRelevanceFieldEnum = (typeof IndicatorPointOrderByRelevanceFieldEnum)[keyof typeof IndicatorPointOrderByRelevanceFieldEnum]
export const NullsOrder = {
first: 'first',
last: 'last'
@@ -537,157 +531,11 @@ export const NullsOrder = {
export type NullsOrder = (typeof NullsOrder)[keyof typeof NullsOrder]
export const NewsOrderByRelevanceFieldEnum = {
title: 'title',
summary: 'summary',
content: 'content',
source: 'source',
url: 'url'
export const JsonNullValueFilter = {
DbNull: DbNull,
JsonNull: JsonNull,
AnyNull: AnyNull
} as const
export type NewsOrderByRelevanceFieldEnum = (typeof NewsOrderByRelevanceFieldEnum)[keyof typeof NewsOrderByRelevanceFieldEnum]
export const AlertOrderByRelevanceFieldEnum = {
indicatorName: 'indicatorName',
period: 'period',
sourceA: 'sourceA',
sourceB: 'sourceB'
} as const
export type AlertOrderByRelevanceFieldEnum = (typeof AlertOrderByRelevanceFieldEnum)[keyof typeof AlertOrderByRelevanceFieldEnum]
export const ServerNodeOrderByRelevanceFieldEnum = {
uuid: 'uuid',
name: 'name',
cpuName: 'cpuName',
os: 'os',
region: 'region',
status: 'status'
} as const
export type ServerNodeOrderByRelevanceFieldEnum = (typeof ServerNodeOrderByRelevanceFieldEnum)[keyof typeof ServerNodeOrderByRelevanceFieldEnum]
export const ServerNodeHistoryOrderByRelevanceFieldEnum = {
uuid: 'uuid'
} as const
export type ServerNodeHistoryOrderByRelevanceFieldEnum = (typeof ServerNodeHistoryOrderByRelevanceFieldEnum)[keyof typeof ServerNodeHistoryOrderByRelevanceFieldEnum]
export const ServiceCheckOrderByRelevanceFieldEnum = {
name: 'name',
url: 'url',
status: 'status',
message: 'message'
} as const
export type ServiceCheckOrderByRelevanceFieldEnum = (typeof ServiceCheckOrderByRelevanceFieldEnum)[keyof typeof ServiceCheckOrderByRelevanceFieldEnum]
export const Econ_SowInventoryOrderByRelevanceFieldEnum = {
month: 'month'
} as const
export type Econ_SowInventoryOrderByRelevanceFieldEnum = (typeof Econ_SowInventoryOrderByRelevanceFieldEnum)[keyof typeof Econ_SowInventoryOrderByRelevanceFieldEnum]
export const ScraperCacheOrderByRelevanceFieldEnum = {
scraperName: 'scraperName',
lastDataHash: 'lastDataHash'
} as const
export type ScraperCacheOrderByRelevanceFieldEnum = (typeof ScraperCacheOrderByRelevanceFieldEnum)[keyof typeof ScraperCacheOrderByRelevanceFieldEnum]
export const OilPriceHistoryOrderByRelevanceFieldEnum = {
province: 'province',
fuel: 'fuel',
source: 'source'
} as const
export type OilPriceHistoryOrderByRelevanceFieldEnum = (typeof OilPriceHistoryOrderByRelevanceFieldEnum)[keyof typeof OilPriceHistoryOrderByRelevanceFieldEnum]
export const MihomeAccountOrderByRelevanceFieldEnum = {
key: 'key',
userId: 'userId',
status: 'status',
lastError: 'lastError'
} as const
export type MihomeAccountOrderByRelevanceFieldEnum = (typeof MihomeAccountOrderByRelevanceFieldEnum)[keyof typeof MihomeAccountOrderByRelevanceFieldEnum]
export const MihomeHomeOrderByRelevanceFieldEnum = {
homeId: 'homeId',
name: 'name'
} as const
export type MihomeHomeOrderByRelevanceFieldEnum = (typeof MihomeHomeOrderByRelevanceFieldEnum)[keyof typeof MihomeHomeOrderByRelevanceFieldEnum]
export const MihomeDeviceOrderByRelevanceFieldEnum = {
did: 'did',
homeId: 'homeId',
model: 'model',
name: 'name',
alias: 'alias'
} as const
export type MihomeDeviceOrderByRelevanceFieldEnum = (typeof MihomeDeviceOrderByRelevanceFieldEnum)[keyof typeof MihomeDeviceOrderByRelevanceFieldEnum]
export const MihomeEnvSampleOrderByRelevanceFieldEnum = {
origin: 'origin'
} as const
export type MihomeEnvSampleOrderByRelevanceFieldEnum = (typeof MihomeEnvSampleOrderByRelevanceFieldEnum)[keyof typeof MihomeEnvSampleOrderByRelevanceFieldEnum]
export const HabitOrderByRelevanceFieldEnum = {
name: 'name',
period: 'period',
color: 'color',
icon: 'icon',
note: 'note'
} as const
export type HabitOrderByRelevanceFieldEnum = (typeof HabitOrderByRelevanceFieldEnum)[keyof typeof HabitOrderByRelevanceFieldEnum]
export const CheckInOrderByRelevanceFieldEnum = {
date: 'date',
note: 'note',
source: 'source'
} as const
export type CheckInOrderByRelevanceFieldEnum = (typeof CheckInOrderByRelevanceFieldEnum)[keyof typeof CheckInOrderByRelevanceFieldEnum]
export const CheckListOrderByRelevanceFieldEnum = {
title: 'title',
scene: 'scene',
note: 'note'
} as const
export type CheckListOrderByRelevanceFieldEnum = (typeof CheckListOrderByRelevanceFieldEnum)[keyof typeof CheckListOrderByRelevanceFieldEnum]
export const CheckListItemOrderByRelevanceFieldEnum = {
title: 'title',
note: 'note'
} as const
export type CheckListItemOrderByRelevanceFieldEnum = (typeof CheckListItemOrderByRelevanceFieldEnum)[keyof typeof CheckListItemOrderByRelevanceFieldEnum]
export const CheckListTemplateOrderByRelevanceFieldEnum = {
name: 'name',
scene: 'scene'
} as const
export type CheckListTemplateOrderByRelevanceFieldEnum = (typeof CheckListTemplateOrderByRelevanceFieldEnum)[keyof typeof CheckListTemplateOrderByRelevanceFieldEnum]
export type JsonNullValueFilter = (typeof JsonNullValueFilter)[keyof typeof JsonNullValueFilter]
+6 -4
View File
@@ -10,9 +10,6 @@
*/
export type * from './models/User.ts'
export type * from './models/Article.ts'
export type * from './models/Strategy.ts'
export type * from './models/Indicator.ts'
export type * from './models/IndicatorPoint.ts'
export type * from './models/News.ts'
export type * from './models/Alert.ts'
export type * from './models/ServerNode.ts'
@@ -20,7 +17,6 @@ export type * from './models/ServerNodeHistory.ts'
export type * from './models/ServiceCheck.ts'
export type * from './models/Econ_SowInventory.ts'
export type * from './models/ScraperCache.ts'
export type * from './models/OilPriceHistory.ts'
export type * from './models/MihomeAccount.ts'
export type * from './models/MihomeHome.ts'
export type * from './models/MihomeDevice.ts'
@@ -31,4 +27,10 @@ export type * from './models/CheckIn.ts'
export type * from './models/CheckList.ts'
export type * from './models/CheckListItem.ts'
export type * from './models/CheckListTemplate.ts'
export type * from './models/Project.ts'
export type * from './models/ProjectMember.ts'
export type * from './models/BoardColumn.ts'
export type * from './models/Task.ts'
export type * from './models/TaskMember.ts'
export type * from './models/TaskTag.ts'
export type * from './commonInputTypes.ts'
+131 -7
View File
@@ -285,7 +285,6 @@ export type AlertOrderByWithRelationInput = {
diff?: Prisma.SortOrder
diffRatio?: Prisma.SortOrder
createdAt?: Prisma.SortOrder
_relevance?: Prisma.AlertOrderByRelevanceInput
}
export type AlertWhereUniqueInput = Prisma.AtLeast<{
@@ -426,12 +425,6 @@ export type AlertUncheckedUpdateManyInput = {
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
}
export type AlertOrderByRelevanceInput = {
fields: Prisma.AlertOrderByRelevanceFieldEnum | Prisma.AlertOrderByRelevanceFieldEnum[]
sort: Prisma.SortOrder
search: string
}
export type AlertCountOrderByAggregateInput = {
id?: Prisma.SortOrder
indicatorName?: Prisma.SortOrder
@@ -487,6 +480,14 @@ export type AlertSumOrderByAggregateInput = {
diffRatio?: Prisma.SortOrder
}
export type FloatFieldUpdateOperationsInput = {
set?: number
increment?: number
decrement?: number
multiply?: number
divide?: number
}
export type AlertSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
@@ -502,7 +503,31 @@ export type AlertSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs =
createdAt?: boolean
}, ExtArgs["result"]["alert"]>
export type AlertSelectCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
id?: boolean
indicatorName?: boolean
period?: boolean
valueA?: boolean
valueB?: boolean
sourceA?: boolean
sourceB?: boolean
diff?: boolean
diffRatio?: boolean
createdAt?: boolean
}, ExtArgs["result"]["alert"]>
export type AlertSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
id?: boolean
indicatorName?: boolean
period?: boolean
valueA?: boolean
valueB?: boolean
sourceA?: boolean
sourceB?: boolean
diff?: boolean
diffRatio?: boolean
createdAt?: boolean
}, ExtArgs["result"]["alert"]>
export type AlertSelectScalar = {
id?: boolean
@@ -650,6 +675,30 @@ export interface AlertDelegate<ExtArgs extends runtime.Types.Extensions.Internal
*/
createMany<T extends AlertCreateManyArgs>(args?: Prisma.SelectSubset<T, AlertCreateManyArgs<ExtArgs>>): Prisma.PrismaPromise<Prisma.BatchPayload>
/**
* Create many Alerts and returns the data saved in the database.
* @param {AlertCreateManyAndReturnArgs} args - Arguments to create many Alerts.
* @example
* // Create many Alerts
* const alert = await prisma.alert.createManyAndReturn({
* data: [
* // ... provide data here
* ]
* })
*
* // Create many Alerts and only return the `id`
* const alertWithIdOnly = await prisma.alert.createManyAndReturn({
* select: { id: true },
* data: [
* // ... provide data here
* ]
* })
* Note, that providing `undefined` is treated as the value not being there.
* Read more here: https://pris.ly/d/null-undefined
*
*/
createManyAndReturn<T extends AlertCreateManyAndReturnArgs>(args?: Prisma.SelectSubset<T, AlertCreateManyAndReturnArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$AlertPayload<ExtArgs>, T, "createManyAndReturn", GlobalOmitOptions>>
/**
* Delete a Alert.
* @param {AlertDeleteArgs} args - Arguments to delete one Alert.
@@ -714,6 +763,36 @@ export interface AlertDelegate<ExtArgs extends runtime.Types.Extensions.Internal
*/
updateMany<T extends AlertUpdateManyArgs>(args: Prisma.SelectSubset<T, AlertUpdateManyArgs<ExtArgs>>): Prisma.PrismaPromise<Prisma.BatchPayload>
/**
* Update zero or more Alerts and returns the data updated in the database.
* @param {AlertUpdateManyAndReturnArgs} args - Arguments to update many Alerts.
* @example
* // Update many Alerts
* const alert = await prisma.alert.updateManyAndReturn({
* where: {
* // ... provide filter here
* },
* data: [
* // ... provide data here
* ]
* })
*
* // Update zero or more Alerts and only return the `id`
* const alertWithIdOnly = await prisma.alert.updateManyAndReturn({
* select: { id: true },
* where: {
* // ... provide filter here
* },
* data: [
* // ... provide data here
* ]
* })
* Note, that providing `undefined` is treated as the value not being there.
* Read more here: https://pris.ly/d/null-undefined
*
*/
updateManyAndReturn<T extends AlertUpdateManyAndReturnArgs>(args: Prisma.SelectSubset<T, AlertUpdateManyAndReturnArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$AlertPayload<ExtArgs>, T, "updateManyAndReturn", GlobalOmitOptions>>
/**
* Create or update one Alert.
* @param {AlertUpsertArgs} args - Arguments to update or create a Alert.
@@ -1125,6 +1204,25 @@ export type AlertCreateManyArgs<ExtArgs extends runtime.Types.Extensions.Interna
skipDuplicates?: boolean
}
/**
* Alert createManyAndReturn
*/
export type AlertCreateManyAndReturnArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
/**
* Select specific fields to fetch from the Alert
*/
select?: Prisma.AlertSelectCreateManyAndReturn<ExtArgs> | null
/**
* Omit specific fields from the Alert
*/
omit?: Prisma.AlertOmit<ExtArgs> | null
/**
* The data used to create many Alerts.
*/
data: Prisma.AlertCreateManyInput | Prisma.AlertCreateManyInput[]
skipDuplicates?: boolean
}
/**
* Alert update
*/
@@ -1165,6 +1263,32 @@ export type AlertUpdateManyArgs<ExtArgs extends runtime.Types.Extensions.Interna
limit?: number
}
/**
* Alert updateManyAndReturn
*/
export type AlertUpdateManyAndReturnArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
/**
* Select specific fields to fetch from the Alert
*/
select?: Prisma.AlertSelectUpdateManyAndReturn<ExtArgs> | null
/**
* Omit specific fields from the Alert
*/
omit?: Prisma.AlertOmit<ExtArgs> | null
/**
* The data used to update Alerts.
*/
data: Prisma.XOR<Prisma.AlertUpdateManyMutationInput, Prisma.AlertUncheckedUpdateManyInput>
/**
* Filter which Alerts to update
*/
where?: Prisma.AlertWhereInput
/**
* Limit how many Alerts to update.
*/
limit?: number
}
/**
* Alert upsert
*/
+117 -7
View File
@@ -242,7 +242,6 @@ export type ArticleOrderByWithRelationInput = {
date?: Prisma.SortOrder
createdAt?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder
_relevance?: Prisma.ArticleOrderByRelevanceInput
}
export type ArticleWhereUniqueInput = Prisma.AtLeast<{
@@ -353,12 +352,6 @@ export type ArticleUncheckedUpdateManyInput = {
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
}
export type ArticleOrderByRelevanceInput = {
fields: Prisma.ArticleOrderByRelevanceFieldEnum | Prisma.ArticleOrderByRelevanceFieldEnum[]
sort: Prisma.SortOrder
search: string
}
export type ArticleCountOrderByAggregateInput = {
id?: Prisma.SortOrder
title?: Prisma.SortOrder
@@ -409,7 +402,25 @@ export type ArticleSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs
updatedAt?: boolean
}, ExtArgs["result"]["article"]>
export type ArticleSelectCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
id?: boolean
title?: boolean
slug?: boolean
content?: boolean
date?: boolean
createdAt?: boolean
updatedAt?: boolean
}, ExtArgs["result"]["article"]>
export type ArticleSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
id?: boolean
title?: boolean
slug?: boolean
content?: boolean
date?: boolean
createdAt?: boolean
updatedAt?: boolean
}, ExtArgs["result"]["article"]>
export type ArticleSelectScalar = {
id?: boolean
@@ -551,6 +562,30 @@ export interface ArticleDelegate<ExtArgs extends runtime.Types.Extensions.Intern
*/
createMany<T extends ArticleCreateManyArgs>(args?: Prisma.SelectSubset<T, ArticleCreateManyArgs<ExtArgs>>): Prisma.PrismaPromise<Prisma.BatchPayload>
/**
* Create many Articles and returns the data saved in the database.
* @param {ArticleCreateManyAndReturnArgs} args - Arguments to create many Articles.
* @example
* // Create many Articles
* const article = await prisma.article.createManyAndReturn({
* data: [
* // ... provide data here
* ]
* })
*
* // Create many Articles and only return the `id`
* const articleWithIdOnly = await prisma.article.createManyAndReturn({
* select: { id: true },
* data: [
* // ... provide data here
* ]
* })
* Note, that providing `undefined` is treated as the value not being there.
* Read more here: https://pris.ly/d/null-undefined
*
*/
createManyAndReturn<T extends ArticleCreateManyAndReturnArgs>(args?: Prisma.SelectSubset<T, ArticleCreateManyAndReturnArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$ArticlePayload<ExtArgs>, T, "createManyAndReturn", GlobalOmitOptions>>
/**
* Delete a Article.
* @param {ArticleDeleteArgs} args - Arguments to delete one Article.
@@ -615,6 +650,36 @@ export interface ArticleDelegate<ExtArgs extends runtime.Types.Extensions.Intern
*/
updateMany<T extends ArticleUpdateManyArgs>(args: Prisma.SelectSubset<T, ArticleUpdateManyArgs<ExtArgs>>): Prisma.PrismaPromise<Prisma.BatchPayload>
/**
* Update zero or more Articles and returns the data updated in the database.
* @param {ArticleUpdateManyAndReturnArgs} args - Arguments to update many Articles.
* @example
* // Update many Articles
* const article = await prisma.article.updateManyAndReturn({
* where: {
* // ... provide filter here
* },
* data: [
* // ... provide data here
* ]
* })
*
* // Update zero or more Articles and only return the `id`
* const articleWithIdOnly = await prisma.article.updateManyAndReturn({
* select: { id: true },
* where: {
* // ... provide filter here
* },
* data: [
* // ... provide data here
* ]
* })
* Note, that providing `undefined` is treated as the value not being there.
* Read more here: https://pris.ly/d/null-undefined
*
*/
updateManyAndReturn<T extends ArticleUpdateManyAndReturnArgs>(args: Prisma.SelectSubset<T, ArticleUpdateManyAndReturnArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$ArticlePayload<ExtArgs>, T, "updateManyAndReturn", GlobalOmitOptions>>
/**
* Create or update one Article.
* @param {ArticleUpsertArgs} args - Arguments to update or create a Article.
@@ -1023,6 +1088,25 @@ export type ArticleCreateManyArgs<ExtArgs extends runtime.Types.Extensions.Inter
skipDuplicates?: boolean
}
/**
* Article createManyAndReturn
*/
export type ArticleCreateManyAndReturnArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
/**
* Select specific fields to fetch from the Article
*/
select?: Prisma.ArticleSelectCreateManyAndReturn<ExtArgs> | null
/**
* Omit specific fields from the Article
*/
omit?: Prisma.ArticleOmit<ExtArgs> | null
/**
* The data used to create many Articles.
*/
data: Prisma.ArticleCreateManyInput | Prisma.ArticleCreateManyInput[]
skipDuplicates?: boolean
}
/**
* Article update
*/
@@ -1063,6 +1147,32 @@ export type ArticleUpdateManyArgs<ExtArgs extends runtime.Types.Extensions.Inter
limit?: number
}
/**
* Article updateManyAndReturn
*/
export type ArticleUpdateManyAndReturnArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
/**
* Select specific fields to fetch from the Article
*/
select?: Prisma.ArticleSelectUpdateManyAndReturn<ExtArgs> | null
/**
* Omit specific fields from the Article
*/
omit?: Prisma.ArticleOmit<ExtArgs> | null
/**
* The data used to update Articles.
*/
data: Prisma.XOR<Prisma.ArticleUpdateManyMutationInput, Prisma.ArticleUncheckedUpdateManyInput>
/**
* Filter which Articles to update
*/
where?: Prisma.ArticleWhereInput
/**
* Limit how many Articles to update.
*/
limit?: number
}
/**
* Article upsert
*/
File diff suppressed because it is too large Load Diff
+139 -7
View File
@@ -267,7 +267,6 @@ export type CheckInOrderByWithRelationInput = {
createdAt?: Prisma.SortOrder
habit?: Prisma.HabitOrderByWithRelationInput
user?: Prisma.UserOrderByWithRelationInput
_relevance?: Prisma.CheckInOrderByRelevanceInput
}
export type CheckInWhereUniqueInput = Prisma.AtLeast<{
@@ -399,12 +398,6 @@ export type CheckInOrderByRelationAggregateInput = {
_count?: Prisma.SortOrder
}
export type CheckInOrderByRelevanceInput = {
fields: Prisma.CheckInOrderByRelevanceFieldEnum | Prisma.CheckInOrderByRelevanceFieldEnum[]
sort: Prisma.SortOrder
search: string
}
export type CheckInHabitIdDateSeqCompoundUniqueInput = {
habitId: number
date: string
@@ -739,7 +732,31 @@ export type CheckInSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs
user?: boolean | Prisma.UserDefaultArgs<ExtArgs>
}, ExtArgs["result"]["checkIn"]>
export type CheckInSelectCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
id?: boolean
habitId?: boolean
userId?: boolean
date?: boolean
seq?: boolean
note?: boolean
source?: boolean
createdAt?: boolean
habit?: boolean | Prisma.HabitDefaultArgs<ExtArgs>
user?: boolean | Prisma.UserDefaultArgs<ExtArgs>
}, ExtArgs["result"]["checkIn"]>
export type CheckInSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
id?: boolean
habitId?: boolean
userId?: boolean
date?: boolean
seq?: boolean
note?: boolean
source?: boolean
createdAt?: boolean
habit?: boolean | Prisma.HabitDefaultArgs<ExtArgs>
user?: boolean | Prisma.UserDefaultArgs<ExtArgs>
}, ExtArgs["result"]["checkIn"]>
export type CheckInSelectScalar = {
id?: boolean
@@ -757,6 +774,14 @@ export type CheckInInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs
habit?: boolean | Prisma.HabitDefaultArgs<ExtArgs>
user?: boolean | Prisma.UserDefaultArgs<ExtArgs>
}
export type CheckInIncludeCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
habit?: boolean | Prisma.HabitDefaultArgs<ExtArgs>
user?: boolean | Prisma.UserDefaultArgs<ExtArgs>
}
export type CheckInIncludeUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
habit?: boolean | Prisma.HabitDefaultArgs<ExtArgs>
user?: boolean | Prisma.UserDefaultArgs<ExtArgs>
}
export type $CheckInPayload<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
name: "CheckIn"
@@ -890,6 +915,30 @@ export interface CheckInDelegate<ExtArgs extends runtime.Types.Extensions.Intern
*/
createMany<T extends CheckInCreateManyArgs>(args?: Prisma.SelectSubset<T, CheckInCreateManyArgs<ExtArgs>>): Prisma.PrismaPromise<Prisma.BatchPayload>
/**
* Create many CheckIns and returns the data saved in the database.
* @param {CheckInCreateManyAndReturnArgs} args - Arguments to create many CheckIns.
* @example
* // Create many CheckIns
* const checkIn = await prisma.checkIn.createManyAndReturn({
* data: [
* // ... provide data here
* ]
* })
*
* // Create many CheckIns and only return the `id`
* const checkInWithIdOnly = await prisma.checkIn.createManyAndReturn({
* select: { id: true },
* data: [
* // ... provide data here
* ]
* })
* Note, that providing `undefined` is treated as the value not being there.
* Read more here: https://pris.ly/d/null-undefined
*
*/
createManyAndReturn<T extends CheckInCreateManyAndReturnArgs>(args?: Prisma.SelectSubset<T, CheckInCreateManyAndReturnArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$CheckInPayload<ExtArgs>, T, "createManyAndReturn", GlobalOmitOptions>>
/**
* Delete a CheckIn.
* @param {CheckInDeleteArgs} args - Arguments to delete one CheckIn.
@@ -954,6 +1003,36 @@ export interface CheckInDelegate<ExtArgs extends runtime.Types.Extensions.Intern
*/
updateMany<T extends CheckInUpdateManyArgs>(args: Prisma.SelectSubset<T, CheckInUpdateManyArgs<ExtArgs>>): Prisma.PrismaPromise<Prisma.BatchPayload>
/**
* Update zero or more CheckIns and returns the data updated in the database.
* @param {CheckInUpdateManyAndReturnArgs} args - Arguments to update many CheckIns.
* @example
* // Update many CheckIns
* const checkIn = await prisma.checkIn.updateManyAndReturn({
* where: {
* // ... provide filter here
* },
* data: [
* // ... provide data here
* ]
* })
*
* // Update zero or more CheckIns and only return the `id`
* const checkInWithIdOnly = await prisma.checkIn.updateManyAndReturn({
* select: { id: true },
* where: {
* // ... provide filter here
* },
* data: [
* // ... provide data here
* ]
* })
* Note, that providing `undefined` is treated as the value not being there.
* Read more here: https://pris.ly/d/null-undefined
*
*/
updateManyAndReturn<T extends CheckInUpdateManyAndReturnArgs>(args: Prisma.SelectSubset<T, CheckInUpdateManyAndReturnArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$CheckInPayload<ExtArgs>, T, "updateManyAndReturn", GlobalOmitOptions>>
/**
* Create or update one CheckIn.
* @param {CheckInUpsertArgs} args - Arguments to update or create a CheckIn.
@@ -1389,6 +1468,29 @@ export type CheckInCreateManyArgs<ExtArgs extends runtime.Types.Extensions.Inter
skipDuplicates?: boolean
}
/**
* CheckIn createManyAndReturn
*/
export type CheckInCreateManyAndReturnArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
/**
* Select specific fields to fetch from the CheckIn
*/
select?: Prisma.CheckInSelectCreateManyAndReturn<ExtArgs> | null
/**
* Omit specific fields from the CheckIn
*/
omit?: Prisma.CheckInOmit<ExtArgs> | null
/**
* The data used to create many CheckIns.
*/
data: Prisma.CheckInCreateManyInput | Prisma.CheckInCreateManyInput[]
skipDuplicates?: boolean
/**
* Choose, which related nodes to fetch as well
*/
include?: Prisma.CheckInIncludeCreateManyAndReturn<ExtArgs> | null
}
/**
* CheckIn update
*/
@@ -1433,6 +1535,36 @@ export type CheckInUpdateManyArgs<ExtArgs extends runtime.Types.Extensions.Inter
limit?: number
}
/**
* CheckIn updateManyAndReturn
*/
export type CheckInUpdateManyAndReturnArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
/**
* Select specific fields to fetch from the CheckIn
*/
select?: Prisma.CheckInSelectUpdateManyAndReturn<ExtArgs> | null
/**
* Omit specific fields from the CheckIn
*/
omit?: Prisma.CheckInOmit<ExtArgs> | null
/**
* The data used to update CheckIns.
*/
data: Prisma.XOR<Prisma.CheckInUpdateManyMutationInput, Prisma.CheckInUncheckedUpdateManyInput>
/**
* Filter which CheckIns to update
*/
where?: Prisma.CheckInWhereInput
/**
* Limit how many CheckIns to update.
*/
limit?: number
/**
* Choose, which related nodes to fetch as well
*/
include?: Prisma.CheckInIncludeUpdateManyAndReturn<ExtArgs> | null
}
/**
* CheckIn upsert
*/
+135 -7
View File
@@ -259,7 +259,6 @@ export type CheckListOrderByWithRelationInput = {
updatedAt?: Prisma.SortOrder
user?: Prisma.UserOrderByWithRelationInput
items?: Prisma.CheckListItemOrderByRelationAggregateInput
_relevance?: Prisma.CheckListOrderByRelevanceInput
}
export type CheckListWhereUniqueInput = Prisma.AtLeast<{
@@ -395,12 +394,6 @@ export type CheckListOrderByRelationAggregateInput = {
_count?: Prisma.SortOrder
}
export type CheckListOrderByRelevanceInput = {
fields: Prisma.CheckListOrderByRelevanceFieldEnum | Prisma.CheckListOrderByRelevanceFieldEnum[]
sort: Prisma.SortOrder
search: string
}
export type CheckListCountOrderByAggregateInput = {
id?: Prisma.SortOrder
userId?: Prisma.SortOrder
@@ -710,7 +703,29 @@ export type CheckListSelect<ExtArgs extends runtime.Types.Extensions.InternalArg
_count?: boolean | Prisma.CheckListCountOutputTypeDefaultArgs<ExtArgs>
}, ExtArgs["result"]["checkList"]>
export type CheckListSelectCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
id?: boolean
userId?: boolean
title?: boolean
scene?: boolean
note?: boolean
archived?: boolean
createdAt?: boolean
updatedAt?: boolean
user?: boolean | Prisma.UserDefaultArgs<ExtArgs>
}, ExtArgs["result"]["checkList"]>
export type CheckListSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
id?: boolean
userId?: boolean
title?: boolean
scene?: boolean
note?: boolean
archived?: boolean
createdAt?: boolean
updatedAt?: boolean
user?: boolean | Prisma.UserDefaultArgs<ExtArgs>
}, ExtArgs["result"]["checkList"]>
export type CheckListSelectScalar = {
id?: boolean
@@ -729,6 +744,12 @@ export type CheckListInclude<ExtArgs extends runtime.Types.Extensions.InternalAr
items?: boolean | Prisma.CheckList$itemsArgs<ExtArgs>
_count?: boolean | Prisma.CheckListCountOutputTypeDefaultArgs<ExtArgs>
}
export type CheckListIncludeCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
user?: boolean | Prisma.UserDefaultArgs<ExtArgs>
}
export type CheckListIncludeUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
user?: boolean | Prisma.UserDefaultArgs<ExtArgs>
}
export type $CheckListPayload<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
name: "CheckList"
@@ -862,6 +883,30 @@ export interface CheckListDelegate<ExtArgs extends runtime.Types.Extensions.Inte
*/
createMany<T extends CheckListCreateManyArgs>(args?: Prisma.SelectSubset<T, CheckListCreateManyArgs<ExtArgs>>): Prisma.PrismaPromise<Prisma.BatchPayload>
/**
* Create many CheckLists and returns the data saved in the database.
* @param {CheckListCreateManyAndReturnArgs} args - Arguments to create many CheckLists.
* @example
* // Create many CheckLists
* const checkList = await prisma.checkList.createManyAndReturn({
* data: [
* // ... provide data here
* ]
* })
*
* // Create many CheckLists and only return the `id`
* const checkListWithIdOnly = await prisma.checkList.createManyAndReturn({
* select: { id: true },
* data: [
* // ... provide data here
* ]
* })
* Note, that providing `undefined` is treated as the value not being there.
* Read more here: https://pris.ly/d/null-undefined
*
*/
createManyAndReturn<T extends CheckListCreateManyAndReturnArgs>(args?: Prisma.SelectSubset<T, CheckListCreateManyAndReturnArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$CheckListPayload<ExtArgs>, T, "createManyAndReturn", GlobalOmitOptions>>
/**
* Delete a CheckList.
* @param {CheckListDeleteArgs} args - Arguments to delete one CheckList.
@@ -926,6 +971,36 @@ export interface CheckListDelegate<ExtArgs extends runtime.Types.Extensions.Inte
*/
updateMany<T extends CheckListUpdateManyArgs>(args: Prisma.SelectSubset<T, CheckListUpdateManyArgs<ExtArgs>>): Prisma.PrismaPromise<Prisma.BatchPayload>
/**
* Update zero or more CheckLists and returns the data updated in the database.
* @param {CheckListUpdateManyAndReturnArgs} args - Arguments to update many CheckLists.
* @example
* // Update many CheckLists
* const checkList = await prisma.checkList.updateManyAndReturn({
* where: {
* // ... provide filter here
* },
* data: [
* // ... provide data here
* ]
* })
*
* // Update zero or more CheckLists and only return the `id`
* const checkListWithIdOnly = await prisma.checkList.updateManyAndReturn({
* select: { id: true },
* where: {
* // ... provide filter here
* },
* data: [
* // ... provide data here
* ]
* })
* Note, that providing `undefined` is treated as the value not being there.
* Read more here: https://pris.ly/d/null-undefined
*
*/
updateManyAndReturn<T extends CheckListUpdateManyAndReturnArgs>(args: Prisma.SelectSubset<T, CheckListUpdateManyAndReturnArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$CheckListPayload<ExtArgs>, T, "updateManyAndReturn", GlobalOmitOptions>>
/**
* Create or update one CheckList.
* @param {CheckListUpsertArgs} args - Arguments to update or create a CheckList.
@@ -1361,6 +1436,29 @@ export type CheckListCreateManyArgs<ExtArgs extends runtime.Types.Extensions.Int
skipDuplicates?: boolean
}
/**
* CheckList createManyAndReturn
*/
export type CheckListCreateManyAndReturnArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
/**
* Select specific fields to fetch from the CheckList
*/
select?: Prisma.CheckListSelectCreateManyAndReturn<ExtArgs> | null
/**
* Omit specific fields from the CheckList
*/
omit?: Prisma.CheckListOmit<ExtArgs> | null
/**
* The data used to create many CheckLists.
*/
data: Prisma.CheckListCreateManyInput | Prisma.CheckListCreateManyInput[]
skipDuplicates?: boolean
/**
* Choose, which related nodes to fetch as well
*/
include?: Prisma.CheckListIncludeCreateManyAndReturn<ExtArgs> | null
}
/**
* CheckList update
*/
@@ -1405,6 +1503,36 @@ export type CheckListUpdateManyArgs<ExtArgs extends runtime.Types.Extensions.Int
limit?: number
}
/**
* CheckList updateManyAndReturn
*/
export type CheckListUpdateManyAndReturnArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
/**
* Select specific fields to fetch from the CheckList
*/
select?: Prisma.CheckListSelectUpdateManyAndReturn<ExtArgs> | null
/**
* Omit specific fields from the CheckList
*/
omit?: Prisma.CheckListOmit<ExtArgs> | null
/**
* The data used to update CheckLists.
*/
data: Prisma.XOR<Prisma.CheckListUpdateManyMutationInput, Prisma.CheckListUncheckedUpdateManyInput>
/**
* Filter which CheckLists to update
*/
where?: Prisma.CheckListWhereInput
/**
* Limit how many CheckLists to update.
*/
limit?: number
/**
* Choose, which related nodes to fetch as well
*/
include?: Prisma.CheckListIncludeUpdateManyAndReturn<ExtArgs> | null
}
/**
* CheckList upsert
*/
+137 -7
View File
@@ -270,7 +270,6 @@ export type CheckListItemOrderByWithRelationInput = {
createdAt?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder
list?: Prisma.CheckListOrderByWithRelationInput
_relevance?: Prisma.CheckListItemOrderByRelevanceInput
}
export type CheckListItemWhereUniqueInput = Prisma.AtLeast<{
@@ -411,12 +410,6 @@ export type CheckListItemOrderByRelationAggregateInput = {
_count?: Prisma.SortOrder
}
export type CheckListItemOrderByRelevanceInput = {
fields: Prisma.CheckListItemOrderByRelevanceFieldEnum | Prisma.CheckListItemOrderByRelevanceFieldEnum[]
sort: Prisma.SortOrder
search: string
}
export type CheckListItemCountOrderByAggregateInput = {
id?: Prisma.SortOrder
listId?: Prisma.SortOrder
@@ -627,7 +620,31 @@ export type CheckListItemSelect<ExtArgs extends runtime.Types.Extensions.Interna
list?: boolean | Prisma.CheckListDefaultArgs<ExtArgs>
}, ExtArgs["result"]["checkListItem"]>
export type CheckListItemSelectCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
id?: boolean
listId?: boolean
title?: boolean
note?: boolean
done?: boolean
doneAt?: boolean
sortOrder?: boolean
createdAt?: boolean
updatedAt?: boolean
list?: boolean | Prisma.CheckListDefaultArgs<ExtArgs>
}, ExtArgs["result"]["checkListItem"]>
export type CheckListItemSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
id?: boolean
listId?: boolean
title?: boolean
note?: boolean
done?: boolean
doneAt?: boolean
sortOrder?: boolean
createdAt?: boolean
updatedAt?: boolean
list?: boolean | Prisma.CheckListDefaultArgs<ExtArgs>
}, ExtArgs["result"]["checkListItem"]>
export type CheckListItemSelectScalar = {
id?: boolean
@@ -645,6 +662,12 @@ export type CheckListItemOmit<ExtArgs extends runtime.Types.Extensions.InternalA
export type CheckListItemInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
list?: boolean | Prisma.CheckListDefaultArgs<ExtArgs>
}
export type CheckListItemIncludeCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
list?: boolean | Prisma.CheckListDefaultArgs<ExtArgs>
}
export type CheckListItemIncludeUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
list?: boolean | Prisma.CheckListDefaultArgs<ExtArgs>
}
export type $CheckListItemPayload<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
name: "CheckListItem"
@@ -778,6 +801,30 @@ export interface CheckListItemDelegate<ExtArgs extends runtime.Types.Extensions.
*/
createMany<T extends CheckListItemCreateManyArgs>(args?: Prisma.SelectSubset<T, CheckListItemCreateManyArgs<ExtArgs>>): Prisma.PrismaPromise<Prisma.BatchPayload>
/**
* Create many CheckListItems and returns the data saved in the database.
* @param {CheckListItemCreateManyAndReturnArgs} args - Arguments to create many CheckListItems.
* @example
* // Create many CheckListItems
* const checkListItem = await prisma.checkListItem.createManyAndReturn({
* data: [
* // ... provide data here
* ]
* })
*
* // Create many CheckListItems and only return the `id`
* const checkListItemWithIdOnly = await prisma.checkListItem.createManyAndReturn({
* select: { id: true },
* data: [
* // ... provide data here
* ]
* })
* Note, that providing `undefined` is treated as the value not being there.
* Read more here: https://pris.ly/d/null-undefined
*
*/
createManyAndReturn<T extends CheckListItemCreateManyAndReturnArgs>(args?: Prisma.SelectSubset<T, CheckListItemCreateManyAndReturnArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$CheckListItemPayload<ExtArgs>, T, "createManyAndReturn", GlobalOmitOptions>>
/**
* Delete a CheckListItem.
* @param {CheckListItemDeleteArgs} args - Arguments to delete one CheckListItem.
@@ -842,6 +889,36 @@ export interface CheckListItemDelegate<ExtArgs extends runtime.Types.Extensions.
*/
updateMany<T extends CheckListItemUpdateManyArgs>(args: Prisma.SelectSubset<T, CheckListItemUpdateManyArgs<ExtArgs>>): Prisma.PrismaPromise<Prisma.BatchPayload>
/**
* Update zero or more CheckListItems and returns the data updated in the database.
* @param {CheckListItemUpdateManyAndReturnArgs} args - Arguments to update many CheckListItems.
* @example
* // Update many CheckListItems
* const checkListItem = await prisma.checkListItem.updateManyAndReturn({
* where: {
* // ... provide filter here
* },
* data: [
* // ... provide data here
* ]
* })
*
* // Update zero or more CheckListItems and only return the `id`
* const checkListItemWithIdOnly = await prisma.checkListItem.updateManyAndReturn({
* select: { id: true },
* where: {
* // ... provide filter here
* },
* data: [
* // ... provide data here
* ]
* })
* Note, that providing `undefined` is treated as the value not being there.
* Read more here: https://pris.ly/d/null-undefined
*
*/
updateManyAndReturn<T extends CheckListItemUpdateManyAndReturnArgs>(args: Prisma.SelectSubset<T, CheckListItemUpdateManyAndReturnArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$CheckListItemPayload<ExtArgs>, T, "updateManyAndReturn", GlobalOmitOptions>>
/**
* Create or update one CheckListItem.
* @param {CheckListItemUpsertArgs} args - Arguments to update or create a CheckListItem.
@@ -1277,6 +1354,29 @@ export type CheckListItemCreateManyArgs<ExtArgs extends runtime.Types.Extensions
skipDuplicates?: boolean
}
/**
* CheckListItem createManyAndReturn
*/
export type CheckListItemCreateManyAndReturnArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
/**
* Select specific fields to fetch from the CheckListItem
*/
select?: Prisma.CheckListItemSelectCreateManyAndReturn<ExtArgs> | null
/**
* Omit specific fields from the CheckListItem
*/
omit?: Prisma.CheckListItemOmit<ExtArgs> | null
/**
* The data used to create many CheckListItems.
*/
data: Prisma.CheckListItemCreateManyInput | Prisma.CheckListItemCreateManyInput[]
skipDuplicates?: boolean
/**
* Choose, which related nodes to fetch as well
*/
include?: Prisma.CheckListItemIncludeCreateManyAndReturn<ExtArgs> | null
}
/**
* CheckListItem update
*/
@@ -1321,6 +1421,36 @@ export type CheckListItemUpdateManyArgs<ExtArgs extends runtime.Types.Extensions
limit?: number
}
/**
* CheckListItem updateManyAndReturn
*/
export type CheckListItemUpdateManyAndReturnArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
/**
* Select specific fields to fetch from the CheckListItem
*/
select?: Prisma.CheckListItemSelectUpdateManyAndReturn<ExtArgs> | null
/**
* Omit specific fields from the CheckListItem
*/
omit?: Prisma.CheckListItemOmit<ExtArgs> | null
/**
* The data used to update CheckListItems.
*/
data: Prisma.XOR<Prisma.CheckListItemUpdateManyMutationInput, Prisma.CheckListItemUncheckedUpdateManyInput>
/**
* Filter which CheckListItems to update
*/
where?: Prisma.CheckListItemWhereInput
/**
* Limit how many CheckListItems to update.
*/
limit?: number
/**
* Choose, which related nodes to fetch as well
*/
include?: Prisma.CheckListItemIncludeUpdateManyAndReturn<ExtArgs> | null
}
/**
* CheckListItem upsert
*/
+135 -7
View File
@@ -253,7 +253,6 @@ export type CheckListTemplateOrderByWithRelationInput = {
createdAt?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder
user?: Prisma.UserOrderByWithRelationInput
_relevance?: Prisma.CheckListTemplateOrderByRelevanceInput
}
export type CheckListTemplateWhereUniqueInput = Prisma.AtLeast<{
@@ -384,12 +383,6 @@ export type CheckListTemplateOrderByRelationAggregateInput = {
_count?: Prisma.SortOrder
}
export type CheckListTemplateOrderByRelevanceInput = {
fields: Prisma.CheckListTemplateOrderByRelevanceFieldEnum | Prisma.CheckListTemplateOrderByRelevanceFieldEnum[]
sort: Prisma.SortOrder
search: string
}
export type CheckListTemplateCountOrderByAggregateInput = {
id?: Prisma.SortOrder
userId?: Prisma.SortOrder
@@ -585,7 +578,29 @@ export type CheckListTemplateSelect<ExtArgs extends runtime.Types.Extensions.Int
user?: boolean | Prisma.CheckListTemplate$userArgs<ExtArgs>
}, ExtArgs["result"]["checkListTemplate"]>
export type CheckListTemplateSelectCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
id?: boolean
userId?: boolean
name?: boolean
scene?: boolean
items?: boolean
builtin?: boolean
createdAt?: boolean
updatedAt?: boolean
user?: boolean | Prisma.CheckListTemplate$userArgs<ExtArgs>
}, ExtArgs["result"]["checkListTemplate"]>
export type CheckListTemplateSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
id?: boolean
userId?: boolean
name?: boolean
scene?: boolean
items?: boolean
builtin?: boolean
createdAt?: boolean
updatedAt?: boolean
user?: boolean | Prisma.CheckListTemplate$userArgs<ExtArgs>
}, ExtArgs["result"]["checkListTemplate"]>
export type CheckListTemplateSelectScalar = {
id?: boolean
@@ -602,6 +617,12 @@ export type CheckListTemplateOmit<ExtArgs extends runtime.Types.Extensions.Inter
export type CheckListTemplateInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
user?: boolean | Prisma.CheckListTemplate$userArgs<ExtArgs>
}
export type CheckListTemplateIncludeCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
user?: boolean | Prisma.CheckListTemplate$userArgs<ExtArgs>
}
export type CheckListTemplateIncludeUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
user?: boolean | Prisma.CheckListTemplate$userArgs<ExtArgs>
}
export type $CheckListTemplatePayload<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
name: "CheckListTemplate"
@@ -734,6 +755,30 @@ export interface CheckListTemplateDelegate<ExtArgs extends runtime.Types.Extensi
*/
createMany<T extends CheckListTemplateCreateManyArgs>(args?: Prisma.SelectSubset<T, CheckListTemplateCreateManyArgs<ExtArgs>>): Prisma.PrismaPromise<Prisma.BatchPayload>
/**
* Create many CheckListTemplates and returns the data saved in the database.
* @param {CheckListTemplateCreateManyAndReturnArgs} args - Arguments to create many CheckListTemplates.
* @example
* // Create many CheckListTemplates
* const checkListTemplate = await prisma.checkListTemplate.createManyAndReturn({
* data: [
* // ... provide data here
* ]
* })
*
* // Create many CheckListTemplates and only return the `id`
* const checkListTemplateWithIdOnly = await prisma.checkListTemplate.createManyAndReturn({
* select: { id: true },
* data: [
* // ... provide data here
* ]
* })
* Note, that providing `undefined` is treated as the value not being there.
* Read more here: https://pris.ly/d/null-undefined
*
*/
createManyAndReturn<T extends CheckListTemplateCreateManyAndReturnArgs>(args?: Prisma.SelectSubset<T, CheckListTemplateCreateManyAndReturnArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$CheckListTemplatePayload<ExtArgs>, T, "createManyAndReturn", GlobalOmitOptions>>
/**
* Delete a CheckListTemplate.
* @param {CheckListTemplateDeleteArgs} args - Arguments to delete one CheckListTemplate.
@@ -798,6 +843,36 @@ export interface CheckListTemplateDelegate<ExtArgs extends runtime.Types.Extensi
*/
updateMany<T extends CheckListTemplateUpdateManyArgs>(args: Prisma.SelectSubset<T, CheckListTemplateUpdateManyArgs<ExtArgs>>): Prisma.PrismaPromise<Prisma.BatchPayload>
/**
* Update zero or more CheckListTemplates and returns the data updated in the database.
* @param {CheckListTemplateUpdateManyAndReturnArgs} args - Arguments to update many CheckListTemplates.
* @example
* // Update many CheckListTemplates
* const checkListTemplate = await prisma.checkListTemplate.updateManyAndReturn({
* where: {
* // ... provide filter here
* },
* data: [
* // ... provide data here
* ]
* })
*
* // Update zero or more CheckListTemplates and only return the `id`
* const checkListTemplateWithIdOnly = await prisma.checkListTemplate.updateManyAndReturn({
* select: { id: true },
* where: {
* // ... provide filter here
* },
* data: [
* // ... provide data here
* ]
* })
* Note, that providing `undefined` is treated as the value not being there.
* Read more here: https://pris.ly/d/null-undefined
*
*/
updateManyAndReturn<T extends CheckListTemplateUpdateManyAndReturnArgs>(args: Prisma.SelectSubset<T, CheckListTemplateUpdateManyAndReturnArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$CheckListTemplatePayload<ExtArgs>, T, "updateManyAndReturn", GlobalOmitOptions>>
/**
* Create or update one CheckListTemplate.
* @param {CheckListTemplateUpsertArgs} args - Arguments to update or create a CheckListTemplate.
@@ -1232,6 +1307,29 @@ export type CheckListTemplateCreateManyArgs<ExtArgs extends runtime.Types.Extens
skipDuplicates?: boolean
}
/**
* CheckListTemplate createManyAndReturn
*/
export type CheckListTemplateCreateManyAndReturnArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
/**
* Select specific fields to fetch from the CheckListTemplate
*/
select?: Prisma.CheckListTemplateSelectCreateManyAndReturn<ExtArgs> | null
/**
* Omit specific fields from the CheckListTemplate
*/
omit?: Prisma.CheckListTemplateOmit<ExtArgs> | null
/**
* The data used to create many CheckListTemplates.
*/
data: Prisma.CheckListTemplateCreateManyInput | Prisma.CheckListTemplateCreateManyInput[]
skipDuplicates?: boolean
/**
* Choose, which related nodes to fetch as well
*/
include?: Prisma.CheckListTemplateIncludeCreateManyAndReturn<ExtArgs> | null
}
/**
* CheckListTemplate update
*/
@@ -1276,6 +1374,36 @@ export type CheckListTemplateUpdateManyArgs<ExtArgs extends runtime.Types.Extens
limit?: number
}
/**
* CheckListTemplate updateManyAndReturn
*/
export type CheckListTemplateUpdateManyAndReturnArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
/**
* Select specific fields to fetch from the CheckListTemplate
*/
select?: Prisma.CheckListTemplateSelectUpdateManyAndReturn<ExtArgs> | null
/**
* Omit specific fields from the CheckListTemplate
*/
omit?: Prisma.CheckListTemplateOmit<ExtArgs> | null
/**
* The data used to update CheckListTemplates.
*/
data: Prisma.XOR<Prisma.CheckListTemplateUpdateManyMutationInput, Prisma.CheckListTemplateUncheckedUpdateManyInput>
/**
* Filter which CheckListTemplates to update
*/
where?: Prisma.CheckListTemplateWhereInput
/**
* Limit how many CheckListTemplates to update.
*/
limit?: number
/**
* Choose, which related nodes to fetch as well
*/
include?: Prisma.CheckListTemplateIncludeUpdateManyAndReturn<ExtArgs> | null
}
/**
* CheckListTemplate upsert
*/
+113 -7
View File
@@ -228,7 +228,6 @@ export type Econ_SowInventoryOrderByWithRelationInput = {
inventory?: Prisma.SortOrder
createdAt?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder
_relevance?: Prisma.Econ_SowInventoryOrderByRelevanceInput
}
export type Econ_SowInventoryWhereUniqueInput = Prisma.AtLeast<{
@@ -319,12 +318,6 @@ export type Econ_SowInventoryUncheckedUpdateManyInput = {
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
}
export type Econ_SowInventoryOrderByRelevanceInput = {
fields: Prisma.Econ_SowInventoryOrderByRelevanceFieldEnum | Prisma.Econ_SowInventoryOrderByRelevanceFieldEnum[]
sort: Prisma.SortOrder
search: string
}
export type Econ_SowInventoryCountOrderByAggregateInput = {
id?: Prisma.SortOrder
month?: Prisma.SortOrder
@@ -369,7 +362,21 @@ export type Econ_SowInventorySelect<ExtArgs extends runtime.Types.Extensions.Int
updatedAt?: boolean
}, ExtArgs["result"]["econ_SowInventory"]>
export type Econ_SowInventorySelectCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
id?: boolean
month?: boolean
inventory?: boolean
createdAt?: boolean
updatedAt?: boolean
}, ExtArgs["result"]["econ_SowInventory"]>
export type Econ_SowInventorySelectUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
id?: boolean
month?: boolean
inventory?: boolean
createdAt?: boolean
updatedAt?: boolean
}, ExtArgs["result"]["econ_SowInventory"]>
export type Econ_SowInventorySelectScalar = {
id?: boolean
@@ -507,6 +514,30 @@ export interface Econ_SowInventoryDelegate<ExtArgs extends runtime.Types.Extensi
*/
createMany<T extends Econ_SowInventoryCreateManyArgs>(args?: Prisma.SelectSubset<T, Econ_SowInventoryCreateManyArgs<ExtArgs>>): Prisma.PrismaPromise<Prisma.BatchPayload>
/**
* Create many Econ_SowInventories and returns the data saved in the database.
* @param {Econ_SowInventoryCreateManyAndReturnArgs} args - Arguments to create many Econ_SowInventories.
* @example
* // Create many Econ_SowInventories
* const econ_SowInventory = await prisma.econ_SowInventory.createManyAndReturn({
* data: [
* // ... provide data here
* ]
* })
*
* // Create many Econ_SowInventories and only return the `id`
* const econ_SowInventoryWithIdOnly = await prisma.econ_SowInventory.createManyAndReturn({
* select: { id: true },
* data: [
* // ... provide data here
* ]
* })
* Note, that providing `undefined` is treated as the value not being there.
* Read more here: https://pris.ly/d/null-undefined
*
*/
createManyAndReturn<T extends Econ_SowInventoryCreateManyAndReturnArgs>(args?: Prisma.SelectSubset<T, Econ_SowInventoryCreateManyAndReturnArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$Econ_SowInventoryPayload<ExtArgs>, T, "createManyAndReturn", GlobalOmitOptions>>
/**
* Delete a Econ_SowInventory.
* @param {Econ_SowInventoryDeleteArgs} args - Arguments to delete one Econ_SowInventory.
@@ -571,6 +602,36 @@ export interface Econ_SowInventoryDelegate<ExtArgs extends runtime.Types.Extensi
*/
updateMany<T extends Econ_SowInventoryUpdateManyArgs>(args: Prisma.SelectSubset<T, Econ_SowInventoryUpdateManyArgs<ExtArgs>>): Prisma.PrismaPromise<Prisma.BatchPayload>
/**
* Update zero or more Econ_SowInventories and returns the data updated in the database.
* @param {Econ_SowInventoryUpdateManyAndReturnArgs} args - Arguments to update many Econ_SowInventories.
* @example
* // Update many Econ_SowInventories
* const econ_SowInventory = await prisma.econ_SowInventory.updateManyAndReturn({
* where: {
* // ... provide filter here
* },
* data: [
* // ... provide data here
* ]
* })
*
* // Update zero or more Econ_SowInventories and only return the `id`
* const econ_SowInventoryWithIdOnly = await prisma.econ_SowInventory.updateManyAndReturn({
* select: { id: true },
* where: {
* // ... provide filter here
* },
* data: [
* // ... provide data here
* ]
* })
* Note, that providing `undefined` is treated as the value not being there.
* Read more here: https://pris.ly/d/null-undefined
*
*/
updateManyAndReturn<T extends Econ_SowInventoryUpdateManyAndReturnArgs>(args: Prisma.SelectSubset<T, Econ_SowInventoryUpdateManyAndReturnArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$Econ_SowInventoryPayload<ExtArgs>, T, "updateManyAndReturn", GlobalOmitOptions>>
/**
* Create or update one Econ_SowInventory.
* @param {Econ_SowInventoryUpsertArgs} args - Arguments to update or create a Econ_SowInventory.
@@ -977,6 +1038,25 @@ export type Econ_SowInventoryCreateManyArgs<ExtArgs extends runtime.Types.Extens
skipDuplicates?: boolean
}
/**
* Econ_SowInventory createManyAndReturn
*/
export type Econ_SowInventoryCreateManyAndReturnArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
/**
* Select specific fields to fetch from the Econ_SowInventory
*/
select?: Prisma.Econ_SowInventorySelectCreateManyAndReturn<ExtArgs> | null
/**
* Omit specific fields from the Econ_SowInventory
*/
omit?: Prisma.Econ_SowInventoryOmit<ExtArgs> | null
/**
* The data used to create many Econ_SowInventories.
*/
data: Prisma.Econ_SowInventoryCreateManyInput | Prisma.Econ_SowInventoryCreateManyInput[]
skipDuplicates?: boolean
}
/**
* Econ_SowInventory update
*/
@@ -1017,6 +1097,32 @@ export type Econ_SowInventoryUpdateManyArgs<ExtArgs extends runtime.Types.Extens
limit?: number
}
/**
* Econ_SowInventory updateManyAndReturn
*/
export type Econ_SowInventoryUpdateManyAndReturnArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
/**
* Select specific fields to fetch from the Econ_SowInventory
*/
select?: Prisma.Econ_SowInventorySelectUpdateManyAndReturn<ExtArgs> | null
/**
* Omit specific fields from the Econ_SowInventory
*/
omit?: Prisma.Econ_SowInventoryOmit<ExtArgs> | null
/**
* The data used to update Econ_SowInventories.
*/
data: Prisma.XOR<Prisma.Econ_SowInventoryUpdateManyMutationInput, Prisma.Econ_SowInventoryUncheckedUpdateManyInput>
/**
* Filter which Econ_SowInventories to update
*/
where?: Prisma.Econ_SowInventoryWhereInput
/**
* Limit how many Econ_SowInventories to update.
*/
limit?: number
}
/**
* Econ_SowInventory upsert
*/
+143 -7
View File
@@ -303,7 +303,6 @@ export type HabitOrderByWithRelationInput = {
updatedAt?: Prisma.SortOrder
user?: Prisma.UserOrderByWithRelationInput
records?: Prisma.CheckInOrderByRelationAggregateInput
_relevance?: Prisma.HabitOrderByRelevanceInput
}
export type HabitWhereUniqueInput = Prisma.AtLeast<{
@@ -479,12 +478,6 @@ export type HabitOrderByRelationAggregateInput = {
_count?: Prisma.SortOrder
}
export type HabitOrderByRelevanceInput = {
fields: Prisma.HabitOrderByRelevanceFieldEnum | Prisma.HabitOrderByRelevanceFieldEnum[]
sort: Prisma.SortOrder
search: string
}
export type HabitCountOrderByAggregateInput = {
id?: Prisma.SortOrder
userId?: Prisma.SortOrder
@@ -858,7 +851,37 @@ export type HabitSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs =
_count?: boolean | Prisma.HabitCountOutputTypeDefaultArgs<ExtArgs>
}, ExtArgs["result"]["habit"]>
export type HabitSelectCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
id?: boolean
userId?: boolean
name?: boolean
period?: boolean
target?: boolean
color?: boolean
icon?: boolean
note?: boolean
archived?: boolean
sortOrder?: boolean
createdAt?: boolean
updatedAt?: boolean
user?: boolean | Prisma.UserDefaultArgs<ExtArgs>
}, ExtArgs["result"]["habit"]>
export type HabitSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
id?: boolean
userId?: boolean
name?: boolean
period?: boolean
target?: boolean
color?: boolean
icon?: boolean
note?: boolean
archived?: boolean
sortOrder?: boolean
createdAt?: boolean
updatedAt?: boolean
user?: boolean | Prisma.UserDefaultArgs<ExtArgs>
}, ExtArgs["result"]["habit"]>
export type HabitSelectScalar = {
id?: boolean
@@ -881,6 +904,12 @@ export type HabitInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs =
records?: boolean | Prisma.Habit$recordsArgs<ExtArgs>
_count?: boolean | Prisma.HabitCountOutputTypeDefaultArgs<ExtArgs>
}
export type HabitIncludeCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
user?: boolean | Prisma.UserDefaultArgs<ExtArgs>
}
export type HabitIncludeUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
user?: boolean | Prisma.UserDefaultArgs<ExtArgs>
}
export type $HabitPayload<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
name: "Habit"
@@ -1018,6 +1047,30 @@ export interface HabitDelegate<ExtArgs extends runtime.Types.Extensions.Internal
*/
createMany<T extends HabitCreateManyArgs>(args?: Prisma.SelectSubset<T, HabitCreateManyArgs<ExtArgs>>): Prisma.PrismaPromise<Prisma.BatchPayload>
/**
* Create many Habits and returns the data saved in the database.
* @param {HabitCreateManyAndReturnArgs} args - Arguments to create many Habits.
* @example
* // Create many Habits
* const habit = await prisma.habit.createManyAndReturn({
* data: [
* // ... provide data here
* ]
* })
*
* // Create many Habits and only return the `id`
* const habitWithIdOnly = await prisma.habit.createManyAndReturn({
* select: { id: true },
* data: [
* // ... provide data here
* ]
* })
* Note, that providing `undefined` is treated as the value not being there.
* Read more here: https://pris.ly/d/null-undefined
*
*/
createManyAndReturn<T extends HabitCreateManyAndReturnArgs>(args?: Prisma.SelectSubset<T, HabitCreateManyAndReturnArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$HabitPayload<ExtArgs>, T, "createManyAndReturn", GlobalOmitOptions>>
/**
* Delete a Habit.
* @param {HabitDeleteArgs} args - Arguments to delete one Habit.
@@ -1082,6 +1135,36 @@ export interface HabitDelegate<ExtArgs extends runtime.Types.Extensions.Internal
*/
updateMany<T extends HabitUpdateManyArgs>(args: Prisma.SelectSubset<T, HabitUpdateManyArgs<ExtArgs>>): Prisma.PrismaPromise<Prisma.BatchPayload>
/**
* Update zero or more Habits and returns the data updated in the database.
* @param {HabitUpdateManyAndReturnArgs} args - Arguments to update many Habits.
* @example
* // Update many Habits
* const habit = await prisma.habit.updateManyAndReturn({
* where: {
* // ... provide filter here
* },
* data: [
* // ... provide data here
* ]
* })
*
* // Update zero or more Habits and only return the `id`
* const habitWithIdOnly = await prisma.habit.updateManyAndReturn({
* select: { id: true },
* where: {
* // ... provide filter here
* },
* data: [
* // ... provide data here
* ]
* })
* Note, that providing `undefined` is treated as the value not being there.
* Read more here: https://pris.ly/d/null-undefined
*
*/
updateManyAndReturn<T extends HabitUpdateManyAndReturnArgs>(args: Prisma.SelectSubset<T, HabitUpdateManyAndReturnArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$HabitPayload<ExtArgs>, T, "updateManyAndReturn", GlobalOmitOptions>>
/**
* Create or update one Habit.
* @param {HabitUpsertArgs} args - Arguments to update or create a Habit.
@@ -1521,6 +1604,29 @@ export type HabitCreateManyArgs<ExtArgs extends runtime.Types.Extensions.Interna
skipDuplicates?: boolean
}
/**
* Habit createManyAndReturn
*/
export type HabitCreateManyAndReturnArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
/**
* Select specific fields to fetch from the Habit
*/
select?: Prisma.HabitSelectCreateManyAndReturn<ExtArgs> | null
/**
* Omit specific fields from the Habit
*/
omit?: Prisma.HabitOmit<ExtArgs> | null
/**
* The data used to create many Habits.
*/
data: Prisma.HabitCreateManyInput | Prisma.HabitCreateManyInput[]
skipDuplicates?: boolean
/**
* Choose, which related nodes to fetch as well
*/
include?: Prisma.HabitIncludeCreateManyAndReturn<ExtArgs> | null
}
/**
* Habit update
*/
@@ -1565,6 +1671,36 @@ export type HabitUpdateManyArgs<ExtArgs extends runtime.Types.Extensions.Interna
limit?: number
}
/**
* Habit updateManyAndReturn
*/
export type HabitUpdateManyAndReturnArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
/**
* Select specific fields to fetch from the Habit
*/
select?: Prisma.HabitSelectUpdateManyAndReturn<ExtArgs> | null
/**
* Omit specific fields from the Habit
*/
omit?: Prisma.HabitOmit<ExtArgs> | null
/**
* The data used to update Habits.
*/
data: Prisma.XOR<Prisma.HabitUpdateManyMutationInput, Prisma.HabitUncheckedUpdateManyInput>
/**
* Filter which Habits to update
*/
where?: Prisma.HabitWhereInput
/**
* Limit how many Habits to update.
*/
limit?: number
/**
* Choose, which related nodes to fetch as well
*/
include?: Prisma.HabitIncludeUpdateManyAndReturn<ExtArgs> | null
}
/**
* Habit upsert
*/
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+115 -7
View File
@@ -233,7 +233,6 @@ export type MihomeAccountOrderByWithRelationInput = {
status?: Prisma.SortOrder
lastError?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder
_relevance?: Prisma.MihomeAccountOrderByRelevanceInput
}
export type MihomeAccountWhereUniqueInput = Prisma.AtLeast<{
@@ -334,12 +333,6 @@ export type MihomeAccountUncheckedUpdateManyInput = {
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
}
export type MihomeAccountOrderByRelevanceInput = {
fields: Prisma.MihomeAccountOrderByRelevanceFieldEnum | Prisma.MihomeAccountOrderByRelevanceFieldEnum[]
sort: Prisma.SortOrder
search: string
}
export type MihomeAccountCountOrderByAggregateInput = {
id?: Prisma.SortOrder
key?: Prisma.SortOrder
@@ -386,7 +379,23 @@ export type MihomeAccountSelect<ExtArgs extends runtime.Types.Extensions.Interna
updatedAt?: boolean
}, ExtArgs["result"]["mihomeAccount"]>
export type MihomeAccountSelectCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
id?: boolean
key?: boolean
userId?: boolean
status?: boolean
lastError?: boolean
updatedAt?: boolean
}, ExtArgs["result"]["mihomeAccount"]>
export type MihomeAccountSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
id?: boolean
key?: boolean
userId?: boolean
status?: boolean
lastError?: boolean
updatedAt?: boolean
}, ExtArgs["result"]["mihomeAccount"]>
export type MihomeAccountSelectScalar = {
id?: boolean
@@ -526,6 +535,30 @@ export interface MihomeAccountDelegate<ExtArgs extends runtime.Types.Extensions.
*/
createMany<T extends MihomeAccountCreateManyArgs>(args?: Prisma.SelectSubset<T, MihomeAccountCreateManyArgs<ExtArgs>>): Prisma.PrismaPromise<Prisma.BatchPayload>
/**
* Create many MihomeAccounts and returns the data saved in the database.
* @param {MihomeAccountCreateManyAndReturnArgs} args - Arguments to create many MihomeAccounts.
* @example
* // Create many MihomeAccounts
* const mihomeAccount = await prisma.mihomeAccount.createManyAndReturn({
* data: [
* // ... provide data here
* ]
* })
*
* // Create many MihomeAccounts and only return the `id`
* const mihomeAccountWithIdOnly = await prisma.mihomeAccount.createManyAndReturn({
* select: { id: true },
* data: [
* // ... provide data here
* ]
* })
* Note, that providing `undefined` is treated as the value not being there.
* Read more here: https://pris.ly/d/null-undefined
*
*/
createManyAndReturn<T extends MihomeAccountCreateManyAndReturnArgs>(args?: Prisma.SelectSubset<T, MihomeAccountCreateManyAndReturnArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$MihomeAccountPayload<ExtArgs>, T, "createManyAndReturn", GlobalOmitOptions>>
/**
* Delete a MihomeAccount.
* @param {MihomeAccountDeleteArgs} args - Arguments to delete one MihomeAccount.
@@ -590,6 +623,36 @@ export interface MihomeAccountDelegate<ExtArgs extends runtime.Types.Extensions.
*/
updateMany<T extends MihomeAccountUpdateManyArgs>(args: Prisma.SelectSubset<T, MihomeAccountUpdateManyArgs<ExtArgs>>): Prisma.PrismaPromise<Prisma.BatchPayload>
/**
* Update zero or more MihomeAccounts and returns the data updated in the database.
* @param {MihomeAccountUpdateManyAndReturnArgs} args - Arguments to update many MihomeAccounts.
* @example
* // Update many MihomeAccounts
* const mihomeAccount = await prisma.mihomeAccount.updateManyAndReturn({
* where: {
* // ... provide filter here
* },
* data: [
* // ... provide data here
* ]
* })
*
* // Update zero or more MihomeAccounts and only return the `id`
* const mihomeAccountWithIdOnly = await prisma.mihomeAccount.updateManyAndReturn({
* select: { id: true },
* where: {
* // ... provide filter here
* },
* data: [
* // ... provide data here
* ]
* })
* Note, that providing `undefined` is treated as the value not being there.
* Read more here: https://pris.ly/d/null-undefined
*
*/
updateManyAndReturn<T extends MihomeAccountUpdateManyAndReturnArgs>(args: Prisma.SelectSubset<T, MihomeAccountUpdateManyAndReturnArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$MihomeAccountPayload<ExtArgs>, T, "updateManyAndReturn", GlobalOmitOptions>>
/**
* Create or update one MihomeAccount.
* @param {MihomeAccountUpsertArgs} args - Arguments to update or create a MihomeAccount.
@@ -997,6 +1060,25 @@ export type MihomeAccountCreateManyArgs<ExtArgs extends runtime.Types.Extensions
skipDuplicates?: boolean
}
/**
* MihomeAccount createManyAndReturn
*/
export type MihomeAccountCreateManyAndReturnArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
/**
* Select specific fields to fetch from the MihomeAccount
*/
select?: Prisma.MihomeAccountSelectCreateManyAndReturn<ExtArgs> | null
/**
* Omit specific fields from the MihomeAccount
*/
omit?: Prisma.MihomeAccountOmit<ExtArgs> | null
/**
* The data used to create many MihomeAccounts.
*/
data: Prisma.MihomeAccountCreateManyInput | Prisma.MihomeAccountCreateManyInput[]
skipDuplicates?: boolean
}
/**
* MihomeAccount update
*/
@@ -1037,6 +1119,32 @@ export type MihomeAccountUpdateManyArgs<ExtArgs extends runtime.Types.Extensions
limit?: number
}
/**
* MihomeAccount updateManyAndReturn
*/
export type MihomeAccountUpdateManyAndReturnArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
/**
* Select specific fields to fetch from the MihomeAccount
*/
select?: Prisma.MihomeAccountSelectUpdateManyAndReturn<ExtArgs> | null
/**
* Omit specific fields from the MihomeAccount
*/
omit?: Prisma.MihomeAccountOmit<ExtArgs> | null
/**
* The data used to update MihomeAccounts.
*/
data: Prisma.XOR<Prisma.MihomeAccountUpdateManyMutationInput, Prisma.MihomeAccountUncheckedUpdateManyInput>
/**
* Filter which MihomeAccounts to update
*/
where?: Prisma.MihomeAccountWhereInput
/**
* Limit how many MihomeAccounts to update.
*/
limit?: number
}
/**
* MihomeAccount upsert
*/
+157 -7
View File
@@ -366,7 +366,6 @@ export type MihomeDeviceOrderByWithRelationInput = {
home?: Prisma.MihomeHomeOrderByWithRelationInput
samples?: Prisma.MihomeEnvSampleOrderByRelationAggregateInput
hours?: Prisma.MihomeEnvHourOrderByRelationAggregateInput
_relevance?: Prisma.MihomeDeviceOrderByRelevanceInput
}
export type MihomeDeviceWhereUniqueInput = Prisma.AtLeast<{
@@ -597,12 +596,6 @@ export type MihomeDeviceOrderByRelationAggregateInput = {
_count?: Prisma.SortOrder
}
export type MihomeDeviceOrderByRelevanceInput = {
fields: Prisma.MihomeDeviceOrderByRelevanceFieldEnum | Prisma.MihomeDeviceOrderByRelevanceFieldEnum[]
sort: Prisma.SortOrder
search: string
}
export type MihomeDeviceCountOrderByAggregateInput = {
id?: Prisma.SortOrder
did?: Prisma.SortOrder
@@ -744,6 +737,10 @@ export type NullableDateTimeFieldUpdateOperationsInput = {
set?: Date | string | null
}
export type BoolFieldUpdateOperationsInput = {
set?: boolean
}
export type MihomeDeviceCreateNestedOneWithoutSamplesInput = {
create?: Prisma.XOR<Prisma.MihomeDeviceCreateWithoutSamplesInput, Prisma.MihomeDeviceUncheckedCreateWithoutSamplesInput>
connectOrCreate?: Prisma.MihomeDeviceCreateOrConnectWithoutSamplesInput
@@ -1201,7 +1198,47 @@ export type MihomeDeviceSelect<ExtArgs extends runtime.Types.Extensions.Internal
_count?: boolean | Prisma.MihomeDeviceCountOutputTypeDefaultArgs<ExtArgs>
}, ExtArgs["result"]["mihomeDevice"]>
export type MihomeDeviceSelectCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
id?: boolean
did?: boolean
homeId?: boolean
model?: boolean
name?: boolean
alias?: boolean
tempSiid?: boolean
tempPiid?: boolean
humSiid?: boolean
humPiid?: boolean
battSiid?: boolean
battPiid?: boolean
battery?: boolean
batteryAt?: boolean
enabled?: boolean
lastSyncAt?: boolean
createdAt?: boolean
home?: boolean | Prisma.MihomeHomeDefaultArgs<ExtArgs>
}, ExtArgs["result"]["mihomeDevice"]>
export type MihomeDeviceSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
id?: boolean
did?: boolean
homeId?: boolean
model?: boolean
name?: boolean
alias?: boolean
tempSiid?: boolean
tempPiid?: boolean
humSiid?: boolean
humPiid?: boolean
battSiid?: boolean
battPiid?: boolean
battery?: boolean
batteryAt?: boolean
enabled?: boolean
lastSyncAt?: boolean
createdAt?: boolean
home?: boolean | Prisma.MihomeHomeDefaultArgs<ExtArgs>
}, ExtArgs["result"]["mihomeDevice"]>
export type MihomeDeviceSelectScalar = {
id?: boolean
@@ -1230,6 +1267,12 @@ export type MihomeDeviceInclude<ExtArgs extends runtime.Types.Extensions.Interna
hours?: boolean | Prisma.MihomeDevice$hoursArgs<ExtArgs>
_count?: boolean | Prisma.MihomeDeviceCountOutputTypeDefaultArgs<ExtArgs>
}
export type MihomeDeviceIncludeCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
home?: boolean | Prisma.MihomeHomeDefaultArgs<ExtArgs>
}
export type MihomeDeviceIncludeUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
home?: boolean | Prisma.MihomeHomeDefaultArgs<ExtArgs>
}
export type $MihomeDevicePayload<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
name: "MihomeDevice"
@@ -1373,6 +1416,30 @@ export interface MihomeDeviceDelegate<ExtArgs extends runtime.Types.Extensions.I
*/
createMany<T extends MihomeDeviceCreateManyArgs>(args?: Prisma.SelectSubset<T, MihomeDeviceCreateManyArgs<ExtArgs>>): Prisma.PrismaPromise<Prisma.BatchPayload>
/**
* Create many MihomeDevices and returns the data saved in the database.
* @param {MihomeDeviceCreateManyAndReturnArgs} args - Arguments to create many MihomeDevices.
* @example
* // Create many MihomeDevices
* const mihomeDevice = await prisma.mihomeDevice.createManyAndReturn({
* data: [
* // ... provide data here
* ]
* })
*
* // Create many MihomeDevices and only return the `id`
* const mihomeDeviceWithIdOnly = await prisma.mihomeDevice.createManyAndReturn({
* select: { id: true },
* data: [
* // ... provide data here
* ]
* })
* Note, that providing `undefined` is treated as the value not being there.
* Read more here: https://pris.ly/d/null-undefined
*
*/
createManyAndReturn<T extends MihomeDeviceCreateManyAndReturnArgs>(args?: Prisma.SelectSubset<T, MihomeDeviceCreateManyAndReturnArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$MihomeDevicePayload<ExtArgs>, T, "createManyAndReturn", GlobalOmitOptions>>
/**
* Delete a MihomeDevice.
* @param {MihomeDeviceDeleteArgs} args - Arguments to delete one MihomeDevice.
@@ -1437,6 +1504,36 @@ export interface MihomeDeviceDelegate<ExtArgs extends runtime.Types.Extensions.I
*/
updateMany<T extends MihomeDeviceUpdateManyArgs>(args: Prisma.SelectSubset<T, MihomeDeviceUpdateManyArgs<ExtArgs>>): Prisma.PrismaPromise<Prisma.BatchPayload>
/**
* Update zero or more MihomeDevices and returns the data updated in the database.
* @param {MihomeDeviceUpdateManyAndReturnArgs} args - Arguments to update many MihomeDevices.
* @example
* // Update many MihomeDevices
* const mihomeDevice = await prisma.mihomeDevice.updateManyAndReturn({
* where: {
* // ... provide filter here
* },
* data: [
* // ... provide data here
* ]
* })
*
* // Update zero or more MihomeDevices and only return the `id`
* const mihomeDeviceWithIdOnly = await prisma.mihomeDevice.updateManyAndReturn({
* select: { id: true },
* where: {
* // ... provide filter here
* },
* data: [
* // ... provide data here
* ]
* })
* Note, that providing `undefined` is treated as the value not being there.
* Read more here: https://pris.ly/d/null-undefined
*
*/
updateManyAndReturn<T extends MihomeDeviceUpdateManyAndReturnArgs>(args: Prisma.SelectSubset<T, MihomeDeviceUpdateManyAndReturnArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$MihomeDevicePayload<ExtArgs>, T, "updateManyAndReturn", GlobalOmitOptions>>
/**
* Create or update one MihomeDevice.
* @param {MihomeDeviceUpsertArgs} args - Arguments to update or create a MihomeDevice.
@@ -1882,6 +1979,29 @@ export type MihomeDeviceCreateManyArgs<ExtArgs extends runtime.Types.Extensions.
skipDuplicates?: boolean
}
/**
* MihomeDevice createManyAndReturn
*/
export type MihomeDeviceCreateManyAndReturnArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
/**
* Select specific fields to fetch from the MihomeDevice
*/
select?: Prisma.MihomeDeviceSelectCreateManyAndReturn<ExtArgs> | null
/**
* Omit specific fields from the MihomeDevice
*/
omit?: Prisma.MihomeDeviceOmit<ExtArgs> | null
/**
* The data used to create many MihomeDevices.
*/
data: Prisma.MihomeDeviceCreateManyInput | Prisma.MihomeDeviceCreateManyInput[]
skipDuplicates?: boolean
/**
* Choose, which related nodes to fetch as well
*/
include?: Prisma.MihomeDeviceIncludeCreateManyAndReturn<ExtArgs> | null
}
/**
* MihomeDevice update
*/
@@ -1926,6 +2046,36 @@ export type MihomeDeviceUpdateManyArgs<ExtArgs extends runtime.Types.Extensions.
limit?: number
}
/**
* MihomeDevice updateManyAndReturn
*/
export type MihomeDeviceUpdateManyAndReturnArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
/**
* Select specific fields to fetch from the MihomeDevice
*/
select?: Prisma.MihomeDeviceSelectUpdateManyAndReturn<ExtArgs> | null
/**
* Omit specific fields from the MihomeDevice
*/
omit?: Prisma.MihomeDeviceOmit<ExtArgs> | null
/**
* The data used to update MihomeDevices.
*/
data: Prisma.XOR<Prisma.MihomeDeviceUpdateManyMutationInput, Prisma.MihomeDeviceUncheckedUpdateManyInput>
/**
* Filter which MihomeDevices to update
*/
where?: Prisma.MihomeDeviceWhereInput
/**
* Limit how many MihomeDevices to update.
*/
limit?: number
/**
* Choose, which related nodes to fetch as well
*/
include?: Prisma.MihomeDeviceIncludeUpdateManyAndReturn<ExtArgs> | null
}
/**
* MihomeDevice upsert
*/
+133
View File
@@ -566,7 +566,27 @@ export type MihomeEnvHourSelect<ExtArgs extends runtime.Types.Extensions.Interna
device?: boolean | Prisma.MihomeDeviceDefaultArgs<ExtArgs>
}, ExtArgs["result"]["mihomeEnvHour"]>
export type MihomeEnvHourSelectCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
id?: boolean
deviceId?: boolean
hour?: boolean
temperatureAvg?: boolean
humidityAvg?: boolean
raw?: boolean
createdAt?: boolean
device?: boolean | Prisma.MihomeDeviceDefaultArgs<ExtArgs>
}, ExtArgs["result"]["mihomeEnvHour"]>
export type MihomeEnvHourSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
id?: boolean
deviceId?: boolean
hour?: boolean
temperatureAvg?: boolean
humidityAvg?: boolean
raw?: boolean
createdAt?: boolean
device?: boolean | Prisma.MihomeDeviceDefaultArgs<ExtArgs>
}, ExtArgs["result"]["mihomeEnvHour"]>
export type MihomeEnvHourSelectScalar = {
id?: boolean
@@ -582,6 +602,12 @@ export type MihomeEnvHourOmit<ExtArgs extends runtime.Types.Extensions.InternalA
export type MihomeEnvHourInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
device?: boolean | Prisma.MihomeDeviceDefaultArgs<ExtArgs>
}
export type MihomeEnvHourIncludeCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
device?: boolean | Prisma.MihomeDeviceDefaultArgs<ExtArgs>
}
export type MihomeEnvHourIncludeUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
device?: boolean | Prisma.MihomeDeviceDefaultArgs<ExtArgs>
}
export type $MihomeEnvHourPayload<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
name: "MihomeEnvHour"
@@ -713,6 +739,30 @@ export interface MihomeEnvHourDelegate<ExtArgs extends runtime.Types.Extensions.
*/
createMany<T extends MihomeEnvHourCreateManyArgs>(args?: Prisma.SelectSubset<T, MihomeEnvHourCreateManyArgs<ExtArgs>>): Prisma.PrismaPromise<Prisma.BatchPayload>
/**
* Create many MihomeEnvHours and returns the data saved in the database.
* @param {MihomeEnvHourCreateManyAndReturnArgs} args - Arguments to create many MihomeEnvHours.
* @example
* // Create many MihomeEnvHours
* const mihomeEnvHour = await prisma.mihomeEnvHour.createManyAndReturn({
* data: [
* // ... provide data here
* ]
* })
*
* // Create many MihomeEnvHours and only return the `id`
* const mihomeEnvHourWithIdOnly = await prisma.mihomeEnvHour.createManyAndReturn({
* select: { id: true },
* data: [
* // ... provide data here
* ]
* })
* Note, that providing `undefined` is treated as the value not being there.
* Read more here: https://pris.ly/d/null-undefined
*
*/
createManyAndReturn<T extends MihomeEnvHourCreateManyAndReturnArgs>(args?: Prisma.SelectSubset<T, MihomeEnvHourCreateManyAndReturnArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$MihomeEnvHourPayload<ExtArgs>, T, "createManyAndReturn", GlobalOmitOptions>>
/**
* Delete a MihomeEnvHour.
* @param {MihomeEnvHourDeleteArgs} args - Arguments to delete one MihomeEnvHour.
@@ -777,6 +827,36 @@ export interface MihomeEnvHourDelegate<ExtArgs extends runtime.Types.Extensions.
*/
updateMany<T extends MihomeEnvHourUpdateManyArgs>(args: Prisma.SelectSubset<T, MihomeEnvHourUpdateManyArgs<ExtArgs>>): Prisma.PrismaPromise<Prisma.BatchPayload>
/**
* Update zero or more MihomeEnvHours and returns the data updated in the database.
* @param {MihomeEnvHourUpdateManyAndReturnArgs} args - Arguments to update many MihomeEnvHours.
* @example
* // Update many MihomeEnvHours
* const mihomeEnvHour = await prisma.mihomeEnvHour.updateManyAndReturn({
* where: {
* // ... provide filter here
* },
* data: [
* // ... provide data here
* ]
* })
*
* // Update zero or more MihomeEnvHours and only return the `id`
* const mihomeEnvHourWithIdOnly = await prisma.mihomeEnvHour.updateManyAndReturn({
* select: { id: true },
* where: {
* // ... provide filter here
* },
* data: [
* // ... provide data here
* ]
* })
* Note, that providing `undefined` is treated as the value not being there.
* Read more here: https://pris.ly/d/null-undefined
*
*/
updateManyAndReturn<T extends MihomeEnvHourUpdateManyAndReturnArgs>(args: Prisma.SelectSubset<T, MihomeEnvHourUpdateManyAndReturnArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$MihomeEnvHourPayload<ExtArgs>, T, "updateManyAndReturn", GlobalOmitOptions>>
/**
* Create or update one MihomeEnvHour.
* @param {MihomeEnvHourUpsertArgs} args - Arguments to update or create a MihomeEnvHour.
@@ -1210,6 +1290,29 @@ export type MihomeEnvHourCreateManyArgs<ExtArgs extends runtime.Types.Extensions
skipDuplicates?: boolean
}
/**
* MihomeEnvHour createManyAndReturn
*/
export type MihomeEnvHourCreateManyAndReturnArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
/**
* Select specific fields to fetch from the MihomeEnvHour
*/
select?: Prisma.MihomeEnvHourSelectCreateManyAndReturn<ExtArgs> | null
/**
* Omit specific fields from the MihomeEnvHour
*/
omit?: Prisma.MihomeEnvHourOmit<ExtArgs> | null
/**
* The data used to create many MihomeEnvHours.
*/
data: Prisma.MihomeEnvHourCreateManyInput | Prisma.MihomeEnvHourCreateManyInput[]
skipDuplicates?: boolean
/**
* Choose, which related nodes to fetch as well
*/
include?: Prisma.MihomeEnvHourIncludeCreateManyAndReturn<ExtArgs> | null
}
/**
* MihomeEnvHour update
*/
@@ -1254,6 +1357,36 @@ export type MihomeEnvHourUpdateManyArgs<ExtArgs extends runtime.Types.Extensions
limit?: number
}
/**
* MihomeEnvHour updateManyAndReturn
*/
export type MihomeEnvHourUpdateManyAndReturnArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
/**
* Select specific fields to fetch from the MihomeEnvHour
*/
select?: Prisma.MihomeEnvHourSelectUpdateManyAndReturn<ExtArgs> | null
/**
* Omit specific fields from the MihomeEnvHour
*/
omit?: Prisma.MihomeEnvHourOmit<ExtArgs> | null
/**
* The data used to update MihomeEnvHours.
*/
data: Prisma.XOR<Prisma.MihomeEnvHourUpdateManyMutationInput, Prisma.MihomeEnvHourUncheckedUpdateManyInput>
/**
* Filter which MihomeEnvHours to update
*/
where?: Prisma.MihomeEnvHourWhereInput
/**
* Limit how many MihomeEnvHours to update.
*/
limit?: number
/**
* Choose, which related nodes to fetch as well
*/
include?: Prisma.MihomeEnvHourIncludeUpdateManyAndReturn<ExtArgs> | null
}
/**
* MihomeEnvHour upsert
*/
+149 -7
View File
@@ -348,7 +348,6 @@ export type MihomeEnvSampleOrderByWithRelationInput = {
raw?: Prisma.SortOrderInput | Prisma.SortOrder
createdAt?: Prisma.SortOrder
device?: Prisma.MihomeDeviceOrderByWithRelationInput
_relevance?: Prisma.MihomeEnvSampleOrderByRelevanceInput
}
export type MihomeEnvSampleWhereUniqueInput = Prisma.AtLeast<{
@@ -550,12 +549,6 @@ export type MihomeEnvSampleOrderByRelationAggregateInput = {
_count?: Prisma.SortOrder
}
export type MihomeEnvSampleOrderByRelevanceInput = {
fields: Prisma.MihomeEnvSampleOrderByRelevanceFieldEnum | Prisma.MihomeEnvSampleOrderByRelevanceFieldEnum[]
sort: Prisma.SortOrder
search: string
}
export type MihomeEnvSampleDeviceIdDateCompoundUniqueInput = {
deviceId: number
date: Date | string
@@ -857,7 +850,43 @@ export type MihomeEnvSampleSelect<ExtArgs extends runtime.Types.Extensions.Inter
device?: boolean | Prisma.MihomeDeviceDefaultArgs<ExtArgs>
}, ExtArgs["result"]["mihomeEnvSample"]>
export type MihomeEnvSampleSelectCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
id?: boolean
deviceId?: boolean
date?: boolean
temperature?: boolean
humidity?: boolean
temperatureAvg?: boolean
humidityAvg?: boolean
temperatureMin?: boolean
temperatureMax?: boolean
humidityMin?: boolean
humidityMax?: boolean
origin?: boolean
sampledAt?: boolean
raw?: boolean
createdAt?: boolean
device?: boolean | Prisma.MihomeDeviceDefaultArgs<ExtArgs>
}, ExtArgs["result"]["mihomeEnvSample"]>
export type MihomeEnvSampleSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
id?: boolean
deviceId?: boolean
date?: boolean
temperature?: boolean
humidity?: boolean
temperatureAvg?: boolean
humidityAvg?: boolean
temperatureMin?: boolean
temperatureMax?: boolean
humidityMin?: boolean
humidityMax?: boolean
origin?: boolean
sampledAt?: boolean
raw?: boolean
createdAt?: boolean
device?: boolean | Prisma.MihomeDeviceDefaultArgs<ExtArgs>
}, ExtArgs["result"]["mihomeEnvSample"]>
export type MihomeEnvSampleSelectScalar = {
id?: boolean
@@ -881,6 +910,12 @@ export type MihomeEnvSampleOmit<ExtArgs extends runtime.Types.Extensions.Interna
export type MihomeEnvSampleInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
device?: boolean | Prisma.MihomeDeviceDefaultArgs<ExtArgs>
}
export type MihomeEnvSampleIncludeCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
device?: boolean | Prisma.MihomeDeviceDefaultArgs<ExtArgs>
}
export type MihomeEnvSampleIncludeUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
device?: boolean | Prisma.MihomeDeviceDefaultArgs<ExtArgs>
}
export type $MihomeEnvSamplePayload<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
name: "MihomeEnvSample"
@@ -1020,6 +1055,30 @@ export interface MihomeEnvSampleDelegate<ExtArgs extends runtime.Types.Extension
*/
createMany<T extends MihomeEnvSampleCreateManyArgs>(args?: Prisma.SelectSubset<T, MihomeEnvSampleCreateManyArgs<ExtArgs>>): Prisma.PrismaPromise<Prisma.BatchPayload>
/**
* Create many MihomeEnvSamples and returns the data saved in the database.
* @param {MihomeEnvSampleCreateManyAndReturnArgs} args - Arguments to create many MihomeEnvSamples.
* @example
* // Create many MihomeEnvSamples
* const mihomeEnvSample = await prisma.mihomeEnvSample.createManyAndReturn({
* data: [
* // ... provide data here
* ]
* })
*
* // Create many MihomeEnvSamples and only return the `id`
* const mihomeEnvSampleWithIdOnly = await prisma.mihomeEnvSample.createManyAndReturn({
* select: { id: true },
* data: [
* // ... provide data here
* ]
* })
* Note, that providing `undefined` is treated as the value not being there.
* Read more here: https://pris.ly/d/null-undefined
*
*/
createManyAndReturn<T extends MihomeEnvSampleCreateManyAndReturnArgs>(args?: Prisma.SelectSubset<T, MihomeEnvSampleCreateManyAndReturnArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$MihomeEnvSamplePayload<ExtArgs>, T, "createManyAndReturn", GlobalOmitOptions>>
/**
* Delete a MihomeEnvSample.
* @param {MihomeEnvSampleDeleteArgs} args - Arguments to delete one MihomeEnvSample.
@@ -1084,6 +1143,36 @@ export interface MihomeEnvSampleDelegate<ExtArgs extends runtime.Types.Extension
*/
updateMany<T extends MihomeEnvSampleUpdateManyArgs>(args: Prisma.SelectSubset<T, MihomeEnvSampleUpdateManyArgs<ExtArgs>>): Prisma.PrismaPromise<Prisma.BatchPayload>
/**
* Update zero or more MihomeEnvSamples and returns the data updated in the database.
* @param {MihomeEnvSampleUpdateManyAndReturnArgs} args - Arguments to update many MihomeEnvSamples.
* @example
* // Update many MihomeEnvSamples
* const mihomeEnvSample = await prisma.mihomeEnvSample.updateManyAndReturn({
* where: {
* // ... provide filter here
* },
* data: [
* // ... provide data here
* ]
* })
*
* // Update zero or more MihomeEnvSamples and only return the `id`
* const mihomeEnvSampleWithIdOnly = await prisma.mihomeEnvSample.updateManyAndReturn({
* select: { id: true },
* where: {
* // ... provide filter here
* },
* data: [
* // ... provide data here
* ]
* })
* Note, that providing `undefined` is treated as the value not being there.
* Read more here: https://pris.ly/d/null-undefined
*
*/
updateManyAndReturn<T extends MihomeEnvSampleUpdateManyAndReturnArgs>(args: Prisma.SelectSubset<T, MihomeEnvSampleUpdateManyAndReturnArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$MihomeEnvSamplePayload<ExtArgs>, T, "updateManyAndReturn", GlobalOmitOptions>>
/**
* Create or update one MihomeEnvSample.
* @param {MihomeEnvSampleUpsertArgs} args - Arguments to update or create a MihomeEnvSample.
@@ -1525,6 +1614,29 @@ export type MihomeEnvSampleCreateManyArgs<ExtArgs extends runtime.Types.Extensio
skipDuplicates?: boolean
}
/**
* MihomeEnvSample createManyAndReturn
*/
export type MihomeEnvSampleCreateManyAndReturnArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
/**
* Select specific fields to fetch from the MihomeEnvSample
*/
select?: Prisma.MihomeEnvSampleSelectCreateManyAndReturn<ExtArgs> | null
/**
* Omit specific fields from the MihomeEnvSample
*/
omit?: Prisma.MihomeEnvSampleOmit<ExtArgs> | null
/**
* The data used to create many MihomeEnvSamples.
*/
data: Prisma.MihomeEnvSampleCreateManyInput | Prisma.MihomeEnvSampleCreateManyInput[]
skipDuplicates?: boolean
/**
* Choose, which related nodes to fetch as well
*/
include?: Prisma.MihomeEnvSampleIncludeCreateManyAndReturn<ExtArgs> | null
}
/**
* MihomeEnvSample update
*/
@@ -1569,6 +1681,36 @@ export type MihomeEnvSampleUpdateManyArgs<ExtArgs extends runtime.Types.Extensio
limit?: number
}
/**
* MihomeEnvSample updateManyAndReturn
*/
export type MihomeEnvSampleUpdateManyAndReturnArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
/**
* Select specific fields to fetch from the MihomeEnvSample
*/
select?: Prisma.MihomeEnvSampleSelectUpdateManyAndReturn<ExtArgs> | null
/**
* Omit specific fields from the MihomeEnvSample
*/
omit?: Prisma.MihomeEnvSampleOmit<ExtArgs> | null
/**
* The data used to update MihomeEnvSamples.
*/
data: Prisma.XOR<Prisma.MihomeEnvSampleUpdateManyMutationInput, Prisma.MihomeEnvSampleUncheckedUpdateManyInput>
/**
* Filter which MihomeEnvSamples to update
*/
where?: Prisma.MihomeEnvSampleWhereInput
/**
* Limit how many MihomeEnvSamples to update.
*/
limit?: number
/**
* Choose, which related nodes to fetch as well
*/
include?: Prisma.MihomeEnvSampleIncludeUpdateManyAndReturn<ExtArgs> | null
}
/**
* MihomeEnvSample upsert
*/
+113 -7
View File
@@ -217,7 +217,6 @@ export type MihomeHomeOrderByWithRelationInput = {
name?: Prisma.SortOrder
createdAt?: Prisma.SortOrder
devices?: Prisma.MihomeDeviceOrderByRelationAggregateInput
_relevance?: Prisma.MihomeHomeOrderByRelevanceInput
}
export type MihomeHomeWhereUniqueInput = Prisma.AtLeast<{
@@ -303,12 +302,6 @@ export type MihomeHomeUncheckedUpdateManyInput = {
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
}
export type MihomeHomeOrderByRelevanceInput = {
fields: Prisma.MihomeHomeOrderByRelevanceFieldEnum | Prisma.MihomeHomeOrderByRelevanceFieldEnum[]
sort: Prisma.SortOrder
search: string
}
export type MihomeHomeCountOrderByAggregateInput = {
id?: Prisma.SortOrder
homeId?: Prisma.SortOrder
@@ -439,7 +432,19 @@ export type MihomeHomeSelect<ExtArgs extends runtime.Types.Extensions.InternalAr
_count?: boolean | Prisma.MihomeHomeCountOutputTypeDefaultArgs<ExtArgs>
}, ExtArgs["result"]["mihomeHome"]>
export type MihomeHomeSelectCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
id?: boolean
homeId?: boolean
name?: boolean
createdAt?: boolean
}, ExtArgs["result"]["mihomeHome"]>
export type MihomeHomeSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
id?: boolean
homeId?: boolean
name?: boolean
createdAt?: boolean
}, ExtArgs["result"]["mihomeHome"]>
export type MihomeHomeSelectScalar = {
id?: boolean
@@ -453,6 +458,8 @@ export type MihomeHomeInclude<ExtArgs extends runtime.Types.Extensions.InternalA
devices?: boolean | Prisma.MihomeHome$devicesArgs<ExtArgs>
_count?: boolean | Prisma.MihomeHomeCountOutputTypeDefaultArgs<ExtArgs>
}
export type MihomeHomeIncludeCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {}
export type MihomeHomeIncludeUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {}
export type $MihomeHomePayload<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
name: "MihomeHome"
@@ -581,6 +588,30 @@ export interface MihomeHomeDelegate<ExtArgs extends runtime.Types.Extensions.Int
*/
createMany<T extends MihomeHomeCreateManyArgs>(args?: Prisma.SelectSubset<T, MihomeHomeCreateManyArgs<ExtArgs>>): Prisma.PrismaPromise<Prisma.BatchPayload>
/**
* Create many MihomeHomes and returns the data saved in the database.
* @param {MihomeHomeCreateManyAndReturnArgs} args - Arguments to create many MihomeHomes.
* @example
* // Create many MihomeHomes
* const mihomeHome = await prisma.mihomeHome.createManyAndReturn({
* data: [
* // ... provide data here
* ]
* })
*
* // Create many MihomeHomes and only return the `id`
* const mihomeHomeWithIdOnly = await prisma.mihomeHome.createManyAndReturn({
* select: { id: true },
* data: [
* // ... provide data here
* ]
* })
* Note, that providing `undefined` is treated as the value not being there.
* Read more here: https://pris.ly/d/null-undefined
*
*/
createManyAndReturn<T extends MihomeHomeCreateManyAndReturnArgs>(args?: Prisma.SelectSubset<T, MihomeHomeCreateManyAndReturnArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$MihomeHomePayload<ExtArgs>, T, "createManyAndReturn", GlobalOmitOptions>>
/**
* Delete a MihomeHome.
* @param {MihomeHomeDeleteArgs} args - Arguments to delete one MihomeHome.
@@ -645,6 +676,36 @@ export interface MihomeHomeDelegate<ExtArgs extends runtime.Types.Extensions.Int
*/
updateMany<T extends MihomeHomeUpdateManyArgs>(args: Prisma.SelectSubset<T, MihomeHomeUpdateManyArgs<ExtArgs>>): Prisma.PrismaPromise<Prisma.BatchPayload>
/**
* Update zero or more MihomeHomes and returns the data updated in the database.
* @param {MihomeHomeUpdateManyAndReturnArgs} args - Arguments to update many MihomeHomes.
* @example
* // Update many MihomeHomes
* const mihomeHome = await prisma.mihomeHome.updateManyAndReturn({
* where: {
* // ... provide filter here
* },
* data: [
* // ... provide data here
* ]
* })
*
* // Update zero or more MihomeHomes and only return the `id`
* const mihomeHomeWithIdOnly = await prisma.mihomeHome.updateManyAndReturn({
* select: { id: true },
* where: {
* // ... provide filter here
* },
* data: [
* // ... provide data here
* ]
* })
* Note, that providing `undefined` is treated as the value not being there.
* Read more here: https://pris.ly/d/null-undefined
*
*/
updateManyAndReturn<T extends MihomeHomeUpdateManyAndReturnArgs>(args: Prisma.SelectSubset<T, MihomeHomeUpdateManyAndReturnArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$MihomeHomePayload<ExtArgs>, T, "updateManyAndReturn", GlobalOmitOptions>>
/**
* Create or update one MihomeHome.
* @param {MihomeHomeUpsertArgs} args - Arguments to update or create a MihomeHome.
@@ -1075,6 +1136,25 @@ export type MihomeHomeCreateManyArgs<ExtArgs extends runtime.Types.Extensions.In
skipDuplicates?: boolean
}
/**
* MihomeHome createManyAndReturn
*/
export type MihomeHomeCreateManyAndReturnArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
/**
* Select specific fields to fetch from the MihomeHome
*/
select?: Prisma.MihomeHomeSelectCreateManyAndReturn<ExtArgs> | null
/**
* Omit specific fields from the MihomeHome
*/
omit?: Prisma.MihomeHomeOmit<ExtArgs> | null
/**
* The data used to create many MihomeHomes.
*/
data: Prisma.MihomeHomeCreateManyInput | Prisma.MihomeHomeCreateManyInput[]
skipDuplicates?: boolean
}
/**
* MihomeHome update
*/
@@ -1119,6 +1199,32 @@ export type MihomeHomeUpdateManyArgs<ExtArgs extends runtime.Types.Extensions.In
limit?: number
}
/**
* MihomeHome updateManyAndReturn
*/
export type MihomeHomeUpdateManyAndReturnArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
/**
* Select specific fields to fetch from the MihomeHome
*/
select?: Prisma.MihomeHomeSelectUpdateManyAndReturn<ExtArgs> | null
/**
* Omit specific fields from the MihomeHome
*/
omit?: Prisma.MihomeHomeOmit<ExtArgs> | null
/**
* The data used to update MihomeHomes.
*/
data: Prisma.XOR<Prisma.MihomeHomeUpdateManyMutationInput, Prisma.MihomeHomeUncheckedUpdateManyInput>
/**
* Filter which MihomeHomes to update
*/
where?: Prisma.MihomeHomeWhereInput
/**
* Limit how many MihomeHomes to update.
*/
limit?: number
}
/**
* MihomeHome upsert
*/
+121 -7
View File
@@ -264,7 +264,6 @@ export type NewsOrderByWithRelationInput = {
publishedAt?: Prisma.SortOrder
impactRank?: Prisma.SortOrder
createdAt?: Prisma.SortOrder
_relevance?: Prisma.NewsOrderByRelevanceInput
}
export type NewsWhereUniqueInput = Prisma.AtLeast<{
@@ -396,12 +395,6 @@ export type NewsUncheckedUpdateManyInput = {
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
}
export type NewsOrderByRelevanceInput = {
fields: Prisma.NewsOrderByRelevanceFieldEnum | Prisma.NewsOrderByRelevanceFieldEnum[]
sort: Prisma.SortOrder
search: string
}
export type NewsTitlePublishedAtCompoundUniqueInput = {
title: string
publishedAt: Date | string
@@ -471,7 +464,29 @@ export type NewsSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = r
createdAt?: boolean
}, ExtArgs["result"]["news"]>
export type NewsSelectCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
id?: boolean
title?: boolean
summary?: boolean
content?: boolean
source?: boolean
url?: boolean
publishedAt?: boolean
impactRank?: boolean
createdAt?: boolean
}, ExtArgs["result"]["news"]>
export type NewsSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
id?: boolean
title?: boolean
summary?: boolean
content?: boolean
source?: boolean
url?: boolean
publishedAt?: boolean
impactRank?: boolean
createdAt?: boolean
}, ExtArgs["result"]["news"]>
export type NewsSelectScalar = {
id?: boolean
@@ -617,6 +632,30 @@ export interface NewsDelegate<ExtArgs extends runtime.Types.Extensions.InternalA
*/
createMany<T extends NewsCreateManyArgs>(args?: Prisma.SelectSubset<T, NewsCreateManyArgs<ExtArgs>>): Prisma.PrismaPromise<Prisma.BatchPayload>
/**
* Create many News and returns the data saved in the database.
* @param {NewsCreateManyAndReturnArgs} args - Arguments to create many News.
* @example
* // Create many News
* const news = await prisma.news.createManyAndReturn({
* data: [
* // ... provide data here
* ]
* })
*
* // Create many News and only return the `id`
* const newsWithIdOnly = await prisma.news.createManyAndReturn({
* select: { id: true },
* data: [
* // ... provide data here
* ]
* })
* Note, that providing `undefined` is treated as the value not being there.
* Read more here: https://pris.ly/d/null-undefined
*
*/
createManyAndReturn<T extends NewsCreateManyAndReturnArgs>(args?: Prisma.SelectSubset<T, NewsCreateManyAndReturnArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$NewsPayload<ExtArgs>, T, "createManyAndReturn", GlobalOmitOptions>>
/**
* Delete a News.
* @param {NewsDeleteArgs} args - Arguments to delete one News.
@@ -681,6 +720,36 @@ export interface NewsDelegate<ExtArgs extends runtime.Types.Extensions.InternalA
*/
updateMany<T extends NewsUpdateManyArgs>(args: Prisma.SelectSubset<T, NewsUpdateManyArgs<ExtArgs>>): Prisma.PrismaPromise<Prisma.BatchPayload>
/**
* Update zero or more News and returns the data updated in the database.
* @param {NewsUpdateManyAndReturnArgs} args - Arguments to update many News.
* @example
* // Update many News
* const news = await prisma.news.updateManyAndReturn({
* where: {
* // ... provide filter here
* },
* data: [
* // ... provide data here
* ]
* })
*
* // Update zero or more News and only return the `id`
* const newsWithIdOnly = await prisma.news.updateManyAndReturn({
* select: { id: true },
* where: {
* // ... provide filter here
* },
* data: [
* // ... provide data here
* ]
* })
* Note, that providing `undefined` is treated as the value not being there.
* Read more here: https://pris.ly/d/null-undefined
*
*/
updateManyAndReturn<T extends NewsUpdateManyAndReturnArgs>(args: Prisma.SelectSubset<T, NewsUpdateManyAndReturnArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$NewsPayload<ExtArgs>, T, "updateManyAndReturn", GlobalOmitOptions>>
/**
* Create or update one News.
* @param {NewsUpsertArgs} args - Arguments to update or create a News.
@@ -1091,6 +1160,25 @@ export type NewsCreateManyArgs<ExtArgs extends runtime.Types.Extensions.Internal
skipDuplicates?: boolean
}
/**
* News createManyAndReturn
*/
export type NewsCreateManyAndReturnArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
/**
* Select specific fields to fetch from the News
*/
select?: Prisma.NewsSelectCreateManyAndReturn<ExtArgs> | null
/**
* Omit specific fields from the News
*/
omit?: Prisma.NewsOmit<ExtArgs> | null
/**
* The data used to create many News.
*/
data: Prisma.NewsCreateManyInput | Prisma.NewsCreateManyInput[]
skipDuplicates?: boolean
}
/**
* News update
*/
@@ -1131,6 +1219,32 @@ export type NewsUpdateManyArgs<ExtArgs extends runtime.Types.Extensions.Internal
limit?: number
}
/**
* News updateManyAndReturn
*/
export type NewsUpdateManyAndReturnArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
/**
* Select specific fields to fetch from the News
*/
select?: Prisma.NewsSelectUpdateManyAndReturn<ExtArgs> | null
/**
* Omit specific fields from the News
*/
omit?: Prisma.NewsOmit<ExtArgs> | null
/**
* The data used to update News.
*/
data: Prisma.XOR<Prisma.NewsUpdateManyMutationInput, Prisma.NewsUncheckedUpdateManyInput>
/**
* Filter which News to update
*/
where?: Prisma.NewsWhereInput
/**
* Limit how many News to update.
*/
limit?: number
}
/**
* News upsert
*/
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+117 -7
View File
@@ -238,7 +238,6 @@ export type ScraperCacheOrderByWithRelationInput = {
metadata?: Prisma.SortOrderInput | Prisma.SortOrder
createdAt?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder
_relevance?: Prisma.ScraperCacheOrderByRelevanceInput
}
export type ScraperCacheWhereUniqueInput = Prisma.AtLeast<{
@@ -349,12 +348,6 @@ export type ScraperCacheUncheckedUpdateManyInput = {
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
}
export type ScraperCacheOrderByRelevanceInput = {
fields: Prisma.ScraperCacheOrderByRelevanceFieldEnum | Prisma.ScraperCacheOrderByRelevanceFieldEnum[]
sort: Prisma.SortOrder
search: string
}
export type ScraperCacheCountOrderByAggregateInput = {
id?: Prisma.SortOrder
scraperName?: Prisma.SortOrder
@@ -403,7 +396,25 @@ export type ScraperCacheSelect<ExtArgs extends runtime.Types.Extensions.Internal
updatedAt?: boolean
}, ExtArgs["result"]["scraperCache"]>
export type ScraperCacheSelectCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
id?: boolean
scraperName?: boolean
lastDataHash?: boolean
lastUpdateAt?: boolean
metadata?: boolean
createdAt?: boolean
updatedAt?: boolean
}, ExtArgs["result"]["scraperCache"]>
export type ScraperCacheSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
id?: boolean
scraperName?: boolean
lastDataHash?: boolean
lastUpdateAt?: boolean
metadata?: boolean
createdAt?: boolean
updatedAt?: boolean
}, ExtArgs["result"]["scraperCache"]>
export type ScraperCacheSelectScalar = {
id?: boolean
@@ -545,6 +556,30 @@ export interface ScraperCacheDelegate<ExtArgs extends runtime.Types.Extensions.I
*/
createMany<T extends ScraperCacheCreateManyArgs>(args?: Prisma.SelectSubset<T, ScraperCacheCreateManyArgs<ExtArgs>>): Prisma.PrismaPromise<Prisma.BatchPayload>
/**
* Create many ScraperCaches and returns the data saved in the database.
* @param {ScraperCacheCreateManyAndReturnArgs} args - Arguments to create many ScraperCaches.
* @example
* // Create many ScraperCaches
* const scraperCache = await prisma.scraperCache.createManyAndReturn({
* data: [
* // ... provide data here
* ]
* })
*
* // Create many ScraperCaches and only return the `id`
* const scraperCacheWithIdOnly = await prisma.scraperCache.createManyAndReturn({
* select: { id: true },
* data: [
* // ... provide data here
* ]
* })
* Note, that providing `undefined` is treated as the value not being there.
* Read more here: https://pris.ly/d/null-undefined
*
*/
createManyAndReturn<T extends ScraperCacheCreateManyAndReturnArgs>(args?: Prisma.SelectSubset<T, ScraperCacheCreateManyAndReturnArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$ScraperCachePayload<ExtArgs>, T, "createManyAndReturn", GlobalOmitOptions>>
/**
* Delete a ScraperCache.
* @param {ScraperCacheDeleteArgs} args - Arguments to delete one ScraperCache.
@@ -609,6 +644,36 @@ export interface ScraperCacheDelegate<ExtArgs extends runtime.Types.Extensions.I
*/
updateMany<T extends ScraperCacheUpdateManyArgs>(args: Prisma.SelectSubset<T, ScraperCacheUpdateManyArgs<ExtArgs>>): Prisma.PrismaPromise<Prisma.BatchPayload>
/**
* Update zero or more ScraperCaches and returns the data updated in the database.
* @param {ScraperCacheUpdateManyAndReturnArgs} args - Arguments to update many ScraperCaches.
* @example
* // Update many ScraperCaches
* const scraperCache = await prisma.scraperCache.updateManyAndReturn({
* where: {
* // ... provide filter here
* },
* data: [
* // ... provide data here
* ]
* })
*
* // Update zero or more ScraperCaches and only return the `id`
* const scraperCacheWithIdOnly = await prisma.scraperCache.updateManyAndReturn({
* select: { id: true },
* where: {
* // ... provide filter here
* },
* data: [
* // ... provide data here
* ]
* })
* Note, that providing `undefined` is treated as the value not being there.
* Read more here: https://pris.ly/d/null-undefined
*
*/
updateManyAndReturn<T extends ScraperCacheUpdateManyAndReturnArgs>(args: Prisma.SelectSubset<T, ScraperCacheUpdateManyAndReturnArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$ScraperCachePayload<ExtArgs>, T, "updateManyAndReturn", GlobalOmitOptions>>
/**
* Create or update one ScraperCache.
* @param {ScraperCacheUpsertArgs} args - Arguments to update or create a ScraperCache.
@@ -1017,6 +1082,25 @@ export type ScraperCacheCreateManyArgs<ExtArgs extends runtime.Types.Extensions.
skipDuplicates?: boolean
}
/**
* ScraperCache createManyAndReturn
*/
export type ScraperCacheCreateManyAndReturnArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
/**
* Select specific fields to fetch from the ScraperCache
*/
select?: Prisma.ScraperCacheSelectCreateManyAndReturn<ExtArgs> | null
/**
* Omit specific fields from the ScraperCache
*/
omit?: Prisma.ScraperCacheOmit<ExtArgs> | null
/**
* The data used to create many ScraperCaches.
*/
data: Prisma.ScraperCacheCreateManyInput | Prisma.ScraperCacheCreateManyInput[]
skipDuplicates?: boolean
}
/**
* ScraperCache update
*/
@@ -1057,6 +1141,32 @@ export type ScraperCacheUpdateManyArgs<ExtArgs extends runtime.Types.Extensions.
limit?: number
}
/**
* ScraperCache updateManyAndReturn
*/
export type ScraperCacheUpdateManyAndReturnArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
/**
* Select specific fields to fetch from the ScraperCache
*/
select?: Prisma.ScraperCacheSelectUpdateManyAndReturn<ExtArgs> | null
/**
* Omit specific fields from the ScraperCache
*/
omit?: Prisma.ScraperCacheOmit<ExtArgs> | null
/**
* The data used to update ScraperCaches.
*/
data: Prisma.XOR<Prisma.ScraperCacheUpdateManyMutationInput, Prisma.ScraperCacheUncheckedUpdateManyInput>
/**
* Filter which ScraperCaches to update
*/
where?: Prisma.ScraperCacheWhereInput
/**
* Limit how many ScraperCaches to update.
*/
limit?: number
}
/**
* ScraperCache upsert
*/
+153 -7
View File
@@ -464,7 +464,6 @@ export type ServerNodeOrderByWithRelationInput = {
recordedAt?: Prisma.SortOrder
createdAt?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder
_relevance?: Prisma.ServerNodeOrderByRelevanceInput
}
export type ServerNodeWhereUniqueInput = Prisma.AtLeast<{
@@ -755,12 +754,6 @@ export type ServerNodeUncheckedUpdateManyInput = {
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
}
export type ServerNodeOrderByRelevanceInput = {
fields: Prisma.ServerNodeOrderByRelevanceFieldEnum | Prisma.ServerNodeOrderByRelevanceFieldEnum[]
sort: Prisma.SortOrder
search: string
}
export type ServerNodeCountOrderByAggregateInput = {
id?: Prisma.SortOrder
uuid?: Prisma.SortOrder
@@ -921,7 +914,61 @@ export type ServerNodeSelect<ExtArgs extends runtime.Types.Extensions.InternalAr
updatedAt?: boolean
}, ExtArgs["result"]["serverNode"]>
export type ServerNodeSelectCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
id?: boolean
uuid?: boolean
name?: boolean
cpuName?: boolean
os?: boolean
region?: boolean
memTotal?: boolean
diskTotal?: boolean
cpuCores?: boolean
cpuUsage?: boolean
ramUsed?: boolean
swapUsed?: boolean
load1?: boolean
load5?: boolean
load15?: boolean
diskUsed?: boolean
netIn?: boolean
netOut?: boolean
uptime?: boolean
process?: boolean
connections?: boolean
status?: boolean
recordedAt?: boolean
createdAt?: boolean
updatedAt?: boolean
}, ExtArgs["result"]["serverNode"]>
export type ServerNodeSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
id?: boolean
uuid?: boolean
name?: boolean
cpuName?: boolean
os?: boolean
region?: boolean
memTotal?: boolean
diskTotal?: boolean
cpuCores?: boolean
cpuUsage?: boolean
ramUsed?: boolean
swapUsed?: boolean
load1?: boolean
load5?: boolean
load15?: boolean
diskUsed?: boolean
netIn?: boolean
netOut?: boolean
uptime?: boolean
process?: boolean
connections?: boolean
status?: boolean
recordedAt?: boolean
createdAt?: boolean
updatedAt?: boolean
}, ExtArgs["result"]["serverNode"]>
export type ServerNodeSelectScalar = {
id?: boolean
@@ -1099,6 +1146,30 @@ export interface ServerNodeDelegate<ExtArgs extends runtime.Types.Extensions.Int
*/
createMany<T extends ServerNodeCreateManyArgs>(args?: Prisma.SelectSubset<T, ServerNodeCreateManyArgs<ExtArgs>>): Prisma.PrismaPromise<Prisma.BatchPayload>
/**
* Create many ServerNodes and returns the data saved in the database.
* @param {ServerNodeCreateManyAndReturnArgs} args - Arguments to create many ServerNodes.
* @example
* // Create many ServerNodes
* const serverNode = await prisma.serverNode.createManyAndReturn({
* data: [
* // ... provide data here
* ]
* })
*
* // Create many ServerNodes and only return the `id`
* const serverNodeWithIdOnly = await prisma.serverNode.createManyAndReturn({
* select: { id: true },
* data: [
* // ... provide data here
* ]
* })
* Note, that providing `undefined` is treated as the value not being there.
* Read more here: https://pris.ly/d/null-undefined
*
*/
createManyAndReturn<T extends ServerNodeCreateManyAndReturnArgs>(args?: Prisma.SelectSubset<T, ServerNodeCreateManyAndReturnArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$ServerNodePayload<ExtArgs>, T, "createManyAndReturn", GlobalOmitOptions>>
/**
* Delete a ServerNode.
* @param {ServerNodeDeleteArgs} args - Arguments to delete one ServerNode.
@@ -1163,6 +1234,36 @@ export interface ServerNodeDelegate<ExtArgs extends runtime.Types.Extensions.Int
*/
updateMany<T extends ServerNodeUpdateManyArgs>(args: Prisma.SelectSubset<T, ServerNodeUpdateManyArgs<ExtArgs>>): Prisma.PrismaPromise<Prisma.BatchPayload>
/**
* Update zero or more ServerNodes and returns the data updated in the database.
* @param {ServerNodeUpdateManyAndReturnArgs} args - Arguments to update many ServerNodes.
* @example
* // Update many ServerNodes
* const serverNode = await prisma.serverNode.updateManyAndReturn({
* where: {
* // ... provide filter here
* },
* data: [
* // ... provide data here
* ]
* })
*
* // Update zero or more ServerNodes and only return the `id`
* const serverNodeWithIdOnly = await prisma.serverNode.updateManyAndReturn({
* select: { id: true },
* where: {
* // ... provide filter here
* },
* data: [
* // ... provide data here
* ]
* })
* Note, that providing `undefined` is treated as the value not being there.
* Read more here: https://pris.ly/d/null-undefined
*
*/
updateManyAndReturn<T extends ServerNodeUpdateManyAndReturnArgs>(args: Prisma.SelectSubset<T, ServerNodeUpdateManyAndReturnArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$ServerNodePayload<ExtArgs>, T, "updateManyAndReturn", GlobalOmitOptions>>
/**
* Create or update one ServerNode.
* @param {ServerNodeUpsertArgs} args - Arguments to update or create a ServerNode.
@@ -1589,6 +1690,25 @@ export type ServerNodeCreateManyArgs<ExtArgs extends runtime.Types.Extensions.In
skipDuplicates?: boolean
}
/**
* ServerNode createManyAndReturn
*/
export type ServerNodeCreateManyAndReturnArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
/**
* Select specific fields to fetch from the ServerNode
*/
select?: Prisma.ServerNodeSelectCreateManyAndReturn<ExtArgs> | null
/**
* Omit specific fields from the ServerNode
*/
omit?: Prisma.ServerNodeOmit<ExtArgs> | null
/**
* The data used to create many ServerNodes.
*/
data: Prisma.ServerNodeCreateManyInput | Prisma.ServerNodeCreateManyInput[]
skipDuplicates?: boolean
}
/**
* ServerNode update
*/
@@ -1629,6 +1749,32 @@ export type ServerNodeUpdateManyArgs<ExtArgs extends runtime.Types.Extensions.In
limit?: number
}
/**
* ServerNode updateManyAndReturn
*/
export type ServerNodeUpdateManyAndReturnArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
/**
* Select specific fields to fetch from the ServerNode
*/
select?: Prisma.ServerNodeSelectUpdateManyAndReturn<ExtArgs> | null
/**
* Omit specific fields from the ServerNode
*/
omit?: Prisma.ServerNodeOmit<ExtArgs> | null
/**
* The data used to update ServerNodes.
*/
data: Prisma.XOR<Prisma.ServerNodeUpdateManyMutationInput, Prisma.ServerNodeUncheckedUpdateManyInput>
/**
* Filter which ServerNodes to update
*/
where?: Prisma.ServerNodeWhereInput
/**
* Limit how many ServerNodes to update.
*/
limit?: number
}
/**
* ServerNode upsert
*/
+127 -7
View File
@@ -319,7 +319,6 @@ export type ServerNodeHistoryOrderByWithRelationInput = {
process?: Prisma.SortOrder
connections?: Prisma.SortOrder
createdAt?: Prisma.SortOrder
_relevance?: Prisma.ServerNodeHistoryOrderByRelevanceInput
}
export type ServerNodeHistoryWhereUniqueInput = Prisma.AtLeast<{
@@ -481,12 +480,6 @@ export type ServerNodeHistoryUncheckedUpdateManyInput = {
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
}
export type ServerNodeHistoryOrderByRelevanceInput = {
fields: Prisma.ServerNodeHistoryOrderByRelevanceFieldEnum | Prisma.ServerNodeHistoryOrderByRelevanceFieldEnum[]
sort: Prisma.SortOrder
search: string
}
export type ServerNodeHistoryUuidPeriodCompoundUniqueInput = {
uuid: string
period: Date | string
@@ -578,7 +571,35 @@ export type ServerNodeHistorySelect<ExtArgs extends runtime.Types.Extensions.Int
createdAt?: boolean
}, ExtArgs["result"]["serverNodeHistory"]>
export type ServerNodeHistorySelectCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
id?: boolean
uuid?: boolean
period?: boolean
cpu?: boolean
ram?: boolean
load?: boolean
disk?: boolean
netIn?: boolean
netOut?: boolean
process?: boolean
connections?: boolean
createdAt?: boolean
}, ExtArgs["result"]["serverNodeHistory"]>
export type ServerNodeHistorySelectUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
id?: boolean
uuid?: boolean
period?: boolean
cpu?: boolean
ram?: boolean
load?: boolean
disk?: boolean
netIn?: boolean
netOut?: boolean
process?: boolean
connections?: boolean
createdAt?: boolean
}, ExtArgs["result"]["serverNodeHistory"]>
export type ServerNodeHistorySelectScalar = {
id?: boolean
@@ -730,6 +751,30 @@ export interface ServerNodeHistoryDelegate<ExtArgs extends runtime.Types.Extensi
*/
createMany<T extends ServerNodeHistoryCreateManyArgs>(args?: Prisma.SelectSubset<T, ServerNodeHistoryCreateManyArgs<ExtArgs>>): Prisma.PrismaPromise<Prisma.BatchPayload>
/**
* Create many ServerNodeHistories and returns the data saved in the database.
* @param {ServerNodeHistoryCreateManyAndReturnArgs} args - Arguments to create many ServerNodeHistories.
* @example
* // Create many ServerNodeHistories
* const serverNodeHistory = await prisma.serverNodeHistory.createManyAndReturn({
* data: [
* // ... provide data here
* ]
* })
*
* // Create many ServerNodeHistories and only return the `id`
* const serverNodeHistoryWithIdOnly = await prisma.serverNodeHistory.createManyAndReturn({
* select: { id: true },
* data: [
* // ... provide data here
* ]
* })
* Note, that providing `undefined` is treated as the value not being there.
* Read more here: https://pris.ly/d/null-undefined
*
*/
createManyAndReturn<T extends ServerNodeHistoryCreateManyAndReturnArgs>(args?: Prisma.SelectSubset<T, ServerNodeHistoryCreateManyAndReturnArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$ServerNodeHistoryPayload<ExtArgs>, T, "createManyAndReturn", GlobalOmitOptions>>
/**
* Delete a ServerNodeHistory.
* @param {ServerNodeHistoryDeleteArgs} args - Arguments to delete one ServerNodeHistory.
@@ -794,6 +839,36 @@ export interface ServerNodeHistoryDelegate<ExtArgs extends runtime.Types.Extensi
*/
updateMany<T extends ServerNodeHistoryUpdateManyArgs>(args: Prisma.SelectSubset<T, ServerNodeHistoryUpdateManyArgs<ExtArgs>>): Prisma.PrismaPromise<Prisma.BatchPayload>
/**
* Update zero or more ServerNodeHistories and returns the data updated in the database.
* @param {ServerNodeHistoryUpdateManyAndReturnArgs} args - Arguments to update many ServerNodeHistories.
* @example
* // Update many ServerNodeHistories
* const serverNodeHistory = await prisma.serverNodeHistory.updateManyAndReturn({
* where: {
* // ... provide filter here
* },
* data: [
* // ... provide data here
* ]
* })
*
* // Update zero or more ServerNodeHistories and only return the `id`
* const serverNodeHistoryWithIdOnly = await prisma.serverNodeHistory.updateManyAndReturn({
* select: { id: true },
* where: {
* // ... provide filter here
* },
* data: [
* // ... provide data here
* ]
* })
* Note, that providing `undefined` is treated as the value not being there.
* Read more here: https://pris.ly/d/null-undefined
*
*/
updateManyAndReturn<T extends ServerNodeHistoryUpdateManyAndReturnArgs>(args: Prisma.SelectSubset<T, ServerNodeHistoryUpdateManyAndReturnArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$ServerNodeHistoryPayload<ExtArgs>, T, "updateManyAndReturn", GlobalOmitOptions>>
/**
* Create or update one ServerNodeHistory.
* @param {ServerNodeHistoryUpsertArgs} args - Arguments to update or create a ServerNodeHistory.
@@ -1207,6 +1282,25 @@ export type ServerNodeHistoryCreateManyArgs<ExtArgs extends runtime.Types.Extens
skipDuplicates?: boolean
}
/**
* ServerNodeHistory createManyAndReturn
*/
export type ServerNodeHistoryCreateManyAndReturnArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
/**
* Select specific fields to fetch from the ServerNodeHistory
*/
select?: Prisma.ServerNodeHistorySelectCreateManyAndReturn<ExtArgs> | null
/**
* Omit specific fields from the ServerNodeHistory
*/
omit?: Prisma.ServerNodeHistoryOmit<ExtArgs> | null
/**
* The data used to create many ServerNodeHistories.
*/
data: Prisma.ServerNodeHistoryCreateManyInput | Prisma.ServerNodeHistoryCreateManyInput[]
skipDuplicates?: boolean
}
/**
* ServerNodeHistory update
*/
@@ -1247,6 +1341,32 @@ export type ServerNodeHistoryUpdateManyArgs<ExtArgs extends runtime.Types.Extens
limit?: number
}
/**
* ServerNodeHistory updateManyAndReturn
*/
export type ServerNodeHistoryUpdateManyAndReturnArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
/**
* Select specific fields to fetch from the ServerNodeHistory
*/
select?: Prisma.ServerNodeHistorySelectUpdateManyAndReturn<ExtArgs> | null
/**
* Omit specific fields from the ServerNodeHistory
*/
omit?: Prisma.ServerNodeHistoryOmit<ExtArgs> | null
/**
* The data used to update ServerNodeHistories.
*/
data: Prisma.XOR<Prisma.ServerNodeHistoryUpdateManyMutationInput, Prisma.ServerNodeHistoryUncheckedUpdateManyInput>
/**
* Filter which ServerNodeHistories to update
*/
where?: Prisma.ServerNodeHistoryWhereInput
/**
* Limit how many ServerNodeHistories to update.
*/
limit?: number
}
/**
* ServerNodeHistory upsert
*/
+121 -7
View File
@@ -264,7 +264,6 @@ export type ServiceCheckOrderByWithRelationInput = {
lastCheckAt?: Prisma.SortOrder
createdAt?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder
_relevance?: Prisma.ServiceCheckOrderByRelevanceInput
}
export type ServiceCheckWhereUniqueInput = Prisma.AtLeast<{
@@ -395,12 +394,6 @@ export type ServiceCheckUncheckedUpdateManyInput = {
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
}
export type ServiceCheckOrderByRelevanceInput = {
fields: Prisma.ServiceCheckOrderByRelevanceFieldEnum | Prisma.ServiceCheckOrderByRelevanceFieldEnum[]
sort: Prisma.SortOrder
search: string
}
export type ServiceCheckCountOrderByAggregateInput = {
id?: Prisma.SortOrder
name?: Prisma.SortOrder
@@ -461,7 +454,29 @@ export type ServiceCheckSelect<ExtArgs extends runtime.Types.Extensions.Internal
updatedAt?: boolean
}, ExtArgs["result"]["serviceCheck"]>
export type ServiceCheckSelectCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
id?: boolean
name?: boolean
url?: boolean
status?: boolean
latencyMs?: boolean
message?: boolean
lastCheckAt?: boolean
createdAt?: boolean
updatedAt?: boolean
}, ExtArgs["result"]["serviceCheck"]>
export type ServiceCheckSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
id?: boolean
name?: boolean
url?: boolean
status?: boolean
latencyMs?: boolean
message?: boolean
lastCheckAt?: boolean
createdAt?: boolean
updatedAt?: boolean
}, ExtArgs["result"]["serviceCheck"]>
export type ServiceCheckSelectScalar = {
id?: boolean
@@ -607,6 +622,30 @@ export interface ServiceCheckDelegate<ExtArgs extends runtime.Types.Extensions.I
*/
createMany<T extends ServiceCheckCreateManyArgs>(args?: Prisma.SelectSubset<T, ServiceCheckCreateManyArgs<ExtArgs>>): Prisma.PrismaPromise<Prisma.BatchPayload>
/**
* Create many ServiceChecks and returns the data saved in the database.
* @param {ServiceCheckCreateManyAndReturnArgs} args - Arguments to create many ServiceChecks.
* @example
* // Create many ServiceChecks
* const serviceCheck = await prisma.serviceCheck.createManyAndReturn({
* data: [
* // ... provide data here
* ]
* })
*
* // Create many ServiceChecks and only return the `id`
* const serviceCheckWithIdOnly = await prisma.serviceCheck.createManyAndReturn({
* select: { id: true },
* data: [
* // ... provide data here
* ]
* })
* Note, that providing `undefined` is treated as the value not being there.
* Read more here: https://pris.ly/d/null-undefined
*
*/
createManyAndReturn<T extends ServiceCheckCreateManyAndReturnArgs>(args?: Prisma.SelectSubset<T, ServiceCheckCreateManyAndReturnArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$ServiceCheckPayload<ExtArgs>, T, "createManyAndReturn", GlobalOmitOptions>>
/**
* Delete a ServiceCheck.
* @param {ServiceCheckDeleteArgs} args - Arguments to delete one ServiceCheck.
@@ -671,6 +710,36 @@ export interface ServiceCheckDelegate<ExtArgs extends runtime.Types.Extensions.I
*/
updateMany<T extends ServiceCheckUpdateManyArgs>(args: Prisma.SelectSubset<T, ServiceCheckUpdateManyArgs<ExtArgs>>): Prisma.PrismaPromise<Prisma.BatchPayload>
/**
* Update zero or more ServiceChecks and returns the data updated in the database.
* @param {ServiceCheckUpdateManyAndReturnArgs} args - Arguments to update many ServiceChecks.
* @example
* // Update many ServiceChecks
* const serviceCheck = await prisma.serviceCheck.updateManyAndReturn({
* where: {
* // ... provide filter here
* },
* data: [
* // ... provide data here
* ]
* })
*
* // Update zero or more ServiceChecks and only return the `id`
* const serviceCheckWithIdOnly = await prisma.serviceCheck.updateManyAndReturn({
* select: { id: true },
* where: {
* // ... provide filter here
* },
* data: [
* // ... provide data here
* ]
* })
* Note, that providing `undefined` is treated as the value not being there.
* Read more here: https://pris.ly/d/null-undefined
*
*/
updateManyAndReturn<T extends ServiceCheckUpdateManyAndReturnArgs>(args: Prisma.SelectSubset<T, ServiceCheckUpdateManyAndReturnArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$ServiceCheckPayload<ExtArgs>, T, "updateManyAndReturn", GlobalOmitOptions>>
/**
* Create or update one ServiceCheck.
* @param {ServiceCheckUpsertArgs} args - Arguments to update or create a ServiceCheck.
@@ -1081,6 +1150,25 @@ export type ServiceCheckCreateManyArgs<ExtArgs extends runtime.Types.Extensions.
skipDuplicates?: boolean
}
/**
* ServiceCheck createManyAndReturn
*/
export type ServiceCheckCreateManyAndReturnArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
/**
* Select specific fields to fetch from the ServiceCheck
*/
select?: Prisma.ServiceCheckSelectCreateManyAndReturn<ExtArgs> | null
/**
* Omit specific fields from the ServiceCheck
*/
omit?: Prisma.ServiceCheckOmit<ExtArgs> | null
/**
* The data used to create many ServiceChecks.
*/
data: Prisma.ServiceCheckCreateManyInput | Prisma.ServiceCheckCreateManyInput[]
skipDuplicates?: boolean
}
/**
* ServiceCheck update
*/
@@ -1121,6 +1209,32 @@ export type ServiceCheckUpdateManyArgs<ExtArgs extends runtime.Types.Extensions.
limit?: number
}
/**
* ServiceCheck updateManyAndReturn
*/
export type ServiceCheckUpdateManyAndReturnArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
/**
* Select specific fields to fetch from the ServiceCheck
*/
select?: Prisma.ServiceCheckSelectUpdateManyAndReturn<ExtArgs> | null
/**
* Omit specific fields from the ServiceCheck
*/
omit?: Prisma.ServiceCheckOmit<ExtArgs> | null
/**
* The data used to update ServiceChecks.
*/
data: Prisma.XOR<Prisma.ServiceCheckUpdateManyMutationInput, Prisma.ServiceCheckUncheckedUpdateManyInput>
/**
* Filter which ServiceChecks to update
*/
where?: Prisma.ServiceCheckWhereInput
/**
* Limit how many ServiceChecks to update.
*/
limit?: number
}
/**
* ServiceCheck upsert
*/
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+13 -13
View File
@@ -1,14 +1,14 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover" />
<meta name="theme-color" content="#1f2937" />
<title>iBoard - 经济数据与策略</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover" />
<meta name="theme-color" content="#1f2937" />
<title>iBoard - 个人看板</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
+340 -122
View File
@@ -1,14 +1,17 @@
{
"name": "iboard",
"version": "1.0.0",
"version": "0.2.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "iboard",
"version": "1.0.0",
"version": "0.2.1",
"dependencies": {
"@prisma/adapter-mariadb": "^7.9.1",
"@journeyapps/wa-sqlite": "^2.0.7",
"@powersync/react": "^2.1.0",
"@powersync/web": "^2.5.0",
"@prisma/adapter-pg": "^7.9.1",
"@prisma/client": "^7.7.0",
"@zythum02/mijia-api": "^0.1.4",
"antd-mobile": "^5.42.3",
@@ -30,8 +33,8 @@
"ws": "^8.21.2"
},
"devDependencies": {
"@tauri-apps/api": "^2.0.0",
"@tauri-apps/cli": "^2.0.0",
"@tauri-apps/api": "^2.12.4",
"@tauri-apps/cli": "^2.12.4",
"@vitejs/plugin-react": "^4.3.1",
"cheerio": "^1.2.0",
"concurrently": "^9.0.0",
@@ -1187,6 +1190,11 @@
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@journeyapps/wa-sqlite": {
"version": "2.0.8",
"resolved": "https://registry.npmjs.org/@journeyapps/wa-sqlite/-/wa-sqlite-2.0.8.tgz",
"integrity": "sha512-WRXEms2515S/0DFSWzmlggHu9hSJomhugDUO17rWkL6VpokV/hfjIDLs954Egx/eqrk3WADmiOMzOc7MuJbznQ=="
},
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.13",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
@@ -1615,64 +1623,91 @@
"node": "^22.20 || ^24.12 || >=25"
}
},
"node_modules/@prisma/adapter-mariadb": {
"version": "7.9.1",
"resolved": "https://registry.npmjs.org/@prisma/adapter-mariadb/-/adapter-mariadb-7.9.1.tgz",
"integrity": "sha512-4aZUbIg2av7XFSQyM9cQeAp9YP9HiPr8H7ehe2rDo375IZXILEzQrNXZ/zLetgRQ2ojlaUl/brArFdOVS3n1xA==",
"node_modules/@powersync/common": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/@powersync/common/-/common-2.4.0.tgz",
"integrity": "sha512-ngVogy+B8NQeI5l+0Es8qMxnGozUie8Q6R4+0/AHA3oBAhDG5UmeYw/LquTzZJSOPnCmvGgFSqovblV+JKzxKQ==",
"license": "Apache-2.0"
},
"node_modules/@powersync/react": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@powersync/react/-/react-2.1.0.tgz",
"integrity": "sha512-ADIpakSLhstv0F9xrUIF+oCArFnAQPrcB9mNql6xH0Nup9EgBug38Ks+mv3yqbc33z60pBm9tuHs8uSIbkhuqg==",
"license": "Apache-2.0",
"peerDependencies": {
"@powersync/common": "^2.4.0",
"react": "*"
}
},
"node_modules/@powersync/shared-internals": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@powersync/shared-internals/-/shared-internals-1.4.0.tgz",
"integrity": "sha512-OiqqDuW6tePW4Ed7C5IWutBKrtcqKuTMlVMQNwUZtKtqr7ZrqKlb+YP5l4niuz8mpqDYo3/HNU0dpc+3mneSBQ==",
"license": "Apache-2.0",
"peerDependencies": {
"@powersync/common": "^2.4.0"
}
},
"node_modules/@powersync/web": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/@powersync/web/-/web-2.5.0.tgz",
"integrity": "sha512-/K2ZUjIdqvtFeoLfPetPcE+uioShBWzrTrOa8hpoZ7i62S8wgj47i+VPnKTXI7WQmXuxTGGZFUFnahinf6wjPg==",
"license": "Apache-2.0",
"dependencies": {
"@prisma/driver-adapter-utils": "7.9.1",
"mariadb": "3.4.5"
}
},
"node_modules/@prisma/adapter-mariadb/node_modules/@types/node": {
"version": "24.13.3",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz",
"integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==",
"license": "MIT",
"dependencies": {
"undici-types": "~7.18.0"
}
},
"node_modules/@prisma/adapter-mariadb/node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
"@journeyapps/wa-sqlite": "2.0.7",
"@powersync/common": "2.4.0",
"@powersync/shared-internals": "1.4.0",
"comlink": "^4.4.2",
"commander": "^12.1.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/@prisma/adapter-mariadb/node_modules/lru-cache": {
"version": "10.4.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
"license": "ISC"
},
"node_modules/@prisma/adapter-mariadb/node_modules/mariadb": {
"version": "3.4.5",
"resolved": "https://registry.npmjs.org/mariadb/-/mariadb-3.4.5.tgz",
"integrity": "sha512-gThTYkhIS5rRqkVr+Y0cIdzr+GRqJ9sA2Q34e0yzmyhMCwyApf3OKAC1jnF23aSlIOqJuyaUFUcj7O1qZslmmQ==",
"license": "LGPL-2.1-or-later",
"dependencies": {
"@types/geojson": "^7946.0.16",
"@types/node": "^24.0.13",
"denque": "^2.1.0",
"iconv-lite": "^0.6.3",
"lru-cache": "^10.4.3"
"bin": {
"powersync-web": "bin/powersync.cjs"
},
"engines": {
"node": ">= 14"
"peerDependencies": {
"@powersync/common": "^2.4.0",
"@powersync/shared-internals": "^1.4.0"
}
},
"node_modules/@prisma/adapter-mariadb/node_modules/undici-types": {
"version": "7.18.2",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
"integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
"license": "MIT"
"node_modules/@powersync/web/node_modules/@journeyapps/wa-sqlite": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@journeyapps/wa-sqlite/-/wa-sqlite-2.0.7.tgz",
"integrity": "sha512-mc6xUricl4QB2s8QTRGS+FofSr1cjwDtf6dPXAdMBuGWodjfEgkSJGDPWQ7zVqgc7hMfXYLT7WIfEBAxara8YA=="
},
"node_modules/@powersync/web/node_modules/commander": {
"version": "12.1.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",
"integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==",
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/@prisma/adapter-pg": {
"version": "7.10.1",
"resolved": "https://registry.npmjs.org/@prisma/adapter-pg/-/adapter-pg-7.10.1.tgz",
"integrity": "sha512-EP9ruPYHjKP80WZlb5qVaEXKgZjJieBFjAxcBtQnDAHD6+16UN4keVmBh6L/RUpuDQT2FQRbvKNgq5d4Ix+IkQ==",
"license": "Apache-2.0",
"dependencies": {
"@prisma/driver-adapter-utils": "7.10.1",
"@types/pg": "^8.16.0",
"pg": "^8.16.3",
"postgres-array": "3.0.4"
}
},
"node_modules/@prisma/adapter-pg/node_modules/@prisma/debug": {
"version": "7.10.1",
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.10.1.tgz",
"integrity": "sha512-eFCAkGZ13D/qPg48JBlG5IWi9cSBBOC7oG3abDPcAkYioDWlIjh0YJJLK2J7qo5S+xWWD8I6egAN/8KbCaiMCA==",
"license": "Apache-2.0"
},
"node_modules/@prisma/adapter-pg/node_modules/@prisma/driver-adapter-utils": {
"version": "7.10.1",
"resolved": "https://registry.npmjs.org/@prisma/driver-adapter-utils/-/driver-adapter-utils-7.10.1.tgz",
"integrity": "sha512-DJ4c6xvVnm3s9ZUyql9nCUicADSqyvH+CjXRCM9L6m5zJxCNmebCElG6X4irICq3rosBwlX+S19Dxq/4IrZk3A==",
"license": "Apache-2.0",
"dependencies": {
"@prisma/debug": "7.10.1"
}
},
"node_modules/@prisma/client": {
"version": "7.7.0",
@@ -1747,21 +1782,6 @@
"zeptomatch": "2.1.0"
}
},
"node_modules/@prisma/driver-adapter-utils": {
"version": "7.9.1",
"resolved": "https://registry.npmjs.org/@prisma/driver-adapter-utils/-/driver-adapter-utils-7.9.1.tgz",
"integrity": "sha512-vmHehG7nn/heW32DXXpp13DxxAxVVe6n250oEt3dOL2E/4bt3olktKZN0mzSuxMMronyMSkbeW2uCOn3F4g8RQ==",
"license": "Apache-2.0",
"dependencies": {
"@prisma/debug": "7.9.1"
}
},
"node_modules/@prisma/driver-adapter-utils/node_modules/@prisma/debug": {
"version": "7.9.1",
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.9.1.tgz",
"integrity": "sha512-/cpVZ4itxtcgB8GHBvZtcmuEjq+lWsLrRJxFMbwZrT1RIdtuKmUm7PPGo/wzfbYpBrk+9WmmBE8CHJw2rybKDQ==",
"license": "Apache-2.0"
},
"node_modules/@prisma/engines": {
"version": "7.7.0",
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-7.7.0.tgz",
@@ -2505,9 +2525,9 @@
"license": "MIT"
},
"node_modules/@tauri-apps/api": {
"version": "2.11.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.11.1.tgz",
"integrity": "sha512-M2FPuYND2m+wh5hfW9ZpSdxMPdEJovPBWwoHJmwUpysTYNHaOkVFN419m/K0LIgjb/7KU2vBgsUepJWugQCvAA==",
"version": "2.12.4",
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.12.4.tgz",
"integrity": "sha512-pVmfis+4zT8H0Qhp3xPZx9Ju7kpzvA6rr/PSJPyuwfvucTQ8sVt61NYU9KMkK703UFlYadFmjKA4HhybOtN0gg==",
"license": "Apache-2.0 OR MIT",
"funding": {
"type": "opencollective",
@@ -2515,9 +2535,9 @@
}
},
"node_modules/@tauri-apps/cli": {
"version": "2.11.4",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.11.4.tgz",
"integrity": "sha512-R8xGtMpwyetawSqm9kYOuMmEqkhUbvcUy8n0aNXIxollKBLESUu5f4Fx+64hgASYm1H+jSWq6jCW6zqTnH6hqQ==",
"version": "2.12.4",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.12.4.tgz",
"integrity": "sha512-JI9bHfudSRJikGOrjbXEm6RjzzvmKuLc+JixB1NE8hNzQkNlCCIC3pTES3uypkR9lxHrsbxzcnSwhI2IxBQphw==",
"dev": true,
"license": "Apache-2.0 OR MIT",
"bin": {
@@ -2531,23 +2551,23 @@
"url": "https://opencollective.com/tauri"
},
"optionalDependencies": {
"@tauri-apps/cli-darwin-arm64": "2.11.4",
"@tauri-apps/cli-darwin-x64": "2.11.4",
"@tauri-apps/cli-linux-arm-gnueabihf": "2.11.4",
"@tauri-apps/cli-linux-arm64-gnu": "2.11.4",
"@tauri-apps/cli-linux-arm64-musl": "2.11.4",
"@tauri-apps/cli-linux-riscv64-gnu": "2.11.4",
"@tauri-apps/cli-linux-x64-gnu": "2.11.4",
"@tauri-apps/cli-linux-x64-musl": "2.11.4",
"@tauri-apps/cli-win32-arm64-msvc": "2.11.4",
"@tauri-apps/cli-win32-ia32-msvc": "2.11.4",
"@tauri-apps/cli-win32-x64-msvc": "2.11.4"
"@tauri-apps/cli-darwin-arm64": "2.12.4",
"@tauri-apps/cli-darwin-x64": "2.12.4",
"@tauri-apps/cli-linux-arm-gnueabihf": "2.12.4",
"@tauri-apps/cli-linux-arm64-gnu": "2.12.4",
"@tauri-apps/cli-linux-arm64-musl": "2.12.4",
"@tauri-apps/cli-linux-riscv64-gnu": "2.12.4",
"@tauri-apps/cli-linux-x64-gnu": "2.12.4",
"@tauri-apps/cli-linux-x64-musl": "2.12.4",
"@tauri-apps/cli-win32-arm64-msvc": "2.12.4",
"@tauri-apps/cli-win32-ia32-msvc": "2.12.4",
"@tauri-apps/cli-win32-x64-msvc": "2.12.4"
}
},
"node_modules/@tauri-apps/cli-darwin-arm64": {
"version": "2.11.4",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.11.4.tgz",
"integrity": "sha512-1ryOF3ZhpZ/nemHV5zVwBQBz9jDGKmKPvWPADOhc83ig0P4bMc2iER4NbC6r9sjeIZ6RVQ4g3RZIYvezhcl4TQ==",
"version": "2.12.4",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.12.4.tgz",
"integrity": "sha512-OxqKSckIkoJ6vX2AjifIJpTasFC0Hbdoo7gEiCmAjauABO3U8m2MRsLWe+QTiBwDx1mIrvgA+rvI7tvDnbnHVg==",
"cpu": [
"arm64"
],
@@ -2562,9 +2582,9 @@
}
},
"node_modules/@tauri-apps/cli-darwin-x64": {
"version": "2.11.4",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.11.4.tgz",
"integrity": "sha512-uFsGQAAfuyz1k/yGLmkWfkBlgKAqZfxqlHmLWx81QU27RJWfmbNHCIq8T8w1e+VClleIuZUjpHWfoE4E3DLo3A==",
"version": "2.12.4",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.12.4.tgz",
"integrity": "sha512-fz76/77wMMJOaaUN6n3CybRG0P/MTEjjqYk81mGPyhi9m6Ue5T+LWBnJP/AkPooNatBPI2vtulGqomyMJ44lCQ==",
"cpu": [
"x64"
],
@@ -2579,9 +2599,9 @@
}
},
"node_modules/@tauri-apps/cli-linux-arm-gnueabihf": {
"version": "2.11.4",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.11.4.tgz",
"integrity": "sha512-IaHZn5CdBL21oUmjiVOS1ctw6Ip1O0pjp70FwOWmYz1myWe0SY96ZIj2FYf7pT0m8bI2h/hrs5ZbEXXh44/MkQ==",
"version": "2.12.4",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.12.4.tgz",
"integrity": "sha512-K0w8Bof6Lt8QKLAYhRy0yy36ZH5iA/35VuSOvnxyjvCUZ+A3JKsUTlALviH/sVgAlisaPzzmeJSH8XOn4Q2HBA==",
"cpu": [
"arm"
],
@@ -2596,9 +2616,9 @@
}
},
"node_modules/@tauri-apps/cli-linux-arm64-gnu": {
"version": "2.11.4",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.11.4.tgz",
"integrity": "sha512-N41/ukTRVe6XSuUTESuFdGeOW2i7k62tK+6gHK5Kd5/q5RPvvi19GaWAVPPb9u95HSGmTChSolBfzynUsssFaA==",
"version": "2.12.4",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.12.4.tgz",
"integrity": "sha512-j5rOnjmRUwWrm1oosf0zBYl4D+fqydgUROFCk0hvKZxvbTT8SOERes0QB7rBV/XWNy+kM6pCKzb3oSm2sRglzg==",
"cpu": [
"arm64"
],
@@ -2613,9 +2633,9 @@
}
},
"node_modules/@tauri-apps/cli-linux-arm64-musl": {
"version": "2.11.4",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.11.4.tgz",
"integrity": "sha512-v277UnT/fB64xAfSroL5N3Km3tLmvATWqJJw/wRI+g6o+HkeD0slyE7gOhNs1MbjE41R7bQOTxMVoL3aomUJmw==",
"version": "2.12.4",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.12.4.tgz",
"integrity": "sha512-aOue3Fy6Jb7+oWcfAVUsVHfeTybSCt2CZoU06aO4jv4d2Ks/KnaTPwMAK3CLEduGOvBLY1lbBBrIyomqLLH5Sg==",
"cpu": [
"arm64"
],
@@ -2630,9 +2650,9 @@
}
},
"node_modules/@tauri-apps/cli-linux-riscv64-gnu": {
"version": "2.11.4",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.11.4.tgz",
"integrity": "sha512-qqgNkQ2u1yZHxjhxsZaxUtRDW8dIqIYm33rx/mzwQv0SfY9x1B+iraj8vWeFiXjjSVVhEMepXSOts1TqPzvXNQ==",
"version": "2.12.4",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.12.4.tgz",
"integrity": "sha512-Fx6GShY/RvST+t6LMlTHAwd66+1tEaAAGrnU7cysVfaiRP1BKng5Vl6rVmO6rSLOA1Sp1dRzNYM4f0OWQGb7qA==",
"cpu": [
"riscv64"
],
@@ -2647,9 +2667,9 @@
}
},
"node_modules/@tauri-apps/cli-linux-x64-gnu": {
"version": "2.11.4",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.11.4.tgz",
"integrity": "sha512-2VRNWl84FOH0m2giiDkO2h0QXlcMJeX+zJDpI5kDIQAx6s+geF3v48F4DXfJez4GS/FdoDGnPnw1C2iYGbQ7bQ==",
"version": "2.12.4",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.12.4.tgz",
"integrity": "sha512-tNzZSa79Z1nRpHxEpQ38lsAFpKlFlYvoXzjTV+lRRn/zSf3wrdUC3wnLcq2XEadchPu4ttK0zivhYzr+XGD19Q==",
"cpu": [
"x64"
],
@@ -2664,9 +2684,9 @@
}
},
"node_modules/@tauri-apps/cli-linux-x64-musl": {
"version": "2.11.4",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.11.4.tgz",
"integrity": "sha512-o9GyhYor/nc7xarmwDE3ka2szuW3uuZzXjHWh64Q8YX5AtSgxdQkFWzrY4O8KiGtVNvFBI14H3Q49Qj5TOIP/A==",
"version": "2.12.4",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.12.4.tgz",
"integrity": "sha512-jmZ1pr0xEqUhuEusWtt9Yt+GF6lCnGHUmIL+KIknpbjK/XQ2qkhDBhaxK/JMHM2A8dAacVXCxXJk2EXpwhQUfA==",
"cpu": [
"x64"
],
@@ -2681,9 +2701,9 @@
}
},
"node_modules/@tauri-apps/cli-win32-arm64-msvc": {
"version": "2.11.4",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.11.4.tgz",
"integrity": "sha512-ld5Ehb598m0VkYyylRPNeCFsBe/km0jxis6KgMpl3IGY6I/i1RwQXO05I1AsXUXO2WC6AvB/Lw4qTf/asiuEiQ==",
"version": "2.12.4",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.12.4.tgz",
"integrity": "sha512-fGo8pgEHZxdSYgZ/J98m1usJ7NHKmKbq7k6uT0x1rpOq6Bg2YVzzpaAblWk710f5LHYBJIL5QIFS5IrD+4gmTg==",
"cpu": [
"arm64"
],
@@ -2698,9 +2718,9 @@
}
},
"node_modules/@tauri-apps/cli-win32-ia32-msvc": {
"version": "2.11.4",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.11.4.tgz",
"integrity": "sha512-12Hxi0XX/H5VFxO/bGgHkFWhml9VMgEOu9CidjeCeTNQ1l6fpUlbiGgSP7CLI3PFtW9/FfbeHieZ+kyWK5H7CA==",
"version": "2.12.4",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.12.4.tgz",
"integrity": "sha512-ezl8sQIcjYsye3TUnTH5VcWmWzccZASlQm7hlZt0dYyj1XD0qpVW47u2xYVrIKui0F8uQv/7QGZixdWiClBsbg==",
"cpu": [
"ia32"
],
@@ -2715,9 +2735,9 @@
}
},
"node_modules/@tauri-apps/cli-win32-x64-msvc": {
"version": "2.11.4",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.11.4.tgz",
"integrity": "sha512-+vDiqBIU5dMISg/wNvX3sF+ZHfgJGJ5T0AcO+EHNXV9GGAG+P5fzodlDXD3QdKCRgZxMoCm5PPvj3BqLNjBthw==",
"version": "2.12.4",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.12.4.tgz",
"integrity": "sha512-DJhDeCqjLDNMWYFy7ZS8cfeAHX2RK0RbQMLiA3bMVVkXRDJ4NBYiHriGqsUhsr9iG41aTb+dJGkFPmPw1gWwVw==",
"cpu": [
"x64"
],
@@ -2867,6 +2887,17 @@
"undici-types": "~8.3.0"
}
},
"node_modules/@types/pg": {
"version": "8.23.1",
"resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.23.1.tgz",
"integrity": "sha512-fKVHpikPdg4GKks3JuLEhvwSyvwzF23hnabPy6DD8ljVbC7+6J5dQzdv4arV6jqq57djnMgs1HKBxX4P8aBI3A==",
"license": "MIT",
"dependencies": {
"@types/node": "*",
"pg-protocol": "*",
"pg-types": "^2.2.0"
}
},
"node_modules/@types/react": {
"version": "19.2.14",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
@@ -3193,6 +3224,21 @@
"integrity": "sha512-YOf0VSj5nUPI27doTtXF+BBnsiRq3qY7avHqfIWnppxTLGyvkLq1QV2RTxkwoZwJ60ywLfZ0raFF4J/G886i7A==",
"license": "MIT"
},
"node_modules/better-sqlite3": {
"version": "13.0.3",
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-13.0.3.tgz",
"integrity": "sha512-RbOBxmLBG8uvFUc15X9+9SFemKcQ0WBuISBVkpuiaUB2qblC8UWlHEjdWVoZ8AdhSwmoEgsiXKfopX0CQxaACQ==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"node-addon-api": "^8.0.0"
},
"engines": {
"node": ">=22"
}
},
"node_modules/body-parser": {
"version": "1.20.6",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.6.tgz",
@@ -3575,6 +3621,12 @@
"node": ">= 0.8"
}
},
"node_modules/comlink": {
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/comlink/-/comlink-4.4.2.tgz",
"integrity": "sha512-OxGdvBmJuNKSCMO4NTl1L47VRp6xn2wG4F/2hYzB6tiCb709otOxtEYCSvK80PtjODfXXZu8ds+Nw5kVCjqd2g==",
"license": "Apache-2.0"
},
"node_modules/commander": {
"version": "13.1.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz",
@@ -5362,6 +5414,17 @@
"node": ">= 0.6"
}
},
"node_modules/node-addon-api": {
"version": "8.9.2",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.9.2.tgz",
"integrity": "sha512-VijLXbi3UACN69I0JVXJsX4tjACjNoQDgv2gTF6sx2wWEi8tkSg2eX8p5gSIFi8z2+DL3oHmY6OyKce38SDolg==",
"license": "MIT",
"optional": true,
"peer": true,
"engines": {
"node": "^18 || ^20 || >= 21"
}
},
"node_modules/node-cron": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/node-cron/-/node-cron-3.0.3.tgz",
@@ -5579,6 +5642,104 @@
"integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==",
"license": "MIT"
},
"node_modules/pg": {
"version": "8.23.1",
"resolved": "https://registry.npmjs.org/pg/-/pg-8.23.1.tgz",
"integrity": "sha512-aL96AHANtWjPLDOLqnhx+ngp9+UK7ETEU8VJrDCGvsSSi/mGLcWYsS6Herg7lmaBJe4uwrfqsa7gTEFaSizDoQ==",
"license": "MIT",
"dependencies": {
"pg-connection-string": "^2.14.1",
"pg-pool": "^3.14.0",
"pg-protocol": "^1.16.1",
"pg-types": "2.2.0",
"pgpass": "1.0.5"
},
"engines": {
"node": ">= 16.0.0"
},
"optionalDependencies": {
"pg-cloudflare": "^1.4.1"
},
"peerDependencies": {
"pg-native": ">=3.0.1"
},
"peerDependenciesMeta": {
"pg-native": {
"optional": true
}
}
},
"node_modules/pg-cloudflare": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.4.1.tgz",
"integrity": "sha512-6PQbsFWZcp9EmJEwy5cGQ2La+AMWpP46lgbb8X+U/XsHIUweYDNCpeuKck5RxL2MdVFi7krbbEi5nX4Zh7JhrQ==",
"license": "MIT",
"optional": true
},
"node_modules/pg-connection-string": {
"version": "2.14.1",
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.14.1.tgz",
"integrity": "sha512-qR3kGNPBLpCNtz0evbKA0Y/MRFXwSSdT+pTJvYp/bXTcReZbvX1kzF0IyTc1QnxqF7AZbOeBhNL8R5mYQZV/MA==",
"license": "MIT"
},
"node_modules/pg-int8": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
"integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
"license": "ISC",
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/pg-pool": {
"version": "3.14.0",
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.14.0.tgz",
"integrity": "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==",
"license": "MIT",
"peerDependencies": {
"pg": ">=8.0"
}
},
"node_modules/pg-protocol": {
"version": "1.16.1",
"resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.16.1.tgz",
"integrity": "sha512-p9VOFMiHB/ZbJATetbg+99PxssTVSQRnyuPSQ67mN1+1KBOjZaZ83ZQzltnxPhJwSsC3nwVjJ10DVJlerbFzLg==",
"license": "MIT"
},
"node_modules/pg-types": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
"integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
"license": "MIT",
"dependencies": {
"pg-int8": "1.0.1",
"postgres-array": "~2.0.0",
"postgres-bytea": "~1.0.0",
"postgres-date": "~1.0.4",
"postgres-interval": "^1.1.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/pg-types/node_modules/postgres-array": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
"integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/pgpass": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
"integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
"license": "MIT",
"dependencies": {
"split2": "^4.1.0"
}
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@@ -5619,6 +5780,45 @@
"url": "https://github.com/sponsors/porsager"
}
},
"node_modules/postgres-array": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.4.tgz",
"integrity": "sha512-nAUSGfSDGOaOAEGwqsRY27GPOea7CNipJPOA7lPbdEpx5Kg3qzdP0AaWC5MlhTWV9s4hFX39nomVZ+C4tnGOJQ==",
"license": "MIT",
"engines": {
"node": ">=12"
}
},
"node_modules/postgres-bytea": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz",
"integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/postgres-date": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
"integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/postgres-interval": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
"integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
"license": "MIT",
"dependencies": {
"xtend": "^4.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/prisma": {
"version": "7.7.0",
"resolved": "https://registry.npmjs.org/prisma/-/prisma-7.7.0.tgz",
@@ -6470,6 +6670,15 @@
"node": ">=0.10.0"
}
},
"node_modules/split2": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
"integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
"license": "ISC",
"engines": {
"node": ">= 10.x"
}
},
"node_modules/sprintf-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
@@ -6938,6 +7147,15 @@
}
}
},
"node_modules/xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
"license": "MIT",
"engines": {
"node": ">=0.4"
}
},
"node_modules/y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
+11 -6
View File
@@ -1,6 +1,6 @@
{
"name": "iboard",
"version": "0.2.0",
"version": "0.2.1",
"type": "module",
"scripts": {
"tauri": "tauri",
@@ -8,14 +8,16 @@
"dev:server": "node --watch --experimental-strip-types --no-warnings server/index.js",
"dev": "concurrently -n web,api -c blue,green \"npm run dev:web\" \"npm run dev:server\"",
"build": "vite build",
"build:shell": "vite build --mode shell",
"build:win": "node scripts/build-windows.mjs",
"build:win:debug": "node scripts/build-windows.mjs --debug",
"win:dev": "node scripts/dev-windows-live.mjs",
"build:ota": "node scripts/build-ota.mjs",
"preview": "vite preview",
"start": "node --experimental-strip-types --no-warnings server/index.js",
"db:migrate": "prisma migrate deploy",
"db:seed": "node scripts/seed.cjs",
"registry:sync": "node scripts/sync-registry.mjs",
"data:refresh": "node --no-warnings scripts/refresh-indicators.cjs",
"data:discover": "node --no-warnings scripts/discover-mcp-tools.cjs",
"icons": "node scripts/generate-icons.mjs",
"tauri:dev": "tauri dev",
"tauri:build": "tauri build",
@@ -30,7 +32,10 @@
"publish": "bash scripts/wsl/build-apk.sh release"
},
"dependencies": {
"@prisma/adapter-mariadb": "^7.9.1",
"@journeyapps/wa-sqlite": "^2.0.7",
"@powersync/react": "^2.1.0",
"@powersync/web": "^2.5.0",
"@prisma/adapter-pg": "^7.9.1",
"@prisma/client": "^7.7.0",
"@zythum02/mijia-api": "^0.1.4",
"antd-mobile": "^5.42.3",
@@ -52,8 +57,8 @@
"ws": "^8.21.2"
},
"devDependencies": {
"@tauri-apps/api": "^2.0.0",
"@tauri-apps/cli": "^2.0.0",
"@tauri-apps/api": "^2.12.4",
"@tauri-apps/cli": "^2.12.4",
"@vitejs/plugin-react": "^4.3.1",
"cheerio": "^1.2.0",
"concurrently": "^9.0.0",
@@ -1,62 +0,0 @@
-- CreateTable
CREATE TABLE `User` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`name` VARCHAR(191) NOT NULL,
`email` VARCHAR(191) NOT NULL,
`password` VARCHAR(191) NOT NULL,
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`updatedAt` DATETIME(3) NOT NULL,
UNIQUE INDEX `User_email_key`(`email`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `Article` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`title` VARCHAR(191) NOT NULL,
`slug` VARCHAR(191) NOT NULL,
`content` VARCHAR(191) NOT NULL,
`date` VARCHAR(191) NOT NULL,
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`updatedAt` DATETIME(3) NOT NULL,
UNIQUE INDEX `Article_slug_key`(`slug`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `Strategy` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`name` VARCHAR(191) NOT NULL,
`description` VARCHAR(191) NOT NULL,
`data` JSON NOT NULL,
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`updatedAt` DATETIME(3) NOT NULL,
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `Indicator` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`name` VARCHAR(191) NOT NULL,
`groupName` VARCHAR(191) NOT NULL,
`data` JSON NOT NULL,
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`updatedAt` DATETIME(3) NOT NULL,
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `SowInventory` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`month` VARCHAR(191) NOT NULL,
`inventory` INTEGER NOT NULL,
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`updatedAt` DATETIME(3) NOT NULL,
UNIQUE INDEX `SowInventory_month_key`(`month`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
@@ -1,20 +0,0 @@
/*
Warnings:
- You are about to drop the `SowInventory` table. If the table is not empty, all the data it contains will be lost.
*/
-- DropTable
DROP TABLE `SowInventory`;
-- CreateTable
CREATE TABLE `Econ_SowInventory` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`month` VARCHAR(191) NOT NULL,
`inventory` INTEGER NOT NULL,
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`updatedAt` DATETIME(3) NOT NULL,
UNIQUE INDEX `Econ_SowInventory_month_key`(`month`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
@@ -1,13 +0,0 @@
-- CreateTable
CREATE TABLE `ScraperCache` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`scraperName` VARCHAR(191) NOT NULL,
`lastDataHash` VARCHAR(191) NULL,
`lastUpdateAt` DATETIME(3) NOT NULL,
`metadata` JSON NULL,
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`updatedAt` DATETIME(3) NOT NULL,
UNIQUE INDEX `ScraperCache_scraperName_key`(`scraperName`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
@@ -1,5 +0,0 @@
-- Add unique constraint on Strategy.name to enable idempotent upsert in seed script
ALTER TABLE `Strategy` ADD UNIQUE INDEX `Strategy_name_key`(`name`);
-- Add unique constraint on Indicator.name to enable idempotent upsert in seed script
ALTER TABLE `Indicator` ADD UNIQUE INDEX `Indicator_name_key`(`name`);
@@ -1,24 +0,0 @@
-- 经济指标: 明细数据拆到独立表, 兼容年/季/月/日频率
-- 第一步: 建 IndicatorPoint 表 + Indicator 加元数据列 (data 列暂保留, 由迁移脚本搬运后删除)
-- CreateTable
CREATE TABLE `IndicatorPoint` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`indicatorId` INTEGER NOT NULL,
`period` VARCHAR(191) NOT NULL,
`value` DOUBLE NOT NULL,
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`updatedAt` DATETIME(3) NOT NULL,
UNIQUE INDEX `IndicatorPoint_indicatorId_period_key`(`indicatorId`, `period`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- AlterTable: 元数据列 (data JSON 拆出)
ALTER TABLE `Indicator` ADD COLUMN `title` VARCHAR(191) NOT NULL DEFAULT '',
ADD COLUMN `unit` VARCHAR(191) NOT NULL DEFAULT '',
ADD COLUMN `source` VARCHAR(191) NOT NULL DEFAULT '',
ADD COLUMN `frequency` VARCHAR(191) NOT NULL DEFAULT 'month';
-- AddForeignKey
ALTER TABLE `IndicatorPoint` ADD CONSTRAINT `IndicatorPoint_indicatorId_fkey` FOREIGN KEY (`indicatorId`) REFERENCES `Indicator`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
@@ -1,3 +0,0 @@
-- 第二步: 明细数据已迁移到 IndicatorPoint, 删除 Indicator.data JSON 列
-- AlterTable
ALTER TABLE `Indicator` DROP COLUMN `data`;
@@ -1,2 +0,0 @@
-- 指标隐藏标记: 数据与获取脚本保留, 前端展示过滤 hidden=true
ALTER TABLE `Indicator` ADD COLUMN `hidden` BOOLEAN NOT NULL DEFAULT false;
@@ -1,32 +0,0 @@
-- CreateTable
CREATE TABLE `News` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`title` VARCHAR(191) NOT NULL,
`summary` VARCHAR(191) NOT NULL DEFAULT '',
`source` VARCHAR(191) NOT NULL DEFAULT '',
`url` VARCHAR(191) NOT NULL DEFAULT '',
`publishedAt` DATETIME(3) NOT NULL,
`impactRank` INTEGER NOT NULL,
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
UNIQUE INDEX `News_title_publishedAt_key`(`title`, `publishedAt`),
INDEX `News_publishedAt_idx`(`publishedAt`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `Alert` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`indicatorName` VARCHAR(191) NOT NULL,
`period` VARCHAR(191) NOT NULL,
`valueA` DOUBLE NOT NULL,
`valueB` DOUBLE NOT NULL,
`sourceA` VARCHAR(191) NOT NULL,
`sourceB` VARCHAR(191) NOT NULL,
`diff` DOUBLE NOT NULL,
`diffRatio` DOUBLE NOT NULL,
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
INDEX `Alert_createdAt_idx`(`createdAt`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
@@ -1,68 +0,0 @@
-- 服务器监控 + 服务健康检查
-- CreateTable
CREATE TABLE `ServerNode` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`uuid` VARCHAR(191) NOT NULL,
`name` VARCHAR(191) NOT NULL,
`cpuName` VARCHAR(191) NOT NULL DEFAULT '',
`os` VARCHAR(191) NOT NULL DEFAULT '',
`region` VARCHAR(191) NOT NULL DEFAULT '',
`memTotal` BIGINT NOT NULL DEFAULT 0,
`diskTotal` BIGINT NOT NULL DEFAULT 0,
`cpuCores` INTEGER NOT NULL DEFAULT 0,
`cpuUsage` DOUBLE NOT NULL DEFAULT 0,
`ramUsed` BIGINT NOT NULL DEFAULT 0,
`swapUsed` BIGINT NOT NULL DEFAULT 0,
`load1` DOUBLE NOT NULL DEFAULT 0,
`load5` DOUBLE NOT NULL DEFAULT 0,
`load15` DOUBLE NOT NULL DEFAULT 0,
`diskUsed` BIGINT NOT NULL DEFAULT 0,
`netIn` BIGINT NOT NULL DEFAULT 0,
`netOut` BIGINT NOT NULL DEFAULT 0,
`uptime` BIGINT NOT NULL DEFAULT 0,
`process` INTEGER NOT NULL DEFAULT 0,
`connections` INTEGER NOT NULL DEFAULT 0,
`status` VARCHAR(191) NOT NULL DEFAULT 'unknown',
`recordedAt` DATETIME(3) NOT NULL,
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`updatedAt` DATETIME(3) NOT NULL,
UNIQUE INDEX `ServerNode_uuid_key`(`uuid`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `ServerNodeHistory` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`uuid` VARCHAR(191) NOT NULL,
`period` DATETIME(3) NOT NULL,
`cpu` DOUBLE NOT NULL DEFAULT 0,
`ram` BIGINT NOT NULL DEFAULT 0,
`load` DOUBLE NOT NULL DEFAULT 0,
`disk` BIGINT NOT NULL DEFAULT 0,
`netIn` BIGINT NOT NULL DEFAULT 0,
`netOut` BIGINT NOT NULL DEFAULT 0,
`process` INTEGER NOT NULL DEFAULT 0,
`connections` INTEGER NOT NULL DEFAULT 0,
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
UNIQUE INDEX `ServerNodeHistory_uuid_period_key`(`uuid`, `period`),
INDEX `ServerNodeHistory_uuid_period_idx`(`uuid`, `period`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `ServiceCheck` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`name` VARCHAR(191) NOT NULL,
`url` VARCHAR(191) NOT NULL,
`status` VARCHAR(191) NOT NULL DEFAULT 'unknown',
`latencyMs` INTEGER NOT NULL DEFAULT 0,
`message` VARCHAR(191) NOT NULL DEFAULT '',
`lastCheckAt` DATETIME(3) NOT NULL,
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`updatedAt` DATETIME(3) NOT NULL,
UNIQUE INDEX `ServiceCheck_name_key`(`name`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
@@ -1,2 +0,0 @@
-- AlterTable (news.content 全文, 须在 news_and_alerts 建表之后执行)
ALTER TABLE `News` ADD COLUMN `content` TEXT NULL;
@@ -1,17 +0,0 @@
-- 浙江省油价历史表
-- - date: 调价日次日 (即数据实际抓取日, 便于按时间排序)
-- - province + fuel: 浙江 / 92/95/0
-- - unique (date, province, fuel): 防 cron 重复入库
-- - DOUBLE 容纳 999.999 元/升
CREATE TABLE OilPriceHistory (
id INT NOT NULL AUTO_INCREMENT,
date DATETIME(3) NOT NULL,
province VARCHAR(191) NOT NULL,
fuel VARCHAR(8) NOT NULL,
price DOUBLE NOT NULL,
source VARCHAR(191) NOT NULL DEFAULT 'apizero',
createdAt DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE INDEX OilPriceHistory_date_province_fuel_key(date, province, fuel),
INDEX OilPriceHistory_province_fuel_date_idx(province, fuel, date)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
@@ -1,95 +0,0 @@
-- CreateTable
CREATE TABLE `MihomeAccount` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`key` VARCHAR(191) NOT NULL DEFAULT 'default',
`userId` VARCHAR(191) NOT NULL DEFAULT '',
`status` VARCHAR(191) NOT NULL DEFAULT 'unknown',
`lastError` VARCHAR(191) NOT NULL DEFAULT '',
`updatedAt` DATETIME(3) NOT NULL,
UNIQUE INDEX `MihomeAccount_key_key`(`key`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `MihomeHome` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`homeId` VARCHAR(191) NOT NULL,
`name` VARCHAR(191) NOT NULL,
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
UNIQUE INDEX `MihomeHome_homeId_key`(`homeId`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `MihomeDevice` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`did` VARCHAR(191) NOT NULL,
`homeId` VARCHAR(191) NOT NULL,
`model` VARCHAR(191) NOT NULL,
`name` VARCHAR(191) NOT NULL,
`alias` VARCHAR(191) NULL,
`tempSiid` INTEGER NOT NULL,
`tempPiid` INTEGER NOT NULL,
`humSiid` INTEGER NOT NULL,
`humPiid` INTEGER NOT NULL,
`battSiid` INTEGER NOT NULL,
`battPiid` INTEGER NOT NULL,
`battery` INTEGER NULL,
`batteryAt` DATETIME(3) NULL,
`enabled` BOOLEAN NOT NULL DEFAULT true,
`lastSyncAt` DATETIME(3) NULL,
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
UNIQUE INDEX `MihomeDevice_did_key`(`did`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `MihomeEnvSample` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`deviceId` INTEGER NOT NULL,
`date` DATETIME(3) NOT NULL,
`temperature` DOUBLE NULL,
`humidity` DOUBLE NULL,
`temperatureAvg` DOUBLE NULL,
`humidityAvg` DOUBLE NULL,
`temperatureMin` DOUBLE NULL,
`temperatureMax` DOUBLE NULL,
`humidityMin` DOUBLE NULL,
`humidityMax` DOUBLE NULL,
`origin` VARCHAR(191) NOT NULL DEFAULT 'live',
`sampledAt` DATETIME(3) NULL,
`raw` JSON NULL,
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
INDEX `MihomeEnvSample_deviceId_date_idx`(`deviceId`, `date`),
UNIQUE INDEX `MihomeEnvSample_deviceId_date_key`(`deviceId`, `date`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `MihomeEnvHour` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`deviceId` INTEGER NOT NULL,
`hour` DATETIME(3) NOT NULL,
`temperatureAvg` DOUBLE NULL,
`humidityAvg` DOUBLE NULL,
`raw` JSON NULL,
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
INDEX `MihomeEnvHour_deviceId_hour_idx`(`deviceId`, `hour`),
UNIQUE INDEX `MihomeEnvHour_deviceId_hour_key`(`deviceId`, `hour`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- AddForeignKey
ALTER TABLE `MihomeDevice` ADD CONSTRAINT `MihomeDevice_homeId_fkey` FOREIGN KEY (`homeId`) REFERENCES `MihomeHome`(`homeId`) ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `MihomeEnvSample` ADD CONSTRAINT `MihomeEnvSample_deviceId_fkey` FOREIGN KEY (`deviceId`) REFERENCES `MihomeDevice`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `MihomeEnvHour` ADD CONSTRAINT `MihomeEnvHour_deviceId_fkey` FOREIGN KEY (`deviceId`) REFERENCES `MihomeDevice`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
@@ -1,99 +0,0 @@
-- CreateTable
CREATE TABLE `Habit` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`userId` INTEGER NOT NULL,
`name` VARCHAR(191) NOT NULL,
`period` VARCHAR(191) NOT NULL DEFAULT 'day',
`target` INTEGER NOT NULL DEFAULT 1,
`color` VARCHAR(191) NOT NULL DEFAULT '#1677ff',
`icon` VARCHAR(191) NOT NULL DEFAULT '',
`note` VARCHAR(191) NOT NULL DEFAULT '',
`archived` BOOLEAN NOT NULL DEFAULT false,
`sortOrder` INTEGER NOT NULL DEFAULT 0,
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`updatedAt` DATETIME(3) NOT NULL,
INDEX `Habit_userId_archived_sortOrder_idx`(`userId`, `archived`, `sortOrder`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `CheckIn` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`habitId` INTEGER NOT NULL,
`userId` INTEGER NOT NULL,
`date` VARCHAR(10) NOT NULL,
`seq` INTEGER NOT NULL DEFAULT 1,
`note` VARCHAR(191) NOT NULL DEFAULT '',
`source` VARCHAR(191) NOT NULL DEFAULT 'user',
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
UNIQUE INDEX `CheckIn_habitId_date_seq_key`(`habitId`, `date`, `seq`),
INDEX `CheckIn_userId_date_idx`(`userId`, `date`),
INDEX `CheckIn_habitId_date_idx`(`habitId`, `date`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `CheckList` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`userId` INTEGER NOT NULL,
`title` VARCHAR(191) NOT NULL,
`scene` VARCHAR(191) NOT NULL DEFAULT '',
`note` VARCHAR(191) NOT NULL DEFAULT '',
`archived` BOOLEAN NOT NULL DEFAULT false,
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`updatedAt` DATETIME(3) NOT NULL,
INDEX `CheckList_userId_archived_idx`(`userId`, `archived`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `CheckListItem` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`listId` INTEGER NOT NULL,
`title` VARCHAR(191) NOT NULL,
`note` VARCHAR(191) NOT NULL DEFAULT '',
`done` BOOLEAN NOT NULL DEFAULT false,
`doneAt` DATETIME(3) NULL,
`sortOrder` INTEGER NOT NULL DEFAULT 0,
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`updatedAt` DATETIME(3) NOT NULL,
INDEX `CheckListItem_listId_sortOrder_idx`(`listId`, `sortOrder`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `CheckListTemplate` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`userId` INTEGER NULL,
`name` VARCHAR(191) NOT NULL,
`scene` VARCHAR(191) NOT NULL DEFAULT '',
`items` JSON NOT NULL,
`builtin` BOOLEAN NOT NULL DEFAULT false,
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`updatedAt` DATETIME(3) NOT NULL,
INDEX `CheckListTemplate_userId_builtin_idx`(`userId`, `builtin`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- AddForeignKey
ALTER TABLE `Habit` ADD CONSTRAINT `Habit_userId_fkey` FOREIGN KEY (`userId`) REFERENCES `User`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `CheckIn` ADD CONSTRAINT `CheckIn_habitId_fkey` FOREIGN KEY (`habitId`) REFERENCES `Habit`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `CheckIn` ADD CONSTRAINT `CheckIn_userId_fkey` FOREIGN KEY (`userId`) REFERENCES `User`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `CheckList` ADD CONSTRAINT `CheckList_userId_fkey` FOREIGN KEY (`userId`) REFERENCES `User`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `CheckListItem` ADD CONSTRAINT `CheckListItem_listId_fkey` FOREIGN KEY (`listId`) REFERENCES `CheckList`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `CheckListTemplate` ADD CONSTRAINT `CheckListTemplate_userId_fkey` FOREIGN KEY (`userId`) REFERENCES `User`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;

Some files were not shown because too many files have changed in this diff Show More