use serde::{Deserialize, Serialize}; use std::collections::HashMap; /// The main data model that mirrors the frontend's DataModel type. /// This is sent via WebSocket as the initial state and updated via patches. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct DataModel { #[serde(rename = "server-info")] pub server_info: ServerInfo, #[serde(rename = "package-data")] pub package_data: HashMap, #[serde(rename = "peer-health", default, skip_serializing_if = "HashMap::is_empty")] pub peer_health: HashMap, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub notifications: Vec, pub ui: UIData, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct Notification { pub id: String, pub level: NotificationLevel, pub title: String, pub message: String, pub timestamp: String, #[serde(skip_serializing_if = "Option::is_none")] pub app_id: Option, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "lowercase")] pub enum NotificationLevel { Info, Warning, Error, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct ServerInfo { pub id: String, pub version: String, pub name: Option, pub pubkey: String, #[serde(rename = "status-info")] pub status_info: StatusInfo, #[serde(rename = "lan-address")] pub lan_address: Option, #[serde(rename = "tor-address")] pub tor_address: Option, #[serde(rename = "node-address", skip_serializing_if = "Option::is_none")] pub node_address: Option, pub unread: u32, #[serde(rename = "wifi-ssids")] pub wifi_ssids: Vec, #[serde(rename = "zram-enabled")] pub zram_enabled: bool, /// True if this node's keys are derived from a BIP-39 seed. #[serde(rename = "seed-backed", default)] pub seed_backed: bool, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct StatusInfo { pub restarting: bool, #[serde(rename = "shutting-down")] pub shutting_down: bool, pub updated: bool, #[serde(rename = "backup-progress")] pub backup_progress: Option, #[serde(rename = "update-progress")] pub update_progress: Option, /// True after the first container scan completes. Frontend should /// not show install buttons until this is true. #[serde(rename = "containers-scanned", default)] pub containers_scanned: bool, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct UIData { pub name: Option, #[serde(rename = "ack-welcome")] pub ack_welcome: String, pub marketplace: UIMarketplaceData, pub theme: String, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct UIMarketplaceData { #[serde(rename = "selected-hosts")] pub selected_hosts: Vec, #[serde(rename = "known-hosts")] pub known_hosts: HashMap, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct MarketplaceHost { pub name: String, pub url: String, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "kebab-case")] pub enum PackageState { Installing, Installed, Stopping, Stopped, Exited, Starting, Running, Restarting, #[serde(rename = "creating-backup")] CreatingBackup, #[serde(rename = "restoring-backup")] RestoringBackup, Removing, #[serde(rename = "backing-up")] BackingUp, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct PackageDataEntry { pub state: PackageState, /// Container health: "healthy", "unhealthy", "starting", or null #[serde(skip_serializing_if = "Option::is_none")] pub health: Option, /// Container exit code (only set when state is Exited): 0 = clean, non-zero = crash #[serde(rename = "exit-code", skip_serializing_if = "Option::is_none")] pub exit_code: Option, #[serde(rename = "static-files")] pub static_files: StaticFiles, pub manifest: Manifest, pub installed: Option, #[serde(rename = "install-progress")] pub install_progress: Option, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct StaticFiles { pub license: String, pub instructions: String, pub icon: String, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct Manifest { pub id: String, pub title: String, pub version: String, pub description: Description, #[serde(rename = "release-notes")] pub release_notes: String, pub license: String, #[serde(rename = "wrapper-repo")] pub wrapper_repo: String, #[serde(rename = "upstream-repo")] pub upstream_repo: String, #[serde(rename = "support-site")] pub support_site: String, #[serde(rename = "marketing-site")] pub marketing_site: String, #[serde(rename = "donation-url")] pub donation_url: Option, pub author: Option, pub website: Option, pub interfaces: Option, /// App tier: "core", "recommended", or "optional" #[serde(default)] pub tier: Option, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct Description { pub short: String, pub long: String, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct Interfaces { pub main: Option, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct MainInterface { pub ui: Option, #[serde(rename = "tor-config")] pub tor_config: Option, #[serde(rename = "lan-config")] pub lan_config: Option, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct InstalledPackageDataEntry { #[serde(rename = "current-dependents")] pub current_dependents: HashMap, #[serde(rename = "current-dependencies")] pub current_dependencies: HashMap, #[serde(rename = "last-backup")] pub last_backup: Option, #[serde(rename = "interface-addresses")] pub interface_addresses: HashMap, pub status: ServiceStatus, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct CurrentDependencyInfo { #[serde(rename = "health-checks")] pub health_checks: Vec, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct InterfaceAddress { #[serde(rename = "tor-address")] pub tor_address: String, #[serde(rename = "lan-address")] pub lan_address: Option, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "lowercase")] pub enum ServiceStatus { Stopped, Starting, Running, Stopping, Restarting, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct InstallProgress { pub size: u64, pub downloaded: u64, } /// WebSocket message sent to clients #[derive(Debug, Clone, Serialize, Deserialize)] pub struct WebSocketMessage { pub rev: u32, #[serde(skip_serializing_if = "Option::is_none")] pub data: Option, #[serde(skip_serializing_if = "Option::is_none")] pub patch: Option>, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PatchOperation { pub op: String, pub path: String, #[serde(skip_serializing_if = "Option::is_none")] pub value: Option, #[serde(skip_serializing_if = "Option::is_none")] pub from: Option, } impl DataModel { /// Read build version from /opt/archipelago/build-info.txt if available, /// falling back to Cargo.toml version. This allows sequential CI build /// numbers to be reflected in the UI without recompiling the binary. fn detect_build_version() -> String { if let Ok(content) = std::fs::read_to_string("/opt/archipelago/build-info.txt") { for line in content.lines() { if let Some(v) = line.strip_prefix("version=") { let v = v.trim(); if !v.is_empty() { return v.to_string(); } } } } env!("CARGO_PKG_VERSION").to_string() } /// Create a new empty data model with default values pub fn new() -> Self { Self { server_info: ServerInfo { id: uuid::Uuid::new_v4().to_string(), version: Self::detect_build_version(), name: Some("Archipelago".to_string()), pubkey: String::new(), status_info: StatusInfo { restarting: false, shutting_down: false, updated: false, backup_progress: None, update_progress: None, containers_scanned: false, }, lan_address: Some("http://localhost:8100".to_string()), tor_address: None, node_address: None, unread: 0, wifi_ssids: vec![], zram_enabled: false, seed_backed: false, }, package_data: HashMap::new(), peer_health: HashMap::new(), notifications: Vec::new(), ui: UIData { name: None, ack_welcome: String::new(), marketplace: UIMarketplaceData { selected_hosts: vec![], known_hosts: HashMap::new(), }, theme: "dark".to_string(), }, } } } impl Default for DataModel { fn default() -> Self { Self::new() } }