Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion internal-docs/doc-kanban-unification/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ due: 2026-07-01 # 可选
## 5. 退役/暂缓

- **退役**:桌面 `KanbanBoard` 全屏模态、`kanban_local.rs`/`kanban.json`、`kanbanSync.ts`(本地优先看板改由 Markdown 卡片承载)。保留文件以防引用编译错误时再删,但从 App 接线移除。
- **暂缓(后续 PRD)**:云端 `kanban_*` 表、Web `Kanban.tsx` 页面、看板的云端同步。本轮聚焦桌面本地体验(用户强调「本地」)。文档/卡片本就走现有文档同步链路。
- ~~**暂缓(后续 PRD)**:云端 `kanban_*` 表、Web `Kanban.tsx` 页面、看板的云端同步。~~ **→ 已立项(2026-06-23)**:该"后续 PRD"即 **[`../kanban/unification-v2.md`](../kanban/unification-v2.md)**——决定**退役**云端 DB 看板,把其功能搬到文档型看板并云端本地互通。本轮聚焦桌面本地体验(用户强调「本地」)。文档/卡片本就走现有文档同步链路。

## 6. 验收标准(Acceptance Criteria)

Expand Down
2 changes: 2 additions & 0 deletions internal-docs/kanban/design.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# JType 看板(Kanban)技术设计文档

> 🛑 **整体退役(2026-06-23)**:本文描述的**云端 DB 看板**(`kanban_*` 表 + `handlers/kanban/*` + `Kanban.tsx`)已决定**退役**,看板收敛到文档型(`.md` 卡片 + `.board` 视图)。当前真相源见 **[`unification-v2.md`](./unification-v2.md)**。本文仅作历史设计留存,不反映方向。

状态:CLOUD(第 2–5、8–11 节)已实现 v1。**LOCAL(第 6 节)与 Desktop↔Cloud 同步(第 7 节)已撤回**——`kanban_local.rs` 曾实现后在提交 `1576515` 中整体删除(−672 行),当前 `main` 不存在。第 6/7 节保留为历史设计意图,**不反映当前代码**。桌面看板现为纯文件式(`.board` + `.md` 卡片)经文档同步管线收敛——详见 [`next-features-design.md` §0](./next-features-design.md) 与 [`gaps-and-roadmap.md`](./gaps-and-roadmap.md)。
初始日期:2026-06-13
更新日期:2026-06-20(标注 §6/§7 已撤回)
Expand Down
19 changes: 13 additions & 6 deletions internal-docs/kanban/gaps-and-roadmap.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
# JType 看板 — 缺口盘点与优先级路线图

> 🛑 **部分推翻(2026-06-23)**:本文 §0 的"**DB 看板保留为次要/遗留**"口径已被推翻——决定**退役 DB 看板**,看板彻底收敛到文档型并云端本地互通。当前真相源见 **[`unification-v2.md`](./unification-v2.md)**。
> 另:本文 2026-06-20 后已有变化——**B1 评论(`0016_kanban_comments`)、D2 Webhook(`0017_kanban_webhooks`)均已落地(落在 DB 看板上)**;§1 表格里它们的"待设计/待做"状态过时。下方路线图作为历史审计留存。

状态:现状审计 + 决策收敛(基于 `main` HEAD 实际代码 + 用户 2026-06-20 逐条决策)
初始日期:2026-06-20
更新日期:2026-06-20
更新日期:2026-06-23(加退役横幅 + 标注 B1/D2 已落地)

> 配套文档:
> - [`design.md`](./design.md) — v1 三环境设计。**第 6/7 节已过时**(依赖的 `kanban_local.rs` 已在提交 `1576515` 删除)。
Expand All @@ -13,7 +16,9 @@

## 0. 一句话现状

看板有**两套互不相通的系统**:云端 DB 看板(`Kanban.tsx` + `kanban_*` 表,成熟,但桌面不可达)与**文件看板**(`.board` + `.md` 卡片,用户实际在用,桌面↔云端经文档同步双向可见)。两者复用同一个 `BoardSurface`,故视图级功能对两套都生效。模型已定为 **markdown 卡片**;**DB 看板暂保留为次要/遗留**,新 per-card 功能以文件看板为真相源。
看板有**两套互不相通的系统**:云端 DB 看板(`Kanban.tsx` + `kanban_*` 表,成熟,但桌面不可达)与**文件看板**(`.board` + `.md` 卡片,用户实际在用,桌面↔云端经文档同步双向可见)。两者复用同一个 `BoardSurface`,故视图级功能对两套都生效。模型已定为 **markdown 卡片**;~~DB 看板暂保留为次要/遗留~~ **→ 已改为退役 DB 看板(2026-06-23,见 [`unification-v2.md`](./unification-v2.md))**,新 per-card 功能以文件看板为真相源。

