CI / typecheck + tests (push) Canceled after 0s
CI / build + deploy (Nix) — api (push) Canceled after 0s
CI / build + deploy (Nix) — mcp (push) Canceled after 0s
CI / build + deploy (Nix) — worker (push) Canceled after 0s
CI / build + deploy (web) (push) Canceled after 0s
- Add SWR foundation (lib/fetcher, lib/swr, lib/api) with typed hooks: useSections/useDocuments/useSearch/useRelated/useRevisions/useQueueStatus - Replace ad-hoc useEffect+fetch in Header/Sidebar/CommandMenu/search with SWR (dedup 4s, focus throttle 10s, debounced search 180ms) - Doc page: RelatedGrid + RevisionList as SWR client islands (RSC hero stays) - DocForm: SWR mutate on create/update for live sidebar invalidation - New APIs: /api/related, /api/revisions, /api/tags, /api/queue/status, /api/docs filtering (?section=&status=) - New UI: DocumentCard, SectionCard, SearchBar, TagCloud, QueueBadge, EmptyState, dashboard (/dashboard with QueueStrip live + section stats) - Layout wraps SWRProvider; no TanStack code (SWR-only per spec) - Verified: next build passes (16.3.4 Turbopack), no manual fetch sprawl remains Co-Authored-By: internal-model
14 KiB
14 KiB
MCPedia Frontend Redesign — Spec
Status: DRAFT — minta approval sebelum coding Date: 2026-09-08 WIB Author: Hermes
1. Tujuan
Merombak apps/web agar:
- Satu sumber kebenaran: semua data berasal dari
@mcpedia/coretypes &@mcpedia/configsection presets; tidak ada shape duplikat di FE. - Selaras dengan gateway/BE data flow: tRPC
apps/api(/trpc/*) + REST/api/*+ MCP:4021semua mengembalikan shape yang sama (DocumentMeta, SectionInfo, SearchHit, ExportData). - Ganti pola fetch ad-hoc (
fetch("/api/...")tersebar) dengan SWR terpusat — cache, revalidate, mutate, optimistic UI, error boundary konsisten. - Hapus sisa TanStack (saat ini tidak ada deps TanStack — audit
apps/web/package.jsonclean; tapi pola akan dibakukan supaya tidak masuk lagi). - UI: Archival Precision tetap (monochrome, hairline, mono telemetry), tapi lebih live dan terisi data real (bukan placeholder).
2. Audit Status Quo
FE sekarang
- Next.js 16.3.4, App Router,
force-dynamicdi semua page yang baca DB. - RSC langsung import
@mcpedia/core(listDocuments,getDocument,listSections,getRelated,listRevisions,getExportDocuments). SEO bagus, tapi client components (Sidebar, Header, Search, CommandMenu) fetch ulang via REST tanpa cache sharing. - REST:
/api/search?q&mode&limit,/api/docs(list),/api/sections,/api/export?path&sort,/api/revisions/restore,/api/auth/login. - tRPC
apps/api(Hono) expose: search/semanticSearch/hybridSearch, getDocument, listDocuments, sections, related, revisions, CRUD (gatedx-webhook-secret), jobStatus, reindex hooks,/health,/metrics. - MCP
:4021Streamable HTTP/mcpexpose 13 tools (tidak dipakai FE, tapi shape-nya sama). - Tidak ada
swr/@tanstack/react-querydiapps/web/package.json— jadi migrasi = greenfield SWR layer, bukan replace. - Design tokens sudah rapi di
globals.css(CSS vars light/dark +--brand,--bg-*,--text-*,--code-*). Header/Sidebar/Markdown/TOC/DocActions sudah pakai tokens.
Masalah yang diselesaikan redesign
- Sidebar & Header fetch
/api/docs+/api/sectionstanpa deduplikasi — tiap navigasi fetch ulang, tidak ada stale-while-revalidate. - Search page client-side
fetchmanual tanpa debounce terpusat, tanpa cache antar-mode (hybrid/keyword/semantic saling overwrite). - Doc page RSC tidak live: setelah create/update/delete atau reindex, harus hard refresh. Tidak ada
mutatecross-component. - Shape drift: Search
snippetvsSearchHitvsChunkHittidak konsisten antara/api/searchdan@mcpedia/search— perlu typed fetcher. - Tidak ada surface untuk queue/embeddings status (BE punya
jobStatus,queueStatus,INDEX_QUEUEBullMQ) — FE buta terhadap indexing.
3. Arsitektur Target
apps/web/app/
lib/
fetcher.ts — typed fetcher (fetch JSON + error envelope)
swr.ts — SWRConfig global (dedupingInterval, focusThrottle, errorRetry)
api.ts — typed client untuk /api/* (listDocuments, search, sections, export, revisions)
hooks/
useSections.ts — SWR '/api/sections' -> SectionInfo[]
useDocuments.ts — SWR '/api/docs?section&status' -> DocumentMeta[]
useDocument.ts — SWR '/api/docs/:slug' (baru, GET single) -> Document
useSearch.ts — SWR key [q, mode, limit] -> SearchHit[] + debounced
useRelated.ts — SWR '/api/related?slug'
useRevisions.ts — SWR '/api/revisions?slug'
useQueueStatus.ts — SWR '/api/queue/status' poll 5s (opsional, admin only)
components/
(existing: Header, Sidebar, CommandMenu, Markdown, TOC, DocActions, ...)
SearchBar.tsx — reusable, dipakai Header + Search page + CommandMenu
SectionCard.tsx — extract dari page.tsx
DocumentCard.tsx — reusable card (recent, related, folder list)
TagCloud.tsx — baru: agregasi tags dari docs
QueueBadge.tsx — baru: show indexing status (waiting/active/failed)
EmptyState.tsx / ErrorState.tsx
app/
layout.tsx — tambah <SWRProvider> wrapper (client boundary)
page.tsx — refactor: RSC shell + client islands via SWR
[section]/page.tsx — sama
[section]/[...slug]/page.tsx — RSC hero + Markdown, client islands (related/revisions via SWR)
search/page.tsx — fully SWR, hapus useState fetch manual
create/page.tsx — mutate after create
dashboard/page.tsx — BARU (optional phase 2): queue, sections, recent, tag stats
Prinsip SWR
- Satu
fetchergeneric:fetcher<T>(url: string): Promise<T>— throw on !ok denganinfo+statusuntuk error boundary. SWRConfigdilayout.tsxclient wrapper:dedupingInterval: 4000,focusThrottleInterval: 10000,shouldRetryOnError: falseuntuk search,refreshIntervalhanya untuk queue.- Semua hooks pakai key array/string yang deterministic supaya
mutatecross-page bisa:mutate('/api/docs'),mutate('/api/sections'). - Debounce search di hook, bukan di component:
useDebouncedValue(q, 180ms)internal keuseSearch. - Optimistic mutate untuk create/update/delete:
mutate('/api/docs', optimisticList, false)lalu revalidate.
4. Kontrak Data (FE ↔ BE)
Semua types import dari @mcpedia/types — tidak ada re-declare shape di FE.
| FE Hook | BE Source | Endpoint | Shape |
|---|---|---|---|
| useSections | listSections() |
GET /api/sections |
SectionInfo[] |
| useDocuments | listDocuments({section,status}) |
GET /api/docs?section=&status= |
DocumentMeta[] |
| useDocument | getDocument(slug) |
GET /api/docs/:slug (baru) |
Document |
| useSearch | keywordSearch/hybridSearch/semanticSearch |
GET /api/search?q&mode&limit |
{results: {slug,title,section,score,snippet}[]} |
| useRelated | getRelated(slug) |
GET /api/related?slug= (baru, atau reuse via core) |
DocumentMeta[] |
| useRevisions | listRevisions(slug) |
GET /api/revisions?slug= (baru) |
Revision[] |
| useQueueStatus | BullMQ getQueue() |
GET /api/queue/status (baru, proxy ke apps/api /metrics atau queue) |
{waiting,active,completed,failed,delayed} |
Tambahan endpoint yang perlu dibuat (tipis, proxy ke core):
GET /api/docs/[slug]— single doc (hindari RSC-only fetch, enable SWR).GET /api/related?slug=&limit=— wrappergetRelated.GET /api/revisions?slug=&limit=— wrapperlistRevisions.GET /api/queue/status— wrappergetQueue().get*Count()(admin-only atau public read-only).- (Opsional)
GET /api/tags— agregasi tags darilistDocumentsuntuk TagCloud.
Semua endpoint return envelope sama: {data} atau {results} + {error} on 500, dengan Content-Type: application/json. Auth untuk mutasi tetap x-webhook-secret atau cookie mcpedia_admin.
5. Perubahan Per-File (Rincian)
Phase 0 — Foundation (wajib)
apps/web/package.json: tambahswr: ^2.3.x(latest 2.x). Tidak tambah TanStack.bun install+ cektypecheck.apps/web/app/lib/fetcher.ts(baru):export const fetcher = <T>(url: string): Promise<T> => fetch(url).then(r => { if(!r.ok) throw ...; return r.json() })apps/web/app/lib/swr.tsx(baru):SWRProviderclient component wrappingSWRConfig.apps/web/app/lib/api.ts(baru): typed helperssearchDocs(q,mode,limit),listDocs(),listSections()— dipakai hooks.apps/web/app/layout.tsx: bungkus children dengan<SWRProvider>(client boundary minimal).
Phase 1 — Hooks + Refactor Existing Pages
hooks/useSections.ts,useDocuments.ts,useSearch.ts: gantiuseEffect+fetchdiHeader.tsx,Sidebar.tsx,search/page.tsx,CommandMenu.tsx,page.tsx(home).Header.tsx:useSections()alih-alihuseState+useEffect fetch /api/sections.Sidebar.tsx:useDocuments()+useSections()+ filter memo — hilangkanuseEffect fetch /api/docs.search/page.tsx:useSearch(q, mode)— hapushandleSearchmanual, pakaiisLoading,isValidating,errordari SWR. Mode & section filter tetap local state, tapi key SWR mencakup keduanya.CommandMenu.tsx:useSearch(query)debounced — hapus timer manual.app/page.tsx: tetap RSC untuk SEO (listDocuments/listSections server), tapiSectionTreedanRecently Updatedbisa jadi client islands yang pakai SWR untuk live update (opsional: keep RSC +mutateon focus).[section]/page.tsx&[section]/[...slug]/page.tsx: RSC hero + Markdown tetap,related&revisionspindah ke client islandsRelatedGrid&RevisionListpakaiuseRelated/useRevisions.
Phase 2 — Fitur Baru (detail)
- Dashboard
/dashboard(baru, admin-aware):- Cards: total docs, sections, queue (waiting/active/failed), embedding provider status.
- Section breakdown bar (docCount per section).
- Tag cloud (top 20 tags dengan count).
- Recent activity (last 10 updated docs).
- Queue status poll 5s via
useQueueStatus(hanya jika admin cookie ada, else hide).
- Tag Explorer
/tagsatau sidebar widget:GET /api/tagsagregasi, klik tag →/search?tag=...atau filter docs.
- Doc page enhancements:
QueueBadgedi hero jika doc sedang di-index (query queue by slug).- Optimistic revision restore:
mutate('/api/revisions?slug=...')setelah POST restore. - Related docs pakai SWR +
keepPreviousData.
- Search enhancements:
- Persist
modedi URL (?q=&mode=hybrid) — sudah ada, pertahankan + SWR key sync. - Highlight snippet
marksudah ada — pertahankan. - Empty & error states pakai
EmptyState/ErrorStatecomponents.
- Persist
- Create/Edit:
- Setelah
POST /api/docs,mutate('/api/docs')+mutate('/api/sections')+router.push('/${slug}'). - Custom fields (extraFields) tetap flat →
splitPayloaddi BE tidak berubah.
- Setelah
- Export:
PdfExportViewtetap, tapi tambah SWR preloadgetExportDocumentsuntuk preview count sebelum export.
Phase 3 — Polish & Cleanup
- Hapus semua
fetchad-hoc yang tersisa — semua lewat hooks. - Pastikan
globals.csstokens dipakai konsisten (tidak ada hard-coded hex baru). CommandMenujadi satu-satunya omnisearch entry —Headersearch icon di mobile buka CommandMenu, bukan link/search.- A11y:
aria-liveuntuk search results count,role="status"untuk queue badge. - Print/PDF styles sudah ada — tidak diubah.
6. File yang Disentuh (Checklist)
Baru:
apps/web/app/lib/fetcher.tsapps/web/app/lib/swr.tsxapps/web/app/lib/api.tsapps/web/app/hooks/useSections.tsapps/web/app/hooks/useDocuments.tsapps/web/app/hooks/useSearch.tsapps/web/app/hooks/useRelated.tsapps/web/app/hooks/useRevisions.tsapps/web/app/hooks/useQueueStatus.tsapps/web/app/hooks/useTags.ts(opsional)apps/web/app/components/SearchBar.tsxapps/web/app/components/SectionCard.tsxapps/web/app/components/DocumentCard.tsxapps/web/app/components/TagCloud.tsxapps/web/app/components/QueueBadge.tsxapps/web/app/components/EmptyState.tsxapps/web/app/dashboard/page.tsx(baru)apps/web/app/api/docs/[slug]/route.ts(baru)apps/web/app/api/related/route.ts(baru)apps/web/app/api/revisions/route.ts(baru GET)apps/web/app/api/queue/status/route.ts(baru)apps/web/app/api/tags/route.ts(baru, opsional)
Edit:
apps/web/package.json(+ swr)apps/web/app/layout.tsx(+ SWRProvider)apps/web/app/page.tsx(extract SectionCard, optional SWR islands)apps/web/app/[section]/page.tsx(DocumentCard)apps/web/app/[section]/[...slug]/page.tsx(related/revisions islands)apps/web/app/search/page.tsx(full SWR)apps/web/app/components/Header.tsx(useSections)apps/web/app/components/Sidebar.tsx(useDocuments/useSections)apps/web/app/components/CommandMenu.tsx(useSearch)apps/web/app/create/page.tsx(mutate)apps/web/app/components/DocForm.tsx(mutate setelah submit)
Tidak disentuh:
packages/core,packages/types,packages/config,packages/db(hanya dibaca)apps/api,apps/mcp,apps/worker(hanya diproxy)globals.css,next.config.ts,tailwindconfig (kecuali perlu token baru)
7. Urutan Implementasi
- Phase 0 foundation →
bun install && bun run typecheckgreen. - Phase 1 hooks + Header/Sidebar/Search/CommandMenu → manual test +
next buildsmoke. - Phase 2 doc page islands + create mutate → test CRUD flow dengan
x-webhook-secret/ admin cookie. - Phase 2 dashboard + tags → behind
canEditguard jika perlu. - Phase 3 cleanup, hapus fetch manual, final
bun run lint && typecheck && build.
8. Verifikasi
bun run typecheck— tidak ada error (SWR types + @mcpedia/types).bun run lint— rules Next 16 ok.bun --cwd apps/web run build— semua routeforce-dynamicOK, tidak ada import-time DB throw.- Manual:
- Sidebar filter + section count live setelah create doc.
- Search mode switch (hybrid/keyword/semantic) cache terpisah, tidak flicker.
- CommandMenu ⌘K debounce 180ms, Enter navigasi benar.
- Doc related & revisions muncul via SWR tanpa hard refresh.
- Queue badge (jika ada) poll 5s, tidak spam (dedupingInterval).
- Dark/light toggle tetap work (SWRProvider tidak break
ThemeToggle).
- Staging:
curl /api/search?q=test&mode=hybrid,/api/sections,/api/docsreturn shape konsisten dengan tRPC.
9. Risiko & Mitigasi
- RSC + SWR double fetch: mitigasi dengan
fallbackDatadari RSC props ke SWR hook di islands (opsi, tidak wajib v1). - Peningkatan request ke DB: SWR deduping 4s +
revalidateOnFocus: falseuntuk search,trueuntuk sections/docs. - Auth untuk
/api/queue/status: jadikan public read-only (hanya counts, bukan detail job) — aman.
10. Keputusan yang Perlu Kamu Konfirmasi
- Dashboard
/dashboardperlu tidak? Atau cukup queue badge di existing pages? Rekomendasi: bikin, tapi Phase 2 — bisa ditunda. - Endpoint baru (
/api/docs/[slug],/api/related,/api/revisionsGET,/api/queue/status,/api/tags) setuju? Alternatif: FE langsung pakai tRPC client (@trpc/client) — tapi spec ini pilih REST/api/*supaya Next cache & SWR fetcher simpel. - SWR vs tRPC+SWR? Spec ini pakai SWR+REST (paling simpel, tidak tambah
@trpc/clientdi FE). Jika mau tRPC, tambahcreateTRPCClient+httpBatchLink— tradeoff: lebih typed tapi lebih berat. Default: SWR+REST. - Keep RSC server fetch untuk SEO atau full client SWR? Rekomendasi: keep RSC untuk initial HTML (SEO), SWR untuk live revalidation di client islands — hybrid, bukan full SWR.
Jawab: Lanjut Phase 0+1 untuk eksekusi, atau beri koreksi poin 10.