archy/core/archipelago/src/data_model.rs

326 lines
10 KiB
Rust
Raw Normal View History

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<String, PackageDataEntry>,
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
#[serde(
rename = "peer-health",
default,
skip_serializing_if = "HashMap::is_empty"
)]
pub peer_health: HashMap<String, bool>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub notifications: Vec<Notification>,
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<String>,
}
#[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<String>,
pub pubkey: String,
#[serde(rename = "status-info")]
pub status_info: StatusInfo,
#[serde(rename = "lan-address")]
pub lan_address: Option<String>,
#[serde(rename = "tor-address")]
pub tor_address: Option<String>,
#[serde(rename = "node-address", skip_serializing_if = "Option::is_none")]
pub node_address: Option<String>,
pub unread: u32,
#[serde(rename = "wifi-ssids")]
pub wifi_ssids: Vec<String>,
#[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<f32>,
#[serde(rename = "update-progress")]
pub update_progress: Option<f32>,
/// 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<String>,
#[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<String>,
#[serde(rename = "known-hosts")]
pub known_hosts: HashMap<String, MarketplaceHost>,
}
#[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,
Updating,
}
#[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<String>,
fix: overhaul container lifecycle — recovery, health, uninstall, UI state Container recovery: - Health monitor: MAX_RESTART_ATTEMPTS 3→10, interval 60s→120s - Dependency-aware restarts: won't restart services before their deps - Reset dependent counters when a dependency recovers - Handle "created" state containers (were invisible to health monitor) - Added IndeedHub, mempool-api, mysql to tier system - Crash recovery: podman start timeout 30s→120s with retry - Podman client: socket timeout 5s→30s, added restart policy UI state representation: - Exit code 0 shows "stopped" (gray), not "crashed" (red) - Exit code 137 shows "killed (OOM)" - Non-zero exit shows "crashed" (red) - Added exit_code field to PackageDataEntry Install/uninstall fixes: - Install returns error when container doesn't start (was silent success) - Post-install hooks awaited instead of fire-and-forget tokio::spawn - Uninstall: graceful rm before force, volume prune, network cleanup - Uninstall returns error on partial failure (was 200 OK) Config consistency: - DB passwords read from /var/lib/archipelago/secrets/ (was hardcoded) - Bitcoin: added ZMQ ports 28332/28333 for LND block notifications - IndeedHub port 7777→8190 (was conflicting with strfry) - Marketplace versions: LND 0.17.4→0.18.4, Mempool 2.5.0→3.0.0 Performance: - Metrics collector interval 60s→300s (was duplicating health monitor) - Podman client: proper error propagation instead of unwrap_or_default Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 07:03:57 +01:00
/// 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<i32>,
#[serde(rename = "static-files")]
pub static_files: StaticFiles,
pub manifest: Manifest,
pub installed: Option<InstalledPackageDataEntry>,
#[serde(rename = "install-progress")]
pub install_progress: Option<InstallProgress>,
/// Pinned image version from image-versions.sh when it differs from running version
#[serde(rename = "available-update", skip_serializing_if = "Option::is_none")]
pub available_update: Option<String>,
}
#[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<String>,
pub author: Option<String>,
pub website: Option<String>,
pub interfaces: Option<Interfaces>,
/// App tier: "core", "recommended", or "optional"
#[serde(default)]
pub tier: Option<String>,
}
#[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<MainInterface>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct MainInterface {
pub ui: Option<String>,
#[serde(rename = "tor-config")]
pub tor_config: Option<String>,
#[serde(rename = "lan-config")]
pub lan_config: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct InstalledPackageDataEntry {
#[serde(rename = "current-dependents")]
pub current_dependents: HashMap<String, CurrentDependencyInfo>,
#[serde(rename = "current-dependencies")]
pub current_dependencies: HashMap<String, CurrentDependencyInfo>,
#[serde(rename = "last-backup")]
pub last_backup: Option<String>,
#[serde(rename = "interface-addresses")]
pub interface_addresses: HashMap<String, InterfaceAddress>,
pub status: ServiceStatus,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct CurrentDependencyInfo {
#[serde(rename = "health-checks")]
pub health_checks: Vec<String>,
}
#[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<String>,
}
#[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<DataModel>,
#[serde(skip_serializing_if = "Option::is_none")]
pub patch: Option<Vec<PatchOperation>>,
}
#[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_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub from: Option<String>,
}
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()
}
}