221 lines
15 KiB
Markdown
221 lines
15 KiB
Markdown
# 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
|
|
|
|
```ts
|
|
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 (2 s, then 10 s if that timed out: a hotspot is slow, not absent)
|
|
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"
|
|
portable: boolean; // this launcher ships on an ODIAN drive: it runs that drive's
|
|
// install (any letter), the folder is fixed, no self-update
|
|
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
|
|
ai_url: string; // MCP serving /api/ai/... over the internet; "" = none
|
|
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 }
|
|
|
|
interface MarketItem {
|
|
key: string; // stable across versions: "tiles:<style>/<dataset>" | "src:<path>"
|
|
kind: "tiles" | "src";
|
|
title: string;
|
|
description: string;
|
|
bytes: number; // what installing it downloads, in full
|
|
required: boolean; // part of every install; never offered for removal
|
|
presence: "installed" | "partial" | "absent"; // partial: a download that stopped
|
|
selected: boolean; // chosen: installed with the version, kept by updates
|
|
}
|
|
|
|
interface Market {
|
|
version: string; // the installed version, else the DHP's current one
|
|
installed: boolean; // false: a choice waits for Install
|
|
core_bytes: number; // the app and database: what every install downloads besides the items
|
|
items: MarketItem[]; // required first
|
|
free_bytes: number | null; // free space where ODIAN is (or will be) installed
|
|
}
|
|
```
|
|
|
|
## 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, copying from an ODIAN drive (SSD) instead of the site; no sign-in needed. `path` is the drive's root (a pack store) or a folder holding an install, and every file copied is checked against the manifest. A tile set the PC already records as complete for the same index (`.blocks-done.json`) is left as it stands instead of being re-read: that record is written last, so a copy stopped halfway still reads as partial and resumes. The copy lands in the install folder of Settings, so a drive can install onto the PC it is plugged into; an install folder that is the drive's own ODIAN (or inside it) is refused, since there is nothing to copy, and so is a drive carrying an older ODIAN than the one installed there |
|
|
| `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 |
|
|
| `get_market` | `{ drive?: string }` | `Market` | the marketplace: every tile set and source file of the installed version (read from disk), or of the DHP's current one before the first install. With `drive`, what that drive carries instead (what it cannot install is left out), which is how the UI asks before installing from a drive |
|
|
| `apply_market` | `{ selected: string[] }` | `void` | saves the choice of optional items (keys), then applies it to the installed version: unchosen items are removed from the PC, chosen missing ones download (phase `installing`, `install-progress` events). Refused while busy or ODIAN runs, and while an update is required; downloading needs the DHP. Before the first install it only saves the choice |
|
|
| `set_market` | `{ selected: string[], drive?: string }` | `void` | the choice of optional items (keys) for an install about to run; changes nothing installed. Without `drive` it is this PC's standing choice and is saved. With `drive` it filters that one copy and is NOT saved: leaving out a tile set the PC already holds must not read as dropping it, or the next `apply_market` would delete it |
|
|
| `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.
|
|
|
|
## Where a drive's launcher installs
|
|
|
|
A launcher that ships on an ODIAN drive (`site.json` with `"portable": true`) keeps its
|
|
settings beside the exe, so the site and the install folder travel with the drive. The
|
|
folder may be on the drive itself, and ODIAN then runs from the drive on any PC: it is
|
|
stored without its letter, because the next PC hands the drive a different one. It may
|
|
also be a folder on a disk of the PC in front of the operator, and the drive then installs
|
|
ODIAN onto that PC and leaves; that folder is stored whole. A drive's root and a network
|
|
share are refused: Postgres and the tiles need a real disk.
|
|
|
|
## Marketplace
|
|
|
|
The DHP decides what is optional: a manifest tile set or source file with
|
|
`"optional": true` (plus `title` and `description`, from `catalog.json` at assemble) is
|
|
left out of Install and listed by `get_market`. Everything else is part of every install.
|
|
A manifest from before the marketplace flags nothing, so the launcher applies the same rule
|
|
to it: the app, the database and the `dark` tile set come with every install and the rest is
|
|
offered, which is what makes copying an older version from a drive a choice rather than all
|
|
228 GB or nothing. A manifest that flags anything at all is the DHP's to decide and is taken
|
|
exactly as it stands. The choice is kept in the
|
|
install's state (`extras`) and survives updates: an update refreshes the chosen items and
|
|
nothing else. Installing from a drive asks the same way: the marketplace opens on what the
|
|
drive carries, `set_market` records the answer, and `start_install_from_drive` copies it. A PC installed before the marketplace counts what it has on disk as chosen,
|
|
so nothing is removed or downloaded again behind the operator's back.
|
|
|
|
## AI over the internet
|
|
|
|
The client is started with `ODIAN_OFFLINE=1`, which blanks the MCP address whatever the
|
|
environment holds: the AI features (destination prediction, chat, vision) answer "not
|
|
available offline" rather than hanging on a call that cannot arrive. An `ai_url` in Settings
|
|
says this PC has a line out: the flag is left off and that address is passed as
|
|
`ODIAN_MCP_API_URL`. Nothing else moves - the data, the tiles and the map stay this PC's own
|
|
local stack - and emptying the field puts the client back to wholly offline. The AI routes
|
|
need no token of their own; `site.json` may carry an `ai_url` so a site ships with one.
|
|
|
|
## Overlays out of this PC's database
|
|
|
|
The tile server draws its overlays (aeroways, data centres, gas, hospitals, military bases,
|
|
oil, places of worship, schools) from PostGIS. A site reads the extractor's SQLite files; a
|
|
PC has none of those, so the launcher names each layer in with the DSN of
|
|
the local database, whose snapshot carries the same tables. One key per layer is required:
|
|
the server looks for that key, then for a DSN under ITS OWN env name, and a layer it cannot
|
|
resolve draws nothing at all rather than failing loudly.
|
|
|
|
## Live mode
|
|
|
|
The DHP names where live mode forwards. `/v1/health` offers `engine_api_urls`, best first:
|
|
the address its own network reaches, then the one that answers from outside. The PC tries
|
|
the one it already uses, then that list in order, and keeps the first that answers - a
|
|
laptop is at the office in the morning and at home in the evening, and neither address
|
|
suits both. `engine_api_url` remains the single address a launcher that ignores the list
|
|
will use, so a site must make that one the address that works from anywhere. None of them
|
|
answering is not an error: the local API serves this PC's own data, as it does offline.
|
|
|
|
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 |
|