ODIAN Launcher: hub, installer, local stack and remembered sign-in (M2)

- Tauri 2 + Svelte 5 hub in ODIAN dark: patch notes and roadmap, install and
  update progress, local stack status, settings with DHP and MCP addresses.
- crates/odian-packs: Rust installer for pack format v1 (signed manifests,
  resumable verified downloads from a DHP or an ODIAN drive), byte-compatible
  with ODIAN_MCP_Server's Python odian_packs.
- Local stack on 127.0.0.1: Postgres -> Engine API -> tile server -> Odian.exe,
  one Job Object, clean shutdown; database snapshot restore on a fresh PC only.
- Keycloak sign-in through the DHP's MCP, remembered until Sign out with a
  launcher session that renews the Engine token silently.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Kelorastus
2026-09-15 15:23:17 +02:00
commit b7f3061be3
102 changed files with 47611 additions and 0 deletions
+115
View File
@@ -0,0 +1,115 @@
//! Tauri commands: thin wrappers over `Launcher` (see docs/CONTRACT.md).
use tauri::{AppHandle, State};
use tauri_plugin_dialog::DialogExt;
use tauri_plugin_opener::OpenerExt;
use crate::launcher::{Launcher, Status};
use crate::settings::Settings;
use crate::stack::StackEvent;
type CmdResult<T> = Result<T, String>;
#[derive(serde::Serialize)]
pub struct SignedIn {
username: String,
}
#[tauri::command]
pub async fn get_status(launcher: State<'_, Launcher>) -> CmdResult<Status> {
Ok(launcher.status().await)
}
#[tauri::command]
pub async fn refresh(app: AppHandle, launcher: State<'_, Launcher>) -> CmdResult<Status> {
Ok(launcher.refresh(&app).await)
}
#[tauri::command]
pub async fn sign_in(app: AppHandle, launcher: State<'_, Launcher>) -> CmdResult<SignedIn> {
launcher.sign_in(&app).await.map(|username| SignedIn { username })
}
#[tauri::command]
pub async fn cancel_sign_in(launcher: State<'_, Launcher>) -> CmdResult<()> {
launcher.cancel_sign_in();
Ok(())
}
#[tauri::command]
pub async fn sign_out(app: AppHandle, launcher: State<'_, Launcher>) -> CmdResult<Status> {
Ok(launcher.sign_out(&app).await)
}
#[tauri::command]
pub async fn get_notes(launcher: State<'_, Launcher>, version: Option<String>) -> CmdResult<String> {
launcher.notes(version).await
}
/// Links in patch notes open in the system browser; the launcher window never navigates.
#[tauri::command]
pub async fn open_external(app: AppHandle, url: String) -> CmdResult<()> {
let parsed = url::Url::parse(&url).map_err(|_| "That link is not a valid address.".to_string())?;
if !matches!(parsed.scheme(), "http" | "https") {
return Err("Only web links can be opened.".to_string());
}
app
.opener()
.open_url(parsed.as_str(), None::<&str>)
.map_err(|e| format!("cannot open the browser: {e}"))
}
#[tauri::command]
pub async fn start_install(app: AppHandle, launcher: State<'_, Launcher>) -> CmdResult<()> {
launcher.start_install(&app, None).await
}
#[tauri::command]
pub async fn start_install_from_drive(app: AppHandle, launcher: State<'_, Launcher>, path: String) -> CmdResult<()> {
launcher.start_install(&app, Some(std::path::PathBuf::from(path))).await
}
#[tauri::command]
pub async fn cancel_install(launcher: State<'_, Launcher>) -> CmdResult<()> {
launcher.cancel_install()
}
#[tauri::command]
pub async fn launch(app: AppHandle, launcher: State<'_, Launcher>) -> CmdResult<()> {
launcher.launch(&app).await
}
#[tauri::command]
pub async fn stop(app: AppHandle, launcher: State<'_, Launcher>) -> CmdResult<()> {
launcher.stop(&app).await
}
#[tauri::command]
pub async fn get_stack(launcher: State<'_, Launcher>) -> CmdResult<Vec<StackEvent>> {
Ok(launcher.stack_states())
}
#[tauri::command]
pub async fn get_settings(launcher: State<'_, Launcher>) -> CmdResult<Settings> {
Ok(launcher.settings().await)
}
#[tauri::command]
pub async fn set_settings(app: AppHandle, launcher: State<'_, Launcher>, settings: Settings) -> CmdResult<Status> {
launcher.set_settings(&app, settings).await
}
#[tauri::command]
pub async fn pick_folder(app: AppHandle) -> CmdResult<Option<String>> {
// The native dialog blocks its thread, so keep it off the async runtime's workers.
tauri::async_runtime::spawn_blocking(move || {
app
.dialog()
.file()
.blocking_pick_folder()
.and_then(|picked| picked.into_path().ok())
.map(|path| path.display().to_string())
})
.await
.map_err(|e| e.to_string())
}