Files
ODIAN_Launcher/docs/CONTRACT.md
T
Kelorastus 3fdfde7043 M4
2026-09-16 11:42:24 +02:00

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