- Docs index: hero section with search bar, card grid with metadata/tags, recent strip
- Doc page: section badges, card-style related docs, improved revision history
- Sidebar: section icons, active state highlighting, better typography
- Sidebar.tsx is now "use client" — fetches /api/docs at runtime instead of
calling listDocuments() during SSG (CI has no DB, causes ECONNREFUSED)
- GET /api/docs added to route.ts (returns doc list for sidebar)
- Removed SidebarContent.tsx (merged into Sidebar.tsx)
- Fixed isAuthorized double-brace typo
Next.js App Router doesn't match DELETE/PUT on /api/docs/route.ts for
nested paths; need a dynamic segment. POST stays at /api/docs, PUT+DELETE
move to /api/docs/[...slug]. Verified DELETE works locally + via Caddy.
Web UI login sets mcpedia_admin cookie; /api/docs/route.ts required the
x-webhook-secret header which the browser form also sends, but the dual-auth
path was brittle. Now accepts either: header (API/MCP style) OR cookie (web
login). Also set ADMIN_PASSWORD on VPS .env and restart web.
- Install rehype-slug (proper heading anchors) + github-slugger (matching TOC)
- TOC component: auto-generated from h2/h3 headings, clickable anchors
- Dark mode toggle: ThemeToggle (localStorage + system default), class-based
- /docs index page (lists all docs by section)
- Markdown.tsx uses rehype-slug for stable heading ids
- globals.css: @custom-variant dark (.dark class) for class-based dark mode
- layout.tsx: nav includes ThemeToggle
Note: per user instruction, installed real deps (rehype-slug, github-slugger,
@types/hast) instead of hacky inline any-cast hacks.
Home page, search results, and doc page Related links all
generated hrefs as /${section}/${slug} but slug already
includes the section prefix (e.g. 'docs/websocket/contract'),
producing doubled URLs like /docs/docs/websocket/contract → 404.
Fix: href={`/${d.slug}`} everywhere.
echo + manual ssh key writing got 'error in libcrypto' (key mangling)
+ 'too many authentication failures'. Switch to appleboy/ssh-action@v1.1.0
which handles key/permission/identity correctly. Also set
SSH_DEPLOY_HOST=45.127.35.244 (public IP, not Tailscale 100.x).
CI only builds/tests. Deploy is a separate workflow triggered via
workflow_run after CI passes: pull → bun install → web build → restart
all 4 systemd services (web/api/mcp/worker) over SSH.
Secrets (stored in GitHub):
- SSH_DEPLOY_HOST=100.79.111.61
- SSH_DEPLOY_PORT=22
- SSH_DEPLOY_USER=code
- SSH_DEPLOY_KEY=<ed25519 deploy key added to VPS authorized_keys>
getDocument preferred the on-disk file via readFileSync (raw), returning
the full frontmatter block as visible text on doc pages and MCP resources.
ReactMarkdown rendered the '---' delimiters as <hr/>, exposing YAML keys
(id/title/type/etc.) as plain paragraphs.
Root cause: getDocument bypassed parseFile's gray-matter stripping.
The indexer correctly stripped frontmatter (DB body was clean), but
getDocument read raw from disk for the 'source of truth' path.
Fix: use parseFile(abs, relPath).body (same as the indexer) so the
on-disk path returns frontmatter-free markdown. DB fallback unchanged.
Affects web doc pages + MCP mcpedia://docs/{+slug} and /chunks resources.
Verified: frontmatter leakage 0/9 doc pages, headings render as <h2>,
browser snapshot clean.
Added a real test suite (32 tests, 0 external services) using bun:test with
in-process module mocking for @mcpedia/db, @mcpedia/queue, @mcpedia/core.
Enablers:
- apps/api: extracted createApp(deps?) factory + dashboard.ts module from
index.ts so the HTTP surface is unit-testable (real queue is lazy-imported).
- packages/core: exported shouldCreateRevision pure predicate; restoreRevision
gained an opts.reindex seam for the chunk-rebuild contract.
- apps/mcp: renamed smoke.test.ts -> smoke.ts (bun test now owns .test.ts),
updated stale assertions (10 tools, 4 docs in docs section).
- infra: turbo test task (cache:false), test scripts across packages,
@types/bun + tsconfig base types, CI 'Test' step after Build.
Packages with tests: embeddings(5), parser(5), search(8), core(4),
mcp(6 auth-gates), api(8 contracts).
All green: typecheck(4/4), test(6/6 pkgs), build(web). Live API verified
/health, /metrics, /dashboard, /hooks/* auth gate on temp port.
Zero-dependency HTML page (served by the API, exposed on the domain):
- live /metrics pull (queue gauges + uptime, 5s refresh, live-dot status)
- search box calling MCP hybrid_search directly from the browser (CORS-open /mcp),
ranked hits linking to the web doc route /docs/<slug>
- XSS-hardened: all KB fields esc()'d before innerHTML
Verified live: /dashboard 200, /metrics 200, MCP hybrid_search returns real hits,
doc links 200, typecheck green.
- apps/mcp/src/http.ts: serve MCP over Streamable HTTP (MCP 2025-03-26) on
:4021, stateless mode (sessionIdGenerator undefined), CORS on /mcp. Remote
clients can now call the 6 tools + 4 resources without a stdio subprocess.
- deploy/mcpedia-mcp.service: supervised systemd unit (MCP_PORT=4021).
- Caddy: mcp.asepharyana.my.id -> 4021; wiki. domain now also routes /trpc/*
to the API (was swallowed by web -> tRPC was unreachable on the domain).
- apps/api: restoreRevision tRPC mutation now requires x-webhook-secret (the
Web UI calls @mcpedia/core directly, so this only gates the open network
endpoint). Threads the header into tRPC Context. Secures a state-changing
action that was anonymously callable.
Verified live: https://mcp.asepharyana.my.id/mcp initialize/tools/list/
resources/list all 200; restoreRevision no-secret -> unauthorized, with-secret
-> handler; read-only tRPC reachable via domain.
- apps/api: assertWebhookAuth now verifies GitHub X-Hub-Signature-256 HMAC
(raw-body HMAC-SHA256) AND the manual x-webhook-secret header. GitHub does
not send a custom header, so only the HMAC path made the push webhook work.
- root package.json: api/worker scripts use absolute bun path + direct-file
form (bun --cwd apps/api run dev errored in bun 1.3.14).
- deploy/*.service: ExecStart uses /home/code/.bun/bin/bun (systemd PATH lacks bun).
- GitHub push webhook created -> https://wiki.asepharyana.my.id/hooks/reindex
(verified: ping + push deliveries return 200, worker drains, 0 failed).
- Caddy: expose /hooks/* + /health on wiki.asepharyana.my.id -> :4020.
Services mcpedia-api + mcpedia-worker now enabled + active on host.
Serves the Next.js web app on :4016 via bun; wired to
wiki.asepharyana.my.id through the existing Caddy proxy snippet.
EnvironmentFile loads DATABASE_URL/EMBED_*/etc from repo .env.
The home, doc, and search pages queried Postgres during SSG/static data
collection, so 'next build' failed in CI (no DB). Mark them
force-dynamic (and drop generateStaticParams from the doc page) so they
render at request time — instant at this corpus scale and build-safe
without a live database.
Constructor threw if EMBED_BASE_URL/API_KEY/MODEL unset, which broke next
build SSG collection (imports search pkg before .env exists). Validation
now happens in embed() so the package is build-safe; a missing config still
errors clearly when a semantic/hybrid search actually runs.
With the DATABASE_URL import-time throw also removed, the whole monorepo
now builds in CI without any .env present.
The import-time guard broke 'next build' (SSG data collection imports
@mcpedia/config before any .env exists) and any runtime-injected env.
postgres.js connects lazily on first query, so a missing DATABASE_URL now
surfaces as a clear connect error at runtime instead of a cryptic build
failure. Lets CI build without .env present.
The file sat outside app/ so next build ignored it, but turbo typecheck's
tsc compiles all *.tsx and failed on the Next-15-only LayoutProps type.
The real layout is apps/web/app/layout.tsx. Deleting the dead scaffold
lets CI typecheck pass.
Builds the whole monorepo on push/PR to main: bun install --frozen-lockfile,
turbo typecheck, turbo build (incl. Next.js web prerender), and the MCP
in-memory smoke test. No external services required for CI.
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.