docs: close #71/#72 prototypes, set #73 as active frontier

Record the completed six-module control-center and maintenance-inbox
prototype wave. Both prototype pairs passed independent Critical/Important
PASS review and direct browser verification at 768px without production
code changes. Update executive summary, active queue, decision log,
session timeline, and milestone checkpoint.

- #71 resolved: six-module control-center HTML prototype
  (5 scenarios x 6 modules, plain-button switcher, responsive 768px)
- #72 resolved: actionable-only maintenance inbox prototype
  (single-action rows, inline issue-draft, confirmation-first report)
- #73 promoted to active frontier: integrate accepted model into
  production control-center plugin
This commit is contained in:
LLLin000 2026-07-15 00:41:28 +08:00
parent c68b671e0c
commit b97e82ab7c
6 changed files with 2334 additions and 14 deletions

View file

@ -1,11 +1,13 @@
> **Branch:** `feat/ocr-rebuild-ux` | **Last Updated:** 2026-07-14
> **Active work:** [Wayfinder: Make the PaperForge control center self-explanatory](https://github.com/LLLin000/PaperForge/issues/65). The capability vocabulary (#69) and managed-runtime architecture (#70) are resolved; the six-module control-center prototype (#71) and actionable maintenance design (#72) are the live frontier.
> **Branch:** `feat/ocr-rebuild-ux` | **Last Updated:** 2026-07-15
> **Active work:** [Wayfinder: Make the PaperForge control center self-explanatory](https://github.com/LLLin000/PaperForge/issues/65). The capability vocabulary (#69), managed-runtime architecture (#70), six-module control-center prototype (#71), and actionable maintenance design (#72) are resolved; integrating the accepted model (#73) is the frontier.
>
> ---
>
> **Current state:** Retrieval recovery is merged and live. OCR multi-key rebuild/redo now streams progress, supports cooperative stop between papers, and refreshes canonical maintenance state. The control-plane audit found four critical contract gaps; the orthogonal capability/activity/attention model and managed-runtime immutable-slot architecture are chosen across #69 and #70. OCR maintenance now uses a canonical per-row action model with confirmation gates.
>
> Next: prototype the six-module control center [#71](https://github.com/LLLin000/PaperForge/issues/71), design actionable-only maintenance with user-reviewed issue drafts [#72](https://github.com/LLLin000/PaperForge/issues/72), then integrate the accepted model in [#73](https://github.com/LLLin000/PaperForge/issues/73). Do not start production implementation before #73 closes.
> #71 and #72 prototypes are complete: the six-module control-center HTML prototype covers 5 scenarios × 6 modules with independent capability probes, a primary attention zone, responsive single-column layout at 768px, and a plain-button scenario switcher. The maintenance inbox prototype demonstrates actionable-only filtering, single-action rows, inline issue-draft review with local redacted export, and a confirmation-first report flow. Both prototypes were independently reviewed (Critical/Important PASS) and browser-verified at 768px; design decisions documented in `docs/prototypes/`. No production plugin code was changed.
>
> Next: integrate the accepted model into a production control-center plugin [#73](https://github.com/LLLin000/PaperForge/issues/73).
## 1. Architecture
### 1.1 The problem (pre-v2)
@ -151,12 +153,12 @@ The OCR maintenance slice has a canonical All/Recommended state model, selected
Remaining legacy OCR issues (carried forward):
## 4. Active Queue
1. 🔴 **[Control-center Wayfinder](https://github.com/LLLin000/PaperForge/issues/65)** — map charted, audit and research gates resolved, capability model and runtime architecture chosen. Prototypes pending.
1. 🔴 **[Control-center Wayfinder](https://github.com/LLLin000/PaperForge/issues/65)** — map charted, audit and research gates resolved, capability model and runtime architecture chosen. Prototypes completed (#71/#72); integration (#73) is the active frontier.
2. ✅ **[Capability-state vocabulary](https://github.com/LLLin000/PaperForge/issues/69)** — resolved at `issuecomment-4971161072`. Orthogonal availability/activity/attention axes, 6-state capability ordinal, 12 canonical verbs, backend-owned severity and primary actions, maintenance projection.
3. ✅ **[Managed runtime](https://github.com/LLLin000/PaperForge/issues/70)** — resolved at `issuecomment-4971239398`. Plugin-managed immutable runtime slots, system-Python bootstrap with validated-triplet fallback, single `active-runtime.json` pointer, `ManagedRuntime` class with `current()`/`status()`/`ensure()`, fail-closed command resolution.
4. 🔴 **[Control-center prototype](https://github.com/LLLin000/PaperForge/issues/71)** — design six-module information hierarchy and interaction model. No production implementation before #73.
5. 🔴 **[Maintenance prototype](https://github.com/LLLin000/PaperForge/issues/72)** — design actionable-only stale/failed/corrupt inbox with user-reviewed GitHub Issue draft reporting.
6. **Control-center integration** — [#73](https://github.com/LLLin000/PaperForge/issues/73) remains blocked on #71/#72.
4. **[Control-center prototype](https://github.com/LLLin000/PaperForge/issues/71)** — resolved with independent Critical/Important PASS review and browser verification at 768px. Six-module control-center HTML prototype covers 5 scenarios with plain-button switcher, primary attention zone, responsive layout, and capability-gated actions. Design decisions recorded in `docs/prototypes/2026-07-14-six-module-control-center.html/.md`. No production implementation before #73.
5. **[Maintenance prototype](https://github.com/LLLin000/PaperForge/issues/72)** — resolved with independent Critical/Important PASS review and browser verification at 768px. Actionable-only inbox with single-action rows, inline issue-draft review, local redacted export, and confirmation-first report flow. Design decisions recorded in `docs/prototypes/2026-07-14-maintenance-issue-reporting.html/.md`.
6. 🔴 **Control-center integration** — [#73](https://github.com/LLLin000/PaperForge/issues/73) — integrate the accepted prototype model into a production control-center plugin. Build one or more Obsidian plugin settings tabs using the capability-state-action model, managed-runtime contracts, and actionable-only maintenance pattern from #71/#72.
7. 🟡 **Downstream tooling** — section-aware chunking and separate figure/table handling.
8. ⏳ **Compatibility naming cleanup** — deferred post-release.
### 4.1 Immediate Next Steps
@ -172,8 +174,9 @@ Remaining legacy OCR issues (carried forward):
- [x] Study desktop installation/health/recovery patterns ([#68](https://github.com/LLLin000/PaperForge/issues/68))
- [x] Define capability-state vocabulary and managed-runtime architecture ([#69](https://github.com/LLLin000/PaperForge/issues/69), [#70](https://github.com/LLLin000/PaperForge/issues/70))
- [x] Add canonical per-row action routing and redo confirmation gates
- [ ] Prototype six-module control center ([#71](https://github.com/LLLin000/PaperForge/issues/71))
- [ ] Design actionable-only maintenance inbox ([#72](https://github.com/LLLin000/PaperForge/issues/72))
- [x] Prototype six-module control center ([#71](https://github.com/LLLin000/PaperForge/issues/71))
- [x] Design actionable-only maintenance inbox ([#72](https://github.com/LLLin000/PaperForge/issues/72))
- [ ] Integrate accepted prototype model ([#73](https://github.com/LLLin000/PaperForge/issues/73))
---
## 5. Key File Map
@ -322,6 +325,11 @@ Remaining legacy OCR issues (carried forward):
| 2026-07-14 | OCR maintenance actions route through a canonical verb model | `maintenanceActionForRow()` maps backend `display_action``"rebuild"` / `"redo"`. Batch command filtering now uses this verb rather than twin booleans. `maintenanceActionRequiresConfirmation()` gates destructive `redo` behind a browser `confirm()` with localized text. |
| 2026-07-14 | Keep normal readiness in the control center, not permanent status-bar chrome | Obsidian status bars work for active operations and exceptional attention; duplicating six-module health there creates noise and conflicts with the actionable-only maintenance model. |
| 2026-07-14 | Diagnostics stay local until the user reviews an issue draft | Docker-style opaque uploads require a support service and hide bundle contents; PaperForge instead exports redacted data locally and opens a prefilled GitHub draft without token storage or automatic submission. |
| 2026-07-15 | Six-module control center: plain-button scenario switcher, not ARIA tabpanel | Scenario switcher uses `<button>` elements with `aria-current="true"`. No `role="tab"`/`"tablist"`/`"tabpanel"` — clicking a scenario changes the entire page state, inconsistent with tabpanel pattern. |
| 2026-07-15 | Primary attention zone exposes one concrete action, never a generic link | Hero action is the single highest-priority verb (e.g. `rebuild_derived` for OCR), never a "View Details" link. Normal state says "一切正常" with a less prominent refresh action. |
| 2026-07-15 | Maintenance inbox is actionable-only; normal and quality-ok items are absent | Routine quality scores create permanent, non-actionable noise. Maintenance shows only stale/failed/corrupt work with a concrete action; unacceptable OCR is reported through a user-reviewed GitHub Issue draft. |
| 2026-07-15 | Issue-draft flow is confirmation-first, inline redacted, never auto-submitted | User reviews a prefilled GitHub draft panel inline, edits freely, then clicks "Open GitHub" to create. No token storage, no automatic submission. Docker-style opaque upload rejected. |
| 2026-07-15 | Prototype reviews use independent Critical/Important pass/fail gates | Both #71 and #72 prototypes were reviewed by independent reviewer subagents on two dimensions: Critical items (correctness/safety) and Important items (readability/maintainability). All items passed before acceptance. |
---
@ -422,6 +430,7 @@ python -m ruff check paperforge/worker/ocr_*.py
| 2026-07-14 | Capability model, runtime architecture, and maintenance regression fixes | Closed #69 (orthogonal capability/activity/attention model) and #70 (plugin-managed immutable runtime slots). Added canonical per-row maintenance action routing, redo confirmation gate, and cache-manifest preservation across 5 plugin files. Verification: 19/19 maintenance tests pass. | [Capability contract](docs/research/2026-07-14-capability-state-action-contract.md), [Runtime architecture](docs/research/2026-07-14-managed-runtime-architecture.md) |
| 2026-07-14 | Control-center current-contract audit | Inventoried setup, configuration, runtime, readiness, persistence, cache, failure, and recovery contracts across plugin and Python. Closed #66 with a ranked contradiction matrix and preserve/migrate/remove plan; unblocked #69 while #70 remains blocked on desktop/runtime evidence. | [Contract audit](https://github.com/LLLin000/PaperForge/issues/66#issuecomment-4968837257) |
| 2026-07-14 | Obsidian and desktop recovery research | Closed #67 and #68 with primary-source pattern reports. Preserved the six-module IA, independent capability probes, Obsidian-managed plugin updates, module-scoped recovery, local diagnostics, and user-reviewed issue drafts; unblocked #70. | [#67](https://github.com/LLLin000/PaperForge/issues/67#issuecomment-4970653461), [#68](https://github.com/LLLin000/PaperForge/issues/68#issuecomment-4970660288) |
| 2026-07-15 | Six-module control-center + maintenance inbox prototypes completed | Closed #71 (six-module HTML prototype, 5 scenarios, plain-button switcher, responsive 768px) with independent Critical/Important PASS review and browser verification. Closed #72 (actionable-only inbox, inline issue-draft review, confirmation-first report) with same review gate. Both prototype pairs passed all review dimensions. No production code changed. | `docs/prototypes/2026-07-14-six-module-control-center.{html,md}`, `docs/prototypes/2026-07-14-maintenance-issue-reporting.{html,md}` |
## 9. Historical Detail Archive

View file

@ -0,0 +1,831 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>PaperForge — Maintenance Inbox 原型</title>
<style>
[hidden]{display:none!important}
/* ── Reset & base ── */
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
html{font-size:16px;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}
body{
font-family:system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;
color:#171717;background:#fff;line-height:1.5;
min-height:100vh;display:flex;flex-direction:column;
}
/* ── Tokens ── */
:root{
--ink:#171717;--body:#4d4d4d;--mute:#888;--hairline:#ebebeb;--hairline-strong:#a1a1a1;
--canvas:#fff;--soft:#fafafa;--soft-2:#f5f5f5;
--link:#0070f3;--link-deep:#0761d1;--link-bg:#d3e5ff;
--success:#0070f3;--error:#ee0000;--error-soft:#f7d4d6;--error-deep:#c50000;
--warning:#f5a623;--warning-soft:#ffefcf;--warning-deep:#ab570a;
--radius-sm:4px;--radius-md:8px;--radius-lg:12px;
--space-xxs:4px;--space-xs:8px;--space-sm:12px;--space-md:16px;--space-lg:24px;--space-xl:32px;--space-2xl:40px;
--font-body:system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;
--font-mono:ui-monospace,SFMono-Regular,Menlo,Monaco,monospace;
--shadow-sm:0 1px 2px rgba(0,0,0,.04);
--shadow-md:0 4px 12px rgba(0,0,0,.08);
}
/* ── Typography ── */
h1{font-size:24px;font-weight:600;letter-spacing:-.96px;line-height:32px}
h2{font-size:18px;font-weight:500;letter-spacing:-.28px;line-height:24px;color:var(--body)}
h3{font-size:14px;font-weight:500;letter-spacing:-.28px;line-height:20px}
label{font-size:13px;font-weight:500;color:var(--body);display:block;margin-bottom:var(--space-xxs)}
p,body,button,input,select,textarea{font-family:var(--font-body)}
/* ── Layout ── */
.page-wrap{max-width:840px;margin:0 auto;padding:var(--space-xl) var(--space-lg);width:100%}
/* ── Header ── */
.header{display:flex;align-items:center;justify-content:space-between;margin-bottom:var(--space-xl);flex-wrap:wrap;gap:var(--space-sm)}
.header-left{display:flex;align-items:center;gap:var(--space-sm)}
.header-badge{
display:inline-flex;align-items:center;justify-content:center;
background:var(--ink);color:#fff;font-size:12px;font-weight:500;
min-width:24px;height:24px;border-radius:100px;padding:0 var(--space-xs);
letter-spacing:-.28px;
}
/* ── Scenario bar ── */
.scenario-bar{
display:flex;flex-wrap:wrap;gap:var(--space-xs);
padding:var(--space-sm) var(--space-md);
background:var(--soft);border-radius:var(--radius-md);
margin-bottom:var(--space-lg);
align-items:center;
}
.scenario-bar-label{font-size:12px;font-weight:500;color:var(--mute);letter-spacing:-.28px;margin-right:var(--space-xxs);white-space:nowrap}
.scenario-btn{
font-size:12px;font-weight:500;letter-spacing:-.28px;
padding:var(--space-xxs) var(--space-sm);
border:1px solid var(--hairline);border-radius:100px;
background:var(--canvas);color:var(--body);cursor:pointer;
transition:background .15s,border-color .15s,color .15s;
white-space:nowrap;
}
.scenario-btn:hover{background:var(--soft-2);border-color:var(--hairline-strong)}
.scenario-btn:focus-visible{outline:2px solid var(--link);outline-offset:2px;border-color:var(--link)}
.scenario-btn.reset-btn{background:var(--ink);color:#fff;border-color:var(--ink)}
.scenario-btn.reset-btn:hover{background:var(--body)}
.scenario-btn.s-resolve{color:var(--success);border-color:var(--link-bg)}
/* ── Inbox ── */
.inbox{display:flex;flex-direction:column;gap:var(--space-xxs)}
/* ── Item card ── */
.item{
display:grid;grid-template-columns:1fr auto auto;
gap:var(--space-sm);align-items:center;
padding:var(--space-md);
background:var(--canvas);border:1px solid var(--hairline);border-radius:var(--radius-md);
transition:opacity .3s,transform .3s,max-height .4s,padding .3s,margin .3s,border-color .3s;
overflow:hidden;
}
.item.running{
background:var(--soft);
}
.item.removing{
opacity:0;transform:translateX(-16px);max-height:0;padding:0 var(--space-md);margin:0;border-color:transparent;
pointer-events:none;
}
.item.info-area{grid-column:1/-1;border:0;padding:var(--space-sm) 0 0;background:transparent;overflow:visible}
.item.with-border-red{border-color:var(--error-soft)}
.item.with-border-yellow{border-color:var(--warning-soft)}
.item-info{min-width:0}
.item-title{font-size:14px;font-weight:500;margin-bottom:2px}
.item-meta{font-size:12px;color:var(--mute)}
.item-reason{font-size:12px;color:var(--body);margin-top:4px}
.item-actions{display:flex;gap:var(--space-xxs)}
.action-btn{
font-size:12px;font-weight:500;letter-spacing:-.28px;
padding:var(--space-xxs) var(--space-sm);
border:1px solid var(--hairline);border-radius:100px;
background:var(--canvas);color:var(--ink);cursor:pointer;
white-space:nowrap;transition:background .15s,color .15s,border-color .15s;
}
.action-btn:hover{background:var(--soft-2)}
.action-btn:focus-visible{outline:2px solid var(--link);outline-offset:2px}
.action-btn.primary{background:var(--link);color:#fff;border-color:var(--link)}
.action-btn.primary:hover{background:var(--link-deep);border-color:var(--link-deep)}
.action-btn.destructive{color:var(--error);border-color:var(--error-soft)}
.action-btn.destructive:hover{background:var(--error-soft)}
.action-btn.destructive.primary{background:var(--error);color:#fff;border-color:var(--error)}
.action-btn.destructive.primary:hover{background:var(--error-deep);border-color:var(--error-deep)}
.action-btn:disabled{opacity:.4;cursor:not-allowed}
/* ── Running progress ── */
.progress-row{display:flex;align-items:center;gap:var(--space-sm);margin-top:var(--space-xs)}
.progress-bar{
flex:1;height:4px;border-radius:100px;
background:var(--soft-2);overflow:hidden;
}
.progress-fill{
height:100%;border-radius:100px;
background:var(--link);transition:width .5s;
}
.progress-label{font-size:11px;color:var(--mute);white-space:nowrap}
/* ── Badge per action type ── */
.item-badge{
display:inline-flex;align-items:center;gap:var(--space-xxs);
font-size:11px;font-weight:500;letter-spacing:-.28px;
padding:2px var(--space-xs);border-radius:100px;
white-space:nowrap;
}
.item-badge.retry{background:var(--error-soft);color:var(--error-deep)}
.item-badge.rebuild{background:var(--warning-soft);color:var(--warning-deep)}
.item-badge.corrupt{background:var(--error-soft);color:var(--error-deep)}
.item-badge.running-indicator{background:var(--link-bg);color:var(--link-deep)}
.item-badge.draft{background:var(--link-bg);color:var(--link-deep)}
/* ── Confirmation modal ── */
.modal-overlay{
display:none;position:fixed;inset:0;
background:rgba(23,23,23,.4);
z-index:100;align-items:center;justify-content:center;
padding:var(--space-md);
}
.modal-overlay.open{display:flex}
.modal{
background:var(--canvas);border-radius:var(--radius-lg);
padding:var(--space-xl);max-width:480px;width:100%;
box-shadow:var(--shadow-md);
}
.modal h2{font-size:18px;font-weight:600;margin-bottom:var(--space-sm);color:var(--ink)}
.modal p{font-size:14px;color:var(--body);margin-bottom:var(--space-md);line-height:1.6}
.modal .destructive-scope{
background:var(--error-soft);color:var(--error-deep);
font-size:13px;font-weight:500;padding:var(--space-sm) var(--space-md);
border-radius:var(--radius-sm);margin-bottom:var(--space-md);
}
.modal-actions{display:flex;gap:var(--space-sm);justify-content:flex-end}
/* ── Issue Draft panel ── */
.draft-overlay{
display:none;position:fixed;inset:0;
background:rgba(23,23,23,.4);
z-index:100;align-items:center;justify-content:center;
padding:var(--space-md);
}
.draft-overlay.open{display:flex}
.draft-panel{
background:var(--canvas);border-radius:var(--radius-lg);
padding:var(--space-xl);max-width:600px;width:100%;max-height:85vh;
overflow-y:auto;box-shadow:var(--shadow-md);
}
.draft-panel h2{font-size:18px;font-weight:600;margin-bottom:var(--space-sm)}
.draft-panel .draft-subtitle{font-size:13px;color:var(--mute);margin-bottom:var(--space-lg)}
.draft-section{margin-bottom:var(--space-lg)}
.draft-section h3{font-size:13px;font-weight:500;color:var(--body);margin-bottom:var(--space-xs);text-transform:uppercase;letter-spacing:.4px}
.field-table{width:100%;font-size:13px;border-collapse:collapse}
.field-table th{padding:var(--space-xxs) var(--space-xs);text-align:left;color:var(--mute);font-weight:500;border-bottom:1px solid var(--hairline);width:120px}
.field-table td{padding:var(--space-xxs) var(--space-xs);border-bottom:1px solid var(--hairline)}
.field-table td.redacted{color:var(--mute);font-family:var(--font-mono);font-size:11px}
.field-table td.included{color:var(--body)}
.draft-panel textarea{
width:100%;min-height:140px;padding:var(--space-sm);
font-size:13px;font-family:var(--font-mono);
border:1px solid var(--hairline);border-radius:var(--radius-sm);
resize:vertical;color:var(--ink);background:var(--soft);
}
.draft-panel textarea:focus{outline:none;border-color:var(--link);box-shadow:0 0 0 2px var(--link-bg)}
.draft-panel input[type="text"]{
width:100%;padding:var(--space-sm);
font-size:14px;font-weight:500;font-family:var(--font-body);
border:1px solid var(--hairline);border-radius:var(--radius-sm);
color:var(--ink);background:var(--soft);
}
.draft-panel input[type="text"]:focus{outline:none;border-color:var(--link);box-shadow:0 0 0 2px var(--link-bg)}
.draft-notice{
background:var(--warning-soft);color:var(--warning-deep);
font-size:12px;padding:var(--space-sm) var(--space-md);
border-radius:var(--radius-sm);margin-bottom:var(--space-md);
}
/* ── Empty state ── */
.empty-state{
text-align:center;padding:var(--space-2xl) var(--space-lg);
}
.empty-icon{
display:inline-flex;align-items:center;justify-content:center;
width:48px;height:48px;border-radius:100px;
background:var(--soft-2);margin-bottom:var(--space-md);
font-size:20px;
}
.empty-state h3{font-size:16px;font-weight:500;margin-bottom:var(--space-xxs);color:var(--ink)}
.empty-state p{font-size:14px;color:var(--mute);max-width:360px;margin:0 auto}
/* ── Separator ── */
.inbox-sep{font-size:11px;font-weight:500;color:var(--mute);padding:var(--space-sm) 0;letter-spacing:.4px;text-transform:uppercase}
/* ── Footer ── */
.footer-notes{margin-top:var(--space-lg);font-size:11px;color:var(--mute);border-top:1px solid var(--hairline);padding-top:var(--space-sm)}
/* ── Responsive ── */
@media(max-width:768px){
.page-wrap{padding:var(--space-md)}
.header{flex-direction:column;align-items:flex-start}
.scenario-bar{flex-direction:column;align-items:stretch}
.item{grid-template-columns:1fr;gap:var(--space-xs)}
.item-actions{justify-content:flex-end;margin-top:var(--space-xxs)}
.modal{padding:var(--space-md)}
.draft-panel{padding:var(--space-md)}
}
</style>
</head>
<body>
<div class="page-wrap">
<!-- ─── HEADER ─── -->
<div class="header">
<div class="header-left">
<h1>维护</h1>
<span class="header-badge" id="badge">0</span>
</div>
<div style="font-size:12px;color:var(--mute)">
<span id="status-label">全部正常</span>
</div>
</div>
<!-- ─── SCENARIO BAR ─── -->
<div class="scenario-bar">
<span class="scenario-bar-label">场景</span>
<button class="scenario-btn" data-scenario="stale-ocr">过期 OCR 结果</button>
<button class="scenario-btn" data-scenario="failed-ocr">OCR 失败</button>
<button class="scenario-btn" data-scenario="corrupt-index">索引损坏</button>
<button class="scenario-btn" data-scenario="running" id="scn-running">正在重建</button>
<button class="scenario-btn s-resolve" data-scenario="resolve">→ 解决一项</button>
<button class="scenario-btn" data-scenario="unacceptable">质量不合格</button>
<button class="scenario-btn" data-scenario="empty">全部正常</button>
<button class="scenario-btn reset-btn" id="reset-all">重置</button>
</div>
<!-- ─── INBOX ─── -->
<div class="inbox" id="inbox">
<!-- Items rendered by JS -->
</div>
<!-- ─── FOOTER ─── -->
<div class="footer-notes">
PaperForge · 维护标签页原型 · 设计语言 Vercel 灰阶
</div>
</div>
<!-- ─── CONFIRMATION MODAL ─── -->
<div class="modal-overlay" id="confirm-overlay">
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="confirm-title">
<h2 id="confirm-title">确认操作</h2>
<p id="confirm-body">此操作将执行变更。</p>
<div class="destructive-scope" id="confirm-scope" hidden></div>
<div class="modal-actions">
<button class="action-btn" id="confirm-cancel">取消</button>
<button class="action-btn destructive primary" id="confirm-ok">确认执行</button>
</div>
</div>
</div>
<!-- ─── ISSUE DRAFT OVERLAY ─── -->
<div class="draft-overlay" id="draft-overlay">
<div class="draft-panel" role="dialog" aria-modal="true" aria-labelledby="draft-title">
<h2 id="draft-title">提交 Issue</h2>
<p class="draft-subtitle">隐私安全模式 · 预览将包含和排除的字段</p>
<div class="draft-section">
<h3>标题(可编辑)</h3>
<input type="text" id="draft-title-input" value="[PaperForge] OCR quality unacceptable — 2BB8VM5W" aria-label="Issue 标题">
</div>
<div class="draft-section">
<h3>包含的字段</h3>
<table class="field-table">
<tr><th>模块</th><td class="included">ocr</td></tr>
<tr><th>reason.code</th><td class="included">ocr.quality_unacceptable</td></tr>
<tr><th>reason.text</th><td class="included"><span id="draft-reason-text">OCR quality assessment failed</span></td></tr>
<tr><th>error_summary</th><td class="included"><span id="draft-error">character error rate above threshold</span></td></tr>
<tr><th>key/ID</th><td class="included"><span id="draft-key">5T8JX2</span></td></tr>
</table>
</div>
<div class="draft-section">
<h3>排除的字段(已脱敏)</h3>
<table class="field-table">
<tr><th>论文标题</th><td class="redacted">[redacted — contains identifiable content]</td></tr>
<tr><th>vault path</th><td class="redacted">[redacted — local filesystem path]</td></tr>
<tr><th>zotero path</th><td class="redacted">[redacted — local filesystem path]</td></tr>
<tr><th>config values</th><td class="redacted">[redacted — may contain secrets]</td></tr>
<tr><th>API key</th><td class="redacted">[redacted — credential]</td></tr>
</table>
</div>
<div class="draft-section">
<h3>Issue 草稿(可编辑)</h3>
<div class="draft-notice">⚠ 草稿仅本地展示,不会自动提交。编辑完成后,点击下方按钮再 Github 新建 Issue。</div>
<textarea id="draft-body" aria-label="Issue 草稿内容">## Bug: OCR quality assessment failed
**Module:** ocr
**reason.code:** ocr.quality_unacceptable
**reason.text:** OCR quality assessment failed
**error:** character error rate above threshold
**paper key:** 5T8JX2
### Redacted fields
- title: [redacted]
- vault path: [redacted]
- zotero path: [redacted]
- config values: [redacted]
- API key: [redacted]
### Steps to reproduce
1. Open the paper in PaperForge
2. Review OCR output
3. Character error rate exceeds acceptable threshold
### Expected behavior
OCR quality should meet the configured threshold.
### Environment
[User fills in manually — none collected]</textarea>
</div>
<div class="modal-actions">
<button class="action-btn" id="draft-close">关闭</button>
<button class="action-btn primary" id="draft-open-github">在 GitHub 新建 Issue</button>
</div>
</div>
</div>
<script>
/* ─── State ─── */
/*
Each item represents a maintenance inbox entry backed by the capability model.
Fields following the envelope spec from research/2026-07-14-capability-state-action-contract.md
*/
const SCENARIOS = {
'stale-ocr': {
items: [{
id:'S1',module:'ocr',title:'Efficient Retrieval-Augmented Generation',
key:'5T8JX2',capability_state:'needs_action',activity_state:'idle',
severity:'warning',reason_code:'ocr.artifacts_stale',
reason_text:'12 篇论文的派生 OCR 结果已过期',
action_verb:'rebuild_derived',action_label:'重建结果',
destructive:false,destructive_scope:null,destructive_effect:null,
badge_label:'重建',badge_class:'rebuild',
}]
},
'failed-ocr': {
items: [{
id:'F1',module:'ocr',title:'Deep Learning for Medical Imaging — 2024 Review',
key:'9KM2P',capability_state:'needs_action',activity_state:'idle',
severity:'warning',reason_code:'ocr.retryable_error',
reason_text:'OCR 处理中断 — 可重试',
action_verb:'redo',action_label:'重试 OCR',
destructive:true,destructive_scope:'paper',destructive_effect:'删除该论文的现有 OCR 结果并重新处理',
confirmation_prompt:'将删除该论文的所有 OCR 派生数据并重新处理。源代码 PDF 不受影响。',
badge_label:'重试',badge_class:'retry',
}]
},
'corrupt-index': {
items: [{
id:'M1',module:'memory',title:'Memory Index',
key:'——',capability_state:'unavailable',activity_state:'idle',
severity:'error',reason_code:'memory.db_corrupt',
reason_text:'记忆库索引文件损坏,搜索功能不可用',
action_verb:'restore_backup',action_label:'恢复备份',
destructive:true,destructive_scope:'module',destructive_effect:'替换当前索引文件为最近的备份副本',
confirmation_prompt:'将替换损坏的 memory.db 索引文件为最近备份。原始 PDF 来源、OCR 派生数据、论文元数据和设置配置均不受影响。如备份也损坏,后续需完全重建搜索索引。',
badge_label:'损坏',badge_class:'corrupt',
}]
},
'running': {
items: [
{
id:'R1',module:'ocr',title:'Advances in Transformer Architecture',
key:'R3X7H',capability_state:'needs_action',activity_state:'running',
severity:'warning',reason_code:'ocr.artifacts_stale',
reason_text:'正在重建派生结果…',
action_verb:'rebuild_derived',action_label:'重建中',
destructive:false,destructive_scope:null,destructive_effect:null,
badge_label:'运行中',badge_class:'running-indicator',
running:true,progress_label:'3 / 12 篇已完成',
},
{
id:'R2',module:'ocr',title:'Graph Neural Networks in Drug Discovery',
key:'X8Y4K',capability_state:'needs_action',activity_state:'idle',
severity:'warning',reason_code:'ocr.artifacts_stale',
reason_text:'派生 OCR 结果已过期',
action_verb:'rebuild_derived',action_label:'重建结果',
destructive:false,destructive_scope:null,destructive_effect:null,
badge_label:'重建',badge_class:'rebuild',
}
]
},
'unacceptable': {
items: [
{
id:'U1',module:'ocr',title:'Medical Image Analysis — A Systematic Review',
key:'2BB8VM5W',capability_state:'needs_action',activity_state:'idle',
severity:'warning',reason_code:'ocr.quality_unacceptable',
reason_text:'OCR 质量低于阈值,可手动报告',
action_verb:'investigate',action_label:'报告 Issue',
destructive:false,destructive_scope:null,destructive_effect:null,
badge_label:'质量不合格',badge_class:'draft',
issue_reportable:true,
issue_error:'character error rate above threshold (CER 0.23 vs max 0.05)',
issue_title:'Medical Image Analysis — A Systematic Review',
issue_path:'D:\\Vault\\Literature\\Papers\\2BB8VM5W',
issue_zotero:'D:\\Zotero\\storage\\2BB8VM5W',
},
{
id:'U2',module:'ocr',title:'Towards Explainable AI in Radiology',
key:'6M9QN',capability_state:'needs_action',activity_state:'idle',
severity:'warning',reason_code:'ocr.artifacts_stale',
reason_text:'派生 OCR 结果已过期',
action_verb:'rebuild_derived',action_label:'重建结果',
destructive:false,destructive_scope:null,destructive_effect:null,
badge_label:'重建',badge_class:'rebuild',
}
]
},
'empty': { items: [] },
'resolve': null, /* handled specially — resolves the first item */
};
let currentItems = [];
let removedIds = new Set();
/* ─── Render ─── */
function render() {
const inbox = document.getElementById('inbox');
const visible = currentItems.filter(i => !removedIds.has(i.id));
const badge = document.getElementById('badge');
const label = document.getElementById('status-label');
badge.textContent = visible.length;
if (visible.length === 0) {
label.textContent = '全部正常';
inbox.innerHTML = `
<div class="empty-state">
<div class="empty-icon"></div>
<h3>全部正常</h3>
<p>所有模块运行正常,无需维护操作。</p>
</div>
`;
return;
}
label.textContent = `${visible.length} 项需要处理`;
let html = '';
visible.forEach((item, idx) => {
const infoStyle = item.running ? ' style="background:var(--soft);"' : '';
const borderCls = item.severity === 'error' ? ' with-border-red' : (item.severity === 'warning' ? ' with-border-yellow' : '');
html += `<div class="item${borderCls}" data-id="${item.id}"${infoStyle}>`;
html += `<div class="item-info">`;
html += `<div class="item-title">${escHtml(item.title)}</div>`;
html += `<div class="item-meta"><span class="item-badge ${item.badge_class}">${escHtml(item.badge_label)}</span> · ${escHtml(item.key)} · ${escHtml(moduleLabel(item.module))}</div>`;
html += `<div class="item-reason">${escHtml(item.reason_text)}</div>`;
if (item.running) {
html += `<div class="progress-row">`;
html += `<div class="progress-bar"><div class="progress-fill" style="width:${item.progress_pct || 30}%"></div></div>`;
html += `<span class="progress-label">${escHtml(item.progress_label || '')}</span>`;
html += `</div>`;
}
html += `</div>`; /* info */
html += `<div class="item-actions">`;
if (item.activity_state !== 'running' && !item.running) {
if (item.destructive) {
html += `<button class="action-btn destructive" data-action="destructive" data-idx="${idx}">${escHtml(item.action_label)}</button>`;
} else if (item.issue_reportable) {
html += `<button class="action-btn" data-action="issue-draft" data-idx="${idx}">${escHtml(item.action_label)}</button>`;
} else {
html += `<button class="action-btn primary" data-action="resolve" data-idx="${idx}">${escHtml(item.action_label)}</button>`;
}
} else {
html += `<button class="action-btn" disabled>处理中</button>`;
}
html += `</div>`;
html += `</div>`; /* item */
});
inbox.innerHTML = html;
updateBadge();
}
function moduleLabel(m) {
switch(m) {
case 'ocr': return 'OCR';
case 'memory': return '记忆';
case 'library': return '文献库';
default: return m;
}
}
function escHtml(s) {
const d = document.createElement('div');
d.textContent = s;
return d.innerHTML;
}
function updateBadge() {
const visible = currentItems.filter(i => !removedIds.has(i.id));
const badge = document.getElementById('badge');
badge.textContent = visible.length;
}
/* ─── Scenario loading ─── */
function loadScenario(name) {
if (name === 'resolve') {
resolveOne();
return;
}
const data = SCENARIOS[name];
if (!data) return;
removedIds.clear();
// deep clone
currentItems = data.items.map(i => ({
...i,
running: i.running || false,
progress_pct: i.progress_pct || 0,
progress_label: i.progress_label || '',
}));
render();
}
function resolveOne() {
const visible = currentItems.filter(i => !removedIds.has(i.id));
if (visible.length === 0) return;
// Resolve the first non-running item
const target = visible.find(i => i.activity_state !== 'running' && !i.running);
if (!target) return;
removedIds.add(target.id);
render();
focusAfterRemoval();
}
/* ─── Modal helpers: focus trap + inert + restoration ─── */
let lastFocusedElement = null;
let activeFocusTrapOverlay = null;
function openModal(overlay) {
// Ensure other overlay is hidden — only one modal at a time
document.getElementById('confirm-overlay').classList.remove('open');
document.getElementById('draft-overlay').classList.remove('open');
// Inert background content
document.querySelector('.page-wrap').inert = true;
// Track previously focused element for restoration
lastFocusedElement = document.activeElement;
overlay.classList.add('open');
activeFocusTrapOverlay = overlay;
}
function closeModal(overlay, opts) {
opts = opts || {};
overlay.classList.remove('open');
// Restore inert
document.querySelector('.page-wrap').inert = false;
activeFocusTrapOverlay = null;
// Restore focus to element that opened the modal (skip when caller manages it)
if (!opts.skipRestore && lastFocusedElement && lastFocusedElement.focus && typeof lastFocusedElement.focus === 'function') {
try { lastFocusedElement.focus(); } catch (_) {}
}
if (!opts.skipRestore) lastFocusedElement = null;
}
function focusAfterRemoval() {
const visible = currentItems.filter(i => !removedIds.has(i.id));
// Focus deterministically: actionable button > heading
if (visible.length > 0) {
// Try first non-running item's action button
const targetIdx = visible.findIndex(i => i.activity_state !== 'running' && !i.running);
if (targetIdx >= 0) {
const btn = document.querySelector(`.item:not(.removing) [data-action][data-idx="${targetIdx}"]`);
if (btn && typeof btn.focus === 'function') { btn.focus(); return; }
}
// Fallback: any actionable button (skip disabled)
const anyBtn = document.querySelector('.item:not(.removing) .action-btn:not(:disabled):not([hidden])');
if (anyBtn && typeof anyBtn.focus === 'function') { anyBtn.focus(); return; }
}
// Empty/no-actionable state — focus the heading
const h1 = document.querySelector('h1');
if (h1) { h1.setAttribute('tabindex', '-1'); h1.focus(); }
}
// Focus trap: cycle Tab within the active overlay
document.addEventListener('keydown', (e) => {
if (e.key !== 'Tab' || !activeFocusTrapOverlay) return;
const focusable = activeFocusTrapOverlay.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
if (focusable.length === 0) return;
const first = focusable[0];
const last = focusable[focusable.length - 1];
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
last.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
first.focus();
}
});
/* ─── Confirmation flow ─── */
let pendingDestructive = null;
function showConfirmation(item) {
const overlay = document.getElementById('confirm-overlay');
document.getElementById('confirm-title').textContent = '确认 ' + item.action_label;
document.getElementById('confirm-body').textContent = item.confirmation_prompt || '此操作将执行变更。';
const scopeEl = document.getElementById('confirm-scope');
if (item.destructive_scope && item.destructive_effect) {
scopeEl.hidden = false;
scopeEl.textContent = '范围: ' + item.destructive_scope + ' · ' + item.destructive_effect;
} else {
scopeEl.hidden = true;
}
pendingDestructive = item;
openModal(overlay);
document.getElementById('confirm-ok').focus();
}
function closeAnyOverlay() {
const confirmEl = document.getElementById('confirm-overlay');
const draftEl = document.getElementById('draft-overlay');
if (confirmEl.classList.contains('open')) {
confirmEl.classList.remove('open');
pendingDestructive = null;
}
if (draftEl.classList.contains('open')) {
draftEl.classList.remove('open');
draftItem = null;
}
document.querySelector('.page-wrap').inert = false;
activeFocusTrapOverlay = null;
if (lastFocusedElement && lastFocusedElement.focus && typeof lastFocusedElement.focus === 'function') {
try { lastFocusedElement.focus(); } catch (_) {}
}
lastFocusedElement = null;
}
document.getElementById('confirm-cancel').addEventListener('click', () => {
closeModal(document.getElementById('confirm-overlay'));
pendingDestructive = null;
});
document.getElementById('confirm-overlay').addEventListener('click', (e) => {
if (e.target === e.currentTarget) {
closeModal(document.getElementById('confirm-overlay'));
pendingDestructive = null;
}
});
document.getElementById('confirm-ok').addEventListener('click', () => {
const item = pendingDestructive;
if (!item) return;
// Mark confirmed — remove from inbox
removedIds.add(item.id);
closeModal(document.getElementById('confirm-overlay'), { skipRestore: true });
pendingDestructive = null;
lastFocusedElement = null;
render();
focusAfterRemoval();
});
/* ─── Issue Draft flow ─── */
let draftItem = null;
function showIssueDraft(item) {
draftItem = item;
const overlay = document.getElementById('draft-overlay');
document.getElementById('draft-key').textContent = item.key;
document.getElementById('draft-error').textContent = item.issue_error || 'N/A';
document.getElementById('draft-reason-text').textContent = item.reason_text;
const titleInput = document.getElementById('draft-title-input');
titleInput.value = '[PaperForge] OCR quality unacceptable — ' + item.key;
const body = document.getElementById('draft-body');
body.value = `## Bug: OCR quality assessment failed
**Module:** ocr
**reason.code:** ocr.quality_unacceptable
**reason.text:** ${item.reason_text}
**error:** ${item.issue_error || 'N/A'}
**paper key:** ${item.key}
### Redacted fields
- title: [redacted]
- vault path: [redacted]
- zotero path: [redacted]
- config values: [redacted]
- API key: [redacted]
### Steps to reproduce
1. Open the paper in PaperForge
2. Review OCR output
3. Character error rate exceeds acceptable threshold
### Expected behavior
OCR quality should meet the configured threshold.
### Environment
[User fills in manually — none collected]`;
openModal(overlay);
titleInput.focus();
titleInput.select();
}
document.getElementById('draft-close').addEventListener('click', () => {
closeModal(document.getElementById('draft-overlay'));
draftItem = null;
});
document.getElementById('draft-overlay').addEventListener('click', (e) => {
if (e.target === e.currentTarget) {
closeModal(document.getElementById('draft-overlay'));
draftItem = null;
}
});
document.getElementById('draft-open-github').addEventListener('click', () => {
const title = document.getElementById('draft-title-input').value.trim();
const body = document.getElementById('draft-body').value.trim();
// User-edited title and body are included
// We NEVER auto-submit — only open a pre-filled GitHub new-issue URL
const params = new URLSearchParams();
params.set('title', title || '[PaperForge] OCR quality issue');
params.set('body', body);
// Open in new window — user must click "Submit new issue" themselves
const url = 'https://github.com/LLLin000/PaperForge/issues/new?' + params.toString();
window.open(url, '_blank');
// Do NOT close the draft — user may want to continue editing
});
/* ─── Inbox event delegation ─── */
document.getElementById('inbox').addEventListener('click', (e) => {
const btn = e.target.closest('button[data-action]');
if (!btn) return;
const action = btn.dataset.action;
const idx = parseInt(btn.dataset.idx, 10);
const visible = currentItems.filter(i => !removedIds.has(i.id));
const item = visible[idx];
if (!item) return;
switch (action) {
case 'resolve':
removedIds.add(item.id);
render();
focusAfterRemoval();
break;
case 'destructive':
showConfirmation(item);
break;
case 'issue-draft':
showIssueDraft(item);
break;
}
});
/* ─── Scenario button wiring ─── */
document.querySelectorAll('[data-scenario]').forEach(btn => {
btn.addEventListener('click', () => {
loadScenario(btn.dataset.scenario);
});
});
document.getElementById('reset-all').addEventListener('click', () => {
removedIds.clear();
currentItems = [];
render();
});
/* ─── Keyboard support ─── */
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
closeAnyOverlay();
}
});
/* ─── Init ─── */
loadScenario('empty');
</script>
</body>
</html>

View file

@ -0,0 +1,297 @@
# Maintenance Issue Reporting — 原型设计说明
**日期:** 2026-07-15
**方式:** Issue #72
**配套原型:** `2026-07-14-maintenance-issue-reporting.html`
**涉及方式:** #69 (能力状态模型), #70 (托管运行时架构), #71 (Control Center 原型)
**文件路径:** `docs/prototypes/2026-07-14-maintenance-issue-reporting.html`
---
## 1. 设计目标
原生原型用于探索和维护导航 **actionable-only 维护收件箱** 概念验证:
1. 只显示用户需要关心的维护项(不显示"正常"项,不制造焦虑)
2. 每个维护项提供明确的可操作入口按钮
3. 展示能力状态模型availability × activity × attention
4. 提供隐私安全的 Issue 报告流程(可编辑草稿 + 显式打开 GitHub
5. 确定性的场景控制方便评审
### 非目标
- 不是插件代码,不涉及真实数据加载
- 不替代生产维护标签页的实现
- 不涉及缓存策略、TTL 刷新、后端探测
- 不涉及批量操作(全选/执行已选)
---
## 2. 对应的能力状态合同关系(#69
本原型严格按照 [2026-07-14-capability-state-action-contract.md](../research/2026-07-14-capability-state-action-contract.md) 定义的三维能力模型:
### 2.1 可用性轴 (Availability)
| 状态 | 原型场景 | 显示含义 |
|------|---------|---------|
| `needs_action` | 过期 OCR、OCR 失败、不合格 OCR | 黄色提示,可操作 |
| `unavailable` | 索引损坏 | 红色提示,需要修复 |
| `ready` | 全部正常 | 不显示任何维护项 |
### 2.2 活动轴 (Activity)
`running` 状态下,物品保持可见但按钮变为 `disabled` 并显示"处理中"。
进展栏和进度文本也一同显示。**Running 不隐藏 attention** 即使在运行也保持显示。
### 2.3 注意力轴 (Attention)
- 普通状态保持安静,底部和徽章显示计数
- `running` 从不掩盖 warning/error 徽章(合同 §2 明确)
### 2.4 单一主要行动不变量
每个模块至多一个 `action.primary`。原型中:
- `needs_action``rebuild_derived` / `redo` / `investigate`
- `unavailable``restore_backup`
- `ready` → 无操作,收件箱为空
---
## 3. 维护投影规则(#69 §8
维护是派生视图Derived View不是独立模块。本原型通过汇总各模块状态来模拟
- **入口条件:** 模块从 `ready` / `unknown` 转换成 `unavailable` / `needs_action`
- **出口条件:** 用户执行操作 → 项消失 → 如果所有项都消失,回到`全部正常`
- **空状态:** `0 项需要处理` → 显示空状态引导
---
## 4. 破坏性操作确认流程(#69 §5, #68
继承自 Desktop Runtime Recovery Patterns 研究中的三阶恢复模型§4.2.1
| 操作 | 场景 | 确认要求 | 生效范围 |
|------|------|---------|---------|
| `rebuild_derived` | 过期 OCR | 不需要(非破坏性) | selection |
| `redo` | OCR 失败 | 需要确认 | paper |
| `restore_backup` | 索引损坏 | 需要确认 | module |
确认弹窗显示:
- **标题:** `确认 <操作名>`(保留用户看到的操作描述)
- **确认提示:** 描述后果
- **破坏范围:** `范围: <scope> · <effect>`
---
## 5. 隐私安全 Issue 报告流程(#42, #69 §9
直接继承 VS Code预填 Issue Report和 Docker Desktop诊断导出供本地检查再提交的模式。
### 流程
1. 用户点击 "报告 Issue"
2. 弹窗显示 **包含的字段**模块、reason.code、reason.text、error_summary、key
3. 弹窗显示 **排除的字段**论文标题、vault 路径、zotero 路径、配置值、API Key全部标记为 `[redacted]`
4. 用户可以在文本框中任意编辑草稿
5. **⚠ 显式警告:** "草稿仅本地展示,不会自动提交。编辑完成后,点击下方按钮再 Github 新建 Issue。"
6. 用户点击"在 GitHub 新建 Issue" → `window.open(github.com/LLLin000/PaperForge/issues/new?...)` → 用户仍需在 GitHub 页面手动点击 "Submit new issue"
7. 点击后不关闭弹窗(方便用户继续编辑)
8. 草稿不上传、不提交 API、不包含 token
### 不变量
- ❌ NEVER 自动打开 GitHub用户必须显式点击按钮
- ❌ NEVER 自动提交 Issue
- ❌ NEVER 上传任何数据
- ❌ 不包含 token、API key、本地路径、环境变量
---
## 6. UI 设计决策
### 6.1 设计语言
继承 Vercel 灰阶设计体系(来自 DESIGN.md
| Token | 使用位置 | 值 |
|-------|---------|-----|
| ink | 标题、主文本 | #171717 |
| body | 辅助文本、原因 | #4d4d4d |
| mute | 元信息、徽章数字 | #888888 |
| hairline | 卡片描边 | #ebebeb |
| link | 主要操作按钮 | #0070f3 |
| error | 破坏性操作/严重状态 | #ee0000 |
| warning | 警告状态 | #f5a623 |
### 6.2 组件映射
- **场景工具栏:** pill 样式按钮(符合 nav-link 组件定义)
- **物品卡片:** card-marketing-large 变体(圆角 8px, padding 16px, hairline 描边)
- **空状态:** ex-empty-state-card48px 圆形图标 + 标题 + 描述)
- **确认弹窗:** ex-modal-card圆角 12px, shadow-md, 最大宽 480px
- **浮动层:** 半透明 ink 遮罩rgba(23,23,23,.4)
### 6.3 响应式
- 768px 断点:折成单列堆叠布局
- 标题改为纵向排列
- 场景栏改为堆叠
- 物品卡片:信息区域和按钮纵向排列
### 6.4 无状态
- 没有外部依赖vanilla HTML/CSS/JS
- 直接打开 HTML 文件即可完整工作
- 不需要服务端、WebSocket、localStorage
---
## 7. 场景设计理由
| 场景 | 测试的能力模型 | 覆盖的操作类型 |
|------|---------------|---------------|
| 过期 OCR 结果 | `needs_action` + `idle` | 非破坏性的 `rebuild_derived` |
| OCR 失败 | `needs_action` + `idle` | 破坏性的 `redo`(需确认) |
| 索引损坏 | `unavailable` + `idle` | 破坏性的 `restore_backup`(模块级) |
| 正在重建 | `needs_action` + `running` | 混合状态(一个在运行 + 一个待处理) |
| → 解决一项 | 从收件箱移除项 | 跳过 running 项,只解决 idle 项 |
| 质量不合格 | `needs_action` + 可报告 Issue | Issue 草稿流(包含/排除预览) |
| 全部正常 | `ready` | 空状态 |
---
## 8. 从 #69/#70 到本原型的关键路径
```
#69 capability contract #70 managed runtime
│ │
│ ┌──────────────────────────────────┘
│ │
│ ▼
│ Action verbs & reason codes
│ Maintenance projection rules
│ Destructive confirmation contract
│ Issue draft privacy rules
│ ┌──────────────────────────────────┐
│ │ │
│ ▼ │
#71 Control Center Prototype │
│ (module cards from envelope) │
│ │
│ ▼ │
#72 Maintenance Issue Reporting │
│ (actionable inbox + Issue draft) │
```
---
## 9. 浏览器验证步骤
### 9.1 快速验证(桌面端,~1200px 宽)
1. 打开 `docs/prototypes/2026-07-14-maintenance-issue-reporting.html`
2. **初始状态:** 看到 "全部正常" 空状态,徽章显示 "0"
3. **点击 "过期 OCR 结果"** 看到 1 条物品,有 "重建" 徽章和 "重建结果" 按钮(蓝色 primary
4. **点击 "重建结果"** 物品消失,回到空状态
5. **点击 "OCR 失败"** 看到 "重试" 徽章和 "重试 OCR" 按钮
6. **点击 "重试 OCR"** 弹出确认弹窗(显示范围:"paper"级破坏性)
7. **按 Esc** 关闭弹窗。再次点击 "重试 OCR" → **点击 "确认执行"** → 物品消失
8. **点击 "索引损坏"** 看到 "损坏" 徽章、"恢复备份" 按钮。点击 → 看到模块级确认
9. **点击 "正在重建"** 看到 2 条物品。第一条标记 "运行中" 有进度条和 "处理中" 禁用按钮;第二条有 "重建结果" 按钮
10. **点击 "→ 解决一项"** 运行中物品保留,非运行中的物品被移除
11. **点击 "质量不合格"** 看到 "质量不合格" 徽章和 "报告 Issue" 按钮
12. **点击 "报告 Issue"** 弹出 Issue 草稿面板
13. **查看"包含的字段"和"排除的字段"** 标题、路径、配置等已脱敏
14. **点击 "在 GitHub 新建 Issue"** 新标签页打开 GitHub Issues不自动提交草稿面板保持打开
15. **按 Esc** 关闭草稿面板
16. **点击 "重置"** 回到空状态
### 9.2 响应式验证768px 宽)
1. 调整浏览器窗口到 768px 或使用 DevTools 的设备仿真
2. 验证场景栏折为纵向布局
3. 验证物品卡片堆叠排列
4. 确认所有操作按钮仍可点击
5. 确认弹窗仍可读(文本自适应)
### 9.3 键盘验证
1. `Tab` 导航所有场景按钮和操作按钮
2. 确认所有 `:focus-visible` 出现蓝色(`--link`)光环
3. `Enter` 激活焦点按钮
4. `Esc` 关闭弹窗和草稿面板
5. 弹出确认弹窗时焦点自动移到"确认执行"按钮
### 9.4 隐私安全验证
1. 点击 "报告 Issue" → 确认草稿面板没有用户路径、文件内容、API key
2. 确认"排除的字段"区域显示 `[redacted]`
3. 确认草稿面板有关于"不会自动提交"的警告
4. 确认 "在 GitHub 新建 Issue" 按钮行为:在真实浏览器中打开新标签页,不在当前标签页导航
5. 确认 URL 参数仅包含 `title``body`(不包含 token/secret
---
## 10. 已知限制
| 限制 | 描述 | 计划解决方案 |
|------|------|------------|
| 数据完全模拟 | 没有真正的后端探测 | 集成时替换为真实 envelope |
| 无缓存层 | 每次重新加载都重置状态 | #71 阶段加入缓存 |
| 无批量操作 | 不支持全选/执行已选 | #73 或后续迭代 |
| 仅限桌面焦点 | 没有针对移动端布局优化 | 后续可选 |
| 无 i18n | 全部中文硬编码 | 后续接入 `t()` 系统 |
| 无动画 | 移除物品时使用过渡过度动画 | 后续添加入/出动画 |
## 11. 评审修复记录
### 11.1 Fix 1 — 模态框焦点陷阱 + 背景 inert + 焦点恢复
**问题:** 弹窗打开后没有焦点陷阱Tab 可以跳出到背景内容;没有 inert 阻断,键盘用户(屏幕阅读器)可以访问背景元素;关闭后焦点没有回到触发元素。
**方案:**
- `openModal(overlay)`:设置 `.page-wrap``inert = true`、保存 `lastFocusedElement`、打开弹窗
- `closeModal(overlay)`:移除 `.open`、恢复 `inert = false`、将焦点恢复到 `lastFocusedElement`
- `closeAnyOverlay()`:一次性关闭所有可能的弹窗并恢复 inert 和焦点
- 全局 `keydown` 监听 Tab 键:当焦点在弹窗第一个/最后一个可聚焦元素时,循环回到另一端的 `preventDefault` + `focus()`
- 使用 `openModal` 时主动关闭另一个弹窗,确保任何时候只有一个弹窗可见
**验证结果:** inert 在弹窗打开时设置关闭时恢复Escape 调用 `closeAnyOverlay()` 正确关闭所有弹窗并恢复焦点。
### 11.2 Fix 2 — Issue 标题可编辑 + 在 URL 中使用编辑后标题
**问题:** 原标题在 draft-body 中硬编码用户无法在标题区域编辑GitHub URL 中 title 参数是固定的。
**方案:**
- 在 Issue 草稿面板头部添加可编辑的 `<input type="text" id="draft-title-input">`
- `showIssueDraft()` 时预填入 `[PaperForge] OCR quality unacceptable — <key>`
- 用户可自由编辑标题文本
- `draft-open-github` 点击时从 `draft-title-input.value` 读取标题,传入 URL 参数
- 焦点默认移到标题输入框,用户可以直接开始编辑
**验证结果:** 标题输入框存在type="text"预填入正确值URL 使用编辑后标题。
### 11.3 Fix 3 — 索引损坏确认文本明确替换内容与保留数据
**问题:** 原确认提示 "用最近的备份替换当前的索引" 没有说明哪些数据会替换、哪些保留。
**方案:**
- 新确认文本:*"将替换损坏的 memory.db 索引文件为最近备份。原始 PDF 来源、OCR 派生数据、论文元数据和设置配置均不受影响。如备份也损坏,后续需完全重建搜索索引。"*
- 明确列出被替换的内容:`memory.db` 索引文件
- 明确列出保留的内容:原始 PDF 来源、OCR 派生数据、论文元数据、设置配置
- 描述后续故障模式:如备份也损坏,需完全重建
**验证结果:** 确认文本和范围文本均已包含上述信息。
### 11.4 Fix 4 — `[hidden]` 样式权重修复
**问题:** 浏览发现两个弹窗的遮罩同时可见。原因是 `.open { display: flex }` 选择器权重 > HTML `[hidden]` 属性,导致 `hidden` 被覆盖。
**方案:**
- 在 CSS 顶部添加 `[hidden]{display:none!important}`
- `openModal()` 主动关闭另一个弹窗(双重保险)
**验证结果:** 打开草稿面板时确认弹窗不可见,打开确认弹窗时草稿面板不可见。两个弹窗不会同时出现。

View file

@ -0,0 +1,964 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PaperForge 控制中心 · 原型</title>
<style>
/* ===== Tokens (Vercel-inspired grayscale) ===== */
:root {
--ink: #171717;
--body: #4d4d4d;
--mute: #888888;
--hairline: #ebebeb;
--hairline-strong: #a1a1a1;
--canvas: #ffffff;
--canvas-soft: #fafafa;
--canvas-soft-2: #f5f5f5;
--link: #0070f3;
--success: #0070f3;
--error: #ee0000;
--error-soft: #f7d4d6;
--warning: #f5a623;
--warning-soft: #ffefcf;
--warning-deep: #ab570a;
--gray-ok: #22a67e;
--gray-ok-soft: #e0f5ed;
--radius-sm: 6px;
--radius-md: 8px;
--radius-lg: 12px;
--radius-pill: 100px;
--font: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, monospace;
--shadow-sm: 0 1px 2px rgba(0,0,0,0.04);
--shadow-md: 0 1px 3px rgba(0,0,0,0.06), 0 1px 2px rgba(0,0,0,0.04);
--focus-ring: 0 0 0 2px var(--link);
}
/* ===== Reset ===== */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html { font-size: 16px; -webkit-font-smoothing: antialiased; }
body {
font-family: var(--font);
color: var(--body);
background: var(--canvas-soft);
line-height: 1.5;
min-height: 100vh;
}
/* ===== Focus ===== */
:focus-visible {
outline: 2px solid var(--link);
outline-offset: 2px;
border-radius: var(--radius-sm);
}
button:focus-visible, a:focus-visible, [tabindex]:focus-visible {
outline-offset: 2px;
}
button:focus:not(:focus-visible) {
outline: none;
}
/* ===== Layout ===== */
.app {
max-width: 1120px;
margin: 0 auto;
padding: 0 24px 48px;
}
/* ===== Header ===== */
.header {
padding: 40px 0 8px;
border-bottom: 1px solid var(--hairline);
margin-bottom: 32px;
}
.header h1 {
font-size: 24px;
font-weight: 600;
color: var(--ink);
letter-spacing: -0.5px;
margin-bottom: 4px;
}
.header p {
font-size: 14px;
color: var(--mute);
}
/* ===== Scenario Switcher ===== */
.scenarios {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 28px;
padding-top: 20px;
}
.scenario-btn {
font-family: var(--font);
font-size: 13px;
font-weight: 500;
padding: 6px 16px;
border-radius: var(--radius-pill);
border: 1px solid var(--hairline);
background: var(--canvas);
color: var(--body);
cursor: pointer;
transition: all 0.15s ease;
letter-spacing: -0.1px;
}
.scenario-btn:hover {
border-color: var(--hairline-strong);
color: var(--ink);
background: var(--canvas-soft);
}
.scenario-btn.active {
background: var(--ink);
border-color: var(--ink);
color: var(--canvas);
}
/* ===== Primary Attention Zone ===== */
.primary-zone {
background: var(--canvas);
border: 1px solid var(--hairline);
border-radius: var(--radius-lg);
padding: 24px;
margin-bottom: 24px;
box-shadow: var(--shadow-md);
}
.primary-zone.error-primary {
border-left: 3px solid var(--error);
}
.primary-zone.warning-primary {
border-left: 3px solid var(--warning);
}
.primary-zone.ok-primary {
border-left: 3px solid var(--gray-ok);
}
.primary-zone.unknown-primary {
border-left: 3px solid var(--hairline-strong);
}
.primary-label {
font-size: 11px;
font-weight: 600;
color: var(--mute);
text-transform: uppercase;
letter-spacing: 0.6px;
margin-bottom: 8px;
}
.primary-headline {
display: flex;
align-items: center;
gap: 12px;
}
.primary-headline h2 {
font-size: 20px;
font-weight: 600;
color: var(--ink);
letter-spacing: -0.4px;
flex: 1;
}
.primary-action-btn {
font-family: var(--font);
font-size: 14px;
font-weight: 500;
padding: 8px 20px;
border-radius: var(--radius-pill);
border: none;
background: var(--ink);
color: var(--canvas);
cursor: pointer;
white-space: nowrap;
transition: opacity 0.15s ease;
}
.primary-action-btn:hover { opacity: 0.85; }
.primary-action-btn:active { opacity: 0.7; }
.primary-description {
margin-top: 8px;
font-size: 14px;
color: var(--body);
line-height: 1.5;
}
/* ===== Module Grid ===== */
.module-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
}
@media (max-width: 900px) {
.module-grid { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 768px) {
.module-grid { grid-template-columns: 1fr; }
}
/* ===== Module Card ===== */
.module-card {
background: var(--canvas);
border: 1px solid var(--hairline);
border-radius: var(--radius-md);
padding: 20px;
box-shadow: var(--shadow-sm);
transition: box-shadow 0.15s ease;
display: flex;
flex-direction: column;
gap: 10px;
}
.module-card:hover {
box-shadow: var(--shadow-md);
}
.module-card-header {
display: flex;
align-items: center;
gap: 10px;
}
/* ===== State Badge ===== */
.state-badge {
display: inline-flex;
align-items: center;
gap: 5px;
font-size: 12px;
font-weight: 500;
padding: 2px 10px;
border-radius: var(--radius-pill);
line-height: 1.4;
}
.state-badge .dot {
width: 7px;
height: 7px;
border-radius: 50%;
flex-shrink: 0;
}
.state-badge.ok { background: var(--gray-ok-soft); color: #1a7a5c; }
.state-badge.ok .dot { background: var(--gray-ok); }
.state-badge.warning { background: var(--warning-soft); color: var(--warning-deep); }
.state-badge.warning .dot { background: var(--warning); }
.state-badge.error { background: var(--error-soft); color: var(--error); }
.state-badge.error .dot { background: var(--error); }
.state-badge.unknown { background: var(--canvas-soft-2); color: var(--mute); }
.state-badge.unknown .dot { background: var(--hairline-strong); }
.module-name {
font-size: 15px;
font-weight: 600;
color: var(--ink);
flex: 1;
}
.module-reason {
font-size: 13px;
color: var(--mute);
line-height: 1.4;
}
.module-reason strong {
color: var(--body);
font-weight: 500;
}
/* ===== Progress ===== */
.activity-row {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
color: var(--body);
}
.spinner {
width: 14px;
height: 14px;
border: 2px solid var(--hairline);
border-top-color: var(--link);
border-radius: 50%;
animation: spin 0.8s linear infinite;
flex-shrink: 0;
}
@keyframes spin { to { transform: rotate(360deg); } }
.progress-bar {
flex: 1;
height: 4px;
background: var(--canvas-soft-2);
border-radius: 2px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: var(--link);
border-radius: 2px;
transition: width 0.5s ease;
}
/* ===== Action Button ===== */
.module-action-btn {
font-family: var(--font);
font-size: 13px;
font-weight: 500;
padding: 6px 14px;
border-radius: var(--radius-pill);
border: 1px solid var(--hairline);
background: var(--canvas);
color: var(--ink);
cursor: pointer;
align-self: flex-start;
transition: all 0.12s ease;
margin-top: 4px;
}
.module-action-btn:hover {
background: var(--canvas-soft-2);
border-color: var(--hairline-strong);
}
.module-action-btn.primary-action {
background: var(--ink);
border-color: var(--ink);
color: var(--canvas);
}
.module-action-btn.primary-action:hover {
opacity: 0.85;
}
/* ===== Advanced Diagnostics ===== */
.advanced-toggle {
font-family: var(--font);
font-size: 12px;
color: var(--link);
background: none;
border: none;
cursor: pointer;
padding: 4px 0;
align-self: flex-start;
text-decoration: underline 1px dotted transparent;
text-underline-offset: 3px;
transition: text-decoration-color 0.15s;
}
.advanced-toggle:hover {
text-decoration-color: var(--link);
}
.advanced-details {
display: none;
font-size: 12px;
color: var(--mute);
padding: 10px 12px;
background: var(--canvas-soft);
border-radius: var(--radius-sm);
line-height: 1.6;
font-family: var(--font-mono);
}
.advanced-details.open {
display: block;
}
.advanced-details dt {
color: var(--body);
font-weight: 500;
}
.advanced-details dd {
margin-left: 0;
margin-bottom: 4px;
}
/* ===== Snackbar ===== */
.snackbar {
position: fixed;
bottom: 24px;
left: 50%;
transform: translateX(-50%) translateY(80px);
background: var(--ink);
color: var(--canvas);
font-size: 14px;
padding: 10px 20px;
border-radius: var(--radius-pill);
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
opacity: 0;
transition: all 0.3s ease;
pointer-events: none;
z-index: 100;
white-space: nowrap;
}
.snackbar.show {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
</style>
</head>
<body>
<div class="app">
<!-- Header -->
<header class="header">
<h1>PaperForge 控制中心</h1>
<p>安装 · 文献库 · OCR · 记忆 · 维护 · 帮助</p>
</header>
<!-- Scenario Switcher -->
<nav class="scenarios" aria-label="场景切换">
<button class="scenario-btn" data-scenario="first-run" aria-current="true">首次运行</button>
<button class="scenario-btn" data-scenario="partial">部分就绪</button>
<button class="scenario-btn" data-scenario="active-ocr">OCR 进行中</button>
<button class="scenario-btn" data-scenario="stale">快照过期</button>
<button class="scenario-btn" data-scenario="all-ready">一切正常</button>
</nav>
<!-- Primary Attention Zone -->
<div class="primary-zone" id="primaryZone" role="alert" aria-live="polite">
<div class="primary-label">需要注意</div>
<div class="primary-headline">
<h2 id="primaryTitle">PaperForge 尚未安装</h2>
<button class="primary-action-btn" id="primaryBtn">开始安装</button>
</div>
<p class="primary-description" id="primaryDesc">完成安装后即可管理文献、运行 OCR 和构建记忆库。</p>
</div>
<!-- Module Grid -->
<div class="module-grid" id="moduleGrid" role="list">
<!-- Each card generated by JS -->
</div>
</div>
<!-- Snackbar -->
<div class="snackbar" id="snackbar" role="status" aria-live="polite"></div>
<script>
// ===== Module Definitions =====
const MODULES = [
{ id: 'installation', label: '安装', icon: '⚙' },
{ id: 'library', label: '文献库', icon: '▤' },
{ id: 'ocr', label: 'OCR', icon: '◐' },
{ id: 'memory', label: '记忆', icon: '◇' },
{ id: 'maintenance', label: '维护', icon: '△' },
{ id: 'help', label: '帮助', icon: '?' },
];
// ===== Scenario Data =====
const SCENARIOS = {
'first-run': {
primary: {
severity: 'warning',
title: 'PaperForge 尚未安装',
action: { label: '开始安装', verb: 'setup' },
desc: '完成安装后即可管理文献、运行 OCR 和构建记忆库。'
},
modules: {
installation: {
state: 'missing_input', severity: 'warning',
reason: '需要配置',
detail: '安装路径、Python 运行环境和 API 密钥均未设置。',
action: { label: '开始安装', verb: 'setup', primary: true },
diag: { '状态': '未安装', '配置文件': '不存在', '运行环境': '未检测' }
},
library: {
state: 'missing_input', severity: 'warning',
reason: 'Zotero 导出路径未设置',
detail: '需要指定 Zotero 的 Better BibTeX 导出文件路径才能同步文献。',
action: { label: '配置路径', verb: 'set_config', primary: false },
diag: { '同步状态': '未同步', '文献数量': '--', '导出文件': '未指定' }
},
ocr: {
state: 'missing_input', severity: 'warning',
reason: 'API 密钥未配置',
detail: 'OCR 需要有效的 PaddleOCR API 密钥才能处理论文图片。',
action: { label: '配置密钥', verb: 'set_config', primary: false },
diag: { 'API 状态': '未配置', '可用额度': '--', '模型版本': '--' }
},
memory: {
state: 'missing_input', severity: 'warning',
reason: '记忆库尚未创建',
detail: '需要先完成安装并同步文献后才能创建文献记忆库。',
action: { label: '配置记忆库', verb: 'set_config', primary: false },
diag: { '数据库状态': '未创建', '索引状态': '--', '检索就绪': '--' }
},
maintenance: {
state: 'needs_action', severity: 'warning',
reason: '4 个模块需要配置',
detail: '安装、文献库、OCR、记忆四个模块均处于未就绪状态。可点击各模块的按钮单独处理。',
action: null,
diag: { '待处理项目': '4', '影响模块': '安装 / 文献库 / OCR / 记忆' }
},
help: {
state: 'ready', severity: 'ok',
reason: '随时可用',
detail: '帮助文档已就绪,可以通过命令面板或右键菜单访问。',
action: null,
diag: { '文档版本': '2026-07', '文档数量': '12 篇', '最后更新': '2026-07-14' }
}
}
},
'partial': {
primary: {
severity: 'warning',
title: 'OCR 处理结果已过期,记忆库索引需要更新',
action: { label: '重建衍生文件', verb: 'rebuild_derived' },
desc: '12 篇论文的 OCR 衍生文件已过期。同时记忆库索引需要更新5 篇待索引)。点击"重建衍生文件"优先处理过期项。'
},
modules: {
installation: {
state: 'ready', severity: 'ok',
reason: '运行正常',
detail: 'PaperForge 已安装Python 运行环境版本 3.11,配置文件完整。',
action: null,
diag: { '版本': '1.3.0', '运行环境': 'Python 3.11.4 (venv)', '安装方式': 'pip' }
},
library: {
state: 'ready', severity: 'ok',
reason: '已同步',
detail: '文献库已于 2 分钟前自动同步,共 247 篇论文,无新增。',
action: null,
diag: { '文献总数': '247', '同步时间': '2 分钟前', 'Zotero 连接': '正常' }
},
ocr: {
state: 'needs_action', severity: 'warning',
reason: '12 篇论文的衍生文件已过期',
detail: 'OCR 版本更新后,这些论文的解析结果需要重新生成,不会覆盖原始识别结果。',
action: { label: '重建衍生文件', verb: 'rebuild_derived', primary: true },
diag: { '待处理': '12 篇', '原始识别': '已完成', '衍生版本': '过期 (v2→v3)' }
},
memory: {
state: 'needs_action', severity: 'warning',
reason: '索引需要更新',
detail: '有 5 篇新论文已同步,全文索引和向量索引需要重建才能搜索到这些文献。',
action: { label: '重建索引', verb: 'rebuild_index', primary: true },
diag: { '待索引': '5 篇', '当前索引': '242 篇', '向量维度': '768' }
},
maintenance: {
state: 'needs_action', severity: 'warning',
reason: '2 个模块需要注意',
detail: 'OCR 衍生文件过期,记忆库索引需要更新。可点击各模块的按钮单独处理。',
action: null,
items: ['OCR: 12 篇衍生文件过期', '记忆: 5 篇待索引'],
diag: { '待处理': '2 项', '影响模块': 'OCR / 记忆' }
},
help: {
state: 'ready', severity: 'ok',
reason: '随时可用',
detail: '帮助文档已就绪,可以通过命令面板或右键菜单访问。',
action: null,
diag: { '文档版本': '2026-07', '文档数量': '12 篇', '最后更新': '2026-07-14' }
}
}
},
'active-ocr': {
primary: {
severity: 'warning',
title: 'OCR 正在处理 3/12 篇论文',
action: { label: '查看进度', verb: 'investigate' },
desc: 'OCR 批处理正在进行中。已处理 3 篇,预计剩余 4 分钟。同时记忆库索引需要更新。'
},
modules: {
installation: {
state: 'ready', severity: 'ok',
reason: '运行正常',
detail: 'PaperForge 已安装,运行环境版本 1.3.0。',
action: null,
diag: { '版本': '1.3.0', '运行环境': 'Python 3.11.4', '安装方式': 'pip' }
},
library: {
state: 'ready', severity: 'ok',
reason: '已同步',
detail: '文献库同步正常。',
action: null,
diag: { '文献总数': '247', '同步时间': '10 分钟前', 'Zotero 连接': '正常' }
},
ocr: {
state: 'needs_action', severity: 'warning',
reason: 'OCR 批处理进行中',
activity: { label: '处理中', progress: { current: 3, total: 12 } },
detail: '正在处理第 3 篇论文的 OCR 识别,预计剩余 4 分钟。',
action: { label: '查看详情', verb: 'investigate', primary: false },
diag: { '进度': '3/12', '已处理': '3 篇', '队列中': '9 篇', '模型': 'PaddleOCR v4' }
},
memory: {
state: 'needs_action', severity: 'warning',
reason: '索引需要更新',
detail: 'OCR 完成后需要更新全文索引和向量索引。',
action: { label: '稍后重建', verb: 'rebuild_index', primary: false },
diag: { '待索引': '5 篇', '当前索引': '242 篇' }
},
maintenance: {
state: 'needs_action', severity: 'warning',
reason: 'OCR 进行中,记忆库待索引',
detail: 'OCR 批处理运行中3/12。记忆库索引需要更新。各模块均可独立操作。',
action: null,
items: ['OCR: 批处理进行中', '记忆: 5 篇待索引'],
diag: { '待处理': '2 项', '影响模块': 'OCR / 记忆' }
},
help: {
state: 'ready', severity: 'ok',
reason: '随时可用',
detail: '帮助文档已就绪。',
action: null,
diag: { '文档版本': '2026-07', '文档数量': '12 篇' }
}
}
},
'stale': {
primary: {
severity: 'warning',
label: '缓存过期',
title: '部分模块状态已过期',
action: { label: '刷新过期模块', verb: 'probe' },
desc: 'OCR、文献库、记忆库的缓存快照已超过有效期。安装状态和帮助文档仍为最新有效期 3600 秒)。点击"刷新过期模块"重新检测。'
},
modules: {
installation: {
state: 'ready', severity: 'ok',
reason: '运行正常(缓存有效)',
detail: '安装配置缓存 3600 秒有效,上次检测 6 分钟前,仍在有效期内。',
action: null,
diag: { '版本': '1.3.0', '运行环境': 'Python 3.11.4', '有效期': '3600 秒', '下次过期': '54 分钟后' }
},
library: {
state: 'unknown', severity: 'unknown',
reason: '状态未知(缓存已过期)',
detail: '文献库缓存有效期 300 秒,上次检测 6 分钟前(已过期)。需要重新检测。',
action: { label: '检测', verb: 'probe', primary: false },
diag: { '状态': '未知', '上次检测': '6 分钟前', '有效期': '300 秒', '已超时': '3 分钟' }
},
ocr: {
state: 'unknown', severity: 'unknown',
reason: '状态未知(缓存已过期)',
detail: 'OCR 缓存有效期 60 秒,上次检测 6 分钟前(已过期)。需要重新检测。',
action: { label: '检测', verb: 'probe', primary: false },
diag: { '状态': '未知', '上次检测': '6 分钟前', '有效期': '60 秒', '已超时': '5 分钟' }
},
memory: {
state: 'unknown', severity: 'unknown',
reason: '状态未知(缓存已过期)',
detail: '记忆库缓存有效期 300 秒,上次检测 6 分钟前(已过期)。需要重新检测。',
action: { label: '检测', verb: 'probe', primary: false },
diag: { '状态': '未知', '上次检测': '6 分钟前', '有效期': '300 秒', '已超时': '3 分钟' }
},
maintenance: {
state: 'unknown', severity: 'unknown',
reason: '状态未知(依赖模块已过期)',
detail: '维护视图依赖的底层模块缓存已过期,无法投影有效状态。需要重新检测。',
action: { label: '检测', verb: 'probe', primary: false },
diag: { '状态': '未知', '依赖模块': 'OCR / 文献库 / 记忆', '缓存': '已过期' }
},
help: {
state: 'ready', severity: 'ok',
reason: '随时可用(缓存有效)',
detail: '帮助文档缓存 3600 秒有效,上次检测 6 分钟前,仍在有效期内。',
action: null,
diag: { '文档版本': '2026-07', '有效期': '3600 秒', '下次过期': '54 分钟后' }
}
}
},
'all-ready': {
primary: {
severity: 'ok',
title: '一切正常',
action: { label: '刷新状态', verb: 'probe' },
desc: '所有模块均已就绪。PaperForge 正常运行文献库已同步OCR 已就绪,记忆库已索引。'
},
modules: {
installation: {
state: 'ready', severity: 'ok',
reason: '运行正常',
detail: 'PaperForge 已安装并运行正常。',
action: null,
diag: { '版本': '1.3.0', '运行环境': 'Python 3.11.4', '安装方式': 'pip' }
},
library: {
state: 'ready', severity: 'ok',
reason: '已同步',
detail: '文献库同步正常,共 247 篇论文。',
action: null,
diag: { '文献总数': '247', '同步时间': '刚刚', 'Zotero 连接': '正常' }
},
ocr: {
state: 'ready', severity: 'ok',
reason: '已就绪',
detail: 'OCR 服务正常运行API 密钥有效。',
action: null,
diag: { 'API 状态': '有效', '可用额度': '充足', '模型版本': 'PaddleOCR v4' }
},
memory: {
state: 'ready', severity: 'ok',
reason: '已索引',
detail: '全文索引和向量索引均已就绪。',
action: null,
diag: { '已索引': '247 篇', '全文索引': '已构建', '向量索引': '已构建' }
},
maintenance: {
state: 'ready', severity: 'ok',
reason: '无待处理事项',
detail: '所有模块运行正常,无需处理。',
action: null,
items: [],
diag: { '待处理': '0 项', '最后检查': '刚刚' }
},
help: {
state: 'ready', severity: 'ok',
reason: '随时可用',
detail: '帮助文档已就绪。',
action: null,
diag: { '文档版本': '2026-07', '文档数量': '12 篇' }
}
}
}
};
// ===== Render =====
let currentScenario = 'first-run';
function showSnackbar(text) {
const el = document.getElementById('snackbar');
el.textContent = text;
el.classList.add('show');
setTimeout(() => el.classList.remove('show'), 2500);
}
function renderScenario(scenarioId) {
currentScenario = scenarioId;
const data = SCENARIOS[scenarioId];
const grid = document.getElementById('moduleGrid');
const zone = document.getElementById('primaryZone');
// Update active button
document.querySelectorAll('.scenario-btn').forEach(btn => {
const isActive = btn.dataset.scenario === scenarioId;
btn.classList.toggle('active', isActive);
btn.setAttribute('aria-current', isActive ? 'true' : 'false');
});
// Primary zone
const prim = data.primary;
zone.className = 'primary-zone';
if (prim.severity === 'error') zone.classList.add('error-primary');
else if (prim.severity === 'warning') zone.classList.add('warning-primary');
else if (prim.severity === 'ok') zone.classList.add('ok-primary');
else if (prim.severity === 'unknown') zone.classList.add('unknown-primary');
const primaryLabelEl = zone.querySelector('.primary-label');
if (prim.severity === 'ok') primaryLabelEl.textContent = '总览';
else if (prim.severity === 'unknown') primaryLabelEl.textContent = '状态未知';
else if (prim.label) primaryLabelEl.textContent = prim.label;
else primaryLabelEl.textContent = '需要注意';
document.getElementById('primaryTitle').textContent = prim.title;
document.getElementById('primaryDesc').textContent = prim.desc;
const primaryBtn = document.getElementById('primaryBtn');
primaryBtn.textContent = prim.action.label;
primaryBtn.dataset.verb = prim.action.verb;
primaryBtn.style.display = '';
// Module cards
grid.innerHTML = '';
MODULES.forEach(mod => {
const info = data.modules[mod.id];
if (!info) return;
const card = document.createElement('div');
card.className = 'module-card';
card.setAttribute('role', 'listitem');
// Header
const header = document.createElement('div');
header.className = 'module-card-header';
const nameEl = document.createElement('span');
nameEl.className = 'module-name';
nameEl.textContent = mod.label;
// Severity badge
const badge = document.createElement('span');
badge.className = `state-badge ${severityClass(info.severity)}`;
badge.innerHTML = `<span class="dot"></span>${stateLabel(info.state)}`;
if (info.state === 'ready') badge.title = '正常工作';
else if (info.state === 'unknown') badge.title = '缓存已过期,无法确定实际状态';
else if (info.state === 'needs_action') badge.title = '需要操作';
else if (info.state === 'missing_input') badge.title = '缺少配置';
else badge.title = info.reason;
header.appendChild(nameEl);
header.appendChild(badge);
card.appendChild(header);
// Reason
const reasonEl = document.createElement('p');
reasonEl.className = 'module-reason';
reasonEl.textContent = info.reason;
card.appendChild(reasonEl);
// Activity (if running)
if (info.activity) {
const act = document.createElement('div');
act.className = 'activity-row';
act.setAttribute('aria-label', info.activity.label);
const spinner = document.createElement('div');
spinner.className = 'spinner';
spinner.setAttribute('aria-hidden', 'true');
act.appendChild(spinner);
const label = document.createElement('span');
label.textContent = info.activity.label;
act.appendChild(label);
if (info.activity.progress) {
const bar = document.createElement('div');
bar.className = 'progress-bar';
bar.setAttribute('role', 'progressbar');
bar.setAttribute('aria-valuenow', info.activity.progress.current);
bar.setAttribute('aria-valuemin', '0');
bar.setAttribute('aria-valuemax', info.activity.progress.total);
const fill = document.createElement('div');
fill.className = 'progress-fill';
const pct = info.activity.progress.current / info.activity.progress.total * 100;
fill.style.width = pct + '%';
bar.appendChild(fill);
act.appendChild(bar);
const count = document.createElement('span');
count.textContent = `${info.activity.progress.current}/${info.activity.progress.total}`;
act.appendChild(count);
}
card.appendChild(act);
}
// Detail text
if (info.detail) {
const detailEl = document.createElement('p');
detailEl.className = 'module-reason';
detailEl.style.color = 'var(--body)';
detailEl.textContent = info.detail;
card.appendChild(detailEl);
}
// Maintenance items list
if (info.items && info.items.length > 0) {
const itemsList = document.createElement('ul');
itemsList.style.cssText = 'font-size:13px;color:var(--body);list-style:none;padding:0;margin:2px 0 0;';
info.items.forEach(item => {
const li = document.createElement('li');
li.textContent = '· ' + item;
itemsList.appendChild(li);
});
card.appendChild(itemsList);
}
// Action button
if (info.action) {
const btn = document.createElement('button');
btn.className = 'module-action-btn' + (info.action.primary ? ' primary-action' : '');
btn.textContent = info.action.label;
btn.dataset.verb = info.action.verb;
btn.dataset.module = mod.id;
btn.addEventListener('click', () => handleAction(info.action.verb, mod.label));
card.appendChild(btn);
}
// Advanced diagnostics toggle
if (info.diag) {
const toggle = document.createElement('button');
toggle.className = 'advanced-toggle';
toggle.textContent = '诊断信息';
toggle.setAttribute('aria-expanded', 'false');
toggle.addEventListener('click', () => {
const panel = card.querySelector('.advanced-details');
const expanded = panel.classList.toggle('open');
toggle.setAttribute('aria-expanded', expanded);
toggle.textContent = expanded ? '收起诊断信息' : '诊断信息';
});
card.appendChild(toggle);
const diagPanel = document.createElement('dl');
diagPanel.className = 'advanced-details';
Object.entries(info.diag).forEach(([key, val]) => {
const dt = document.createElement('dt');
dt.textContent = key;
const dd = document.createElement('dd');
dd.textContent = val;
diagPanel.appendChild(dt);
diagPanel.appendChild(dd);
});
card.appendChild(diagPanel);
}
grid.appendChild(card);
});
// Focus management: focus the primary action button on scenario change
primaryBtn.focus();
}
function severityClass(sev) {
if (sev === 'ok') return 'ok';
if (sev === 'warning') return 'warning';
if (sev === 'error') return 'error';
return 'unknown';
}
function stateLabel(state) {
const labels = {
'ready': '就绪',
'unknown': '未知',
'unavailable': '不可用',
'missing_input': '待配置',
'needs_action': '需处理',
'limited': '受限'
};
return labels[state] || state;
}
function handleAction(verb, moduleLabel) {
const messages = {
'setup': '正在启动安装流程...',
'set_config': '正在打开配置界面...',
'sync': '正在同步文献库...',
'rebuild_derived': '正在重建衍生文件...',
'rebuild_index': '正在重建索引...',
'redo': '需要确认:此操作将删除现有结果',
'restore_backup': '需要确认:将从备份恢复数据库',
'run': '正在处理...',
'migrate': '正在执行数据库迁移...',
'update': '正在检查更新...',
'investigate': '正在收集诊断信息...',
'probe': '正在检测状态...'
};
const msg = messages[verb] || `正在执行: ${verb}`;
showSnackbar(`${moduleLabel}: ${msg}`);
}
// ===== Event Wiring =====
document.addEventListener('DOMContentLoaded', () => {
// Scenario buttons
document.querySelectorAll('.scenario-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
renderScenario(e.currentTarget.dataset.scenario);
});
btn.addEventListener('keydown', (e) => {
const btns = Array.from(document.querySelectorAll('.scenario-btn'));
const idx = btns.indexOf(e.currentTarget);
let nextIdx = -1;
if (e.key === 'ArrowRight') nextIdx = (idx + 1) % btns.length;
else if (e.key === 'ArrowLeft') nextIdx = (idx - 1 + btns.length) % btns.length;
if (nextIdx >= 0) {
e.preventDefault();
btns[nextIdx].click();
btns[nextIdx].focus();
}
});
});
// Primary action
document.getElementById('primaryBtn').addEventListener('click', () => {
const data = SCENARIOS[currentScenario];
if (data) handleAction(data.primary.action.verb, 'PaperForge');
});
// Initial render
renderScenario('first-run');
});
</script>
</body>
</html>

View file

@ -0,0 +1,212 @@
# Issue #71 — Six-Module Control Center Prototype
**Date:** 2026-07-15
**Author:** ControlCenterPrototype
**Prototype:** `docs/prototypes/2026-07-14-six-module-control-center.html`
**Parent:** Wayfinder #65 — Make the PaperForge control center self-explanatory
**Inputs:** #69 capability state/action contract, #66 contract audit, #67 Obsidian patterns, #68 desktop runtime patterns, DESIGN.md
---
## 1. Design Overview
A first-time and returning-user control center for six modules: 安装 / 文献库 / OCR / 记忆 / 维护 / 帮助. The layout uses a two-zone structure:
- **Primary Attention Zone** (top): shows the single module needing most urgent attention, with its primary action as a dark pill button. Normal state says "一切正常" with a less prominent refresh action.
- **Module Grid** (3-column, responsive to 1-column at 768px): six independent cards, each showing module name, capability state badge, reason text, actionable details, optional progress/activity, a single action button (only when non-ready), and an expandable "诊断信息" diagnostic disclosure.
### Design language
Vercel-inspired grayscale palette from `DESIGN.md`: `#171717` ink, `#4d4d4d` body, `#888888` mute, `#ebebeb` hairline, `#ffffff` canvas, `#fafafa` canvas-soft, `#f5f5f5` canvas-soft-2. Green (`#22a67e`) for ready states uses the same chroma-to-value ratio as the existing warning (`#f5a623`) and error (`#ee0000`) signals, not a saturated green that would clash with the grayscale system. No emojis, no glassmorphism, no gradient text, no cyan-on-black.
---
## 2. State Model (from #69 capability contract)
Three orthogonal axes per module:
```
Availability x Activity x Attention
```
| Axis | Values | Notes |
|------|--------|-------|
| Availability | `unknown < unavailable < missing_input < needs_action < limited < ready` | Ordinal, worst-to-best |
| Activity | `idle` / `running` | Never masks availability badge |
| Attention | Derived from availability | One primary action per module, `null` when ready |
### Contract rules implemented in prototype
| Rule | Implementation |
|------|---------------|
| One primary action per module | Action button shown only when state is not `ready`. Button label maps to verb table ($\leq 12$ verbs). |
| Running never hides warning | Activity spinner + progress bar shown alongside the warning badge. OCR with `needs_action` + progress bar keeps yellow badge. |
| Stale → unknown by TTL | Short-TTL modules (60300s) render as `unknown` (grey) when TTL expired. Long-TTL modules (3600s: installation, help) remain `ready` when still within TTL. Never leaks last-known state. |
| Maintenance is derived view, no batch action | 维护 card has `action: null` in all scenarios. No batch "全部处理" or "run" button. Lists constituent module issues as inline bullets; users act on individual modules. |
| Primary zone exposes one concrete action | Hero action is the single highest-priority verb (e.g. `rebuild_derived` for OCR), never a generic "查看详情". |
| Plain buttons, not ARIA tabpanel | Scenario switcher uses `<button>` elements with `aria-current="true"`. No `role="tab"`, `role="tablist"`, or `role="tabpanel"`. Clicking a scenario changes the entire page state — inconsistent with tabpanel pattern. |
| Quiet ready states | `ready` modules show green "就绪" badge with no action button, just a brief reason text. |
| Advanced details without JSON | Expandable "诊断信息" section uses `dl`/`dt`/`dd` pairs, never raw JSON or Python traceback. |
---
## 3. Scenario State Definitions
### First Run (`first-run`)
| Module | State | Action |
|--------|-------|--------|
| 安装 | `missing_input` — 需要配置 | 开始安装 (primary) |
| 文献库 | `missing_input` — Zotero 导出路径未设置 | 配置路径 |
| OCR | `missing_input` — API 密钥未配置 | 配置密钥 |
| 记忆 | `missing_input` — 记忆库尚未创建 | 配置记忆库 |
| 维护 | `needs_action` — 4 个模块需要注意 | null (派生视图,无批量操作) |
| 帮助 | `ready` — 随时可用 | null |
Primary zone: "PaperForge 尚未安装" (warning, yellow left border).
### Partially Ready (`partial`)
| Module | State | Action |
|--------|-------|--------|
| 安装 | `ready` — 运行正常 | null |
| 文献库 | `ready` — 已同步 | null |
| OCR | `needs_action` — 12 篇论文衍生文件过期 | **重建衍生文件** (primary) |
| 记忆 | `needs_action` — 索引需要更新 | 重建索引 |
| 维护 | `needs_action` — 2 个模块需要注意 | null (派生视图,无批量操作) |
| 帮助 | `ready` — 随时可用 | null |
Primary zone: "OCR 处理结果已过期,记忆库索引需要更新" (warning, yellow left border). Hero action is **"重建衍生文件"** (`rebuild_derived`) — the single highest-priority concrete action. The description also notes the second item: memory index rebuild. Demonstrates **one concrete primary action** when multiple modules need attention.
### Stale Snapshot (`stale`)
Simulates a state where **short-TTL probes have expired** (library 300s, OCR 60s, memory 300s) while **long-TTL probes remain fresh** (installation 3600s, help 3600s). Last probe was 6 minutes ago.
| Module | State | Action |
|--------|-------|--------|
| 安装 | `ready` — 运行正常(缓存有效) | null |
| 文献库 | `unknown` — 状态未知(缓存已过期) | 检测 |
| OCR | `unknown` — 状态未知(缓存已过期) | 检测 |
| 记忆 | `unknown` — 状态未知(缓存已过期) | 检测 |
| 维护 | `unknown` — 状态未知(依赖模块已过期) | 检测 |
| 帮助 | `ready` — 随时可用(缓存有效) | null |
Primary zone: "部分模块状态已过期" (warning, yellow left border, label "缓存过期"). Cards: 2 green, 4 grey. Action is "刷新过期模块" (`probe`). Demonstrates **TTL-respecting fail-closed state**: fresh modules never go unknown when their TTL has not expired.
---
## 4. Interaction & UX Design
### Primary action per attention item (contract rule)
Each non-ready card has **exactly one action button**. The button label maps to a verb from the $12$-verb table. When the module is `ready`, no button appears. This satisfies the #69 invariant: `action.primary` is `null` when ready.
### Active operation feedback
When activity is `running`, the card shows:
- CSS spinner (animated border rotation, `--link` blue to signal process health)
- Activity label text ("处理中")
- Progress bar with `role="progressbar"` and `aria-valuenow`/`aria-valuemin`/`aria-valuemax`
- Fractional counter ("3/12")
- Badge remains visible (yellow warning badge coexists with spinner in "active-ocr" scenario)
### Advanced diagnostic disclosure
The "诊断信息" toggle expands a `dl` panel with key-value diagnostic data. No JSON, no Python error messages, no raw tracebacks. Example entries:
- "版本: 1.3.0"
- "运行环境: Python 3.11.4"
- "API 状态: 有效"
- "全文索引: 已构建"
- "待处理: 2 项"
The toggle is a text-only link (`<button>` styled as underlined text, `--link` blue) that says "诊断信息" when collapsed and "收起诊断信息" when expanded.
### Snackbar feedback
Every action button click shows a translucent snackbar at the bottom of the viewport with a Chinese message describing the action being taken ("PaperForge: 正在启动安装流程...", "OCR: 正在重建衍生文件..."). The snackbar auto-dismisses after 2.5 seconds.
### Keyboard navigation
- Scenario tabs are `role="tablist"` with arrow key navigation (Left/Right arrow switches tab)
- All buttons are natively focusable `<button>` elements
- Focus moves to the primary action button on scenario switch
- `:focus-visible` uses `--link` blue outline (`#0070f3`) at 2px with offset
- Diagnostics toggle reports `aria-expanded` state
### Responsive design
- Default: 3-column grid
- `< 900px`: 2-column grid
- `< 768px`: single-column grid (verified: `720px` card width within `768px` viewport, no overflow, no clipped controls)
- All controls visible and functional at 768px (verified: 17 buttons, 0 clipped)
---
## 5. Adopt/Reject Decisions
| Decision | Adopt/Reject | Rationale |
|----------|-------------|-----------|
| Primary zone with one dominant module | **Adopt** | Follows #69 "one primary action per attention" and #68 "per-module actionable maintenance, never global OK" pattern (VS Code, Docker Desktop). |
| Unknown state as solid grey, never last-known color | **Adopt** | Directly from #69 contract: "Stale is never ready". Grey conveys "no information" without implying readiness or failure. |
| Expandable diagnostics without JSON | **Adopt** | Does not expose XML/JSON structure; uses native `dl`/`dt`/`dd` for definition list. From #68 "local diagnostic export with user review before issue creation" (VS Code pattern). |
| Chinese-first copy with English only behind disclosure | **Adopt** | Target audience reads zh-CN. Diagnostic panel uses same Chinese labels for field names; only version identifiers and technical codes use English when unavoidable ("Python 3.11.4", "PaddleOCR v4"). |
| Maintenance as derived view with item list | **Adopt** | From #69: maintenance aggregates non-ready states; has `items[]` array referencing constituent modules. Shown as bullet list in "首次运行" and "部分就绪". |
| No emoji module icons | **Adopt** | Contract says no emojis. Used monochrome geometric symbols (`◐`, `▤`, `◇`, `△`, `⚙`) as CSS content instead. |
| Scenario switches as styled pills | **Adopt** | Follows Vercel muted pill tab pattern from DESIGN.md (`.tab-ghost`). Active state inverts to solid `--ink` background. |
| Action buttons: dark pill for primary, outlined for secondary | **Adopt** | Matches `button-primary` / `button-secondary` from DESIGN.md. Primary scenario action uses solid `--ink`; module-level primary actions also solid; secondary actions use outlined style. |
| Activity spinner in CSS only (no JS animation frame) | **Adopt** | Simpler, lower energy, no JS dependency. Uses `@keyframes spin` with `border-top-color` trick. |
| Progress bar with ARIA progressbar role | **Adopt** | Accessibility requirement; screen readers get exact numeric progress. |
| Snackbar for action feedback | **Adopt** | From #67 Obsidian Notice pattern: transient message at bottom of viewport with 2.5s auto-dismiss. Used instead of a true Notice to stay in vanilla HTML/JS. |
| Focus management on scenario switch | **Adopt** | MVP of #67 "`checkCallback` probe + re-evaluate" pattern: switching scenario focuses the primary action button so keyboard users don't lose context. |
| No error state (red badge) in current demo | **Reject** (deferred) | The `unavailable` state with `error` severity exists in the contract but is not exercised by the five demo scenarios. A future prototype or #72 maintenance view should demonstrate `unavailable` (e.g., "Python runtime not found"). |
| Maintenance item list as inline bullets | **Adopt** | Clearer than links or hidden details. Each item shows module name + issue type ("OCR: 12 篇衍生文件过期"). Unobtrusive at `13px` body color. |
| TTL values hardcoded in diagnostic text | **Adopt** | Matches #69 note: "actual TTLs should be calibrated during the #71 prototype phase". Each `unknown` card shows both `lastProbe` and `ttl` as plain text under diagnostics. This makes TTL calibration observable. |
| Plain buttons instead of ARIA tabpanel | **Adopt** (after review) | Original used `role="tablist"`/`role="tab"`/`aria-selected` with keyboard arrow navigation. Changed to plain `<button>` with `aria-current` because clicking a scenario changes the entire page state — inconsistent with the tabpanel pattern where each tab reveals a panel with `aria-labelledby`. Arrow key navigation preserved. |
| Maintenance card has no action button | **Adopt** (after review) | Removed all maintenance card `action` fields (were `probe` or `run` in various scenarios). Maintenance is a derived view per #69 contract — no independent action. Users act on individual modules. Item list shows constituent issues. |
| Stale scenario preserves long-TTL modules as ready | **Adopt** (after review) | Original stale scenario set all modules to `unknown`. Fixed: installation and help (3600s TTL) remain `ready` because 6 minutes < 60 minutes. Library/OCR/memory (60300s TTL) render as `unknown`. Maintenance depends on the expired modules so also `unknown`. |
| Partial hero shows concrete action, not generic | **Adopt** (after review) | Primary zone action changed from `investigate` ("查看详情") to `rebuild_derived` ("重建衍生文件"). The priority algorithm selects OCR's action as the most urgent concrete verb across all non-ready modules. |
---
| Test | Result | Detail |
|------|--------|--------|
| First-run scenario | Pass | Primary: "PaperForge 尚未安装". Badges: 1 ready, 5 warning. Maintenance has no action button. |
| Partial scenario | Pass | Primary: "OCR 处理结果已过期,记忆库索引需要更新". Hero action: "重建衍生文件". Badges: 3 ready, 3 warning. Maintenance has no batch action. |
| Active OCR scenario | Pass | Primary: "OCR 正在处理 3/12 篇论文". OCR card has spinner + progress bar. Warning badge visible despite running activity. Maintenance has no action. |
| Stale scenario | Pass | Primary: "部分模块状态已过期" with label "缓存过期". Badges: 2 ready (install+help), 4 unknown (library/OCR/memory/maintenance). Install/help preserved as ready under 3600s TTL. |
| All-ready scenario | Pass | Primary: "一切正常". All 6 badges: ready (green). No action buttons on any card. |
| Diagnostic toggle | Pass | Click opens `dl` panel, toggles label to "收起诊断信息", sets `aria-expanded="true"`. Multiple panels open simultaneously. |
| Action button feedback | Pass | Each action click shows snackbar with module-specific Chinese message. Primary: "PaperForge: 正在启动安装流程...". OCR: "OCR: 正在重建衍生文件...". |
| 768px responsive | Pass | Single column grid (720px cards within 768px viewport). No overflow. All 16 buttons visible and unclipped. Diagnostics fits within card width. |
| Keyboard navigation | Pass | Arrow keys switch scenario buttons. Focus moves to primary action on scenario switch. |
| Plain buttons (no ARIA tab role) | Pass | No `role="tab"`, `role="tablist"`, or `role="tabpanel"`. Active button uses `aria-current="true"`. Arrow key navigation preserved via JS. |
| No external resources | Pass | Zero network requests. System font stack. No external CSS/JS. |
| 768px responsive | Pass | Single column grid ($720\text{ px}$ cards within $768\text{ px}$ viewport). No overflow. All 16 buttons visible and unclipped. Diagnostics fits within card width. Keyboard focusable at this width. |
| Keyboard tab navigation | Pass | Arrow keys switch scenario buttons. Focus moves to primary action on scenario switch. |
---
## 7. Relationship to Issue #72
This prototype is scoped to #71 (control center information hierarchy and interaction model). It does not implement:
- Actionable stale/failed/corrupt work filtering (maintenance inbox)
- Successful update disappearance
- Privacy-safe GitHub Issue draft generation and review
These are the #72 scope and will be addressed in a separate prototype (`docs/prototypes/2026-07-14-maintenance-inbox.html`).
The two prototypes share:
- Same CSS token set and design language
- Same capability state model (`unknown/unavailable/missing_input/needs_action/limited/ready`)
- Same action verb table (`setup/set_config/sync/rebuild_derived/rebuild_index/probe/investigate/run/migrate/update/restore_backup/redo`)
- Same "stale is never ready" fail-closed contract
---
## 8. Open Questions for #79 Design Review
1. **TTL calibration**: Current values (603600s, from contract) are placeholders. Should installation really have a 3600s TTL if config changes at runtime are rare but the consequences of stale config are high?
2. **Error state (`unavailable`, red)**: Not exercised in any scenario. Should a "breakage" scenario be added showing a missing runtime or corrupt database?
3. **Action confirmation**: Destructive actions (`redo`, `restore_backup`) need a confirmation modal in production that this prototype does not mock.
4. **First-run vs Returning-user differentiation**: The "首次运行" scenario shows all missing input. How does the system distinguish true first-run (never probed) from all-modules-unknown (TTL expired)? The contract says `unknown` for both; the banner label could differ.
5. **Primary zone priority algorithm**: Currently hardcoded per scenario. The production system needs a deterministic algorithm: worst severity first, then earliest `updated_at`, then alphabetical by module id.
6. **Help module probes**: The contract TTL is 3600s, but what does probing help actually check? File existence? File content integrity? Version match with plugin?

View file

@ -1,6 +1,6 @@
# OCR-v2 Active Queue
> Status: OCR-v2 is stable; OCR maintenance streaming with canonical per-row actions is implemented; the capability vocabulary and managed-runtime architecture are chosen.
> Last updated: 2026-07-14
> Status: OCR-v2 is stable; OCR maintenance streaming with canonical per-row actions is implemented; the capability vocabulary, managed-runtime architecture, control-center prototype (#71), and maintenance inbox prototype (#72) are all resolved.
> Last updated: 2026-07-15
## Current checkpoint
@ -18,6 +18,9 @@
- **[#69](https://github.com/LLLin000/PaperForge/issues/69) resolved** at `issuecomment-4971161072`: orthogonal availability/activity/attention axes, 6-state capability ordinal, 12 canonical verbs, backend-owned severity and primary actions, maintenance projection.
- **[#70](https://github.com/LLLin000/PaperForge/issues/70) resolved** at `issuecomment-4971239398`: plugin-managed immutable runtime slots, system-Python bootstrap with validated-triplet fallback, single `active-runtime.json` pointer, `ManagedRuntime` class with `current()`/`status()`/`ensure()`, fail-closed command resolution.
- **[#71](https://github.com/LLLin000/PaperForge/issues/71) resolved**: six-module control-center HTML prototype with 5 scenarios, plain-button switcher, primary attention zone, responsive layout (768px breakpoint), and capability-gated actions. Independent Critical/Important PASS review. Design decisions recorded in `docs/prototypes/2026-07-14-six-module-control-center.{html,md}`.
- **[#72](https://github.com/LLLin000/PaperForge/issues/72) resolved**: actionable-only maintenance inbox prototype with single-action rows, inline issue-draft review, local redacted export, and confirmation-first report flow. Independent Critical/Important PASS review. Design decisions recorded in `docs/prototypes/2026-07-14-maintenance-issue-reporting.{html,md}`.
## Verification status
- Focused Python OCR paths: **99 passed, 1 Windows SIGINT test skipped, 1 unrelated empty-result regression deselected**.
@ -25,13 +28,17 @@
- Maintenance regression tests: **19/19 passed** (canonical action routing, confirmation gate, cache manifest preservation).
- Live Obsidian verification: PaperForge 1.5.15 loaded without captured errors; maintenance rendered **734 All** rows and **700 Recommended** rows from the canonical backend flag.
- Live progress-state harness showed the floating progress bar, current key, Stop control, and disabled row actions.
- Prototype #71 (control center): **Critical PASS (5/5), Important PASS (11/11)** — independent reviewer dimensions confirmed.
- Prototype #72 (maintenance inbox): **Critical PASS (4/4), Important PASS (6/6)** — independent reviewer dimensions confirmed.
- Both prototypes browser-verified at 768px viewport with scenario-switching, action-button interactions, expand/collapse diagnostics, and issue-draft flow.
- No production plugin code was modified during prototype work.
- The repository-wide Python suite remains blocked during collection by the pre-existing `test_pr9a_resume_rebuild.py` import of removed `_assert_collections_healthy`.
## Frontier
- [ ] Prototype the six-module control center ([#71](https://github.com/LLLin000/PaperForge/issues/71)) — design information hierarchy and interaction model for 安装 / 文献库 / OCR / 记忆 / 维护 / 帮助.
- [ ] Design actionable-only maintenance inbox with user-reviewed GitHub Issue draft reporting ([#72](https://github.com/LLLin000/PaperForge/issues/72)).
- [ ] Integrate the accepted model ([#73](https://github.com/LLLin000/PaperForge/issues/73)).
- [x] Prototype the six-module control center ([#71](https://github.com/LLLin000/PaperForge/issues/71)) — design information hierarchy and interaction model for 安装 / 文献库 / OCR / 记忆 / 维护 / 帮助.
- [x] Design actionable-only maintenance inbox with user-reviewed GitHub Issue draft reporting ([#72](https://github.com/LLLin000/PaperForge/issues/72)).
- [ ] Integrate the accepted prototype model into a production control-center plugin ([#73](https://github.com/LLLin000/PaperForge/issues/73)).
- [ ] Keep routine OCR quality outside maintenance; successful updates leave the queue, while unacceptable results use a user-reviewed GitHub Issue draft.
## Deferred