Skip to content

CodeNavi Web UI

CodeNavi ships a built-in web UI — a static single-page app served by the server itself at:

http://localhost:8765/ui

No build step, no separate process, no runtime CDN: it's vanilla HTML/CSS/JS with fonts vendored locally, inline-SVG icons (icons.js, extracted from Lucide), and plain-CSS bar charts, served straight from the FastMCP server (the same one that handles /mcp). It talks to the server over a small set of /ui/api/* JSON endpoints.

Auth: when CODENAVI_AUTH_TOKEN is set, the API endpoints require a bearer token. The UI keeps an optional token in browser localStorage; static assets are served openly (they contain no secrets). With auth off (the default), everything is open on localhost.

Streamlit: the old Streamlit UI on :8501 has been retired (2026-05-21). The SPA at :8765/ui is now the only web UI — it reached full feature parity, so the Streamlit app, its port, and its dependency were removed.


Screens

The left sidebar switches between nine screens. Every editable setting lives on the screen it affects (validated server-side against a safe-only whitelist; secret/unknown keys are ignored, and secrets are redacted in everything the UI receives); the Settings screen additionally shows all of those whitelisted keys in one place — both write paths hit the same endpoint, so nothing can drift.

A global reindex badge sits in the top-right header on every screen3: an always-on poller of /reindex-status shows ⟳ Reindexing <codebase>… <n>s whenever an index build is running — including one started outside the UI (reindex.sh, the CLI, or the reindex_* MCP tools) — and clears (refreshing the active screen) when it finishes. The Index screen's Operate panel still shows the detailed per-run status and Cancel button.

System

The home screen — server health and observability.

  • Status cards: server status, version, uptime, GPU, LLM provider/model, active-table chunk count.
  • Embedding & LLM models table.
  • Codebases — sortable table (Name / Path / Files / Docs / Chunks) with an inline share-of-total bar behind each chunk count, a "not indexed" badge for zero-chunk codebases, a totals footer, and click-row-to-drill-into-Index navigation.3 Default sort: Chunks descending. Files = distinct rel_path count per codebase (cheap LanceDB projection — no vectors materialized).
  • Search quality — query-log analytics (total queries, zero-result count, FTS-fallback rate, alias expansions) and feedback vote summary (helpful / not helpful / demoted chunks).
  • Hooks — webhook URL + script-hook directory (events: reindex/crawl/scanner/chat; empty = disable).
  • Configuration — read-only audit view of data/settings.json with secrets redacted. Values not editable anywhere in the UI are changed by editing that file on the server. The embedding provider is deliberately one of them: changing it invalidates all vectors (full reindex of every codebase required), and openai POSTs every code chunk to a remote API, so the server refuses it — on save and before any reindex — unless CODENAVI_ALLOW_REMOTE_CODE_EMBEDDER=true is set in deploy/.env. local is the recommended, highest-quality option.
  • System also shows the installed license (tier, licensed-to, expiry, file-cap usage) and an app-wide expiry banner appears on all screens inside the 14-day warning window.5

Index

