Two reasons an offline dev run's web client had no map: 1. Vite proxied every tile to the public tile host. web_command now sets ODIAN_TILE_SERVER_URL=http://127.0.0.1:18081 for the Vite it starts, and ODIAN_Engine's _WEB/vite.config.ts sends /basemap, /mvt and /netlayer there: the site while it answers, this PC's installed tiles when it does not - the same path the .exe already draws from. 2. The power-grid netlayer had no source offline. datasets.json named every PostGIS overlay but `powerv2_postgis`, so the tile server answered /netlayer/<ds>/powerv2/* with 404 "powerv2_render_sqlite not found" while postgis_power_entities / _links / _topology sat in the local snapshot. Added to POSTGIS_SOURCES; the tile server resolves it through the same ODIAN_TILE_POSTGIS_DSN as the others. Spot-check: with the site unreachable, /netlayer/<ds>/powerv2/bbox/8/128/86/131/89.json answers 200 (721064 B, the same body the site serves) and topology.bin 200.
ODIAN Launcher
The desktop launcher for ODIAN. It signs an operator in with their site's Keycloak account, installs ODIAN from the site (or from an ODIAN drive), forces an update before launch while the site is reachable, and runs ODIAN fully offline otherwise.
| Part | Where | Stack |
|---|---|---|
| Hub UI | src/ |
Svelte 5 + Vite, bundled fonts, no network requests of its own |
| Backend | src-tauri/ |
Tauri 2 (Rust): commands and events in docs/CONTRACT.md |
| Installer | crates/odian-packs/ |
Rust port of the pack format v1 (ODIAN_MCP_Server/src/odian_packs) |
Develop
npm install
npm run dev # the hub in a normal browser, with a mock backend (?scenario=…)
npm run tauri dev # the real launcher window
npm run check # svelte-check
cargo test --workspace # Rust tests (needs ~/.cargo/bin on PATH)
Build the Windows setup (NSIS):
npm run tauri build
How it finds the site
On first run the launcher reads site.json next to its executable and copies it
into its settings (%APPDATA%\fr.odian.launcher\settings.json). Operators can
change the addresses later in Settings. See site.example.json:
{ "name": "ODIAN test site", "site_url": "http://192.168.1.41:30097", "mcp_url": "http://192.168.1.41:30091" }
site_url: the site's update service (/v1/health, versions, packs).mcp_url: the site's MCP, which runs the Keycloak desktop sign-in (/auth/desktop/start,/auth/desktop/exchange).
On an operator PC
<install_dir>\ D:\ODIAN when a D: drive exists, else C:\ODIAN
versions\<v>\ app, API, tile server and runtime files for one version
data\db\<snapshot>\ database dumps restored into the local Postgres
data\tiles\<style>\<dataset>\ tiles
data\src\ source files
state\launcher.json install id, installed version, last operator
state\notes\<v>.md patch notes, for offline reading
The site token lives in Windows Credential Manager under "ODIAN Launcher", one entry per site URL. The install id is generated on the PC the first time it saves its state, so a cloned drive never carries one.
Dev mode
A developer's PC can run the whole stack out of git checkouts instead of the
installed version, so a code change is one Launch away without cutting a pack.
It is deliberately not a checkbox anyone can find: the Dev section of Settings
appears only on a PC holding a dev licence, an Ed25519 document signed with
the same key as a pack manifest and verified by the same code
(src-tauri/src/devmode.rs over odian_packs::signing::verify).
This repo carries one. dev-licence.json at the root is a team licence: it names no
machine and no install, and says so in its signed body ("scope": "team"). Whoever can
clone this repo can run dev mode — repo access is the credential, on purpose, because an
operator has the installed launcher and never the source.
- A launcher started from this repo (
npm run tauri dev) finds it by itself: the launcher looks beside its own exe and two levels up, which is the repo root. - For the launcher installed on your PC, run it once:
powershell -ExecutionPolicy Bypass -File tools\enable-dev-mode.ps1
It still expires, and it is still refused if the signature does not match the release key. A licence that simply forgot its binding is refused as unbound — only one that claims the team scope is let through, so the weaker rule can never be reached by accident.
For a stricter licence — a single PC, or a kit on one drive — mint one on the machine that has the signing key:
python -m odian_packs dev-licence --machine ODIAN-DEV-01 --expires 2027-01-31 --out licence.json
--machine is %COMPUTERNAME%, or use --install-id to bind it to one install
instead (the id in state\launcher.json). A licence with neither binding is
refused — an unbound one would work on every PC it is copied to.
The launcher looks in these places on every refresh, best first:
<checkout folder>\dev-licence.json— where a developer's kit lives, so the licence travels with the checkouts on the same drive.- Beside the running executable, and two levels above it — the repo root for a launcher built from source, which is what makes the committed team licence work with no setup.
<install_dir>\state\dev-licence.json— always supported, and the only place a PC with no licence yet can use: the Dev section is hidden until one is found, so there is no way to name a checkout folder before then. This is whereenable-dev-mode.ps1puts it.
It is signed and bound either way, so where it is read from changes nothing about what it permits. A refusal is logged once, naming the file it refused.
With a licence, Settings gains:
| Control | What it does |
|---|---|
| Checkout folder | The folder holding ODIAN_Engine (e.g. E:\ODIAN_Dev) |
| Launch the whole stack from the checkout | Runs the checkout's API, tile server and Odian.exe |
| Build it first, every launch | Runs _APIuild.bat then toolsuilduild.bat Release before launching |
| Run as | An operator name to run as, instead of the one signed in |
| Launch starts | ODIAN, the web client, or both |
Both is the one worth having. There is one Postgres, one API and one tile server either
way, so the .exe and the browser show the same rows from the same database at the same
moment. This codebase holds the two clients to each other in a dozen places — the satellite
propagator, the network graph, the track filters, the APP-6 placement and sizing — and a
divergence there is invisible in review and obvious the moment both are on screen.
The web client is the checkout's _WEB on Vite's own :5173, with hot reload, and the
launcher opens a browser at
http://localhost:5173/?api=http://127.0.0.1:18080&user=<operator> — ?api= because the
web's own default is localhost:8080 (the repo's api.bat), not this stack's API, and
?user= so both clients author as the same operator, a swapped one included. Its endpoint
guard already trusts any loopback port, so nothing in _WEB needed changing. On first run
"Build it first, every launch" also runs the web client's npm install, whenever
package.json or package-lock.json is newer than node_modules — so a dependency that
arrives with a pull is fetched, without paying for npm on every launch. (It has to be the
manifest times, not "does node_modules exist": a pull leaves the folder in place and Vite
then fails with Failed to resolve import "<name>", which reads like broken source.)
One thing is not local yet: _WEB/vite.config.ts proxies /basemap, /netlayer and
/mvt to https://tiles.odian.fr, so the browser's map comes from the internet while the
.exe beside it draws from this PC's tiles. The data — the part the two clients are held to
agree on — is local for both.
In dev mode every process gets its own console window — the whole point is
seeing the API's and the client's output — and the same database, tiles and
Postgres as an operator run, on the launcher's own ports (55432 / 18080 / 18081),
so a dev run and the repo's run-local.bat (8080) do not collide. Stop still
cuts everything: the job object owns the consoles too.
Run as switches the identity without Keycloak, because the local API mints
its own session from a shared secret and the launcher tells it who to be
(ODIAN_LAUNCH_USER). What it cannot do is carry that identity to the DHP — work
authored as someone else would arrive there under the account that signed in — so
while a different name is set, the launcher does not sync at all, and says so.
Typing your own name in changes nothing and costs nothing.
The local stack
launch starts four processes, all on 127.0.0.1, in this order, each waiting for the previous one:
| Process | Port | Working dir | Ready when |
|---|---|---|---|
Postgres (versions\<v>\runtime\pgsql) |
55432 | state\pg |
pg_ctl start -w returns |
Engine API (versions\<v>\api\odian_api.exe) |
18080 | state\api |
GET /health answers |
Tile server (versions\<v>\tile_server, runtime\python) |
18081 | state\tiles |
GET /health answers |
ODIAN (versions\<v>\Odian\x64\MinGW-Release\Odian.exe) |
— | state\client |
still running after 3 s |
- Every process starts from a clean environment (Windows basics plus the ODIAN
variables set in
src-tauri/src/stack.rs), without a console window, inside one Job Object, so nothing outlives the launcher. - Odian.exe and the API read and write relative to their working directory.
state\clienttherefore holds junctionsassetsandshadersinto the version folder, andversions\<v>\is never written. - The tile server runs cache-only (
ODIAN_TILE_CACHE_ONLY=1) from a generatedstate\tiles\datasets.json, and the client runs withODIAN_OFFLINE=1. - ODIAN signs in as the last operator through the local API's
/auth/mint. The mint secret and the Postgres password are generated on the PC and kept instate\launcher.json. - When Odian.exe closes, the rest stops in reverse order; Postgres stops with
pg_ctl stop -m fast.