// Browser-only stand-in for the Rust backend, so the hub can be designed with // `npm run dev`. Same shapes as docs/CONTRACT.md, driven by timers. // // ?scenario=first-run | update-required | ready | offline-installed | // offline-not-installed | installing | running | sync-conflicts | error // &fail=sign_in | install | restore | launch | notes make that flow fail // &speed=4 run timers 4x faster // ¬es=no-roadmap notes without a Roadmap // // Commands reject with a plain string, like Tauri does. import { mockNotes, type NotesVariant } from "./mock-notes"; import { MOCK_SCENARIOS, SIGN_IN_CANCELLED, type Backend, type InstallProgress, type ConflictChoice, type InstallStage, type LogEvent, type LogLevel, type Market, type MarketItem, type MockControls, type MockScenario, type Settings, type SignInResult, type StackComponent, type StackEvent, type Status, type SyncConflict, type SyncStatus, type Unlisten, } from "./types"; const CURRENT = "0.52.0"; const PREVIOUS = "0.51.0"; const OPERATOR = "m1op"; const SITE_URL = "http://192.168.1.41:30097"; const SITE_NAME = "DHP Alpha"; const MCP_URL = "http://192.168.1.41:30091"; const DEFAULT_DIR = "D:\\ODIAN"; const KB = 1024; const MB = 1024 ** 2; const GB = 1024 ** 3; const FAIL_POINTS = ["sign_in", "install", "restore", "launch", "notes"] as const; type FailPoint = (typeof FAIL_POINTS)[number]; interface StagePlan { stage: InstallStage; /** Simulated wall time; the reported speed stays realistic. */ seconds: number; bytes: number; items: number; bytesPerSec: number; message: (done: number, total: number) => string | null; } function installPlan(kind: "fresh" | "update", source: "site" | "drive"): StagePlan[] { const rate = source === "drive" ? 468 * MB : 112 * MB; const plan: StagePlan[] = [ { stage: "manifest", seconds: 0.8, bytes: 86 * KB, items: 1, bytesPerSec: source === "drive" ? 40 * MB : 2.4 * MB, message: () => `manifest ${CURRENT} (${source === "drive" ? "drive" : SITE_NAME})`, }, { stage: "app", seconds: 3, bytes: kind === "update" ? 96 * MB : 412 * MB, items: kind === "update" ? 138 : 1284, bytesPerSec: rate, message: (d, t) => `app: ${d}/${t} files`, }, { stage: "db", seconds: 4, bytes: kind === "update" ? 2.3 * GB : 18.6 * GB, items: 1, bytesPerSec: rate, message: () => (kind === "update" ? "db odian_20260912.delta" : "db odian_20260912.dump"), }, ]; if (kind === "fresh") { plan.push( { stage: "src", seconds: 2.5, bytes: 3.4 * GB, items: 412, bytesPerSec: rate, message: (d, t) => `src: ${d}/${t} files`, }, { stage: "tiles", seconds: 9, bytes: 212 * GB, items: 1402, bytesPerSec: rate, message: (d, t) => `tiles dark/protomaps_20260423: ${d}/${t} blocks`, }, ); } plan.push( { stage: "restore", seconds: 5, bytes: 0, items: 231, bytesPerSec: 0, message: (d, t) => `pg_restore: ${d}/${t} tables`, }, { stage: "finalize", seconds: 1, bytes: 0, items: 0, bytesPerSec: 0, message: () => "writing install manifest", }, ); return plan; } const STAGE_DONE: Record = { manifest: "Manifest verified", app: "Application files verified", db: "Database snapshot verified", src: "Source data verified", tiles: "Map tiles verified", restore: "Database restored into local Postgres", finalize: "Install finalized", }; const POI = "poi_0192f3a1c4e87b5d9a2e6f10b3c4d5e6"; const TARGET = "tar_0192f3a2d91c7e4fa6b80c1d2e3f4a5b"; const AREA = "are_0192f3a3e0b1c2d3e4f5a6b7c8d9e0f1"; const MOCK_CONFLICTS: SyncConflict[] = [ { tbl: "db_ground_poi", key: { poi_id: POI }, kind: "conflict", fields: ["name", "color"], base: { poi_id: POI, name: "Relay hill", color: "red", lat: 48.61, lon: 2.35 }, mine: { poi_id: POI, name: "Relay hill (north slope)", color: "orange", lat: 48.61, lon: 2.35 }, theirs: { poi_id: POI, name: "Relay hill, confirmed", color: "yellow", lat: 48.61, lon: 2.35 }, reason: null, }, { tbl: "db_ground_target", key: { target_id: TARGET }, kind: "conflict", fields: ["*exists"], base: { target_id: TARGET, name: "Bridge 14", status_id: null }, mine: null, theirs: { target_id: TARGET, name: "Bridge 14", status_id: "st_damaged" }, reason: null, }, { tbl: "db_ground_area", key: { area_id: AREA }, kind: "rejected", fields: [], base: null, mine: { area_id: AREA, name: "NFA Delta (wider)" }, theirs: { area_id: AREA, name: "NFA Delta" }, reason: "only the owner can change or delete this", }, ]; type Payloads = { status: Status; "install-progress": InstallProgress; stack: StackEvent; log: LogEvent; }; type Channel = keyof Payloads; type Listeners = { [K in Channel]: Set<(payload: Payloads[K]) => void> }; const now = () => Math.floor(Date.now() / 1000); const busy = "Busy: wait for the current task to finish."; /** The 0.61.0 catalogue with real sizes: what the marketplace lists in the browser mock. */ const MOCK_MARKET: Omit[] = [ { key: "tiles:dark/protomaps_20260423", kind: "tiles", title: "Dark map", bytes: 2_437_523_419, required: true, description: "The standard ODIAN basemap: roads, water and borders in the dark style, as pre-drawn at the office. Every install carries it." }, { key: "tiles:dark_v2/protomaps_20260423", kind: "tiles", title: "Dark V2 map", bytes: 1_649_512_829, required: false, description: "The newer dark basemap, with buildings and forests. It is the Theatre of Operations' default ground: without it (or the Map source) the Theatre shows relief shading until Dark is picked." }, { key: "tiles:satellite_hd/protomaps_20260423", kind: "tiles", title: "World satellite imagery", bytes: 5_660_999_147, required: false, description: "Photographic basemap of the whole world, for the map and the Theatre of Operations. It covers what the office pre-cached: this PC cannot fetch more imagery offline." }, { key: "tiles:orthophotos/protomaps_20260423", kind: "tiles", title: "France orthophotos", bytes: 6_713_284_879, required: false, description: "High-resolution aerial photographs of France, much sharper than the world imagery over French territory." }, { key: "tiles:heightmap/planet_heightmap", kind: "tiles", title: "Terrain elevation", bytes: 74_367_050_377, required: false, description: "The world height map: real 3D relief for the globe and the Theatre of Operations, and ground elevation wherever ODIAN reads it. Without it the ground is flat." }, { key: "src:osm_source/world_aeroway_v1.sqlite", kind: "src", title: "Aeroway source data (OSM)", bytes: 884_244_480, required: false, description: "Raw OpenStreetMap airports and runways. ODIAN reads aeroways from its own database, so this is only needed to rebuild that layer." }, { key: "src:protomaps_20260423.pmtiles", kind: "src", title: "Map source (world, vector)", bytes: 135_176_708_563, required: false, description: "The vector map data the Dark and Dark V2 basemaps are drawn from. With it this PC draws any tile it lacks, anywhere and at any zoom: the detailed ground the Theatre of Operations needs offline. Without it only the pre-drawn tiles show." }, ]; const MOCK_CORE_BYTES = 2_275_409_041 + 15_356_491_648; function pick(value: string | null, allowed: readonly T[]): T | null { return value !== null && (allowed as readonly string[]).includes(value) ? (value as T) : null; } /** Enforces the contract's derived fields. */ function derive(s: Status): Status { const available = s.online ? (s.available ?? CURRENT) : null; const update_required = s.online && s.installed !== null && available !== null && s.installed !== available; return { ...s, available, signed_in: s.online && s.signed_in, update_required, can_launch: s.installed !== null && !update_required && s.phase === "idle", }; } function initialStatus(scenario: MockScenario): Status { const s: Status = { online: true, site: { url: SITE_URL, name: SITE_NAME }, operator: null, signed_in: false, installed: null, available: CURRENT, update_required: false, can_launch: false, phase: "idle", error: null, install_dir: DEFAULT_DIR, portable: false, sync: null, live: null, map_from_site: null, map_problem: null, }; switch (scenario) { case "first-run": break; case "update-required": Object.assign(s, { installed: PREVIOUS, operator: OPERATOR, signed_in: true }); break; case "ready": Object.assign(s, { installed: CURRENT, operator: OPERATOR, signed_in: true }); break; case "offline-installed": Object.assign(s, { online: false, installed: PREVIOUS, operator: OPERATOR }); break; case "offline-not-installed": Object.assign(s, { online: false }); break; case "installing": Object.assign(s, { operator: OPERATOR, signed_in: true, phase: "installing" }); break; case "running": Object.assign(s, { installed: CURRENT, operator: OPERATOR, signed_in: true, phase: "running", live: true }); break; case "sync-conflicts": Object.assign(s, { installed: CURRENT, operator: OPERATOR, signed_in: true, sync: { pending: 1, conflicts: 2, rejected: 1, waiting: 0 }, }); break; case "error": Object.assign(s, { operator: OPERATOR, signed_in: true, phase: "error", error: "Tile block 0731 failed verification (sha256 mismatch). Everything already verified was kept; retry to resume.", }); break; } return derive(s); } class MockBackend implements Backend { readonly kind = "mock" as const; readonly mock: MockControls; readonly window = { minimize: async () => this.log("info", "Minimize pressed (browser mock)"), toggleMaximize: async () => this.log("info", "Maximize pressed (browser mock)"), close: async () => this.log("info", "Close pressed (browser mock: the tab stays open)"), isMaximized: async () => false, onResized: async (): Promise => () => {}, }; private status: Status; private settings: Settings; private network: boolean; private job = 0; private booted = false; private signInCancelled = false; /** What waits for a decision in the mock "local database". */ private conflicts: SyncConflict[]; /** Marketplace: the optional items chosen, and what is on the mock PC. */ private marketChosen = new Set(); private readonly marketPresent = new Map(); /** Latest event per component, as `get_stack` reports it. */ private readonly stackStates = new Map(); private readonly listeners: Listeners = { status: new Set(), "install-progress": new Set(), stack: new Set(), log: new Set(), }; constructor( private readonly scenario: MockScenario, private readonly fail: FailPoint | null, private readonly speed: number, private readonly notesVariant: NotesVariant, ) { this.status = initialStatus(scenario); this.conflicts = scenario === "sync-conflicts" ? structuredClone(MOCK_CONFLICTS) : []; this.network = this.status.online; this.settings = { site_url: SITE_URL, mcp_url: MCP_URL, ai_url: "", install_dir: DEFAULT_DIR }; if (this.status.installed) { // An installed mock PC has the basemap, Dark V2 and a stopped satellite download. for (const item of MOCK_MARKET) if (item.required) this.marketPresent.set(item.key, "installed"); this.marketPresent.set("tiles:dark_v2/protomaps_20260423", "installed"); this.marketPresent.set("tiles:satellite_hd/protomaps_20260423", "partial"); this.marketChosen = new Set(["tiles:dark_v2/protomaps_20260423", "tiles:satellite_hd/protomaps_20260423"]); } const self = this; this.mock = { get scenario() { return self.scenario; }, get online() { return self.network; }, setOnline(online: boolean) { self.network = online; self.log(online ? "info" : "warn", online ? "Mock: network restored" : "Mock: network cable pulled"); void self.refresh(); }, }; } // ── plumbing ──────────────────────────────────────────────────────────── private emit(channel: K, payload: Payloads[K]) { if (channel === "stack") { const event = payload as StackEvent; this.stackStates.set(event.component, structuredClone(event)); } for (const handler of this.listeners[channel]) handler(structuredClone(payload)); } private listen(channel: K, handler: (payload: Payloads[K]) => void): Promise { this.listeners[channel].add(handler); return Promise.resolve(() => void this.listeners[channel].delete(handler)); } private commit(patch: Partial) { this.status = derive({ ...this.status, ...patch }); this.emit("status", this.status); } private log(level: LogLevel, message: string, at = now()) { this.emit("log", { level, message, at }); } private wait(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms / this.speed)); } private snapshot(): Status { return structuredClone(this.status); } /** Scenario side effects start on the first get_status, once the UI listens. */ private boot() { if (this.booted) return; this.booted = true; const t = now(); const s = this.status; this.log("info", "ODIAN Launcher started", t - 6); this.log( s.online ? "info" : "warn", s.online ? `${SITE_NAME} answered /v1/health (current version ${CURRENT})` : `${SITE_URL} unreachable (2 s timeout): working offline`, t - 5, ); if (s.update_required) { this.log("warn", `Installed ${s.installed} is behind the DHP (${CURRENT}): update required before launch`, t - 4); } if (this.scenario === "running") this.log("info", "Odian.exe is running (pid 14212)", t - 3); if (this.scenario === "error" && s.error) this.log("error", s.error, t - 2); if (this.scenario === "installing") { setTimeout(() => void this.runInstall("fresh", "site", undefined, { index: 4, fraction: 0.27 }), 300); } } // ── commands ──────────────────────────────────────────────────────────── async getStatus(): Promise { this.boot(); return this.snapshot(); } async refresh(): Promise { const wasIdle = this.status.phase === "idle"; const wasOnline = this.status.online; if (wasIdle) this.commit({ phase: "checking" }); await this.wait(this.network ? 450 : 2000); const online = this.network; this.commit({ online, phase: wasIdle && this.status.phase === "checking" ? "idle" : this.status.phase, }); if (online !== wasOnline) { this.log( online ? "info" : "warn", online ? `${SITE_NAME} is reachable again (current version ${CURRENT})` : `${SITE_NAME} unreachable: working offline`, ); if (online && this.status.update_required) { this.log("warn", `Update to ${CURRENT} required before launch while connected`); } } return this.snapshot(); } async signIn(): Promise { if (!this.status.online) throw "Your DHP is unreachable. Connect to the DHP network to sign in."; if (this.status.phase !== "idle") throw busy; this.commit({ phase: "signing_in", error: null }); this.signInCancelled = false; this.log("info", "Opened the system browser for Keycloak sign-in"); // Like the real backend, a cancel ends the wait at once instead of after the browser timeout. for (let waited = 0; waited < 2400 && !this.signInCancelled; waited += 100) await this.wait(100); if (this.signInCancelled) { this.signInCancelled = false; throw SIGN_IN_CANCELLED; } // Re-read after the await: settings or network toggles may have changed the phase. if ((this.status.phase as string) !== "signing_in") throw "Sign-in was interrupted."; if (this.fail === "sign_in") { const message = "Sign-in timed out: the browser never called back to the launcher."; this.commit({ phase: "idle", error: message }); throw message; } this.commit({ phase: "idle", signed_in: true, operator: OPERATOR }); this.log("info", `Signed in as ${OPERATOR}`); return { username: OPERATOR }; } async cancelSignIn(): Promise { if (this.status.phase !== "signing_in") return; this.signInCancelled = true; this.commit({ phase: "idle" }); this.log("info", "Sign-in cancelled"); } async signOut(): Promise { this.commit({ signed_in: false, operator: null }); this.log("info", "Signed out: this PC no longer remembers the operator"); return this.snapshot(); } async openExternal(url: string): Promise { if (!/^https?:\/\//i.test(url)) throw "Only web links can be opened."; this.log("info", `Opened ${url} in the system browser (browser mock: new tab)`); window.open(url, "_blank", "noopener,noreferrer"); } async getStack(): Promise { if (this.scenario === "running" && this.stackStates.size === 0) { for (const component of ["postgres", "api", "tiles", "odian"] as const) { this.stackStates.set(component, { component, state: "ready", message: null }); } } return [...this.stackStates.values()].map((event) => structuredClone(event)); } async getNotes(version?: string): Promise { await this.wait(380); if (this.fail === "notes") throw "Could not load patch notes: the DHP answered HTTP 502."; if (!this.status.online) { if (!this.status.installed) throw "No cached notes: ODIAN is not installed on this machine."; return mockNotes(this.status.installed, this.notesVariant); } return mockNotes(version ?? this.status.available ?? CURRENT, this.notesVariant); } async startInstall(): Promise { const s = this.status; if (!s.online) throw "Your DHP is unreachable. Connect to install, or install from a drive."; if (!s.signed_in) throw "Sign in first: downloading from the DHP needs your DHP sign-in."; if (s.phase !== "idle" && s.phase !== "error") throw busy; const kind = s.installed && s.installed !== s.available ? "update" : "fresh"; void this.runInstall(kind, "site"); } async startInstallFromDrive(path: string): Promise { const s = this.status; if (s.phase !== "idle" && s.phase !== "error") throw busy; if (!path.trim()) throw "Pick the root folder of an ODIAN drive."; const kind = s.installed && s.installed !== CURRENT ? "update" : "fresh"; void this.runInstall(kind, "drive", path); } async getMarket(drive?: string): Promise { await this.wait(250); if (drive !== undefined && !drive.trim()) throw "Pick the root folder of an ODIAN drive."; if (drive === undefined && !this.status.installed && !this.status.online) { throw "Connect to your DHP to see what it offers."; } return { version: this.status.installed ?? this.status.available ?? CURRENT, installed: this.status.installed !== null, core_bytes: MOCK_CORE_BYTES, items: MOCK_MARKET.map((item) => ({ ...item, presence: this.marketPresent.get(item.key) ?? "absent", selected: item.required || this.marketChosen.has(item.key), })), free_bytes: 297_100_000_000, }; } async setMarket(selected: string[], _drive?: string): Promise { const optional = new Set(MOCK_MARKET.filter((item) => !item.required).map((item) => item.key)); this.marketChosen = new Set(selected.filter((key) => optional.has(key))); } async applyMarket(selected: string[]): Promise { const s = this.status; if (s.phase !== "idle" && s.phase !== "error") throw busy; const optional = new Set(MOCK_MARKET.filter((item) => !item.required).map((item) => item.key)); this.marketChosen = new Set(selected.filter((key) => optional.has(key))); if (!s.installed) return; for (const item of MOCK_MARKET) { if (!item.required && !this.marketChosen.has(item.key) && this.marketPresent.has(item.key)) { this.marketPresent.delete(item.key); this.log("info", `Removed ${item.title}.`); } } const adds = MOCK_MARKET.filter((item) => this.marketChosen.has(item.key) && this.marketPresent.get(item.key) !== "installed"); if (adds.length === 0) return; if (!s.online) throw "Your DHP is unreachable: connect to download marketplace items."; void this.runMarketDownload(adds); } private async runMarketDownload(adds: typeof MOCK_MARKET) { const job = ++this.job; this.commit({ phase: "installing", error: null }); this.log("info", `Downloading ${adds.map((item) => item.title).join(", ")}`); const total = adds.reduce((sum, item) => sum + item.bytes, 0); const ticks = 30; for (let t = 0; t <= ticks; t++) { if (t > 0) await this.wait(100); if (job !== this.job) return; this.emit("install-progress", { stage: "tiles", stage_index: 2, stage_count: 2, done_bytes: Math.round((total * t) / ticks), total_bytes: total, done_items: t, total_items: ticks, bytes_per_sec: t < ticks ? 180_000_000 : 0, message: `tiles ${adds[0].title}: ${t}/${ticks} blocks`, }); } for (const item of adds) this.marketPresent.set(item.key, "installed"); this.commit({ phase: "idle" }); this.log("info", "The marketplace items are installed."); } async cancelInstall(): Promise { if (this.status.phase === "restoring") throw "The database restore cannot be cancelled once it has started."; if (this.status.phase !== "installing") throw "Nothing is installing."; this.job++; this.commit({ phase: "idle" }); this.log("warn", "Install cancelled at the next file boundary; everything already verified was kept"); } async launch(): Promise { const s = this.status; if (!s.can_launch) { throw s.update_required ? `Update to ${s.available} before launching while connected.` : "ODIAN is not ready to launch."; } void this.runLaunch(); } async stop(): Promise { const phase = this.status.phase; if (phase !== "running" && phase !== "launching") throw "ODIAN is not running."; void this.runStop(); } async getSettings(): Promise { return structuredClone(this.settings); } async setSettings(settings: Settings): Promise { // The AI address is optional: empty means the client stays offline. for (const [label, url] of ([ ["DHP URL", settings.site_url], ["MCP URL", settings.mcp_url], ...(settings.ai_url.trim() ? [["AI URL", settings.ai_url] as const] : []), ] as const)) { try { new URL(url); } catch { throw `${label} is not a valid URL: ${url}`; } } if (!settings.install_dir.trim()) throw "Install folder cannot be empty."; const siteChanged = settings.site_url !== this.settings.site_url; this.settings = structuredClone(settings); this.commit({ site: { url: settings.site_url, name: settings.site_url === SITE_URL ? SITE_NAME : null }, install_dir: settings.install_dir, }); this.log("info", "Settings saved"); if (siteChanged) void this.refresh(); return this.snapshot(); } private syncCounts(pending: number): SyncStatus { return { pending, conflicts: this.conflicts.filter((c) => c.kind === "conflict").length, rejected: this.conflicts.filter((c) => c.kind === "rejected").length, waiting: 0, }; } async syncNow(): Promise { if (!this.status.online) throw "Your DHP is unreachable: changes stay on this PC until the next connected sync."; if (this.status.phase !== "idle" && this.status.phase !== "error") throw "ODIAN is busy: sync once it has stopped."; const sent = this.status.sync?.pending ?? 0; this.commit({ phase: "syncing", error: null }); await this.wait(1400); const counts = this.syncCounts(0); this.commit({ phase: "idle", sync: counts }); this.log( "info", `Synced with the DHP: ${sent} change(s) sent, 4 received. 0 waiting to send, ${counts.conflicts} conflict(s), ${counts.rejected} refused.`, ); } async getSyncConflicts(): Promise { await this.wait(300); return structuredClone(this.conflicts); } async resolveSyncConflict( tbl: string, key: Record, choice: ConflictChoice, ): Promise { if (this.status.phase !== "idle" && this.status.phase !== "error") throw "ODIAN is busy: review sync once it has stopped."; const index = this.conflicts.findIndex((c) => c.tbl === tbl && JSON.stringify(c.key) === JSON.stringify(key)); if (index < 0) throw `Nothing to resolve for ${tbl}.`; const conflict = this.conflicts[index]; if (choice === "dismiss" && conflict.kind !== "rejected") throw `"dismiss" is not a valid decision for a conflict`; await this.wait(250); this.conflicts.splice(index, 1); const pending = (this.status.sync?.pending ?? 0) + (choice === "mine" || choice === "value" ? 1 : 0); this.commit({ sync: this.syncCounts(pending) }); const outcome = choice === "theirs" ? "took the DHP's version" : choice === "dismiss" ? "kept the refused change on this PC only" : "kept this PC's version; the next sync sends it"; this.log("info", `${tbl}: ${outcome}`); return structuredClone(this.conflicts); } async pickFolder(): Promise { await new Promise((resolve) => setTimeout(resolve, 450)); return "E:\\ODIAN"; } onStatus(handler: (status: Status) => void) { return this.listen("status", handler); } onInstallProgress(handler: (progress: InstallProgress) => void) { return this.listen("install-progress", handler); } onStack(handler: (event: StackEvent) => void) { return this.listen("stack", handler); } onLog(handler: (event: LogEvent) => void) { return this.listen("log", handler); } // ── flows ─────────────────────────────────────────────────────────────── private failFlow(message: string) { this.job++; this.commit({ phase: "error", error: message }); this.log("error", message); } private async runInstall( kind: "fresh" | "update", source: "site" | "drive", path?: string, resume?: { index: number; fraction: number }, ) { const job = ++this.job; const plan = installPlan(kind, source); this.commit({ phase: "installing", error: null }); if (!resume) { const verb = kind === "update" ? `Updating to ODIAN ${CURRENT}` : `Installing ODIAN ${CURRENT}`; this.log("info", source === "drive" ? `${verb} from drive ${path}` : `${verb} from ${SITE_NAME}`); } for (let i = resume?.index ?? 0; i < plan.length; i++) { const step = plan[i]; const phase = step.stage === "restore" ? "restoring" : "installing"; if (this.status.phase !== phase) this.commit({ phase }); const ticks = Math.max(1, Math.round(step.seconds * 10)); const first = resume && i === resume.index ? Math.floor(ticks * resume.fraction) : 0; for (let t = first; t <= ticks; t++) { if (t > first) await this.wait(100); if (job !== this.job) return; const fraction = t / ticks; if (source === "site" && !this.network) { return this.failFlow( `Connection to ${SITE_NAME} lost during ${step.stage}. Everything already verified was kept; retry when you are back online.`, ); } if (this.fail === "install" && step.stage === (kind === "fresh" ? "tiles" : "db") && fraction >= 0.62) { return this.failFlow( step.stage === "tiles" ? "Tile block 0731 failed verification (sha256 mismatch). Everything already verified was kept; retry to resume." : "odian_20260912.delta failed verification (sha256 mismatch). Retry to download it again.", ); } if (this.fail === "restore" && step.stage === "restore" && fraction >= 0.4) { return this.failFlow("pg_restore failed on table track_point: could not extend file (No space left on device)."); } const done = Math.round(step.items * fraction); const wobble = 1 + 0.07 * Math.sin(t / 6) + (Math.random() - 0.5) * 0.06; this.emit("install-progress", { stage: step.stage, stage_index: i + 1, stage_count: plan.length, done_bytes: Math.round(step.bytes * fraction), total_bytes: Math.round(step.bytes), done_items: done, total_items: step.items, bytes_per_sec: step.bytesPerSec > 0 && t < ticks ? Math.round(step.bytesPerSec * wobble) : 0, message: step.message(done, step.items), }); } this.log("info", STAGE_DONE[step.stage]); } if (job !== this.job) return; this.commit({ phase: "idle", installed: CURRENT }); this.log("info", `ODIAN ${CURRENT} is installed in ${this.status.install_dir}`); } private async runLaunch() { const job = ++this.job; this.commit({ phase: "launching", error: null }); this.log("info", `Starting the local stack for ODIAN ${this.status.installed}`); const steps: [StackComponent, number, string][] = [ ["postgres", 1300, "listening on 127.0.0.1:55432"], ["api", 1500, "healthy on 127.0.0.1:18080"], ["tiles", 900, "serving 3 tilesets"], ["odian", 1700, "Odian.exe pid 14212"], ]; for (const [component, ms, ready] of steps) { this.emit("stack", { component, state: "starting", message: null }); await this.wait(ms); if (job !== this.job) return; if (this.fail === "launch" && component === "api") { this.emit("stack", { component, state: "failed", message: "exit code 1" }); this.emit("stack", { component: "postgres", state: "stopped", message: null }); return this.failFlow("Engine API exited with code 1: bind 127.0.0.1:18080: address already in use."); } this.emit("stack", { component, state: "ready", message: ready }); this.log("info", `${component}: ${ready}`); } this.commit({ phase: "running" }); } private async runStop() { const job = ++this.job; this.log("info", "Stopping the local stack"); for (const component of ["odian", "tiles", "api", "postgres"] as const) { await this.wait(380); if (job !== this.job) return; this.emit("stack", { component, state: "stopped", message: null }); } this.commit({ phase: "idle" }); this.log("info", "Local stack stopped cleanly"); } } export function createMockBackend(params: URLSearchParams): Backend { const scenario = pick(params.get("scenario"), MOCK_SCENARIOS) ?? "first-run"; const fail = pick(params.get("fail"), FAIL_POINTS); const speed = Math.min(20, Math.max(0.25, Number(params.get("speed")) || 1)); const notes: NotesVariant = params.get("notes") === "no-roadmap" ? "no-roadmap" : "full"; return new MockBackend(scenario, fail, speed, notes); }