8.3 KiB
UI ↔ backend contract
The Svelte UI (src/) talks to the Rust backend (src-tauri/) only through the
Tauri commands and events below. src/lib/backend.ts is the single place that
calls them. When the page runs outside Tauri (a plain browser, for design work),
it falls back to a mock with the same shapes.
All names are snake_case on the wire (serde defaults), and timestamps are Unix seconds.
Types
type Phase =
| "idle" // nothing running
| "checking" // probing the site
| "signing_in" // browser sign-in in progress
| "installing" // downloading + verifying a version
| "restoring" // loading the database snapshot into local Postgres
| "syncing" // exchanging operator data with the DHP (ODIAN is not running)
| "launching" // starting Postgres → API → tile server → Odian.exe
| "running" // Odian.exe is open
| "error"; // the last long operation failed; see `error`
interface Status {
online: boolean; // the site's update service answered /v1/health
site: { url: string; name: string | null };
operator: string | null; // last signed-in ODIAN username (cached; valid offline)
signed_in: boolean; // this PC remembers the operator's site sign-in until Sign out: the
// launcher renews the short Engine token by itself (launcher session)
installed: string | null; // installed version, e.g. "0.51.0"
available: string | null; // the site's CURRENT version (online only)
update_required: boolean; // online && installed !== null && available !== null && installed !== available
// (never true when nothing is installed: that is a first install)
can_launch: boolean; // installed && !update_required && phase is idle or error
phase: Phase;
error: string | null; // set with phase "error"; cleared by the next command or `refresh`
install_dir: string; // e.g. "D:\\ODIAN"
sync: SyncStatus | null; // counts from the last sync this session; null before one
live: boolean | null; // while ODIAN runs: true = its data comes from the DHP,
// false = from this PC. null when it is not running, or
// the DHP offers no Engine API to forward to
}
interface SyncStatus {
pending: number; // local edits the DHP has not taken yet
conflicts: number; // records both sides changed: the operator decides
rejected: number; // edits the DHP refused (another operator's record, ...)
waiting: number; // DHP changes this PC could not apply yet
}
interface SyncConflict {
tbl: string; // database table, e.g. "db_ground_poi"
key: Record<string, unknown>; // the record's key columns
kind: "conflict" | "rejected";
fields: string[]; // clashing fields; "*exists": one side deleted it; "*value": a whole-value clash
base: unknown; // the DHP value before this PC's edit
mine: unknown; // this PC's value (null = deleted here)
theirs: unknown; // the DHP's value (null = deleted there)
reason: string | null; // why the DHP refused it (rejected only)
}
interface Settings {
site_url: string; // e.g. "http://192.168.1.41:30097"
mcp_url: string; // site MCP, for Keycloak sign-in
install_dir: string;
}
interface InstallProgress {
stage: "manifest" | "app" | "db" | "src" | "tiles" | "restore" | "finalize";
stage_index: number; // 1-based
stage_count: number;
done_bytes: number; // within the current stage
total_bytes: number; // of the current stage; 0 when unknown
done_items: number; // within the current stage
total_items: number;
bytes_per_sec: number;
message: string | null; // e.g. "tiles dark/protomaps_20260423: 412/1402 blocks"
}
interface StackEvent {
component: "postgres" | "api" | "tiles" | "odian";
state: "starting" | "ready" | "stopped" | "failed";
message: string | null;
}
interface LogEvent { level: "info" | "warn" | "error"; message: string; at: number }
Commands (invoke)
| Command | Args | Returns | Notes |
|---|---|---|---|
get_status |
— | Status |
cheap; no network |
refresh |
— | Status |
re-probes the site (2 s timeout); also clears an error phase |
sign_in |
— | { username: string } |
opens the system browser (MCP desktop flow) and waits for the loopback callback. Rejects with "Sign-in was cancelled." after cancel_sign_in (phase returns to idle, not error). |
cancel_sign_in |
— | void |
abandons a sign-in that is waiting for the browser |
sign_out |
— | Status |
forgets the operator on this PC: the site sign-in, its launcher session (ended on the site when reachable) and the cached operator. Offline launch needs a new sign-in afterwards, so the UI confirms first |
get_notes |
{ version?: string } |
string |
Markdown. From the site when online; the cached copy of the installed version offline. Untrusted: the UI must sanitize. |
open_external |
{ url: string } |
void |
opens an http/https link in the system browser (links in patch notes); anything else is refused |
start_install |
— | void |
installs or updates to available; needs signed_in (the site checks the token); progress arrives as events |
start_install_from_drive |
{ path: string } |
void |
same, reading an ODIAN drive (SSD) instead of the site; no sign-in needed |
cancel_install |
— | void |
stops at the next file boundary; everything already verified is kept |
launch |
— | void |
starts the local stack; stack events follow |
stop |
— | void |
stops Odian.exe's stack cleanly |
get_stack |
— | StackEvent[] |
the latest event per component of the current or last launch (for a reloaded UI) |
get_settings |
— | Settings |
|
set_settings |
{ settings: Settings } |
Status |
|
pick_folder |
— | string | null |
native folder picker |
sync_now |
— | void |
pushes and pulls operator data now; only while ODIAN is stopped (phase syncing meanwhile) |
get_sync_conflicts |
— | SyncConflict[] |
what waits for a decision, oldest first; starts the local Postgres just for the read |
resolve_sync_conflict |
{ tbl, key, choice: "theirs" | "mine" | "value" | "dismiss", value?: unknown } |
SyncConflict[] |
records one decision and returns what is left. theirs takes the DHP's version; mine keeps this PC's (the next sync sends it); value keeps a field-by-field pick; dismiss keeps a refused edit on this PC only. Links and folder placements held with a record follow its decision |
Errors reject the promise with a string message.
Planned, not yet available: the size of available for the Install button, and
inspecting a drive (its version and size) before installing from it.
Live mode
When the DHP advertises an Engine API (engine_api_url in its /v1/health), the
launcher starts the local API pointed at it and keeps a token file fresh for it. ODIAN
always talks to the local API; while the DHP answers, that API forwards the shared-data
routes there, so operators see each other's work and tracks are live. The moment the DHP
stops answering, the same API serves this PC's own database instead — no restart, and the
request that discovers the outage is already served locally. Coming back, the launcher
first sends what was edited offline, then hands the token back and ODIAN is live again.
status.live says which of the two is happening.
Operator-data sync
When the installed version ships the sync schema (versions\<v>\sync\*.sql) and the
DHP is reachable, launch first syncs (push, then pull), then starts the stack. When
ODIAN closes, the launcher pushes that session's work. Offline, edits stay on this PC.
A sync that cannot finish is logged and never blocks the launch. Conflicts and refused
edits wait in the local database until resolve_sync_conflict.
Events (listen)
| Event | Payload | When |
|---|---|---|
status |
Status |
whenever any field changes |
install-progress |
InstallProgress |
at most ~10 per second while installing |
stack |
StackEvent |
on each component state change |
log |
LogEvent |
notable backend messages, for the activity panel |