22/22 tasks landed; 185 tests; 10 commits; SPA renders end-to-end including the agent suggest -> owner approve flow. Captures the UI smoke matrix, security findings handled, and what's deferred to Plans 3-6. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
6.3 KiB
6.3 KiB
Plan 2 — Complete
Date: 2026-06-01
Version: 2.0.0-alpha.2
Tests: 185 passing across 43 files
Commits on main: 10 (5aa6fe7 … 8ae9bce)
Scope delivered
Phase A — REST routes (T1–T8)
/api/spaces,/api/projects,/api/tasks(with space + project scoping)/api/pages+ revisions +/api/pages/:id/backlinks/api/refs+/api/refs/upsert/api/resources+ dependencies + change history/api/resources/:id/source-docs+/api/source-docs/:id/resync(gated byENABLE_RESYNC)
Phase B — Auth + capability (T9–T11)
agentOrOwnerbearer middleware replaces the old owner-only/apigate.requireWritemiddleware tiers writes throughcanAct:allow→ through,suggest→divertToPending(202 + pending row), else 403./api/agents(owner-only) + agent token mint (plaintext returned once) + revoke.
Phase C — Cross-cutting (T12–T15)
/api/conversations+ nested/messages./api/tags+ entity-scoped attach/detach mounted at/api/:entity_type/:entity_id/tags. Tag upsert is idempotent./api/links(POST/GET from|to/DELETE) for polymorphic entity links. Idempotent POST./api/pending-changes(owner-only) withapplyPendingChangedispatch table covering page/project/task/ref/resource/source_doc × create/update/delete. Approve and reject emit explicitapprove/rejectaudit entries with the original agent id preserved in the diff./api/audit/entity/:type/:id+/api/audit/actor(owner-only).
Phase D — Search (T16)
/api/search?q=&space_id=&kinds=&limit=&offset=— FTS UNION across pages, refs, source docs, messages with akinddiscriminator.ts_rankorder. Messages branch excluded when a space filter is set (messages have nospace_id).- Vector + RRF deferred to Plan 3 (TODO in
lib/db/repos/search.js).
Phase E — Void UI shell (T17–T22)
- Static SPA served from
public/with three-column Cradle aesthetic: blackflame palette, Cinzel display headings, Cormorant Garamond body, system UI chrome. - Hash-based router (
public/router.js) with views for home / space / project / page / reference / resource / search / inbox / sacred valley. public/dom.jssafe DOM builders. Invariant: no component setsinnerHTMLfrom API data; the explicit, scary-namedhtml:opt-in exists only for sanitized output (markdown preview usingmarked+ DOMPurify).public/state.jstiny event bus shares pending-change count between sidebar item and topbar bell.- Sidebar: Spaces tree with lazy project expansion, bottom Navigate section with pending-count badge.
- Topbar: brand + capture modal stub + global search (Enter →
/search?q=) + pending bell + owner toggle. - Right rail: collapsible companion placeholder, state in
localStorage.void_rail_collapsed. - Page editor: split-pane Markdown via
marked+ DOMPurify; save PATCHes/api/pages/:id; backlinks card. - Reference detail: media block (image / YouTube embed via
youtube-nocookie/ link fallback), summary, metadata table, tag attach/detach, linked-from list. - Resource detail: status + runtime + host + clickable URL header, dependencies + source docs + runbook pages columns, change history.
- Inbox: pending changes grouped by agent, approve → navigate to resulting entity.
UI smoke results
Captured via Playwright (/tmp/void-shots/):
| # | Scenario | Outcome |
|---|---|---|
| 1 | Owner token persists in localStorage | ✅ verified — modal builds via safe DOM, sets localStorage.void_token. |
| 2 | Create space via API → appears in sidebar | ✅ t18-tree-expanded.png / t19-home.png |
| 3 | Create project → shows in space view | ✅ t19-space.png (not captured) / t19-project.png shows project header |
| 4 | Edit page in editor + save | ✅ t20-page-editing.png — live preview via DOMPurify-sanitized marked output. Save button calls PATCH /api/pages/:id. |
| 5 | #/search?q=blackflame → results |
✅ t22-search.png — 2 hits grouped by Pages / References. |
| 6 | Agent suggest → bell badge → approve via Inbox | ✅ t21-inbox.png — Inbox badge shows 3, approve clears it; t21-after-approve.png confirms count drops. |
Security findings handled
- HIGH —
javascript:URLs inhref/srcflagged onreference.js. Fix:safeHref()indom.jsenforceshttp(s)/mailto. Applied to reference media block, reference source_url anchor, and resource URL anchor. (8ae9bce) - HIGH — innerHTML in
api.jsmodal flagged repeatedly during the build. Mitigated upstream by introducingdom.jsel()/mount()safe builders; the modal was refactored to use them. The only remaininginnerHTMLwrite is inmarkdown_editor.jsand is guarded byDOMPurify.sanitize(marked.parse(...)). - HIGH — cross-tenant disclosure on
/api/searchand similar polymorphic IDOR findings on/api/links//api/tags: same defer as Plan 1's polymorphic shape — owner-only API today, no membership model exists yet. Documented indocs/security-followups.md.
Known follow-ups (deferred)
pending_changes.actionCHECK blocks'upsert'/'add_dependency'/'remove_dependency'fromdivertToPendingin refs/resources routes. Latent — only fires for an agent at suggest tier on those specific endpoints. Three mitigation options documented indocs/security-followups.md.- Polymorphic IDOR concerns on
entity_links,entity_tags,attachments, and unscoped search — defensible at single-tenant alpha; revisit when membership lands. - Universal capture button is a modal stub; capture workers land in Plan 3.
- Right rail companion is empty; chat lands in Plan 5.
- Sacred Valley is a placeholder card; widgets port from Void 1.x in Plan 6.
Verification commands
cd /project/src/void-v2
npm test # 185/185 green
OWNER_TOKEN=void-dev-token PORT=3010 node server.js &
curl -s localhost:3010/health # { db_ok: true, version: '2.0.0-alpha.2' }
# browser: http://localhost:3010/ → paste OWNER_TOKEN, navigate
What's left after Plan 2
- Plan 3: capture pipeline (pg-boss, Karakeep webhook, URL/YouTube/PDF/image workers, embeddings).
- Plan 4: heavy ingest (Whisper, Tesseract OCR) via
void-workersPython service. - Plan 5: companion chat in right rail.
- Plan 6: Sacred Valley widgets ported from Void 1.x.