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:
@@ -0,0 +1,98 @@
|
||||
//! Launcher settings: which site to use and where ODIAN is installed.
|
||||
//!
|
||||
//! Stored per Windows user in the launcher's config dir. On first run they are
|
||||
//! seeded from a `site.json` next to the launcher executable (shipped with the
|
||||
//! setup or copied from an ODIAN drive), so operators never type addresses.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tauri::{AppHandle, Manager};
|
||||
|
||||
use crate::fsutil::write_json_atomic;
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||
pub struct Settings {
|
||||
/// The site's update service, e.g. `http://192.168.1.41:30097`.
|
||||
#[serde(default)]
|
||||
pub site_url: String,
|
||||
/// The site's MCP, which runs the Keycloak sign-in.
|
||||
#[serde(default)]
|
||||
pub mcp_url: String,
|
||||
#[serde(default)]
|
||||
pub install_dir: String,
|
||||
/// Display name from site.json; the UI's settings form does not edit it.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub site_name: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default)]
|
||||
struct SiteFile {
|
||||
#[serde(default)]
|
||||
name: Option<String>,
|
||||
#[serde(default)]
|
||||
site_url: String,
|
||||
#[serde(default)]
|
||||
mcp_url: String,
|
||||
}
|
||||
|
||||
fn settings_path(app: &AppHandle) -> Result<PathBuf, String> {
|
||||
let dir = app.path().app_config_dir().map_err(|e| format!("no config dir: {e}"))?;
|
||||
Ok(dir.join("settings.json"))
|
||||
}
|
||||
|
||||
fn default_install_dir() -> String {
|
||||
// Operator PCs keep the ~1 TB of data off the system drive when a D: exists.
|
||||
// Not %LOCALAPPDATA%\ODIAN: on the release PC that folder holds the release
|
||||
// signing key, and deleting a test install must never be able to take it along.
|
||||
if Path::new("D:\\").is_dir() {
|
||||
"D:\\ODIAN".to_string()
|
||||
} else {
|
||||
"C:\\ODIAN".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn read_site_file() -> SiteFile {
|
||||
let Some(dir) = std::env::current_exe().ok().and_then(|exe| exe.parent().map(Path::to_path_buf)) else {
|
||||
return SiteFile::default();
|
||||
};
|
||||
std::fs::read(dir.join("site.json"))
|
||||
.ok()
|
||||
.and_then(|bytes| serde_json::from_slice(&bytes).ok())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn normalize_url(url: &str) -> String {
|
||||
url.trim().trim_end_matches('/').to_string()
|
||||
}
|
||||
|
||||
impl Settings {
|
||||
pub fn load(app: &AppHandle) -> Result<Settings, String> {
|
||||
let path = settings_path(app)?;
|
||||
if let Ok(bytes) = std::fs::read(&path) {
|
||||
let mut settings: Settings =
|
||||
serde_json::from_slice(&bytes).map_err(|e| format!("{} is not valid settings JSON: {e}", path.display()))?;
|
||||
if settings.install_dir.trim().is_empty() {
|
||||
settings.install_dir = default_install_dir();
|
||||
}
|
||||
return Ok(settings);
|
||||
}
|
||||
let site = read_site_file();
|
||||
let settings = Settings {
|
||||
site_url: normalize_url(&site.site_url),
|
||||
mcp_url: normalize_url(&site.mcp_url),
|
||||
install_dir: default_install_dir(),
|
||||
site_name: site.name,
|
||||
};
|
||||
settings.save(app)?;
|
||||
Ok(settings)
|
||||
}
|
||||
|
||||
pub fn save(&self, app: &AppHandle) -> Result<(), String> {
|
||||
write_json_atomic(&settings_path(app)?, self)
|
||||
}
|
||||
|
||||
pub fn install_path(&self) -> PathBuf {
|
||||
PathBuf::from(&self.install_dir)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user