Frontend map
The SeeKi UI is a small Svelte 5 application. It lives under frontend/src/, builds with Vite, and ends up as a handful of static files that the Rust binary serves itself. Everything starts at main.ts, which mounts App.svelte and steps out of the way.
Component tree
main.ts
• App.svelte # orchestrator — fetches, owns state, decides which tree to render
|
|–– SetupWizard.svelte # shown when GET /api/status returns mode: "setup"
| |–– SetupStep1Connection.svelte
| |–– SetupStep2Tables.svelte
| |–– SetupStep3Branding.svelte
| ‘–– SetupStep4Confirm.svelte
|
‘–– normal mode
|–– Sidebar.svelte # translucent chrome, brand, collapse toggle
| ‘–– TableList.svelte # the list of tables the user picks from
|–– Toolbar.svelte # search, filter toggle, column menu, export
| ‘–– ColumnDropdown.svelte
|–– TableHeader.svelte # table name, active sort chip, row count
|–– DataGrid.svelte # wraps RevoGrid — the hero surface
‘–– StatusBar.svelte # pagination, connection status
frontend/src/components/. App.svelte at the root owns every piece of state; children receive props and fire events.Boot sequence
From cold load to first grid render:
main.tsmountsApp.svelteinto#app.App.svelte’sonMountcallsfetchStatus(). If the server is in setup mode, the wizard renders and the data path is skipped.- In normal mode,
App.sveltecallsfetchTables()andfetchDisplayConfig()in parallel. - When the user picks a table,
App.sveltecallsfetchColumns(table)thenfetchRows(params)and passes the result down toDataGrid.
State lives at the top
There is no store, no context, no external state library. App.svelte is the single source of truth; every child receives what it needs as props. This is deliberate — the data flow is small enough that adding a store would cost more than it saves.
State is declared with Svelte 5 runes. A cut-down view of the orchestrator:
let tables: TableInfo[] = $state([]);
let selectedTable: string = $state('');
let columns: ColumnInfo[] = $state([]);
let queryResult: QueryResult | null = $state(null);
let sortState: SortState = $state({ column: null, direction: null });
let filters: FilterState = $state({});
let searchTerm: string = $state('');
let activeFilterCount = $derived(
Object.values(filters).filter((v) => v.trim().length > 0).length
);
let visibleColumns = $derived.by(
() => columns.filter((c) => columnVisibility[c.name] !== false)
);
Children receive these as props. When the user acts — types into search, clicks a sort header, changes a page — the child fires an event, App.svelte updates its state, a $derived or an $effect rebuilds the query params, and fetchRows runs again.
RevoGrid, the hero
DataGrid.svelte is a thin wrapper over RevoGrid. Its job is narrow: take visibleColumns and queryResult.rows, translate them into RevoGrid’s column and data arrays, and render. Three details matter:
- Columns are keyed by
name; widths persist per-table inlocalStorageunder a prefixed key. Yes/NoandNULLare cell templates, not CSS on raw text — the shape and label carry the meaning, not colour alone.- Sort headers are intercepted at the wrapper level and turned into events, so sorting goes through the server every time (no client-side reshuffle that would lie about the row count).
The API client
Every call to the backend goes through frontend/src/lib/api.ts. One helper, one shape, one set of timeouts.
// frontend/src/lib/api.ts
const DEFAULT_TIMEOUT_MS = 30_000;
const SETUP_TIMEOUT_MS = 60_000;
const USE_MOCK = import.meta.env.VITE_MOCK === 'true';
async function apiFetch<T>(path: string): Promise<T> {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT_MS);
// ... fetch, handle non-2xx, parse JSON, assertShape ...
}
Three details worth knowing:
- Timeout by default. Every request aborts after 30 seconds (60 for setup). A slow database never freezes the UI.
- Shape assertions. Responses are checked with
assertShapebefore they reach the renderers. If the server ships an unexpected shape, the user sees a readable error instead of a silent broken grid. - Mock mode. When
VITE_MOCK=true, the client routes to in-memory fixtures infrontend/src/lib/mock.ts. That is howjust dev-mockruns the UI with no backend at all — useful for component work and CI snapshots.
The lib/ folder
| File | Purpose |
|---|---|
lib/api.ts | The HTTP client. All network calls live here. |
lib/types.ts | Shared TypeScript shapes: TableInfo, ColumnInfo, QueryResult, SortState, FilterState. |
lib/constants.ts | Magic strings: localStorage keys, default page size. |
lib/data-grid.ts | Pure helpers used by DataGrid.svelte — formatting, column mapping, value rendering. |
lib/mock.ts | Fixture responses for VITE_MOCK=true. Mirrors the real API shape exactly. |
The .test.ts siblings are Vitest unit tests. Keep them next to the code they exercise.
Theming and tokens
frontend/src/theme/tokens.css is where the design tokens live — colours, spacing, typography, the --sk-* variables the whole UI reads. app.css wires those tokens to base element styles. Component styles go in the component file, scoped by Svelte, and reach for tokens through var(--sk-*). No raw hex, no per-component palette.
Where to go from here
- Architecture — the backend side of every
fetchcall. - Build & release — how
frontend/dist/ends up inside the Rust binary. - Data grid — the product surface these components serve.