Reinterpreted from the plan's YAGNI 'Scale-out' (OpenSearch/object-storage /multi-tenant deferred at KB scale). Phase 4 = make the Phase 3 async + revision system correct, secure, observable, deployable. - T1 (correctness bug): restoreRevision now rebuilds semantic chunks via new @mcpedia/core reindexChunks(slug) so semantic/hybrid search stay consistent after a restore (previously document_chunks held the NEW body while documents.body held the restored OLD body -> stale search). - T2 (security): /hooks/* git-sync webhooks now require x-webhook-secret header matching WEBHOOK_SECRET (401 otherwise); API fails fast at startup if unset. Added WEBHOOK_SECRET to @mcpedia/config + .env.example; set real secret in .env. - T3 (UX): web doc page shows a History panel (revision no/reason/date/length) with per-revision Restore; app/api/revisions/restore/route.ts calls restoreRevision + revalidatePath (server-component only, no client JS). - T4: listRevisions gains offset paging; summary never includes body. - T5 (ops): deploy/mcpedia-api.service + deploy/mcpedia-worker.service systemd units (Restart=on-failure, EnvironmentFile=.env). Not auto-enabled on host. Verified against live imrnes Redis + Postgres: turbo typecheck+build green; restore-rebuilds-chunks (marker present -> gone after restore); webhook 401/200; web restore route redirects to doc + reverts body; revisions API returns summary (no body); systemd-analyze verify passes.
5.7 KiB
MCPedia Phase 4 — Operability & Correctness Hardening
Reinterpretation: the PHASES.md "Scale-out" items (OpenSearch, object storage, multi-tenant, distributed workers) are YAGNI at KB scale (4 docs). Phase 4 = make the Phase 3 async + revision machinery correct, secure, observable, and deployable — not speculative infra. Each task below fixes a real gap found by reading the code, not a hypothetical need.
Tasks
T1 — restoreRevision must rebuild semantic chunks (CORRECTNESS BUG)
Root cause: packages/core/src/revision.service.ts restoreRevision writes
the old body back into documents but never calls indexChunks(slug, body).
So after a restore, keyword search (FTS on documents.body) is correct but
document_chunks/embeddings stay on the new body → semantic + hybrid search
return stale/ghost chunks.
Fix:
- Add
reindexChunks(slug)to@mcpedia/corethat re-runsindexChunks(slug, body)using the livedocuments.body(the new body after the update). - Call it inside
restoreRevisionafter thedocumentsupdate (wrap in try/catch likeindexContentFileso embed failure doesn't abort the restore). - Add a unit-style assertion to the MCP smoke test or a small script: restore →
document_chunkscount matches re-chunked body.
Files: packages/core/src/revision.service.ts, packages/core/src/index.ts,
packages/core/src/document.service.ts (export existing indexChunks if needed).
T2 — Secure the git-sync webhook (SECURITY)
Root cause: apps/api/src/index.ts /hooks/reindex and /hooks/index accept
any request with no WEBHOOK_SECRET check — .env.example defines WEBHOOK_SECRET
but the router never reads it.
Fix:
- In
apps/api/src/index.ts, comparec.req.header("x-webhook-secret")(or?secret=) againstWEBHOOK_SECRET(from@mcpedia/config). If unset/mismatch →401. IfWEBHOOK_SECRETenv is empty, reject at startup with a clear log (fail-fast, don't run an open endpoint). - Add
WEBHOOK_SECRETtopackages/config/src/index.tsexport. - Document the header in README + verify with curl (401 without secret, 200 with).
Files: apps/api/src/index.ts, packages/config/src/index.ts, README.
T3 — Web UI revisions view (UX)
Root cause: Web UI (server components) calls @mcpedia/core directly; there is
no revisions surface even though revisions/restoreRevision tRPC + MCP resource
exist.
Fix (server-component only, no client JS):
- On the doc page (
apps/web/app/[section]/[...slug]/page.tsx), fetchlistRevisions(fullSlug, 10)and render a "History" panel: revision number, reason, createdAt, body length, and a/api/revisions/restorelink/POST that calls the tRPCrestoreRevisionmutation via a server action or a form POST to a small route handler. Simplest: a<form method="post" action="/api/revisions/restore">with hiddenid+ a route handler inapps/webcallingrestoreRevision. Keep it read-mostly; restore is a deliberate action. - Add
apps/web/app/api/revisions/restore/route.ts(POST) →restoreRevision(id)→revalidatePaththe doc.
Files: apps/web/app/[section]/[...slug]/page.tsx,
apps/web/app/api/revisions/restore/route.ts.
T4 — Paginate listRevisions / revisions API (PERF)
Root cause: revision.service.ts listRevisions does select length(body)
(fine) but the tRPC revisions and MCP resource return full revision rows
including the body in some callers; list endpoints should never carry bodies.
Fix:
- Ensure
listRevisionssummary excludesbody(it already does —bodyLengthonly). Addoffsetparam for paging. Confirm MCP resource uses the summary. - No behavior change for the doc page (uses summary).
Files: packages/core/src/revision.service.ts (add offset), router unchanged.
T5 — Deployable as supervised services (OPS)
Root cause: apps/api and apps/worker run only ad-hoc; the host already runs
zeavis via Nix/systemd. Phase-4 operability = provide a systemd unit (or Nix
service) so mcpedia-api + mcpedia-worker start on boot and restart on failure.
Fix (Nix-first, per MEMORY):
- Write
mcpedia-api.service+mcpedia-worker.servicesystemd unit files underdeploy/(bun run api / bun run worker,WorkingDirectory,Restart=on-failure,EnvironmentFilepointing at.env,After=network-online.target). - README section "Run as a service" with
cp deploy/*.service /etc/systemd/system && systemctl daemon-reload && systemctl enable --now mcpedia-api mcpedia-worker. - Do NOT
systemctlon the host without user confirmation (changing live services). Provide the files + instructions only; user runs enable.
Files: deploy/mcpedia-api.service, deploy/mcpedia-worker.service, README.
Verification (all real, against imrnes Redis + Postgres)
turbo run typecheck+turbo run buildgreen.- T1: script — edit a doc, reindex (new revision + new chunks), restore rev #1,
assert
document_chunkscount for that slug now matches re-chunk of rev #1 body andsemanticSearchon a term unique to rev #1 returns it. - T2:
curl -XPOST localhost:4020/hooks/reindex→ 401; with-H "x-webhook-secret: $WEBHOOK_SECRET"→ 200 + jobId. - T3:
next buildincludes the History panel; restore form rebuilds chunks (verified via T1 path through the route handler). - T4:
revisionsAPI returns summaries without body; offset paging works. - T5:
systemd-analyze verify deploy/*.servicepasses (off-host safe check); README documents enable steps.
Out of scope (YAGNI, keep deferred per PHASES.md)
OpenSearch/Elasticsearch, object storage, multi-tenant, distributed workers, pgvector migration. Revisit only when corpus > ~10k docs or query latency bites.