The Markdown component uses 'prose' classes (prose-lg prose-headings:)
but @tailwindcss/typography was not installed/registered. Tables and other
MD elements rendered unstyled. Fix: install @tailwindcss/typography@latest,
register via @plugin directive in globals.css, add custom dark-theme
table styles, and remove redundant tailwind.config.js.
Increase max-width from 3xl to 4xl/5xl/6xl for xl screens to give the
homepage card grid and docs index more breathing room while keeping
the sidebar at 256px.
The search input's onKeyDown handler used window.location.href which is
unavailable during SSR, causing the /docs page to crash. Replaced with
a Link wrapper so it's fully server-rendered.
- 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.