Wikantik Frontend Architecture

Adapted from docs/developer/FrontendArchitecture.md in the Wikantik repository; current as of release 2.4.56.

The wikantik-frontend module implements Wikantik's user interface as a React 19 single-page application. It is built with Vite as part of the standard Maven build, bundled into the WAR, and served at the web root — there is no separate hosting, no CORS configuration, and no legacy JSP templates alongside it.

Status

Fully implemented. The SPA covers reading, inline editing, search, change history, similar-pages discovery, attachment management, comments/mentions, dark mode, the admin panel (users, content, security, knowledge, API keys, KG policy, retrieval quality, page ownership, audit), and the graph viewers.

Architecture

ConcernChoice
FrameworkReact 19 (react, react-dom)
RoutingReact Router v7 (react-router-dom ^7.18)
BuildVite 8 (Rolldown) (@vitejs/plugin-react ^6)
EditorCodeMirror 6 (@uiw/react-codemirror + @codemirror/lang-markdown)
Article renderingServer-rendered HTML (contentHtml) decorated client-side; react-markdown + remark-gfm, remark-math, rehype-katex for the editor preview (see Rendering Pipeline)
Math and code highlightingkatex and lowlight (highlight.js "common" grammars), each code-split and loaded only for pages that need it
Graph visualisationcytoscape + cytoscape-cose-bilkent via react-cytoscapejs
TestingVitest + Testing Library + happy-dom
StylingHand-written CSS with a token-based design system (no framework)
StateReact state + hooks; no external store library

The build runs automatically during mvn package: wikantik-war/pom.xml uses exec-maven-plugin to run npm ci --no-audit --no-fund --ignore-scripts and then npm run build in wikantik-frontend (Node and npm must already be installed), and the resulting dist/ is copied into the WAR.

Routing

The SPA uses client-side routing for all interactive views. Deep links are handled by SpaRoutingFilter (com.wikantik.rest.SpaRoutingFilter in wikantik-rest), which forwards recognised SPA paths to /index.html so React Router can take over.

SpaRoutingFilter matches two categories of path:

The full route table from main.jsx:

RouteComponentNotes
/<Navigate to="/wiki/Main" replace />No home page — always redirects
/wiki/:namePageViewArticle reader (eager-loaded)
/wiki<Navigate to="/wiki/Main" replace />Bare /wiki redirect
/edit/:namePageEditorWiki page editor
/diff/:nameDiffViewerSide-by-side version diff
/searchSearchResultsPageFull-text + faceted search
/page-graphPageGraphViewPage Graph (wikilink edges) viewer
/knowledge-graphKnowledgeGraphViewKG entity/edge viewer
/preferencesUserPreferencesPagePersonal settings
/me/mentionsMentionsPage@-mention inbox
/reset-passwordResetPasswordPagePassword reset flow
/loginLoginPageLogin (dual-registered in web.xml + SPA_EXACT)
/change-passwordChangePasswordPageForced password change flow
/admin (index)OverviewDashboardAdmin overview
/admin/usersAdminUsersPageUser management
/admin/contentAdminContentPageContent & index tools
/admin/securityAdminSecurityPageGroups + policy grants
/admin/knowledge-graphAdminKnowledgePageKG curation
/admin/apikeysAdminApiKeysPageAPI key management
/admin/retrieval-qualityAdminRetrievalQualityPageRetrieval experiment harness
/admin/driftAdminDriftPageVocabulary/SHACL drift burn-down dashboard
/admin/insightsInsightsPanelSearch-visibility / content-intelligence panel
/admin/kg-policyAdminKgPolicyPageKG inclusion policy rules
/admin/kg-policy/explainAdminKgPolicyExplainPer-page policy explanation
/admin/kg-policy/pendingAdminKgPolicyPendingPages with pending review
/admin/kg-policy/bootstrapAdminKgPolicyBootstrapBulk bootstrap tooling
/admin/page-ownershipAdminPageOwnershipPagePage-owner assignment
/admin/auditAdminAuditPageTamper-evident audit log; rows are clickable and open a record-detail modal
/admin/connectorsAdminConnectorsPageConnector list + detail (settings/authorization/runs/pages tabs)
/admin/connectors/newAddConnectorWizardGuided add-connector wizard
/admin/connectors/:idConnectorDetailPageSingle connector detail

