HAProxy UI is a SvelteKit application with an optional Tauri 2 shell. The important structural decision is that all outbound I/O is brokered: in desktop mode the webview never talks to the network, and never touches a secret.
Stack
SvelteKit 2 · Svelte 5 (runes) · TypeScript
Tailwind CSS 4 ·
shadcn-svelte/bits-uicomponents@xyflow/svelte— the graph canvasTanStack Query for server state · Zod for every schema
difffor the red/green configuration diffTauri 2 + Rust (
reqwest,git2,keyring) for the desktop buildVitest for unit tests · Playwright for end-to-end
The broker boundary
┌─────────────────┐ invoke() ┌────────────────────┐ outbound ┌──────────────────────┐│ Webview (UI) │ ─────────▶ │ Rust commands │ ─────────▶ │ Dataplane API ││ Svelte + flows │ ◀───────── │ (src-tauri/src) │ ◀───────── │ git / OS keychain │└─────────────────┘ events() └────────────────────┘ └──────────────────────┘
The frontend calls invoke('cmd', args); the backend pushes back with emit('event', payload). The four Rust command modules are:
Module | Responsibility | Backing library |
|---|---|---|
| All HTTP to the Dataplane API, with the token injected from the keychain |
|
| Read/write/delete per-instance API tokens |
|
| Instance list persistence | serde JSON |
|
|
|
Because the Dataplane request originates in Rust, the desktop build is not subject to the browser’s CORS constraints. It also means the bearer token is resolved inside Rust on each request and never crosses the webview boundary.
In browser mode the same UI runs against fetch() and localStorage. The client picks its transport at runtime by detecting the Tauri runtime — src/lib/api/client.ts dispatches to the Rust path when present.
Connection modes
Mode | Selected when | CORS | Notes |
|---|---|---|---|
Mock | Instance has no | n/a | In-memory fixture, 150 ms latency. The canvas is fully usable with no cluster. |
Vite proxy |
| None — same origin | Recommended for browser dev. The dev server forwards |
Direct |
| Requires | Production-style. Put both behind one reverse proxy, or configure the Dataplane. |
Tauri | Tauri runtime detected | Irrelevant | Request originates in Rust. Any reachable URL works. |
Client selection
createClient(instance) returns one of two implementations:
mockClient— returns a fixture after a 150 ms delay. No network.realClient— talks to the Dataplane v3 REST surface.
Failures are raised as HaproxyApiError, which carries a kind field so the UI can show the right remediation rather than a generic error:
| Meaning |
|---|---|
| 401 / 403 from the Dataplane |
| The live configuration moved underneath you. Split out from |
| 5xx, or another non-OK 4xx |
| Fetch failed or timed out |
| Response body was not JSON, or did not match the schema |
Source layout
src/lib/api/—assemble.ts,normalize.ts,toDataplane.ts,parseHaproxyCfg.ts,diff.ts,apply.ts,client.ts,types.ts(Zod),mocks.ts,v3.tssrc/lib/components/—Visualizer.svelte(canvas), the four inspectors,ImportSheet,DiffPanel,VersionHistorySheet,StatsDashboard, dialogssrc/lib/graph/— xyflow layout and node/edge stylingsrc/lib/nodes/—FrontendNode,BackendNode,ServerNoderendererssrc/lib/queries/— TanStack Query hooks bound tocreateClient()src/lib/drafts/— the in-memory working-draft storesrc-tauri/src/— Rust commands
Race safety
The node inspector carries a mutex (NodeInspector.mutex.ts). Concurrent edits to the same node queue rather than clobbering each other — which matters because the graph, the inspector and the draft store can all be writing in the same tick during a multi-field edit.
Graph configuration is fixed
Every @xyflow/svelte instance uses the same interaction config, and initial node positions in src/lib/graph/layout.ts are integer multiples of the 16px snap grid. This is not cosmetic — it is the contract that makes the canvas behave identically under finger, trackpad and mouse. See iPad Roadmap before changing it.
panOnDrag: [1, 2],zoomOnScroll: false,zoomOnPinch: true,panOnScroll: false,selectionOnDrag: false,selectionKeyCode: 'Meta',snapGrid: [16, 16]