The operations dashboard for the index.

  • Per-codebase cards — files indexed (headline), chunk + language counts, and last-indexed time (hover tooltip) for every configured codebase. Doc/web chunk counts appear only once those stores hold anything (they're 0 unless PDF/DOCX were ingested via DOCS_DIR or the crawler ran). Each card is clickable and selects that codebase in Operate/drill-down below.
  • Sticky codebase selection — Index, Wiki, and Security share one "current codebase": picking it anywhere follows to the other screens, survives page reloads (localStorage), and is kept when the dashboard refreshes after a reindex finishes (it used to snap back to the first codebase).
  • Last reindex — the full multi-line report from the most recent build (Files / Chunks / Errors / Backup / Diff lines) lands here when a reindex finishes.3 Monospace, scrollable, capped at 240 px.
  • Operate — one merged card with the codebase selector and the Incremental / Full / Cancel buttons3 (the selector drives both the drill-down view and the reindex target, so they live together). Live elapsed counter sits next to the buttons; on completion the verbose report jumps up to Last reindex and the Operate card empties.
  • Drill-down — the same selector picks a codebase to see headline metrics (chunks, files, languages, avg chunks/file), distribution charts (by language, by chunk type), and the top files by chunk count.
  • Documents — the indexed documentation for the inspected codebase, with chunk counts. .md/.txt files are indexed automatically during a normal reindex; Reindex docs ingests PDF/DOCX from DOCS_DIR (with the security scan applied). See setup.md for DOCS_DIR/DOCS_ENABLED and api-reference.md for the reindex_docs tool.
  • Backups — create a restore-capable snapshot (index + feedback DB + settings), list snapshots, and Restore one (double-confirmed — an OK/Cancel warning plus a typed restore keyword, since it overwrites the live index; also refuses while a crawl is running). These are manual snapshots, distinct from the lancedb-backup-* snapshots that reindex creates automatically.

Chat

Grounded Q&A across your indexed code, docs, and crawled pages.

  • Session picker with New chat (optionally named) and Delete.
  • A Scope selector to target a single codebase or all of them.
  • LLM picker in the rail — a provider dropdown (ollama, claude, openai, gemini, anythingllm, lmstudio, vllm, openrouter) and a provider-aware model combobox. The model list swaps with the provider: self-hosted backends (ollama, lmstudio, AnythingLLM, vLLM) and Gemini are listed live from their /models-style endpoint; claude/openai/openrouter show a curated, editable suggestion list. Live probes send the API key when the provider needs one — required for Gemini/AnythingLLM, optional for vLLM (which runs open by default), none for ollama/lmstudio. The field is always free-text, so a newer/unlisted model can be typed. A hint line shows backend reachability or API-key presence, and — for any non-local (cloud) provider — warns that chat sends retrieved code chunks to that provider (the local-first default keeps code on-box; the self-hosted ones get no warning). The chosen model is saved to that provider's own config key (LLM_MODEL for ollama/lmstudio, CLAUDE_MODEL/OPENAI_MODEL/GEMINI_MODEL/ANYTHINGLLM_MODEL/VLLM_MODEL/OPENROUTER_MODEL otherwise), so it actually reaches generation. Connection details (base URLs, API keys) live in data/settings.json / deploy/.env. Backed by GET /ui/api/llm/models?provider=…. The openai provider is OpenAI-compatible: OPENAI_BASE_URL (editable on Settings when provider = openai) points the slot at DeepSeek, Groq, Mistral or any compatible API — the key stays in OPENAI_API_KEY, and the rail's hint line discloses the custom endpoint so it's visible that chat is not talking to api.openai.com.
  • Conversation history is persisted server-side per session.
  • One search + one answer per question (CN-776). Each question runs one retrieval pass over the scoped codebase(s) and hands the model up to 12 short excerpts; the model then writes one answer and does not search again or open other files. A collapsible How this chat works note under the input says so, and that the chosen model sets the answer quality (small local models misread excerpts and invent names — check every cited file:line; a citation the excerpts do not contain gets a ⚠️ warning). Messages of 300+ characters (a pasted article, log or spec plus a question) additionally get up to three short LLM-written search queries aimed at the existing code the change would build on; their hits are added to the raw message's own (never replacing them), and any rewrite failure falls back to the plain search. Design and "how could we add X" questions get a step-by-step answer tied to the retrieved files, with assumptions labelled; facts about the code still come only from the excerpts.
  • Call-graph questions are computed, not generated (CN-688). A question shaped like "impact of changing X", "what breaks if I change X" or "who calls X", where X resolves in the relations sidecar, runs impact_analysis / find_callers directly; the result is handed to the model as facts it must not contradict and shown verbatim under the answer in a Call graph (computed, not generated) block. Any other question, or a symbol the graph does not know, takes the plain retrieval path.

Crawler

Observe and control the web crawler (only meaningful when CRAWLER_ENABLED).

  • Operate — the CRAWLER_ENABLED switch (usable while disabled — it's how you turn the crawler on), start a crawl (local/dev/prod, full/changed), and cancel a running one.
  • Base URLs — the configured per-environment seed/API URLs (read-only; set via CRAWL_<ENV>_BASE_URL in .env).
  • Recent runs — a table of crawl runs (env, status, started, URLs, errors) plus an errors-per-run trend chart. Click a run for detail: status, counts, an error breakdown by type, and individual error rows.
  • Forensics:
  • Compare runs — diff two runs to surface regressions and fixes (e.g. after a deploy).
  • Search — semantic search over crawl history in three modes: errors, URLs, referrers (which pages link to a URL), with an optional env filter.
  • Safety scanner — view the crawl-action safety report (total/blocked/safe), run a fresh scan, filter by risk, and toggle allow/block overrides per action.

Semantic code search, using the exact same engine the AI assistant queries over MCP.

  • Code search — natural-language query with an optional codebase filter and exclude-paths / exclude-languages filters. Results render as ranked cards (file, lines, language, chunk type, score) with the raw engine payload — exactly what the assistant receives — shown beneath.
  • Search aliases — list/add/delete query-expansion aliases per scope (global or a specific codebase's manual tier). Changes apply live (no restart), so you can tune an alias and re-run the search above to see its effect.
  • Search tuning — min similarity (floor over retrieval cosine similarity; applies only when no reranker scores the query11; 0 = off, the default), min rerank score, reranker model, max response chars. These are the ranking gates for every search (UI and MCP); save and re-run the query above to see the effect.

Graph

A neighborhood explorer for the code graph — pick a seed symbol and see its callers and callees as an interactive node-link diagram.1

  • Explore — choose a codebase, type a symbol or Class::method seed, pick a direction (callers / callees / both) and hop depth (1–4), and hit Explore. Nodes are colored by kind (function/method, class, file, external) and labelled. Edges are directed calls (solid, arrowed) plus contains edges (class↔method, dashed/faint) derived read-side from the symbol table.2 A legend maps the node-kind colors and both edge styles. The result is bounded (capped node count, flagged when truncated) so even large codebases stay legible.
  • Inspect, expand & jump-to-source7 — nodes are sized by degree (from the node_metrics sidecar6, so hubs render bigger). Click any node for an inspector panel showing its kind, file:line, its degree and PageRank, and caller/callee fan-out within the current view, with two actions: Expand (pull that node's 1-hop neighborhood and merge it into the live graph in place, no full redraw) and Open source (a secret-redacted, path-safe peek at the file around the symbol's line — served by a new /ui/api/source route that reuses the read_file tool's guards). Degrades gracefully: on an index that predates the centrality sidecar6, node sizes are uniform until a reindex/backfill.
  • Backed by the precomputed call-graph sidecar (relations_{codebase}.db) — the same structural data behind the find_callers / impact_analysis MCP tools. Rendered client-side with vendored Cytoscape.js (no build step, no CDN). On-box only; nothing leaves the host.

Security

The scanner's operational surface8: what the security scanner has seen, blocked, and tagged — and the levers to manage it. This is the console for the one scanner shared across the whole product — indexing, search's retrieval gate, the crawler, and the wiki's compile gates all run through it; it is not specific to any single feature. Two distinct populations, labelled in-UI: blocked files never entered the index (they have a scan-ledger row, a quarantine ref, and a report); flagged chunks are in the index — dry-run would-blocks and tag-only medium hits — and the retrieval gate keeps them out of search results ("indexed but never returned").

  • Status strip + dry-run banner — rules loaded by severity, pattern version, auto-merged rule count, scan-ledger totals, retrieval-flag count, and a PII hits counter (chunks with redacted PII, per rule; shows "redaction disabled" when the redaction master switch is off). When SCANNER_DRY_RUN is on, a persistent red banner says so: the scanner tags threats but never blocks. (Dry-run is currently the shipped default — the banner is how you notice.)
  • Flagged chunks — per-codebase browser over the index's safety_flag/threat_ids columns, grouped by file with rule chips and severity roll-ups; each chunk badges would-block vs tag-only with a 500-char preview. One-click Allowlist file (hashes the live file server-side with the scanner's own helper). An index that predates the flag columns gets a "run a full reindex" notice instead of a false empty state. Chunks whose only hits are PII redactions are not threats and don't appear here — the PII counter owns them (a chunk in an otherwise-flagged file stays listed, since it is retrieval-suppressed).
  • Blocked & quarantined — scan-ledger blocked rows joined to data/quarantine/*.blocked refs by hash (stale rows badged when the ref is missing; orphaned refs listed separately), the per-block threat report rendered inline (escaped text — reports embed matched attack content), and Release (allowlists the hash + removes its refs; the report stays for audit; takes effect at next reindex). The last 50 retrieval-flag log lines sit below.
  • Allowlist — list/add/remove SHA-256 entries; adds require a reason and record a timestamp (entries are audit objects, not bare hashes; legacy bare-hash lists still load). Changes apply live — no restart.
  • Scanner settings + rules — the 10 SCANNER_* keys (dry-run, LLM escalation + Ollama endpoint/model, suspicion threshold, redaction toggles, the two PII group toggles, minified-JS line cap) editable with typed validation and immediate effect; below, the full rule set read-only (id, category, severity, scope, truncated pattern) with an auto-merged badge for rules delivered by the boot-time rules merge.
  • Policy changes — the last 50 redaction-policy changes, newest first: every scanner-settings save that changed a SCANNER_* value, and every runtime edit of secret_rules.json (hot-reload logs old/new rule count and pattern version). Auditors ask "who changed the redaction policy and when" — this list is the answer.
  • Scan tester (collapsed) — paste text, pick a language, see exactly what the scanner would do: would-block / tag-only / clean, the matching rules, and the heuristic score. Verdicts reflect production semantics even under dry-run (the note tells you it would block with blocking enabled). Input capped at 64 KB; nothing is written.

Wiki

LLM-compiled knowledge pages over a codebase's own indexed documents — repo .md/.txt and ingested PDF/DOCX from DOCS_DIR — never code.9 Two panes:

  • Page list — every compiled page for the selected codebase, filterable by free text and by kind (C concept / E entity badge). Selecting a page loads it in the article pane.
  • Article — rendered markdown (vendored marked + dompurify, sanitized client-side — no raw HTML from the LLM reaches the DOM) with a Sources provenance section rendered as chips, one per contributing document, each carrying a locator back to where the claim came from (line N for markdown/text, p.N for PDF, §N for DOCX).
  • Staleness badge — next to the codebase selector: N document(s) changed since last compile, counting docs changed/added/removed against the manifest's last run. Blank while a compile is running (the count would be stale mid-flight) and cached 30s otherwise, since the SPA polls compile status every 3s.
  • Compile pre-flight — clicking Compile wiki… first calls GET /ui/api/wiki/estimate, never POST /ui/api/wiki/compile directly: doc count and docs-changed-since-last-compile, an estimated LLM-call count against the WIKI_MAX_LLM_CALLS cap, an estimated page count against WIKI_MAX_PAGES, and the configured LLM provider + model, flagged local or remote. A remote provider (claude/openai/gemini/openrouter — document text would leave the box) shows an explicit warning and requires a second confirming click before compile is called; a local provider (ollama, etc.) has nothing to confirm. Compile and reindex are mutually exclusive — each refuses to start while the other is running (single-flight both ways) — and a Cancel button stops the job before its next LLM call, keeping pages already written.

Two gates, both deliberately ignoring SCANNER_DRY_RUN — dry-run means "index it but tag it," and that must never extend to "feed known prompt-injection to the compile LLM" or "write a page an agent will read over MCP": - Input gate — scan_content(scope="doc") scans every source document before any LLM call. A blocked document is never sent to extraction; a per-document block report (naming the doc hash and the matched rule(s)) lands in the same reports store the Security screen lists. - Output gate — scan_content(scope="wiki") scans every synthesized page before it is written to disk. A blocked page is dropped and counted, never partially written. - Recovery loop — the same pattern as the Security screen: open the block report for the doc hash → Security screen → Allowlist → add the hash with a reason → recompile the wiki. A recompile always re-gates a previously-blocked document even when its content is otherwise unchanged, so allowlisting is guaranteed to take effect on the next run rather than being skipped as "unchanged."

Caps — WIKI_MAX_LLM_CALLS (default 500) hard-stops the whole compile once spent, keeping whatever pages were already written rather than discarding the run; WIKI_MAX_PAGES (default 150) stops new pages from being created once the manifest already holds that many (pages already tracked still re-synthesize when their source docs change). Both are settings.json/env keys, editable on the Settings screen.

Small-VRAM GPUs + a local LLM10 — the compile makes no embedder calls, so on a card under ~17 GiB the server unloads its resident embedder at compile start and evicts any resident Ollama models, letting the compile's LLM load fully on the GPU (without this, an 11 GiB card ran an 8B model mostly on CPU because the embedder held ~8 GiB). The embedder reloads automatically on the next search — a one-time ~30 s wait, after which the usual reindex-time hand-back4 reclaims the card from Ollama. Remote providers don't trigger any of this (they don't use the local GPU). Opt out with CODENAVI_WIKI_FREE_EMBEDDER=0.

Isolation invariants: - Pages are files under data/wiki/, generated by an LLM from retrieved document text. They are never embedded into search and never surface from semantic_search / grep_search. - data/wiki is a reserved path hard-excluded from indexing at the single file-scan chokepoint, checked before and independent of the operator-editable EXCLUDE_DIRS / INDEX_INCLUDE_OVERRIDES — so wiki output can never re-enter the index and be recursively summarized into itself. - Each codebase's wiki compiles only from its own docs. Repo docs are already per-codebase by construction; external PDF/DOCX follow the same DOCS_DIR/<codebase>/ subfolder convention as reindex_docs (see setup.md) — when that subfolder exists it is scanned instead of the flat DOCS_DIR root, so on a multi-codebase server one project's PDFs can't leak into another project's wiki. The flat layout remains a legacy fallback (logged warning) for single-codebase installs.

Quality filter (≥2 sources) — the page list surfaces only pages synthesized from ≥ WIKI_MIN_SOURCES source documents (default 2). Dogfooding showed the value concentrates in these multi-source pages; single-source pages just restate one document and are mostly noise on dense corpora. The list header shows how many single-source pages are hidden with a one-click show all toggle (adds ?min_sources=1); the floor is also a settings.json/env key. The filter is presentation-only and reversible — hidden pages still exist on disk and open by direct slug, and the wiki_pages MCP tool applies the same default with a note on how many were hidden.

MCP tools — two read-only tools mirror the screen for agents: wiki_pages(codebase) lists compiled pages (slug, title, kind) — the multi-source ones by default; wiki_page(slug, codebase) returns one page's rendered markdown, including its Sources section. Both only read already-compiled output — neither triggers a compile.

Settings

Every UI-editable runtime setting in one place12, grouped into cards (Search, Chat LLM, Scanner, Crawler, Wiki, Hooks) with a per-setting description and toggle switches for booleans. The per-screen quick controls above stay where they are — this screen and those forms write through the same whitelisted /ui/api/settings endpoint, so there is no second source of truth. Save changes posts only the keys changed since the screen loaded, so an untouched empty field (webhook URL, scanner overrides — where empty means "disabled"/"no override") is never clobbered. When the provider is openai, a Base URL (OpenAI-compatible) row appears for pointing the slot at DeepSeek/Groq/Mistral-style APIs (blank = api.openai.com). A Temperature row (0–2, blank = provider default; Claude caps at 1) controls chat sampling for every provider — set it low for repeatable demo answers. Deliberately absent: embedding providers (changing one invalidates all vectors — server-side edit only, see System → Configuration above) and API keys (env-only, never in settings.json).


How it relates to the MCP tools

The web UI is a thin presentation layer over the same functionality exposed to agents via MCP. Reindex, crawl, chat, doc ingestion, alias reload, feedback, and backup all map to existing tools/functions documented in api-reference.md; the UI just calls them over /ui/api/* and renders the results. Everything the UI does, an agent can do through MCP — and vice versa.

Dev notes

  • Frontend lives in interfaces/spa/ (index.html, app.js, style.css, vendor/). Editing those is live on browser refresh (static files; no restart).
  • The read-only data layer is interfaces/ui_api.py; the routes are @mcp.custom_route("/ui/...") handlers in server.py. Editing those needs a container restart (the module is import-cached).
  • All /ui/api/* routes are bearer-gated when auth is on and are matched before the /ui/{path:path} static catch-all.
  • The Graph screen calls /api/v1/graph/neighborhood — a versioned, headless-usable API surface (distinct from the UI-only /ui/api/* backend), with the same bearer auth. Its logic lives in core/graph.py, so the same engine can later back an MCP tool or a REST client without a rewrite. Node-level centrality (degree/pagerank) rides on each neighborhood node6; the inspector's Open source peek uses the UI-only /ui/api/source route7 (interfaces/ui_api.py::read_source).

  1. Vendor change reference CN-217. ↩

  2. Vendor change reference CN-218. ↩

  3. Vendor change reference CN-233. ↩↩↩↩

  4. Vendor change reference CN-301. ↩

  5. Vendor change reference CN-315. ↩

  6. Vendor change reference CN-337. ↩↩↩

  7. Vendor change reference CN-338. ↩↩

  8. Vendor change reference CN-365. ↩

  9. Vendor change reference CN-367. ↩

  10. Vendor change reference CN-371. ↩

  11. Vendor change reference CN-397. ↩

  12. Vendor change reference CN-534. ↩