All /admin/* routes are nested under AdminLayout (lazy-loaded), which sends anyone without the Admin role back to the wiki. The data behind them is protected server-side: AdminAuthFilter requires AllPermission for every /admin/* API call (a browser navigation that asks for HTML gets the SPA shell, which carries no admin data).

Graph viewer routes get a full-bleed app-content-full layout; editor and admin routes use app-content-wide; wiki article routes use the narrow reading column.

The REST API lives under /api/ and admin endpoints under /admin/. Two MCP servers (/wikantik-admin-mcp and /knowledge-mcp) plus the OpenAPI tool server at /tools/* are serviced by separate backend modules and are not part of the SPA. Servlet counts are in Wikantik Architecture; MCP tool counts are owned by the admin MCP documentation.

Project Layout

wikantik-frontend/
├── package.json
├── vite.config.js
├── index.html
├── public/
└── src/
    ├── main.jsx                 Entry point + route table
    ├── App.jsx                  Layout shell (Outlet + sidebar context-swap)
    ├── api/
    │   └── client.js            Fetch wrapper for /api/*; dispatches wikantik:version-mismatch
    ├── components/
    │   ├── PageView.jsx         Article rendering (server HTML + client DOM passes)
    │   ├── PageEditor.jsx       Inline editor (CodeMirror 6)
    │   ├── Sidebar.jsx          Cluster-grouped navigation (reader)
    │   ├── QuickOverlay.jsx     Quick switcher (Mod-K / Mod-O) and command palette (Mod-P)
    │   ├── MetadataPanel.jsx    Frontmatter chips
    │   ├── ChangeNotesPanel.jsx History + diffs
    │   ├── SimilarPagesPanel.jsx
    │   ├── AttachmentPanel.jsx
    │   ├── BacklinksPanel.jsx   "Referenced by" backlinks (GET /api/backlinks/{name})
    │   ├── CommentsDrawer.jsx   Threaded comments + @-mention composer
    │   ├── DiffViewer.jsx
    │   ├── MentionsPage.jsx     @-mention inbox (/me/mentions)
    │   ├── PersonalZone.jsx     Personal zone widget
    │   ├── LoginPage.jsx / LoginForm.jsx / SsoLoginButton.jsx
    │   ├── UserPreferencesPage.jsx / ResetPasswordPage.jsx
    │   ├── TableOfContents.jsx / Breadcrumbs.jsx
    │   ├── admin/               Admin panel pages + forms (see below)
    │   ├── pagegraph/           Page Graph viewer (lazy-loaded)
    │   ├── kgraph/              Knowledge Graph viewer (lazy-loaded)
    │   └── ui/                  Shared primitive components (see below)
    ├── commands/                Command registry shared by the overlay, slash menu, toolbar and key bindings
    ├── navigation/              NavigationGuardProvider: confirm-before-leaving dialog while a guard is registered
    ├── newpage/                 NewPageProvider: the New page dialog and its command
    ├── hooks/                   custom hooks (see below)
    ├── utils/                   utility modules (see below)
    └── styles/
        ├── globals.css          Reset, CSS variables, theme tokens
        ├── article.css
        └── admin.css

Markdown Rendering Pipeline

There are two rendering paths, and the reader does not use react-markdown.

Reader (PageView). The server renders the page (Flexmark MarkdownRenderer) and GET /api/pages/{name} returns the result as contentHtml; a /wiki/{name} navigation also embeds the same data in the served index.html (window.__WIKANTIK_PAGE__), so the client can seed its state without a render-critical fetch. PageView injects that HTML and then runs idempotent client-side DOM passes over it:

Editor preview, search snippets and KG chunk views. These render markdown in the browser with react-markdown. The editor preview (EditorPreviewMarkdown, fed by PageEditor) uses this plugin chain:

See docs/user/MathematicalNotation.md for the full math syntax reference.

Graph Viewers

Wikantik ships two distinct, lazy-loaded graph viewers in the SPA. Don't conflate them — they reflect two separate subsystems (see Page Graph vs Knowledge Graph for the long form):

/page-graph — Page Graph viewer

Real wikilink edges between pages. Nodes are pages; edges are wikilinks parsed from page bodies, with co-resident structural information (canonical_id, cluster) layered in. Hubs (nodes above a configurable degree threshold) are highlighted; anomalies (restricted pages, orphans) can be toggled into view. Components live under wikantik-frontend/src/components/pagegraph/:

ComponentRole
PageGraphView.jsxTop-level container — fetches snapshot, owns state
GraphCanvas.jsxCytoscape canvas with CoSE-Bilkent layout
GraphToolbar.jsxEdge-type filters, anomaly toggle, refresh
GraphZoomSlider.jsxManual zoom control (semantic zoom)
GraphLegend.jsxHub threshold and edge-type legend
GraphDetailsDrawer.jsxSelected-node details + incident edges
FilterPanel.jsxPer-cluster / tag filtering
GraphErrorBoundary.jsx / GraphErrorState.jsxFailure UI
graph-data.jsConverts the REST snapshot into Cytoscape elements
graph-style.jsCytoscape stylesheet

Snapshot comes from PageGraphSnapshotResource in wikantik-rest, backed by the Page Graph subsystem (com.wikantik.pagegraph.*). A ?focus=PageName query parameter auto-centres and selects a node on load — useful for linking to the graph from an article.

/knowledge-graph — Knowledge Graph viewer

Reader-facing visualisation of the LLM-extracted entity graph. Nodes are typed entities; edges are co-mention or typed relations between them. Includes tier filter, node-type colours, provenance/status badges, and a large-graph warning gate that prompts before rendering big subgraphs. Components under wikantik-frontend/src/components/kgraph/:

ComponentRole
KnowledgeGraphView.jsxTop-level container
KgGraphToolbar.jsxTier filter, node-type filter, refresh
KgGraphLegend.jsxNode-type colour key
KgGraphDetailsDrawer.jsxSelected-node provenance + incident edges
KgErrorState.jsxFailure UI
kg-graph-data.jsConverts the REST snapshot into Cytoscape elements
kg-graph-style.jsCytoscape stylesheet

Snapshot comes from KnowledgeGraphResource in wikantik-rest, backed by DefaultKnowledgeGraphService in wikantik-knowledge.

Design System

The CSS design system is intentionally minimal and framework-free. Tokens live in src/styles/globals.css and drive light and dark themes. The key tokens from the :root block (light theme; spacing, transition, and radius tokens omitted for brevity):

:root {
  /* Typography */
  --font-display: 'Playfair Display', Georgia, 'Times New Roman', serif;
  --font-body: 'Source Serif 4', Georgia, serif;
  --font-ui: 'DM Sans', -apple-system, BlinkMacSystemFont, sans-serif;
  --font-mono: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace;

  /* Light theme */
  --bg: #FEFCF8;
  --bg-elevated: #FFFFFF;
  --bg-sidebar: #F7F4EE;
  --text: #2D2D2D;
  --text-secondary: #6B6560;
  --text-muted: #9B9590;
  --accent: #C45D3E;           /* terracotta */
  --accent-hover: #A8442A;
  --sage: #7A8B6F;
  --sage-light: #E8EDE4;
  --border: #E8E4DC;
  --border-strong: #D0C8BC;
  --code-bg: #F3F0EA;

  /* Semantic state */
  --success: #2a8d2a;
  --danger: #b13a3a;
  --warning: #b8860b;

  /* Layout */
  --sidebar-width: 280px;
  --content-max-width: 1470px;
  --content-max-width-wide: 1600px;
  --toc-width: 200px;
}

