Integrate Docker support into Archipelago and Neode UI
- Added StateManager and data_model modules to manage application state. - Updated ApiHandler to utilize StateManager for WebSocket connections. - Enhanced Server initialization to include StateManager. - Implemented Docker container querying in Neode UI to populate app data dynamically. - Removed temporary dummy app configurations in favor of real Docker-based applications. - Improved WebSocket reconnection logic and error handling in the UI. - Updated package.json and package-lock.json to include dockerode dependency.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
use crate::api::rpc::RpcHandler;
|
||||
use crate::config::Config;
|
||||
use crate::state::StateManager;
|
||||
use anyhow::Result;
|
||||
use futures_util::{SinkExt, StreamExt};
|
||||
use hyper::{Method, Request, Response, StatusCode};
|
||||
@@ -11,15 +12,17 @@ use tracing::{debug, info};
|
||||
pub struct ApiHandler {
|
||||
_config: Config,
|
||||
rpc_handler: Arc<RpcHandler>,
|
||||
state_manager: Arc<StateManager>,
|
||||
}
|
||||
|
||||
impl ApiHandler {
|
||||
pub async fn new(config: Config) -> Result<Self> {
|
||||
pub async fn new(config: Config, state_manager: Arc<StateManager>) -> Result<Self> {
|
||||
let rpc_handler = Arc::new(RpcHandler::new(config.clone()).await?);
|
||||
|
||||
Ok(Self {
|
||||
_config: config,
|
||||
rpc_handler,
|
||||
state_manager,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -32,7 +35,7 @@ impl ApiHandler {
|
||||
|
||||
// WebSocket upgrade must be handled before consuming the body
|
||||
if method == Method::GET && path == "/ws/db" {
|
||||
return Self::handle_websocket(req).await;
|
||||
return Self::handle_websocket(req, self.state_manager.clone()).await;
|
||||
}
|
||||
|
||||
// Convert body to bytes for non-WS routes
|
||||
@@ -58,6 +61,7 @@ impl ApiHandler {
|
||||
|
||||
async fn handle_websocket(
|
||||
req: Request<hyper::Body>,
|
||||
state_manager: Arc<StateManager>,
|
||||
) -> Result<Response<hyper::Body>> {
|
||||
let (response, ws_fut_opt) = hyper_ws_listener::create_ws(req)
|
||||
.map_err(|e| anyhow::anyhow!("WebSocket upgrade failed: {}", e))?;
|
||||
@@ -80,6 +84,16 @@ impl ApiHandler {
|
||||
|
||||
let (mut tx, mut rx) = ws_stream.split();
|
||||
|
||||
// Send initial data dump
|
||||
let initial_msg = state_manager.get_initial_message().await;
|
||||
if let Ok(json_msg) = serde_json::to_string(&initial_msg) {
|
||||
if let Err(e) = tx.send(Message::Text(json_msg)).await {
|
||||
debug!("Failed to send initial data: {}", e);
|
||||
return;
|
||||
}
|
||||
debug!("Sent initial data dump at revision {}", initial_msg.rev);
|
||||
}
|
||||
|
||||
// Send periodic pings to keep connection alive
|
||||
let ping_interval = tokio::time::interval(tokio::time::Duration::from_secs(30));
|
||||
tokio::pin!(ping_interval);
|
||||
|
||||
250
core/archipelago/src/data_model.rs
Normal file
250
core/archipelago/src/data_model.rs
Normal file
@@ -0,0 +1,250 @@
|
||||
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)]
|
||||
pub struct DataModel {
|
||||
#[serde(rename = "server-info")]
|
||||
pub server_info: ServerInfo,
|
||||
#[serde(rename = "package-data")]
|
||||
pub package_data: HashMap<String, PackageDataEntry>,
|
||||
pub ui: UIData,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
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>,
|
||||
pub unread: u32,
|
||||
#[serde(rename = "wifi-ssids")]
|
||||
pub wifi_ssids: Vec<String>,
|
||||
#[serde(rename = "zram-enabled")]
|
||||
pub zram_enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
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>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
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)]
|
||||
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)]
|
||||
pub struct MarketplaceHost {
|
||||
pub name: String,
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum PackageState {
|
||||
Installing,
|
||||
Installed,
|
||||
Stopping,
|
||||
Stopped,
|
||||
Starting,
|
||||
Running,
|
||||
Restarting,
|
||||
#[serde(rename = "creating-backup")]
|
||||
CreatingBackup,
|
||||
#[serde(rename = "restoring-backup")]
|
||||
RestoringBackup,
|
||||
Removing,
|
||||
#[serde(rename = "backing-up")]
|
||||
BackingUp,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PackageDataEntry {
|
||||
pub state: PackageState,
|
||||
#[serde(rename = "static-files")]
|
||||
pub static_files: StaticFiles,
|
||||
pub manifest: Manifest,
|
||||
pub installed: Option<InstalledPackageDataEntry>,
|
||||
#[serde(rename = "install-progress")]
|
||||
pub install_progress: Option<InstallProgress>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct StaticFiles {
|
||||
pub license: String,
|
||||
pub instructions: String,
|
||||
pub icon: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
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>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Description {
|
||||
pub short: String,
|
||||
pub long: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Interfaces {
|
||||
pub main: Option<MainInterface>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
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)]
|
||||
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)]
|
||||
pub struct CurrentDependencyInfo {
|
||||
#[serde(rename = "health-checks")]
|
||||
pub health_checks: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
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)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum ServiceStatus {
|
||||
Stopped,
|
||||
Starting,
|
||||
Running,
|
||||
Stopping,
|
||||
Restarting,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
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 {
|
||||
/// 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: env!("CARGO_PKG_VERSION").to_string(),
|
||||
name: Some("Archipelago".to_string()),
|
||||
pubkey: String::new(),
|
||||
status_info: StatusInfo {
|
||||
restarting: false,
|
||||
shutting_down: false,
|
||||
updated: false,
|
||||
backup_progress: None,
|
||||
update_progress: None,
|
||||
},
|
||||
lan_address: Some("http://localhost:8100".to_string()),
|
||||
unread: 0,
|
||||
wifi_ssids: vec![],
|
||||
zram_enabled: false,
|
||||
},
|
||||
package_data: HashMap::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()
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,9 @@ mod api;
|
||||
mod auth;
|
||||
mod config;
|
||||
mod container;
|
||||
mod data_model;
|
||||
mod server;
|
||||
mod state;
|
||||
|
||||
use auth::AuthManager;
|
||||
use config::Config;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::api::ApiHandler;
|
||||
use crate::config::Config;
|
||||
use crate::state::StateManager;
|
||||
use anyhow::Result;
|
||||
use hyper::server::conn::Http;
|
||||
use hyper::service::service_fn;
|
||||
@@ -15,7 +16,8 @@ pub struct Server {
|
||||
|
||||
impl Server {
|
||||
pub async fn new(config: Config) -> Result<Self> {
|
||||
let api_handler = Arc::new(ApiHandler::new(config.clone()).await?);
|
||||
let state_manager = Arc::new(StateManager::new());
|
||||
let api_handler = Arc::new(ApiHandler::new(config.clone(), state_manager).await?);
|
||||
|
||||
Ok(Self {
|
||||
_config: config,
|
||||
|
||||
56
core/archipelago/src/state.rs
Normal file
56
core/archipelago/src/state.rs
Normal file
@@ -0,0 +1,56 @@
|
||||
use crate::data_model::{DataModel, WebSocketMessage};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
use tracing::debug;
|
||||
|
||||
/// Manages the application state and broadcasts updates to WebSocket clients
|
||||
pub struct StateManager {
|
||||
data: Arc<RwLock<DataModel>>,
|
||||
revision: Arc<RwLock<u32>>,
|
||||
}
|
||||
|
||||
impl StateManager {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
data: Arc::new(RwLock::new(DataModel::new())),
|
||||
revision: Arc::new(RwLock::new(0)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the current data model and revision
|
||||
pub async fn get_snapshot(&self) -> (DataModel, u32) {
|
||||
let data = self.data.read().await.clone();
|
||||
let rev = *self.revision.read().await;
|
||||
(data, rev)
|
||||
}
|
||||
|
||||
/// Update the data model (will broadcast patches in the future)
|
||||
pub async fn update_data(&self, new_data: DataModel) {
|
||||
let mut data = self.data.write().await;
|
||||
let mut rev = self.revision.write().await;
|
||||
|
||||
*data = new_data;
|
||||
*rev += 1;
|
||||
|
||||
debug!("Data model updated to revision {}", *rev);
|
||||
|
||||
// TODO: In the future, compute JSON patches and broadcast to all connected clients
|
||||
// For now, clients will need to reconnect to get updates
|
||||
}
|
||||
|
||||
/// Get a WebSocket message with the current state
|
||||
pub async fn get_initial_message(&self) -> WebSocketMessage {
|
||||
let (data, rev) = self.get_snapshot().await;
|
||||
WebSocketMessage {
|
||||
rev,
|
||||
data: Some(data),
|
||||
patch: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for StateManager {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user