archy/core/archipelago/src/data_model.rs

362 lines
12 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>,
/// Live label describing the current uninstall step ("Stopping
/// containers (2/5)", "Removing data", …). Set by the uninstall
/// pipeline so the UI can show real progress instead of a generic
/// "Uninstalling…" spinner. Cleared after the package entry is
/// removed.
#[serde(rename = "uninstall-stage", skip_serializing_if = "Option::is_none", default)]
pub uninstall_stage: Option<String>,
/// 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,
/// High-level pipeline phase. Preferred by the UI over the byte
/// counters (podman pull doesn't emit parseable progress on a piped
/// stderr, so `size`/`downloaded` are often 0). Each phase maps to
/// a fixed UI percentage and a descriptive label.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub phase: Option<InstallPhase>,
}
/// Phases of the install / update pipeline, surfaced to the UI so users
/// see where the pipeline is rather than a stuck 0% bar.
///
/// Ordered so each variant's index roughly corresponds to pipeline time.
/// Serialized as kebab-case: "preparing", "pulling-image", …
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub enum InstallPhase {
/// Validating params, checking deps, writing dynamic configs.
Preparing,
/// `podman pull` in progress (the longest phase — up to several
/// minutes for large images on slow networks).
PullingImage,
/// Creating data directories, writing app-specific configs
/// (bitcoin.conf, lnd.conf, searxng settings.yml, chown).
CreatingContainer,
/// `podman run` has returned; container is transitioning to running.
StartingContainer,
/// Post-start loop waiting up to 60s for `State.Status == running`.
WaitingHealthy,
/// Running post-install hooks (chain init, wallet setup, …).
PostInstall,
/// Pipeline finished successfully. Terminal phase, UI clears entry.
Done,
}
/// 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 {
release(v1.7.3-alpha): sidebar version sync + FIPS reconnect + profile pic render Sidebar version detect_build_version() no longer reads /opt/archipelago/build-info.txt first. That file was written by the ISO installer at flash time and never rewritten by OTA or sideload, so after any binary swap the sidebar kept advertising whatever the ISO shipped with. Now just returns env!("CARGO_PKG_VERSION") unconditionally — always matches the running binary. FIPS card The two-column grid in FipsNetworkCard.vue placed version/npub boxes side-by-side on mobile but the anchor-status panel forced col-span-2, creating an unbalanced empty column at every desktop width. Anchor status moves to its own full-width row below the grid. When the anchor is not reached, a "Reconnect" button appears next to the status line; it calls fips.restart (45s timeout), waits 5s for the daemon to come back, then reloads fips.status. Surfaces whether the restart actually recovered the anchor in a status flash. Profile picture render Uploaded profile pictures are stored with an onion-rooted URL so external Nostr clients can fetch them. The local browser isn't Tor-routed though, so the <img src> silently 404'd and the UI fell back to showing initials. Added a displayableUrl() helper on Web5Identities.vue that rewrites http://<onion>/blob/<cid>[?...] to same-origin /blob/<cid> for rendering, while the stored URL keeps its onion prefix so publishing to Nostr still works for external viewers. Pass-through for data: URLs and already-relative paths. Identity row title The identity list header now renders profile.display_name (when set) and keeps identity.name as a muted parenthetical. Before, only the internal name was shown and a user who'd customised their Nostr display_name saw a mismatch between their own UI and what peers rendered. Artefacts: archipelago 99184b95…22dc1b 40350664 archipelago-frontend-1.7.3-alpha.tar.gz 7b933cf4…74a8bc 76987031 Changelog layman-style per the saved feedback. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 11:44:59 -04:00
// Always use the binary's compiled-in version. The ISO installer
// writes /opt/archipelago/build-info.txt at install time, but that
// file is never rewritten by OTA or sideload, so trusting it made
// the sidebar permanently advertise whatever the ISO shipped with
// even after the running binary had moved on. CARGO_PKG_VERSION is
// baked into the binary at compile time, so it always matches what
// is actually running.
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()
}
}