[data-theme="dark"] {
  --bg: #1A1A1E;
  --bg-elevated: #222226;
  --bg-sidebar: #18181C;
  --text: #E8E4DC;
  --text-secondary: #A8A09A;
  --accent: #E07050;           /* lighter terracotta for dark bg */
  --accent-hover: #F08060;
  /* … */
}

body uses var(--font-ui) (DM Sans). Long-form article text uses var(--font-body) (Source Serif 4) applied in article.css. The --content-max-width of 1470px is for the main reading/article layout; admin and wide contexts use --content-max-width-wide: 1600px.

Comments and Mentions

CommentsDrawer renders threaded comment threads anchored to page text selections. It mounts inside PageView alongside BacklinksPanel. The @-mention composer is powered by useMentionPicker + MentionPicker. Unread mention counts are tracked by useUnreadMentions. The personal mention inbox lives at /me/mentions (MentionsPage).

For the full feature spec see docs/user/CommentsAndMentions.md.

Personal Zone

PersonalZone (/components/PersonalZone.jsx) surfaces the current user's recently-edited pages, drafts, and other personal signals. Related hooks: useMyPages, useRecentlyViewed, useDraft, useDrafts.

For the full feature spec see docs/user/PersonalZone.md.

Update Toast

App.jsx listens for the custom DOM event wikantik:version-mismatch, dispatched by api/client.js when the server's X-Build-Version response header (set by BuildVersionFilter in wikantik-rest) differs from the embedded __BUILD_VERSION__ constant (stamped at Vite build time via vite.config.js's buildVersionPlugin). When a mismatch is detected the app renders a non-blocking "A new version is available." toast with a Reload button. Dismissing the toast is remembered for the browser session, per server version (sessionStorage), so a later deploy shows it again. Separately, the stale-asset recovery script in index.html sets a __wikantik_reload retry flag, which main.jsx clears once the app has rendered.