> 订正(2026-06-23):更准确说是**三个渲染面/两套数据层**——① 桌面文件看板、② Web 文件看板(`WebBoardView.tsx`,已与桌面双向同步)、③ Web 云端 DB 看板(`Kanban.tsx`,孤岛)。①②已互通,③待退役。

---

Expand Down Expand Up @@ -69,10 +74,12 @@

## 3. 需要回写修订的过时文档(DOC 任务)

- [ ] [`design.md` 第 6 节「LOCAL — kanban_local.rs」](./design.md):文件已删除,整节失效,标注「已移除」或重写。
- [ ] [`design.md` 第 7 节「同步数据流(Desktop)」](./design.md):依赖已删除的 `take_pending_ops`/`merge_remote_board`,标注实现已撤回。
- [ ] `design.md` 头部状态行:从「尚未接通,为设计意图」升级为「曾实现后移除(提交 1576515)」。
- [ ] [`shared/components/board/types.ts:23`](shared/components/board/types.ts#L23) 注释「web → localStorage」过时:Web 实际已改 `saveDocument` 写 `.board`(C3 设计时顺手修正)。
> ✅ 2026-06-23:design.md 头部 + §6/§7 已加撤回/退役横幅;本文已加退役横幅。文档订正大部完成,余项随 v2 落地清理。

- [x] [`design.md` 第 6 节「LOCAL — kanban_local.rs」](./design.md):已加 ⚠️ 撤回标注 + v2 退役横幅。
- [x] [`design.md` 第 7 节「同步数据流(Desktop)」](./design.md):已标注实现撤回。
- [x] `design.md` 头部状态行:已更新为「曾实现后移除(提交 1576515)」+ v2 退役横幅。
- [ ] [`shared/components/board/types.ts`](shared/components/board/types.ts) 注释「web → localStorage」:`WebBoardView` 已改 `saveDocument` 写 `.board`(随 v2 §5 核对修正)。

---

Expand Down
2 changes: 2 additions & 0 deletions internal-docs/kanban/next-features-design.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# JType 看板 — 下一批功能设计(D1 依赖 / C3 日历 / D2 Webhook)

> ℹ️ **部分更新(2026-06-23)**:D1 依赖 / C3 日历已落地。**D2 Webhook 的基建设计(HMAC 签名 / 重试队列 / SSRF 防护 / 投递 worker)仍有效**,但**触发源已改**——不再挂在云端 DB 看板的 `kanban:*` 事件,而是**重接到文档保存路径**(`save_document`),作用域改用 board 文档 id。详见 **[`unification-v2.md`](./unification-v2.md) §2.5**。本文 D2 章节中"仅 DB 看板会发"的限定已不成立。

状态:**设计提案(仅设计,未实现,不含代码)**
初始日期:2026-06-20
来源:基于 [`gaps-and-roadmap.md`](./gaps-and-roadmap.md) 的用户决策收敛 —— 绿灯三项 D1 / C3 / D2,外加 A1 现状订正。
Expand Down
185 changes: 185 additions & 0 deletions internal-docs/kanban/ticket-links.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
# JType 看板 — ticket link(`OCCSV-3371`)

状态:设计已定稿(用户 2026-06-23 锁定关键决策),待实现
初始日期:2026-06-23
更新日期:2026-06-23

> **目标**:支持工单链接 `OCCSV-3371`,其中 `OCCSV` = 看板的 ticket 前缀(board key),`3371` = 该看板内的顺序卡号。链接在 workspace 上下文里解析(路由 `/workspaces/:id/tickets/:ticket`),不做 workspace-agnostic 的裸链接。
> **配套**:本特性长在 [`unification-v2.md`](./unification-v2.md) 的文档型看板之上,复用其 §1.1 两条架构原则(`documents.id` 唯一键 + web 为主/desktop 为辅)。落地排在 v2 退役③、文件型看板坐稳之后。

---

## 0. 锁定决策(先读)

| 决策 | 选择 | 影响 |
|---|---|---|
| 号存哪 | **只存云端索引**(不进 `.md` frontmatter) | 号是云端特性;离线补号无需改写文档 content,实现更干净 |
| 离线发号 | **A+C**:在线建卡实时发号;离线建卡先无号,首次同步补号 | 号单调连续、零碰撞、不可变 |
| 主次 | **web 为主、desktop 为辅** | 桌面离线看不到号 / 解不了 ticket 链接可接受;只读缓存为可选增强 |

**核心后果**:ticket 号变成**云端能力**。`.md` 文件不带号 → git/桌面离线看不到 `OCCSV-3371`。但 **board key 在 `.board` 里(会同步)**,桌面能识别 `OCCSV-` 是合法前缀、能 linkify,只是离线点不开具体卡。

---

## 1. 数据模型

### 1.1 `.board` 加 board key
`BoardViewConfig`([`shared/lib/board.ts`](shared/lib/board.ts#L23))顶层**新增** board 级字段 `ticketKey: string`:
- 注意现有的 `key`([`board.ts:7`](shared/lib/board.ts#L7))是**列** key,**勿复用**;用 `ticketKey` 避免歧义。
- 格式:大写字母/数字,`^[A-Z][A-Z0-9]{1,9}$`。从 board 标题派生默认值("OCC Services" → `OCCSV`),用户可改。
- **workspace 内唯一**(否则 `OCCSV-3371` 有歧义)。

### 1.2 卡片 frontmatter — **不带号**
决策为"只存云端索引",故卡片 `.md` 的 frontmatter **不新增** ticket/number 字段,保持 [`BoardCardInfo`](services/jtype-core/src/lib.rs#L1342) 现状。卡片身份对内走 `documents.id`(云端 UUID)、对外走 ticket(云端索引计算)。

### 1.3 云端两张薄表(迁移 `0019_card_tickets`)

```sql
-- 0019_card_tickets.up.sql (草案;避开 migration runner 朴素分句器:无行内 -- 注释、语句内无裸分号)

CREATE TABLE board_sequences (
workspace_id CHAR(36) NOT NULL,
ticket_key VARCHAR(16) NOT NULL,
last_number BIGINT NOT NULL DEFAULT 0,
PRIMARY KEY (workspace_id, ticket_key),
CONSTRAINT fk_board_seq_ws FOREIGN KEY (workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE
);

CREATE TABLE card_tickets (
id CHAR(36) NOT NULL PRIMARY KEY,
workspace_id CHAR(36) NOT NULL,
document_id CHAR(36) NOT NULL,
ticket_key VARCHAR(16) NOT NULL,
number BIGINT NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uq_card_tickets_doc (document_id),
UNIQUE KEY uq_card_tickets_ref (workspace_id, ticket_key, number),
CONSTRAINT fk_card_tickets_ws FOREIGN KEY (workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE,
CONSTRAINT fk_card_tickets_doc FOREIGN KEY (document_id) REFERENCES documents(id) ON DELETE CASCADE
);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
```

- `card_tickets` 是 **ticket 的单一真相源**:`ticket_key` + `number` 是**铸号时的快照**,行一旦写入不再改 → ID 永久稳定(board key 改名只影响新卡,旧行仍是 `OCCSV-3371`)。
- `UNIQUE(document_id)`:**一卡一号、终身一号**,让发号**幂等**。
- `UNIQUE(workspace_id, ticket_key, number)`:防重号。
- `board_sequences`:**唯一发号源**,`last_number` 只增不减。

---

## 2. 发号(A + C)

服务端是**唯一发号者**(两条路都过 server),故 `last_number` 权威、无碰撞、无需客户端自愈。

```
在线建卡(web / 联网桌面):
建卡(已有 documents.id) → allocate(workspace, ticketKey, documentId)
→ server 事务: UPDATE board_sequences SET last_number=last_number+1
INSERT card_tickets(documentId, ticketKey, number)
→ 返回 OCCSV-N → 卡片立即显示号 ✅ 即时

离线建卡(断网桌面):
建卡(无号) → 离线引用走 documents.id / path
↓ 联网首次 sync(push)
server 扫"有 board: frontmatter、card_tickets 无行"的文档 → 逐个 allocate(纯插行)
→ 下次该卡在 web 打开即显示号 ⏳ 首同步后
```

- **幂等**:`allocate` 先查 `card_tickets` 是否已有该 `document_id`,有则直接返回,不重复发。离线卡反复同步只补一次。
- **无 content 改写**:号不进 frontmatter → 补号只是 `INSERT` 一行索引,`.md` 文件原封不动,**不触发 content_hash 变化 / 三方合并**。这正是"只存云端索引"相对"写 frontmatter"的最大实现红利。
- **事务**:`UPDATE board_sequences` + `INSERT card_tickets` 同一事务;`UNIQUE(document_id)` 兜底并发重复。

> 备选方案对比(A 云端实时 / B `.board` 计数器 / C 离线补号 / D 设备号段)见 [`unification-v2.md`] 决策过程;B 因"不可变 ID + 离线碰撞"否决,D 因"空洞/乱序"仅在"离线必须即时有号"时才用——本设计取 A+C。

---

## 3. 解析、路由与跳转

### 3.1 ticket 解析路由(Web,主场)
1. 解析 `OCCSV-3371` → `(ticket_key=OCCSV, number=3371)`;非法格式 → 400。
2. 查 `card_tickets`(按 `workspace_id + ticket_key + number`)→ `document_id` → 经 `documents` 取 `relative_path`。
3. 跳到工作区并打开该卡(如 `/workspaces/:id?doc=<path>&card=<id>`,落进 `WebBoardView` 的 peek)。
4. 查不到 → 404 / "已删除"页。**号删了永不复用**(`last_number` 不回退)。

> ticket 作用域 = workspace,故解析路由**带 workspace**:`/workspaces/:id/tickets/:ticket`(前端 `TicketRedirect`)。**不**提供 workspace-agnostic 的裸 ticket 解析——key 仅在 workspace 内唯一,跨 workspace 可能重名,裸链接会有歧义。

### 3.2 Desktop(辅)
- 在线:调云端 resolve 端点,同 web。
- 离线:默认**解不了**(号在云端)。可选增强:同步时拉 `card_tickets` 快照到 `.jtype/tickets.json` 只读缓存,桌面据此显示/解析。**首版不做,标为可选。**

### 3.3 移板 / 删除策略
- **卡片移到别的 board**:保留原 ticket(行键 `document_id`,不重铸)→ ID 稳定。(如需"跨板换号"再单独设计。)
- **删除/归档**:`last_number` 不回退,号永不复用;归档卡保留号。

---

## 4. Markdown 自动链接

仿 [`shared/lib/markdown.ts` `renderWikilinks`](shared/lib/markdown.ts#L137) 新增 `renderTicketLinks`:把正文里的 `OCCSV-3371` 渲成 `<span class="ticket-link" data-ticket="OCCSV-3371">`,平台层解析跳转(与 `data-wikilink` 同套路;记得把 `data-ticket` 加进 [`markdown.ts:194`](shared/lib/markdown.ts#L194) 的 DOMPurify `ADD_ATTR`)。

- **pattern**:`\b([A-Z][A-Z0-9]{1,9})-(\d+)\b`。
- **必须白名单**:只对**已知 board key** 生效,否则 `UTF-8`/`COVID-19`/`SHA-256` 全被误链。
- 渲染层传入当前 vault/workspace 的 `ticketKey` 集合;前缀不在集合内则不链。
- 桌面从 `.board` 文件扫 `ticketKey`(`.board` 会同步,故桌面也有 key 集合);web 从 board 列表 / `card_tickets`。
- **解析**:web 查 `card_tickets`;桌面在线查 resolve,离线靠可选缓存(无则 span 不可点)。

---

## 5. Board key 生命周期

| 事件 | 处理 |
|---|---|
| 派生默认 | 从 board 标题取大写首字母/缩写("OCC Services"→`OCCSV`),用户可改 |
| 唯一性 | 创建/改 key 时校验 workspace 内不重复(云端唯一约束 + 桌面扫 `.board` 校验) |
| 改名 | 已铸号在 `card_tickets` 是快照,不动 → 旧 ID 永久有效。可选:`.board` 存 `aliasKeys[]`,解析路由与 linkify 白名单也认旧前缀 |
| 首卡后冻结 | 建议铸出首张号后默认锁定 key(或仅允许走 alias),避免链接面变动 |

---

## 6. 后端改动点(`services/jtype-web/src/`)

- **迁移** `0019_card_tickets`(§1.3)+ 注册进 [`db/migrations.rs`](services/jtype-web/src/db/migrations.rs)(append-only,紧跟 v2 的 `0018_drop_kanban`)。
- **allocate 端点**:`POST /api/v1/workspaces/:id/tickets/allocate { documentId }` → 解析该文档 board → 取 `ticketKey` → 事务发号 → 返回 `{ ticket, number }`。幂等。
- **push handler 补号**:在 [`handlers/sync.rs`](services/jtype-web/src/handlers/sync.rs) push 处理里,对新到/变更的文档,若 frontmatter 有 `board:` 且 `card_tickets` 无行,则 allocate(纯插行,不改 content)。
- **resolve 端点**:`GET /api/v1/workspaces/:id/tickets/:ticket` → 查表 → 返回卡片(前端 `TicketRedirect` 据此重定向)。
- **board key 校验端点**:创建/改 `.board` 时校验 `ticketKey` 唯一与格式。

## 7. 前端改动点(`services/jtype-web/frontend/src/`)

- `WebBoardView`:加载本看板时一并拉 `card_tickets`(`document_id → ticket` map),卡片渲染 ticket 徽标;创建卡片后调 `allocate` 拿号回填徽标。
- `main.tsx`:加 `/workspaces/:workspaceId/tickets/:ticket` 路由(`TicketRedirect`:解析 → 重定向到 workspace + 打开卡)。
- `shared/lib/markdown.ts`:`renderTicketLinks` + DOMPurify `ADD_ATTR: data-ticket`;平台层(web/desktop)实现 `data-ticket` 点击解析。
- `.board` 编辑 UI:暴露 `ticketKey` 字段(带唯一性校验提示)。

---

## 8. 分期

1. **模型**:`.board` 加 `ticketKey` + 校验;迁移 `0019` 两表。
2. **在线发号(A)**:`allocate` 端点 + `WebBoardView` 显示徽标 + 创建即发号。
3. **解析/路由**:workspace-scoped ticket 路由 + resolve 端点。
4. **离线补号(C)**:push handler 补号。
5. **markdown linkify**:`renderTicketLinks` + board-key 白名单。
6. **可选增强**:桌面 `tickets.json` 只读缓存(按需)。

做完 1+2+3 即可:在线建的卡都有 `OCCSV-3371` 并能 `/browse` 打开。4 是离线增强,5 是正文链接体验。

---

## 9. 验收标准

通用门槛同 [`unification-v2.md` §8](tsc/build/cargo/启动无错)。

- AC1:`.board` 设 `ticketKey=OCCSV`;同 workspace 再设一个 `OCCSV` 被拒(唯一性)。
- AC2:web 新建卡 → 立即显示 `OCCSV-N`,N 较上一张 +1;`card_tickets` 有对应行(键 `document_id`)。
- AC3:`/workspaces/:id/tickets/OCCSV-3371` 打开对应卡;不存在的号 → 404;删卡后该号仍 404 且**不被新卡复用**。
- AC4:断网桌面建卡(无号) → 联网同步后,该卡在 web 显示号;重复同步不重号(幂等)。
- AC5:正文写 `OCCSV-3371` 在预览渲成可点链接并跳转;写 `UTF-8`/`COVID-19` **不**被链接(白名单生效)。
- AC6:把卡片 `board:` 改到另一 board → 其 ticket **不变**(移板保号)。
- AC7:改 board 的 `ticketKey` → 已有卡的 `OCCSV-N` 不变(快照稳定);若启用 alias,旧前缀仍可达。

## 10. 边界与未决

- **跨 workspace 重名 key**:已解决——解析路由带 workspace(`/workspaces/:id/tickets/:ticket`),不提供裸 ticket 解析(key 仅 workspace 内唯一)。
- **桌面离线解析**:默认不支持,依赖可选 `tickets.json` 缓存。
- **board key 派生算法**:标题→缩写规则需定(取大写首字母?前 N 字符?),可先用"标题大写去空格取前 5–6 位 + 去重后缀",允许用户覆盖。
- **大量离线卡补号顺序**:同一 board 多张离线卡在一次 sync 补号,顺序按 push 顺序/创建时间,需定一个确定性排序避免号序随机。
Loading
Loading