2026-01-27 23:06:18 +00:00
|
|
|
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.
|
2026-02-01 13:24:03 +00:00
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
2026-01-27 23:06:18 +00:00
|
|
|
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"
|
|
|
|
|
)]
|
2026-03-09 07:43:12 +00:00
|
|
|
pub peer_health: HashMap<String, bool>,
|
2026-03-12 00:19:30 +00:00
|
|
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
|
|
|
|
pub notifications: Vec<Notification>,
|
2026-01-27 23:06:18 +00:00
|
|
|
pub ui: UIData,
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-12 00:19:30 +00:00
|
|
|
#[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,
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-01 13:24:03 +00:00
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
2026-01-27 23:06:18 +00:00
|
|
|
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>,
|
2026-02-17 15:03:34 +00:00
|
|
|
#[serde(rename = "tor-address")]
|
|
|
|
|
pub tor_address: Option<String>,
|
|
|
|
|
#[serde(rename = "node-address", skip_serializing_if = "Option::is_none")]
|
|
|
|
|
pub node_address: Option<String>,
|
2026-01-27 23:06:18 +00:00
|
|
|
pub unread: u32,
|
|
|
|
|
#[serde(rename = "wifi-ssids")]
|
|
|
|
|
pub wifi_ssids: Vec<String>,
|
|
|
|
|
#[serde(rename = "zram-enabled")]
|
|
|
|
|
pub zram_enabled: bool,
|
2026-03-31 01:41:24 +01:00
|
|
|
/// True if this node's keys are derived from a BIP-39 seed.
|
|
|
|
|
#[serde(rename = "seed-backed", default)]
|
|
|
|
|
pub seed_backed: bool,
|
2026-01-27 23:06:18 +00:00
|
|
|
}
|
|
|
|
|
|
2026-02-01 13:24:03 +00:00
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
2026-01-27 23:06:18 +00:00
|
|
|
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>,
|
2026-03-18 11:46:38 +00:00
|
|
|
/// 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,
|
2026-01-27 23:06:18 +00:00
|
|
|
}
|
|
|
|
|
|
2026-02-01 13:24:03 +00:00
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
2026-01-27 23:06:18 +00:00
|
|
|
pub struct UIData {
|
|
|
|
|
pub name: Option<String>,
|
|
|
|
|
#[serde(rename = "ack-welcome")]
|
|
|
|
|
pub ack_welcome: String,
|
|
|
|
|
pub marketplace: UIMarketplaceData,
|
|
|
|
|
pub theme: String,
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-01 13:24:03 +00:00
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
2026-01-27 23:06:18 +00:00
|
|
|
pub struct UIMarketplaceData {
|
|
|
|
|
#[serde(rename = "selected-hosts")]
|
|
|
|
|
pub selected_hosts: Vec<String>,
|
|
|
|
|
#[serde(rename = "known-hosts")]
|
|
|
|
|
pub known_hosts: HashMap<String, MarketplaceHost>,
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-01 13:24:03 +00:00
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
2026-01-27 23:06:18 +00:00
|
|
|
pub struct MarketplaceHost {
|
|
|
|
|
pub name: String,
|
|
|
|
|
pub url: String,
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-01 13:24:03 +00:00
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
2026-01-27 23:06:18 +00:00
|
|
|
#[serde(rename_all = "kebab-case")]
|
|
|
|
|
pub enum PackageState {
|
|
|
|
|
Installing,
|
|
|
|
|
Installed,
|
|
|
|
|
Stopping,
|
|
|
|
|
Stopped,
|
security+feat: v1.3.0 — pentest remediation, container reliability, UI overhaul
Security (33 pentest findings addressed):
- CRITICAL: backend binds 127.0.0.1, path traversal in tor.rs/dwn fixed
- HIGH: federation requires signatures, XSS login redirect, RBAC viewer restricted
- HIGH: tar slip prevention, S3 SSRF validation, backup ID validation
- MEDIUM: remember-me random secret, TOTP session rotation, password re-auth
- LOW: CSP unsafe-inline removed, CORS dev-only, onion/webhook validation
Container reliability:
- Memory limits on all 37 containers (OOM prevention)
- Exited vs stopped state distinction with health-aware status badges
- Crash recovery coordination (no more restart cascade)
- User-stopped tracking survives reboots
- Tiered boot recovery (databases → core → services → apps)
UI:
- Wallet TransactionsModal, health-aware app status badges
- Restart button on containers, exited/crashed red state
- Mesh view overhaul, glass button updates, BaseModal/ToggleSwitch
- Apps sticky header removed, dev faucet, mutable mock wallet
Infrastructure:
- LND REST port 8080 exposed over Tor (LND Connect fix)
- Nginx cookie_session fix, deploy script Tor config updated
- Dev environment: podman auto-start, boot mode simulation
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 12:44:31 +00:00
|
|
|
Exited,
|
2026-01-27 23:06:18 +00:00
|
|
|
Starting,
|
|
|
|
|
Running,
|
|
|
|
|
Restarting,
|
|
|
|
|
#[serde(rename = "creating-backup")]
|
|
|
|
|
CreatingBackup,
|
|
|
|
|
#[serde(rename = "restoring-backup")]
|
|
|
|
|
RestoringBackup,
|
|
|
|
|
Removing,
|
|
|
|
|
#[serde(rename = "backing-up")]
|
|
|
|
|
BackingUp,
|
2026-04-09 11:47:35 +02:00
|
|
|
Updating,
|
2026-01-27 23:06:18 +00:00
|
|
|
}
|
|
|
|
|
|
2026-02-01 13:24:03 +00:00
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
2026-01-27 23:06:18 +00:00
|
|
|
pub struct PackageDataEntry {
|
|
|
|
|
pub state: PackageState,
|
security+feat: v1.3.0 — pentest remediation, container reliability, UI overhaul
Security (33 pentest findings addressed):
- CRITICAL: backend binds 127.0.0.1, path traversal in tor.rs/dwn fixed
- HIGH: federation requires signatures, XSS login redirect, RBAC viewer restricted
- HIGH: tar slip prevention, S3 SSRF validation, backup ID validation
- MEDIUM: remember-me random secret, TOTP session rotation, password re-auth
- LOW: CSP unsafe-inline removed, CORS dev-only, onion/webhook validation
Container reliability:
- Memory limits on all 37 containers (OOM prevention)
- Exited vs stopped state distinction with health-aware status badges
- Crash recovery coordination (no more restart cascade)
- User-stopped tracking survives reboots
- Tiered boot recovery (databases → core → services → apps)
UI:
- Wallet TransactionsModal, health-aware app status badges
- Restart button on containers, exited/crashed red state
- Mesh view overhaul, glass button updates, BaseModal/ToggleSwitch
- Apps sticky header removed, dev faucet, mutable mock wallet
Infrastructure:
- LND REST port 8080 exposed over Tor (LND Connect fix)
- Nginx cookie_session fix, deploy script Tor config updated
- Dev environment: podman auto-start, boot mode simulation
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 12:44:31 +00:00
|
|
|
/// 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>,
|
2026-01-27 23:06:18 +00:00
|
|
|
#[serde(rename = "static-files")]
|
|
|
|
|
pub static_files: StaticFiles,
|
|
|
|
|
pub manifest: Manifest,
|
|
|
|
|
pub installed: Option<InstalledPackageDataEntry>,
|
|
|
|
|
#[serde(rename = "install-progress")]
|
|
|
|
|
pub install_progress: Option<InstallProgress>,
|
2026-04-21 19:11:36 -04:00
|
|
|
/// 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.
|
2026-04-28 15:00:58 -04:00
|
|
|
#[serde(
|
|
|
|
|
rename = "uninstall-stage",
|
|
|
|
|
skip_serializing_if = "Option::is_none",
|
|
|
|
|
default
|
|
|
|
|
)]
|
2026-04-21 19:11:36 -04:00
|
|
|
pub uninstall_stage: Option<String>,
|
2026-04-09 11:47:35 +02:00
|
|
|
/// 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>,
|
2026-01-27 23:06:18 +00:00
|
|
|
}
|
|
|
|
|
|
2026-02-01 13:24:03 +00:00
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
2026-01-27 23:06:18 +00:00
|
|
|
pub struct StaticFiles {
|
|
|
|
|
pub license: String,
|
|
|
|
|
pub instructions: String,
|
|
|
|
|
pub icon: String,
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-01 13:24:03 +00:00
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
2026-01-27 23:06:18 +00:00
|
|
|
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>,
|
feat: add app tier system — core/recommended/optional (SCALE-02, SCALE-03)
get_app_tier() classifies all apps:
- core: Bitcoin, LND, Electrs, Mempool, BTCPay, DWN, FileBrowser
- recommended: Fedimint, Grafana, Vaultwarden, Kuma, SearXNG, etc.
- optional: everything else
Tier field added to Manifest struct (data_model.rs) and exposed
via WebSocket package data for frontend tier badges.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 03:27:51 +00:00
|
|
|
/// App tier: "core", "recommended", or "optional"
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
pub tier: Option<String>,
|
2026-01-27 23:06:18 +00:00
|
|
|
}
|
|
|
|
|
|
2026-02-01 13:24:03 +00:00
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
2026-01-27 23:06:18 +00:00
|
|
|
pub struct Description {
|
|
|
|
|
pub short: String,
|
|
|
|
|
pub long: String,
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-01 13:24:03 +00:00
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
2026-01-27 23:06:18 +00:00
|
|
|
pub struct Interfaces {
|
|
|
|
|
pub main: Option<MainInterface>,
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-01 13:24:03 +00:00
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
2026-01-27 23:06:18 +00:00
|
|
|
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>,
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-01 13:24:03 +00:00
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
2026-01-27 23:06:18 +00:00
|
|
|
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,
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-01 13:24:03 +00:00
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
2026-01-27 23:06:18 +00:00
|
|
|
pub struct CurrentDependencyInfo {
|
|
|
|
|
#[serde(rename = "health-checks")]
|
|
|
|
|
pub health_checks: Vec<String>,
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-01 13:24:03 +00:00
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
2026-01-27 23:06:18 +00:00
|
|
|
pub struct InterfaceAddress {
|
|
|
|
|
#[serde(rename = "tor-address")]
|
|
|
|
|
pub tor_address: String,
|
|
|
|
|
#[serde(rename = "lan-address")]
|
|
|
|
|
pub lan_address: Option<String>,
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-01 13:24:03 +00:00
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
2026-01-27 23:06:18 +00:00
|
|
|
#[serde(rename_all = "lowercase")]
|
|
|
|
|
pub enum ServiceStatus {
|
|
|
|
|
Stopped,
|
|
|
|
|
Starting,
|
|
|
|
|
Running,
|
|
|
|
|
Stopping,
|
|
|
|
|
Restarting,
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-01 13:24:03 +00:00
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
2026-01-27 23:06:18 +00:00
|
|
|
pub struct InstallProgress {
|
|
|
|
|
pub size: u64,
|
|
|
|
|
pub downloaded: u64,
|
feat(install): phase-based progress bar replaces unparseable pull bytes
Podman emits zero parseable progress when stderr is piped (no TTY), so
the old byte-counter regex never matched in real installs. Users saw
0% for the whole pull, then a jump to 95%, then silence through
create-container, health-check, and post-install hooks.
Replace with 7 explicit lifecycle phases wired through install.rs and
update.rs: Preparing (5%), PullingImage (20%), CreatingContainer (70%),
StartingContainer (80%), WaitingHealthy (88%), PostInstall (95%),
Done (100%). Each maps to a fixed UI progress and status message.
Frontend PHASE_INFO mapper in stores/server.ts prioritizes phase when
present, falls back to byte-counter for legacy. A Math.max forward-only
guard ensures the bar never regresses. Deleted the duplicate watcher
in Discover.vue that was fighting the store's watcher with stale byte
logic. Added shimmer CSS on the fill (with prefers-reduced-motion
opt-out) so the bar looks alive during long phases.
2026-04-23 07:58:43 -04:00
|
|
|
/// 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>,
|
2026-04-29 14:50:33 -04:00
|
|
|
/// Optional explicit message — used to surface install failures so
|
|
|
|
|
/// the UI can keep the app card visible with an error description
|
|
|
|
|
/// instead of silently removing the entry on fail. UI's PHASE_INFO
|
|
|
|
|
/// label takes precedence when phase is set.
|
|
|
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
|
|
|
pub message: Option<String>,
|
feat(install): phase-based progress bar replaces unparseable pull bytes
Podman emits zero parseable progress when stderr is piped (no TTY), so
the old byte-counter regex never matched in real installs. Users saw
0% for the whole pull, then a jump to 95%, then silence through
create-container, health-check, and post-install hooks.
Replace with 7 explicit lifecycle phases wired through install.rs and
update.rs: Preparing (5%), PullingImage (20%), CreatingContainer (70%),
StartingContainer (80%), WaitingHealthy (88%), PostInstall (95%),
Done (100%). Each maps to a fixed UI progress and status message.
Frontend PHASE_INFO mapper in stores/server.ts prioritizes phase when
present, falls back to byte-counter for legacy. A Math.max forward-only
guard ensures the bar never regresses. Deleted the duplicate watcher
in Discover.vue that was fighting the store's watcher with stale byte
logic. Added shimmer CSS on the fill (with prefers-reduced-motion
opt-out) so the bar looks alive during long phases.
2026-04-23 07:58:43 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// 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,
|
2026-01-27 23:06:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// 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 {
|
2026-03-31 23:43:32 +01:00
|
|
|
/// 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 {
|
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.
|
2026-03-31 23:43:32 +01:00
|
|
|
env!("CARGO_PKG_VERSION").to_string()
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-27 23:06:18 +00:00
|
|
|
/// Create a new empty data model with default values
|
|
|
|
|
pub fn new() -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
server_info: ServerInfo {
|
|
|
|
|
id: uuid::Uuid::new_v4().to_string(),
|
2026-03-31 23:43:32 +01:00
|
|
|
version: Self::detect_build_version(),
|
2026-01-27 23:06:18 +00:00
|
|
|
name: Some("Archipelago".to_string()),
|
|
|
|
|
pubkey: String::new(),
|
|
|
|
|
status_info: StatusInfo {
|
|
|
|
|
restarting: false,
|
|
|
|
|
shutting_down: false,
|
|
|
|
|
updated: false,
|
|
|
|
|
backup_progress: None,
|
|
|
|
|
update_progress: None,
|
2026-03-18 11:46:38 +00:00
|
|
|
containers_scanned: false,
|
2026-01-27 23:06:18 +00:00
|
|
|
},
|
|
|
|
|
lan_address: Some("http://localhost:8100".to_string()),
|
2026-02-17 15:03:34 +00:00
|
|
|
tor_address: None,
|
|
|
|
|
node_address: None,
|
2026-01-27 23:06:18 +00:00
|
|
|
unread: 0,
|
|
|
|
|
wifi_ssids: vec![],
|
|
|
|
|
zram_enabled: false,
|
2026-03-31 01:41:24 +01:00
|
|
|
seed_backed: false,
|
2026-01-27 23:06:18 +00:00
|
|
|
},
|
|
|
|
|
package_data: HashMap::new(),
|
2026-03-09 07:43:12 +00:00
|
|
|
peer_health: HashMap::new(),
|
2026-03-12 00:19:30 +00:00
|
|
|
notifications: Vec::new(),
|
2026-01-27 23:06:18 +00:00
|
|
|
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()
|
|
|
|
|
}
|
|
|
|
|
}
|