Files
Void-Homelab/docs/plan-2-complete.md
root fa47419cbd docs: Plan 2 completion summary
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>
2026-06-01 02:27:58 +10:00

89 lines
6.3 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 (T1T8)
- `/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 by `ENABLE_RESYNC`)
### Phase B — Auth + capability (T9T11)
- `agentOrOwner` bearer middleware replaces the old owner-only `/api` gate.
- `requireWrite` middleware tiers writes through `canAct`: `allow` → through, `suggest``divertToPending` (202 + pending row), else 403.
- `/api/agents` (owner-only) + agent token mint (plaintext returned once) + revoke.
### Phase C — Cross-cutting (T12T15)
- `/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) with `applyPendingChange` dispatch table covering page/project/task/ref/resource/source_doc × create/update/delete. Approve and reject emit explicit `approve`/`reject` audit 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 a `kind` discriminator. `ts_rank` order. Messages branch excluded when a space filter is set (messages have no `space_id`).
- Vector + RRF deferred to Plan 3 (TODO in `lib/db/repos/search.js`).
### Phase E — Void UI shell (T17T22)
- 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.js` safe DOM builders. **Invariant**: no component sets `innerHTML` from API data; the explicit, scary-named `html:` opt-in exists only for sanitized output (markdown preview using `marked` + DOMPurify).
- `public/state.js` tiny 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 in `href`/`src`** flagged on `reference.js`. Fix: `safeHref()` in `dom.js` enforces `http(s)` / `mailto`. Applied to reference media block, reference source_url anchor, and resource URL anchor. (`8ae9bce`)
- **HIGH — innerHTML in `api.js` modal** flagged repeatedly during the build. Mitigated upstream by introducing `dom.js` `el()` / `mount()` safe builders; the modal was refactored to use them. The only remaining `innerHTML` write is in `markdown_editor.js` and is guarded by `DOMPurify.sanitize(marked.parse(...))`.
- **HIGH — cross-tenant disclosure on `/api/search`** and 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 in `docs/security-followups.md`.
## Known follow-ups (deferred)
- `pending_changes.action` CHECK blocks `'upsert'` / `'add_dependency'` / `'remove_dependency'` from `divertToPending` in refs/resources routes. Latent — only fires for an agent at suggest tier on those specific endpoints. Three mitigation options documented in `docs/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
```bash
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-workers` Python service.
- **Plan 5**: companion chat in right rail.
- **Plan 6**: Sacred Valley widgets ported from Void 1.x.