# 5-33 导航页与路由守卫执行清单 v1

> 状态：process
>
> Owner：05-editor-mainline / 03-rust-web / control-plane
>
> 创建时间：2026-05-27
>
> 背景：当前本地文件夹入口在没有明确页面时容易回退打开默认页面；未登录、删除页、失效页等路径也缺少统一页面级恢复策略。需要把“导航页作为主页”与“路由守卫”合并设计，避免用户停留在错误页或误打开无关页面。

## 0. 当前落地状态

- [x] Phase A 核心语义已落地：`/` 与 local folder / folder scope 无明确页面时渲染导航页，不再自动打开 recent / first page。
- [x] Phase B recent 闭环已落地：control-plane SQLite recent 表、读写 API、SSR recent 展示、打开 folder/page 写入 recent。
- [x] Phase C 主路径已落地：导航页/星标文件夹进入 scoped 导航页；FileTree 普通文件夹点击只展开；Markdown 普通点击记录 recent 并打开文档页。
- [x] Phase D 验证已落地：补 Rust route/API 测试与 `task500-navigation-page-route-guard-smoke.js` browser smoke，覆盖未登录、删除页回退、recent 分组和导航页首屏无 editor bootstrap。
- [ ] 后续增强：文档页 breadcrumb 文件夹段逐级回到导航页、所有 fetch 404/410 的全局浏览器兜底可继续拆独立 follow-up；当前服务端 HTML shell 已覆盖 canonical route guard。

## 1. 目标

- 本地文件夹和文件夹 scope 没有明确页面时显示导航页，不再自动打开 recent / first page 作为默认正文。
- 导航页是主编辑区的主页状态；点击 Markdown 页面后，文档页覆盖该主页。
- 点击文件夹后，行为对齐星标文件夹：切到 scoped Explorer，并以该文件夹为导航页上下文。
- 导航页展示最近访问的文件夹和最近访问的页面，两组分开显示，并可直接访问。
- 页面级路由守卫统一处理未登录、无权限、页面不存在、页面已删除等场景。
- 首屏性能不因导航页退化：不构建 Page Aggregate，不启动 tiptap island，不额外扫描完整大目录。

## 2. 非目标

- 不新增第二套树真相；导航页只消费 Rust projection / control-plane recent。
- 不把 recent 写入 Markdown frontmatter 或本地文件夹 `.mnote` 作为长期事实。
- 不把文件夹导航页做成真实 Markdown 页面。
- 不改变 local-first Markdown 正文事实源。
- 不在本阶段实现复杂最近访问排序模型，例如全文索引热度、跨设备文件内容同步或多设备路径重绑定。

## 3. 产品语义

### 3.1 导航页状态

- 工作区导航页：`/` 或默认 local workspace 入口，展示工作区级最近访问、打开本地文件夹入口和当前工作区概览。
- 文件夹导航页：`/?sourceKind=local_folder&rootUri=...&fileTreeScope=docs&treeView=filetree`，展示 `docs/` scope 的子文件夹、Markdown 页面和资源入口。
- 导航页不是错误页；它是没有明确文档目标时的正常主页。

### 3.2 点击行为

- 点击最近文件夹：进入该文件夹 scope，主区域显示文件夹导航页。
- 点击最近页面：打开文档页并覆盖导航页。
- 在导航页或 Explorer 中点击 Markdown：打开文档页并覆盖导航页。
- 在文档页点击 breadcrumb 的文件夹段：回到对应文件夹导航页。
- Ctrl / Meta / 中键点击页面：允许浏览器新标签页打开；普通点击优先走 MNote 当前主编辑区。

### 3.3 失效恢复

- 未登录访问受保护页面：跳转 `/auth?next=<encoded-current-url>`。
- 登录后有 `next`：返回原始目标；如果目标已失效，则进入对应导航页。
- Markdown 文件不存在、已删除或 document id 无法解析：回到当前 `rootUri + fileTreeScope` 的导航页，并显示轻量提示。
- 文件夹 scope 不存在或无权限：回到 root 导航页或授权页，不停留在空白错误态。

## 4. 架构判断

- 导航页属于 `05-editor-mainline` 的工作区/文档壳体验，数据入口由 `03-rust-web` 的 route 和 projection 提供。
- Recent 持久化属于 control-plane 用户状态，不属于文件系统事实。
- FileTree / PageTree 内容仍由 Rust projection 生成；前端只负责导航、局部渲染和交互。
- 路由守卫 canonical 逻辑放在 Rust HTML shell route；前端 fetch guard 只做浏览器交互恢复。

## 5. 数据与协议

### 5.1 Recent 记录

- [ ] 在 control-plane 增加用户 recent 记录表或复用已有偏好表能力，字段至少包含：
  - `id`
  - `user_id`
  - `kind`: `folder | page`
  - `source_kind`: MVP 先支持 `local_folder`
  - `root_uri`
  - `relative_path`
  - `document_id`
  - `title`
  - `visited_at`
  - `status`
