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>
323 lines
10 KiB
Rust
323 lines
10 KiB
Rust
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>,
|
|
#[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>,
|
|
/// 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 {
|
|
// 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()
|
|
}
|
|
}
|