Ershu Frontend is the browser interface for Ershu. It covers the project workspace, analysis runs, documentation, search, the knowledge graph, MCP keys, and the admin console. It talks to Ershu only over public HTTP endpoints and holds no database credentials.
Browser
│
├── /api/v1 ──> Ershu API
│ login, projects, runs, documents, admin
│
└── /mcp/ ────> Ershu MCP
endpoint for developer agents
The frontend never connects to PostgreSQL, Redis, or Milvus, and it does not run code analysis itself. Pages create and read tasks through the API; workers process them in the background and the frontend only shows status and results. Permissions, state transitions, and security checks all live on the server. The UI hides controls based on role as a convenience, never as enforcement.
- Project list, creation, overview dashboard, and settings. Settings (members, groups, grants, ownership transfer, deletion) are owner-only.
- Repositories: add remote or local, credentials via the vault, webhooks, polling, and status. Requires
can_manage. - Runs: full-project, single-repository, and incremental runs, with cancel, retry, and resume. Progress updates live.
- Run detail: real-time timeline, agent messages with tool results, and token usage. Follow-up actions in chat or work mode.
- Document tree grouped by repository, Markdown rendering, inline Mermaid diagrams with preview, and a section outline that stays in sync as you scroll.
- Chat with your documents over a streaming connection. Answers come with citations and a source preview panel.
- Code and document search in three modes: hybrid, semantic, and keyword. Filter by repository; results show chunk type, language, path, and line.
- Interactive knowledge graph on a React Flow canvas. Focus on all, API, or dependency nodes; expand and collapse repositories, directories, and callables; filter by node or edge type.
- Resolve a project from a query string, run internal API code search, and query the graph in three modes: change surface, runtime flow, and free-form query.
- Create, scope (all projects or selected ones), and revoke MCP API keys. The help dialog generates copy-ready config for six clients: Claude, Cline, OpenCode, Crush, Goose, and Roo.
- Users and groups.
- Projects, with an admin view that respects the
admin_readableflag, plus members and directory groups. - Model channels for LLM, embedding, and reranker models across OpenAI, OpenAI-compatible, DeepSeek, and Anthropic. Ping a channel to test it, refresh models from the provider catalog, and set pricing.
- Vault for SSH keys and tokens.
- Maintenance: reindex, index reconcile, run retention, stuck-run repair, and cache GC. Export and import documents and knowledge graphs. Dry-run is supported.
- System settings editor with per-setting reset and dependency source management.
- Audit log viewer with action and date filters.
The interface supports Chinese and English and light and dark themes. The sidebar collapses on narrow screens.
- React 19 with react-router 7 (HashRouter)
- Vite 7 for the dev server and production build
- TypeScript 5.7 in strict mode, no emit (Vite builds)
- Tailwind CSS 3 with CSS-variable theming and a
.darkclass toggle - Radix UI primitives wrapped as shadcn-style components (class-variance-authority, clsx, tailwind-merge)
- lucide-react icons
- React Flow (
@xyflow/react) and d3-force for the knowledge graph - markdown-it, DOMPurify, and Mermaid for document rendering
@serenity-kit/opaquefor OPAQUE password authentication- Self-hosted fonts: IBM Plex Sans and JetBrains Mono
No state-management library, HTTP client library, or chart library. Those are all written in this repo. Tests use Node's built-in test runner, not Jest or Vitest.
| Path | What lives here |
|---|---|
src/main.tsx, src/app.tsx |
Entry point, router, providers, lazy loading |
src/pages/ |
Route-level page entry points |
src/features/ |
Feature modules: pages, state, and tests grouped by capability |
src/components/ui/ |
Base UI components |
src/components/layout/ |
Global layout such as the sidebar |
src/lib/api/ |
API client, types, request and error handling |
src/lib/auth-context.tsx |
Login state and session restore |
src/lib/i18n.ts |
Chinese and English routing and messages |
src/lib/theme-provider.tsx |
Light, dark, and system theme |
src/messages/ |
Chinese and English copy |
src/hooks/ |
Real-time events and shared page behavior |
src/styles/ |
Global, document, and chat Markdown styles |
public/ |
Favicon and third-party license files |
- Node.js 24 (see
.nvmrcandenginesinpackage.json) - npm
- A running Ershu API for full integration
- A running Ershu MCP when you check MCP client addresses
Install with a locked dependency tree:
cp .env.example .env.local
npm ciDo not run npm install to change the lockfile casually. Only update package.json and package-lock.json for a deliberate upgrade, and explain why in the commit.
npm run devThe dev server listens on port 3000. Default URLs:
- Chinese:
http://localhost:3000/#/zh - English:
http://localhost:3000/#/en
The dev proxy forwards two paths to local services:
/apitohttp://localhost:8000/mcptohttp://127.0.0.1:8001
If your API or MCP runs elsewhere, set it in .env.local:
VITE_DEV_API_PROXY=http://api-host:8000
VITE_DEV_MCP_PROXY=http://mcp-host:8001Do not point /mcp at the API's port 8000.
-
Open both the Chinese and English home pages and confirm a language switch keeps you on the right page.
-
Switch between light, dark, and system themes and confirm the choice survives a refresh.
-
Check the API health endpoint:
curl -fsS http://localhost:8000/health
-
On first deploy, open
http://localhost:3000/#/zh/setup-adminto create the first admin. -
After login, create or open a project and confirm the repository, run, document, search, and settings pages make successful requests.
A page shell loading is not the same as a working integration. The browser dev tools should not show persistent /api/v1 failures, CORS errors, or rejected cookies.
| Variable | Default | Scope | Purpose |
|---|---|---|---|
VITE_API_BASE_URL |
/api/v1 |
Build | API prefix the browser calls |
VITE_CSRF_COOKIE_NAME |
ershu_csrf |
Build | CSRF cookie name, must match the API |
VITE_DEV_API_PROXY |
http://localhost:8000 |
Dev server only | Proxy target for /api |
VITE_DEV_MCP_PROXY |
http://127.0.0.1:8001 |
Dev server only | Proxy target for /mcp |
VITE_API_BASE_URL and VITE_CSRF_COOKIE_NAME are baked into the static bundle at build time. Changing them requires a fresh npm run build.
Every VITE_ variable ends up in the browser bundle. Put only public addresses and non-sensitive options there. Never store tokens, passwords, or server secrets.
Model channels, repository credentials, and MCP keys are all stored by the backend. The frontend receives only what it needs to display and cannot bypass admin endpoints through environment variables.
The frontend and API together handle:
- OPAQUE password registration and login. The plaintext password never leaves the browser; only OPAQUE protocol messages do.
- HttpOnly session cookies.
- A browser-readable CSRF cookie paired with the
X-Ershu-CSRFheader on unsafe methods. If the API rejects a request withcsrf_invalid, the client refreshes the token and retries once. - Session refresh timed from the access token's expiry and coordinated across tabs with
navigator.locks. - On final auth failure, local state is cleared and the user is sent to the login page for the current language.
Before sending any password or OPAQUE credential, the client requires HTTPS (or localhost). Production should serve the page and API from the same origin over HTTPS. Cookie names, CORS, proxy origins, and CSRF settings must match the API.
The UI is not a permission boundary. Even when the page hides a control based on role, the server must check every request on its own.
Two independent mechanisms, neither of them WebSocket.
Run progress uses the browser's EventSource on /runs/{id}/events with credentials. The connection reconnects automatically, closes on terminal events (completed, failed, cancelled), and falls back to a full snapshot if a server event cannot be parsed.
Document chat uses a ticketed fetch stream. The client posts to get a one-time ticket, then opens the stream with that ticket in a header (EventSource cannot carry custom headers). It parses the SSE frames itself and you can cancel mid-stream.
Tests run on Node's built-in runner with no test framework dependency:
npm testType-check and lint:
npm run lintFormat check:
npm run format:checkProduction build:
npm run buildTest files live next to the source they cover (*.test.mts). They test pure logic such as form state, protocol parsing, API contracts, and graph layout. There are no component or DOM rendering tests. Before submitting, run all four commands above.
Tests must pass without reading backend source. When an endpoint changes, record the expected caller behavior in the frontend tests and treat the published contract or a running Ershu as the source of truth.
Two workflows live in .github/workflows/.
ci.yml runs on every push and pull request to main. It installs with npm ci, runs the tests, lints, checks formatting, and builds.
release.yml runs when you push a tag that starts with v. It runs the same checks, builds dist/, packages it as ershu-frontend-<tag>.tar.gz and ershu-frontend-<tag>.zip, and attaches both to a GitHub Release titled after the tag.
To cut a release:
git tag v1.0.1-20260810
git push origin v1.0.1-20260810If something slipped through and you need to republish the same day, add a suffix:
git tag v1.0.1-20260810-2
git push origin v1.0.1-20260810-2The build outputs to dist/:
npm run buildPreview the build locally:
npm run previewThe app uses HashRouter, so deep links live after the # in the URL and the server never sees them. You do not need a static-server fallback to index.html. Any static host can serve dist/ directly.
Assets are built for the site root, so deploy dist/ at the domain root. A reverse proxy in front must keep two same-origin prefixes:
/api/ -> API service
/mcp/ -> MCP service
The MCP proxy must preserve streaming responses and the raw /mcp/ path. The MCP help dialog builds client addresses from the current page origin, so if production is missing /mcp/, the copied configs will not work either.
Check VITE_API_BASE_URL and the reverse proxy. The default request prefix is /api/v1, and the dev proxy matches /api.
Use the Vite same-origin proxy instead of hitting the API directly from the browser. If you must go cross-origin, the API's CORS_ORIGINS, secure cookies, and proxy headers must match the real page origin.
Check that the API is reachable, that cookies are being saved, that HTTPS and the Secure flag agree, and that the browser supports the session coordination the page relies on.
The dialog uses the current page origin plus /mcp/. Confirm the dev proxy or production reverse proxy forwards that path to the MCP service on port 8001 and keeps the Authorization header.
Restart the dev server. Production VITE_ variables need a fresh npm run build.
Every user-facing string has entries in both src/messages/zh.json and src/messages/en.json. Update both, then check /#/zh and /#/en separately.