- [ ] Recent folder 使用 `root_uri + relative_path` 定位；root 文件夹 `relative_path=""`。
- [ ] Recent page 使用 `root_uri + document_id` 定位，并冗余保存 `relative_path/title` 方便展示。
- [ ] 每个用户每类 recent 保留有限条数，MVP 建议 folder 10 条、page 20 条。
- [ ] list recent 时做授权过滤；无权限、root 不可用、路径不存在的记录不作为可点击主项。
- [ ] localStorage 里已有 recent local roots 只作为迁移/兜底读取，不作为长期 canonical。

### 5.2 API

- [ ] 新增或扩展读取 API：返回当前用户 recent folders / recent pages。
- [ ] 新增写入 API：打开 folder scope 或 Markdown page 后记录 recent。
- [ ] API 写入只记录用户行为，不触碰 Markdown 或 `.mnote` 文件。
- [ ] API 返回项包含可直接用于导航的 URL 参数：`sourceKind/rootUri/fileTreeScope/documentId/workspaceId`。

## 6. Rust HTML Shell

### 6.1 导航页渲染

- [ ] 新增导航页 view model，区分 `workspace` 与 `folder` 两种 scope。
- [ ] 导航页首屏只加载 shallow file projection 和 recent 列表。
- [ ] 导航页不调用 `build_page_aggregate_snapshot`。
- [ ] 导航页不注入 `__MNOTE_PAGE_AGGREGATE__` / `__MNOTE_EDITOR_BOOTSTRAP__`，除非后续明确需要编辑器。
- [ ] 导航页继续使用 `PageLayout` 和现有 workspace sidebar。
- [ ] 文件夹导航页主区域展示：
  - 当前文件夹标题和路径面包屑
  - 子文件夹列表
  - 当前层 Markdown 页面列表
  - 当前层资源列表
  - 最近访问页面和最近访问文件夹

### 6.2 `/` 入口选择规则

- [ ] `/` 未登录继续跳 `/auth?next=/`。
- [ ] `/` 无 `pageId` 且无 `fileTreeScope`：显示工作区导航页。
- [ ] `/` 有 `sourceKind=local_folder&rootUri=...` 但无 `pageId`：显示对应 root 导航页。
- [ ] `/` 有 `fileTreeScope`：显示该 scope 文件夹导航页。
- [ ] `/` 有 `pageId`：尝试打开文档页；失败时回退导航页。
- [ ] 移除或限制 `recent_page_id -> first_page` 的自动正文打开行为，避免文件夹入口误开默认页面。

### 6.3 `/documents/{document_id}` 守卫

- [ ] 未登录访问 `/documents/{document_id}`：`303 /auth?next=<current-url>`。
- [ ] local_folder 缺少 `rootUri`：跳 root/workspace 导航页，并保留错误提示参数。
- [ ] `local_markdown_not_found`：跳 `/?sourceKind=local_folder&rootUri=...&fileTreeScope=<parent>&missingPage=<document_id>`。
- [ ] `local_workspace_access_denied`：跳授权说明页或 root 导航页提示无权限。
- [ ] 其它不可恢复错误保留错误页，但必须带 trace id。

## 7. Browser Runtime

- [ ] 抽出 `openNavigationPageForFolder(rootUri, relativePath, workspaceId)` helper，供最近文件夹、星标文件夹、breadcrumb 文件夹段复用。
- [ ] 点击文件夹时不只展开侧栏；如果是导航动作，应更新 URL `fileTreeScope` 并渲染文件夹导航页。
- [ ] 点击 Markdown 时记录 recent page，然后打开文档页覆盖导航页。
- [ ] 点击导航页或星标 folder scope 时记录 recent folder，然后显示导航页；FileTree 普通文件夹点击只展开子项。
- [ ] fetch 返回 `401` 且当前是浏览器交互：跳 `/auth?next=<current-url>`。
- [ ] fetch 返回 `404/410` 且目标是当前页面：跳当前文件夹导航页。
- [ ] 保留普通业务错误 toast，不把所有 API 错误都变成导航跳转。

## 8. 性能约束

- [ ] 导航页首屏不得扫描完整 workspace root 下所有 Markdown。
- [ ] 文件夹导航页使用 scoped shallow FileTree projection。
- [ ] PageTree scope 可继续给 sidebar 使用，但主导航页首屏不依赖完整 PageTree。
- [ ] Recent list 查询必须限制条数。
- [ ] 大目录 smoke 要验证打开 `design/` scope 不显示 root 兄弟目录，不触发完整 root 渲染。
- [ ] 导航页首屏不启动 tiptap island，避免无文档目标时加载编辑器 bundle。
- [ ] 若需要最近页面标题刷新，只在列表展示时对有限条记录做存在性校验，不批量读取正文。

