[dashboards] Wiki surface in dashboard-pro - #57
Merged
Conversation
The wiki surface needs a typed client and feature-detection before any UI can render. Mirror the CRM pattern so the two optional surfaces stay symmetric and a brain without the wiki schema degrades the same way. - lib/types.ts: WikiPage/WikiSection and response types with keys matching the /wiki REST contract literally, so the UI can't drift from the gateway. - lib/api.ts: wikiAvailable() probes GET /wiki/pages (never throws, like crmAvailable) plus typed wrappers for every /wiki route. Page slugs are arbitrary user text, so they are percent-encoded into every path segment. - lib/auth.ts: cache wikiEnabled on the session; tolerate undefined on cookies minted before the field existed. - app/login: probe the wiki surface once beside the crm probe, never fatal. - Add react-markdown (renderer arrives in a later commit).
The browser must never hold the brain key, so every wiki mutation proxies through a server route that injects x-brain-key. Each handler runs requireSession() BEFORE parsing the body (house rule) so an unauthed request gets 401, not a 400 from validation, and never reaches body parse. One handler per mutation: page create (POST), section write (PUT), and per-section accept-pending / reject-pending / lock plus page archive (DELETE). Inputs are validated to the contract (allowed page_kind, boolean locked, integer display_order, non-empty required strings) so bad values fail fast here instead of as a 500 from a DB constraint. Upstream error bodies are logged server-side and never forwarded to the client — only a safe generic message and the passed-through status code.
The wiki's whole point is regeneration that never stomps human edits, so the UI has to make section ownership and the machine-vs-human handshake legible. - /wiki: page list with kind chips, section counts, pagination (the /contacts idiom), and a "new page" form. When the schema is absent — wikiEnabled===false or GET /wiki/pages 404s — it renders a one-paragraph inline notice instead of an error. A later PR swaps this for a shared setup-state component, so it is kept minimal and self-contained. - /wiki/[slug]: sections in display order, each a panel with an origin chip (yours / generated), lock toggle, evidence chip linking supporting thoughts, inline edit (which takes ownership), and archive with confirm. When a machine writer proposes an update to a human-owned section, an amber review panel above the body diffs current vs proposed with accept / reject — mirroring the CRM proposal accept/reject flow. - MarkdownBody: section bodies are agent-writable over MCP, so they are untrusted. Rendered with react-markdown and NO rehype-raw, so embedded HTML is shown as text and never executed — the XSS boundary. - README: document the optional wiki surface and its degraded empty-state. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adversarial review follow-ups (FIX-FIRST verdict):
- Evidence chips are inert text, not links: /thoughts/[id] parseInt()s
its param, so a digit-leading UUID silently opens an unrelated numeric
thought (parseInt('3fa85f64-...') === 3). Mirrors FieldEvidence's inert
rendering; linking awaits a UUID-capable thought route. Full UUID shown
on hover; README claim updated to match.
- Accept/Reject race: call() now returns the parsed body and the handlers
branch on action. 'no_pending' (draft resolved in another tab or by
another agent) surfaces a distinct notice alongside the refresh instead
of pretending this click decided it. Errors from lock and accept/reject
now render in a header strip so they are visible even while a pending
panel is present (previously gated behind !hasPending).
- Removed the UI-only freeze on archived pages: the gateway deliberately
allows section writes regardless of page status (wiki_write_section has
no page-status check), and freezing created a one-way door of frozen
pending drafts and unlockable locks with no unarchive route. Archive
button stays active-only; badge and banner stay. README notes archived
pages remain fetchable and editable by slug.
- generationSummary() sanitizes agent-controlled labels before display
(strip control + bidi chars, trim, 80-char cap) since they render
beside the Accept/Reject decision.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
This was referenced Jul 4, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Contribution Type
/dashboards)What does this do?
Adds the wiki surface to Dashboard Pro:
/wiki(page list with kind filters, section counts, new-page form) and/wiki/[slug](sections rendered as sanitized markdown with per-section edit, lock/unlock, evidence chips, and the pending-draft review panel — machine proposes, human decides). Sixapp/api/wiki/*route handlers proxy mutations to the REST gateway withrequireSession()before body parse;wikiAvailable()probes at login and cachessession.wikiEnabledbeside the CRM flag. Pages are URL-reachable; nav entries land in the follow-up default-visible-nav PR.Merge order: after #56 (needs the
/wikigateway routes at runtime; gate checks are independent).Requirements
schemas/wiki-pages(+ [schemas] Add wiki_reject_pending RPC to wiki-pages #55 for reject-pending) and the [integrations] Add /wiki routes to open-brain-rest #56 gateway routes. Without them,/wikirenders a minimal setup notice instead of erroring.react-markdown@10with norehype-raw— embedded HTML renders as text and the default urlTransform stripsjavascript:/data:link protocols. Verified with a 23-payload static-render XSS suite (script/img/svg injection, protocol variants, reference links, autolinks, title-attribute breakout).Review pass (fixes included)
/thoughts/{id}: that route currently parses ids withparseInt, which truncates digit-leading UUIDs to a small integer and would silently open the wrong thought. Linking returns once the thoughts route accepts UUIDs.action: a concurrent resolution elsewhere (no_pending) surfaces "This draft was already resolved elsewhere — refreshed." instead of masquerading as success.wiki_write_sectionand wiki-mcp, which have no page-status checks) — archived is a list-visibility state, not a freeze; there is no unarchive route yet.generation_sourcestrings are control/bidi-stripped and length-capped before rendering next to the Accept/Reject decision.Manual verification
npm run lint0 errors;npm run buildcompiles with all routes present; runtime smoke via dev server: unauthenticated mutations 401 before body parse, pages redirect to/login; XSS suite as above; sanitizer behavior test (bidi/control stripping, 80-char cap, non-ASCII preserved). Live-brain browser E2E lands with the integration pass once the gateway PRs are deployed.Checklist
README.mdwith prerequisites, step-by-step instructions, and expected outcomemetadata.jsonhas all required fields🤖 Generated with Claude Code