Admin Panel

Accessed at /admin; its /admin/* API calls are protected server-side by AdminAuthFilter (requires AllPermission). AdminSidebar replaces the reader Sidebar via a context swap in App.jsx whenever location.pathname.startsWith('/admin').

AdminSidebar navigation

Links are grouped into four sections:

People & Access

Content

Knowledge & Search

Observability

Plus an Overview index link at /admin (exact match).

AdminContentPage tabs

AdminContentPage uses a tab bar defined as:

Dashboard | Orphaned Pages | Broken Links | Versions | Chunk Inspector | Index Status

AdminKnowledgePage tabs

AdminKnowledgePage uses a tab bar defined as:

Proposals | Extraction | Node Explorer | Edge Explorer |
Content Embeddings | Hub Proposals | Hub Discovery | LLM Activity

Shared UI Components (components/ui/)

A set of reusable primitives used across both the reader and admin panel:

ComponentPurpose
Badge.jsxInline status/label badge
Card.jsxContent card container
Chip.jsxRemovable filter chip
Combobox.jsxSearchable select with typeahead
ConfirmDialog.jsxConfirmation modal
EmptyState.jsxZero-results placeholder with icon
Icon.jsxIcon wrapper
Modal.jsxAccessible modal dialog
Select.jsxStyled native-select wrapper
Skeleton.jsxLoading skeleton placeholder
Spinner.jsxLoading spinner
Tabs.jsxTab bar
TagInput.jsxFreeform multi-tag input
ToastProvider.jsxToast context + renderer (used by useToast)

Hooks (src/hooks/)

35 hook modules (counted 2026-10-05; most have a .test file). The main groups:

Utils (src/utils/)

57 utility modules, five of them under utils/livePreview/ (counted 2026-10-13; most have a .test file). The main groups:

Authentication

The SPA uses session cookies set by the standard Wikantik login flow. All /api/* calls include the session cookie; ACLs and policy grants are enforced server-side by RestServletBase.checkPagePermission(). There is no token or JWT layer — the SPA is a thin client of the same session the wiki uses server-side.

Google SSO is available at the login page via SsoLoginButton when the server is configured with wikantik.sso.* properties.

Running the Frontend Standalone (Development)

For fast frontend iteration without a full Maven rebuild:

cd wikantik-frontend
npm install
npm run dev         # Vite dev server at http://localhost:5173/

The Vite dev server proxies three path prefixes to http://localhost:8080 (hard-coded in vite.config.js — there is no VITE_API_BASE environment variable):

Proxy pathTarget
/apihttp://localhost:8080
/attachhttp://localhost:8080
/adminhttp://localhost:8080

A running backend instance on port 8080 is required for most functionality.

Unit tests:

npm test            # vitest run (single pass)
npm run test:watch  # vitest watch mode

Note: Vitest concurrency can cause false failures in some hook tests — if a test file fails in a full run, re-run it in isolation before chasing the failure.

Production Build

The production build runs as part of mvn package. To build the frontend directly:

cd wikantik-frontend
npm run build       # outputs to dist/

The vite.config.js buildVersionPlugin stamps a build-version.txt asset and a <meta name="build-version"> tag into index.html at build time; BuildVersionFilter (in wikantik-rest) reads this file and echoes the version in X-Build-Version response headers so the update toast can detect stale bundles.

The wikantik-war module's pom.xml copies dist/ into the WAR during the packaging phase — no manual copy step is required.


Cluster Hub: Wikantik Platform Development Hub: Architecture, MCP Protocols, and Agent Briefings (wikantik-development)