## 9. 验收标准

- [ ] 登录后访问 `/` 显示导航页，而不是自动打开某个默认页面。
- [ ] 打开本地文件夹 root 后显示该文件夹导航页。
- [ ] 点击文件夹进入 scoped 导航页，Sidebar Explorer 与主区域 scope 一致。
- [ ] 点击 Markdown 后打开文档页并覆盖导航页。
- [ ] 最近文件夹和最近页面分组展示；点击最近文件夹进入导航页，点击最近页面打开文档。
- [ ] 删除当前 Markdown 后刷新旧 `/documents/...`，进入父文件夹导航页并显示提示。
- [ ] 未登录访问 `/documents/...` 跳 `/auth?next=...`，登录后能回到目标或导航页兜底。
- [ ] 无权限访问本地 folder 不显示空白文档页。
- [ ] 导航页没有注入页面 aggregate 和 editor bootstrap。
- [ ] 大目录 scope 打开不退化为完整 root 扫描。

## 10. 测试计划

### 10.1 Rust 单元 / route 测试

- [ ] `root_entry_without_page_renders_navigation_home`
- [ ] `root_entry_local_folder_scope_renders_folder_navigation`
- [ ] `root_entry_does_not_auto_open_recent_page_for_folder_scope`
- [ ] `document_shell_unauthenticated_redirects_to_auth_with_next`
- [ ] `document_shell_missing_local_markdown_redirects_to_folder_navigation`
- [ ] `recent_navigation_records_are_user_scoped`
- [ ] `recent_navigation_list_filters_inaccessible_local_roots`

### 10.2 Browser smoke

- [ ] 新增 `task5xx-navigation-home-local-folder-smoke.js`：
  - 登录测试账号
  - 打开本地 folder root
  - 断言主区域是导航页
  - 断言未打开默认 Markdown
- [ ] 新增 `task5xx-navigation-folder-scope-smoke.js`：
  - 点击文件夹
  - 断言 URL 含 `fileTreeScope`
  - 断言主区域标题和 Explorer scope 一致
- [ ] 新增 `task5xx-navigation-recent-smoke.js`：
  - 打开两个文件夹和一个 Markdown
  - 回到导航页
  - 断言 recent folders / recent pages 分组可见并可点击
- [ ] 新增 `task5xx-route-guard-auth-and-deleted-page-smoke.js`：
  - 未登录访问文档 URL 跳 auth
  - 登录后打开文档
  - 删除对应 `.md`
  - 刷新旧文档 URL 后回到文件夹导航页

### 10.3 回归测试

- [ ] `cargo test -p mnote-web root_entry -- --nocapture`
- [ ] `cargo test -p mnote-web local_folder -- --nocapture`
- [ ] `node scripts/task492-sidebar-starred-shortcuts-smoke.js`
- [ ] `node scripts/task497-local-page-tree-filetree-open-performance-smoke.js`
- [ ] `git diff --check`
- [ ] 修改代码后运行 `codegraph sync .`，提交前确认 CodeGraph 无 pending。

## 11. 分阶段执行建议

### Phase A：守住语义

- [ ] 新增导航页 SSR 壳和 route 测试。
- [ ] 改 `/` 入口选择规则，不再在 folder scope 下自动打开默认页面。
- [ ] `/documents/{document_id}` 增加未登录和 missing local markdown 路由恢复。

### Phase B：Recent 闭环

- [ ] control-plane 增加 recent 记录。
- [ ] 导航页 SSR 展示 recent folders/pages。
- [ ] 打开 folder/page 时写入 recent。

### Phase C：浏览器交互对齐

- [ ] 文件夹点击进入导航页 scope。
- [ ] Markdown 点击覆盖导航页。
- [ ] breadcrumb 文件夹段返回导航页。
- [ ] fetch guard 处理 `401/404/410`。

### Phase D：性能与 smoke

- [ ] 确认导航页首屏不构建 editor / aggregate。
- [ ] 大目录 scoped smoke 验证不加载完整 root。
- [ ] 删除页、未登录、recent 点击 smoke 通过。

## 12. 风险与边界

- Recent 写入如果只做前端 localStorage，会和登录用户、授权过滤、SSR 首屏冲突；MVP 应优先 SQLite。
- 文件夹导航页如果复用完整 PageTree 递归扫描，可能在大目录退化；首屏应以 shallow FileTree projection 为主。
- `mnote_recent_page_id` 旧 cookie 不能继续作为文件夹入口自动打开正文的依据；最多用于工作区级“继续编辑”入口展示。
- 路由守卫不能把所有错误都吞成导航页；不可恢复错误仍要保留 trace，方便排障。
- Ctrl / Meta / 中键打开浏览器新标签页不能破坏普通点击覆盖主页的主路径。
