feat: complete Phase 1 foundation hardening + three-mode UI design doc
Phase 1a — Gradient Removal: - Replaced all gradient-button/gradient-card with glass-button/path-option-card - Removed banned gradient CSS classes Phase 1b — Security Hardening: - SecretsManager: AES-256-GCM encryption (core/security) - electrs_status: credentials from env vars instead of hardcoded - port_manager: RwLock proper error handling (no unwrap) - Pinned all 11 :latest manifest images to specific versions - parmanode converter: pinned inferred image versions Phase 1c — Code Quality: - Split rpc.rs (1795 lines) into 6 handler modules (auth, node, container, package, peers) - Removed sideload code (UI, store, RPC client, 3 doc files) - Fixed body background flash on logout/refresh - Replaced 30 TypeScript `any` types with proper types - Deleted HelloWorld.vue, removed TODO comments - Added set -euo pipefail to all shell scripts - Made deploy script verbose with timestamps and elapsed time Also adds: - CLAUDE.md project guide - docs/three-mode-ui-design.md — design spec for Easy/Pro/Chat UI modes - OnlineStatusPill component Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
62d6c13764
commit
486fc39249
@@ -0,0 +1,292 @@
|
||||
use super::RpcHandler;
|
||||
use anyhow::{Context, Result};
|
||||
|
||||
impl RpcHandler {
|
||||
pub(super) async fn handle_container_install(
|
||||
&self,
|
||||
params: Option<serde_json::Value>,
|
||||
) -> Result<serde_json::Value> {
|
||||
let orchestrator = self
|
||||
.orchestrator
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow::anyhow!("Container orchestrator not available (dev mode required)"))?;
|
||||
|
||||
let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
|
||||
let manifest_path = params
|
||||
.get("manifest_path")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| anyhow::anyhow!("Missing manifest_path"))?;
|
||||
|
||||
// Load manifest
|
||||
let manifest_content = tokio::fs::read_to_string(manifest_path)
|
||||
.await
|
||||
.context("Failed to read manifest file")?;
|
||||
let manifest: archipelago_container::AppManifest = serde_yaml::from_str(&manifest_content)
|
||||
.context("Failed to parse manifest")?;
|
||||
|
||||
let container_name = orchestrator
|
||||
.install_container(&manifest, manifest_path)
|
||||
.await
|
||||
.context("Failed to install container")?;
|
||||
|
||||
Ok(serde_json::json!(container_name))
|
||||
}
|
||||
|
||||
pub(super) async fn handle_container_start(
|
||||
&self,
|
||||
params: Option<serde_json::Value>,
|
||||
) -> Result<serde_json::Value> {
|
||||
let orchestrator = self
|
||||
.orchestrator
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow::anyhow!("Container orchestrator not available (dev mode required)"))?;
|
||||
|
||||
let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
|
||||
let app_id = params
|
||||
.get("app_id")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| anyhow::anyhow!("Missing app_id"))?;
|
||||
|
||||
orchestrator
|
||||
.start_container(app_id)
|
||||
.await
|
||||
.context("Failed to start container")?;
|
||||
|
||||
Ok(serde_json::json!({ "status": "started" }))
|
||||
}
|
||||
|
||||
pub(super) async fn handle_container_stop(
|
||||
&self,
|
||||
params: Option<serde_json::Value>,
|
||||
) -> Result<serde_json::Value> {
|
||||
let orchestrator = self
|
||||
.orchestrator
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow::anyhow!("Container orchestrator not available (dev mode required)"))?;
|
||||
|
||||
let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
|
||||
let app_id = params
|
||||
.get("app_id")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| anyhow::anyhow!("Missing app_id"))?;
|
||||
|
||||
orchestrator
|
||||
.stop_container(app_id)
|
||||
.await
|
||||
.context("Failed to stop container")?;
|
||||
|
||||
Ok(serde_json::json!({ "status": "stopped" }))
|
||||
}
|
||||
|
||||
pub(super) async fn handle_container_remove(
|
||||
&self,
|
||||
params: Option<serde_json::Value>,
|
||||
) -> Result<serde_json::Value> {
|
||||
let orchestrator = self
|
||||
.orchestrator
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow::anyhow!("Container orchestrator not available (dev mode required)"))?;
|
||||
|
||||
let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
|
||||
let app_id = params
|
||||
.get("app_id")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| anyhow::anyhow!("Missing app_id"))?;
|
||||
let preserve_data = params
|
||||
.get("preserve_data")
|
||||
.and_then(|v| v.as_bool())
|
||||
.unwrap_or(false);
|
||||
|
||||
orchestrator
|
||||
.remove_container(app_id, preserve_data)
|
||||
.await
|
||||
.context("Failed to remove container")?;
|
||||
|
||||
Ok(serde_json::json!({ "status": "removed" }))
|
||||
}
|
||||
|
||||
pub(super) async fn handle_container_list(&self) -> Result<serde_json::Value> {
|
||||
// Try to get containers from orchestrator first
|
||||
if let Some(orchestrator) = &self.orchestrator {
|
||||
if let Ok(containers) = orchestrator.list_containers().await {
|
||||
if !containers.is_empty() {
|
||||
return Ok(serde_json::to_value(containers)?);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: list containers directly via sudo podman (for bundled apps)
|
||||
let output = tokio::process::Command::new("sudo")
|
||||
.args(["podman", "ps", "-a", "--format", "json"])
|
||||
.output()
|
||||
.await
|
||||
.context("Failed to list containers via podman")?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Ok(serde_json::json!([]));
|
||||
}
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
if stdout.trim().is_empty() {
|
||||
return Ok(serde_json::json!([]));
|
||||
}
|
||||
|
||||
// Parse podman JSON output
|
||||
let podman_containers: Vec<serde_json::Value> = serde_json::from_str(&stdout)
|
||||
.unwrap_or_else(|_| Vec::new());
|
||||
|
||||
// Convert to our ContainerStatus format
|
||||
let containers: Vec<serde_json::Value> = podman_containers
|
||||
.iter()
|
||||
.map(|c| {
|
||||
let state = c.get("State").and_then(|v| v.as_str()).unwrap_or("unknown");
|
||||
let mapped_state = match state.to_lowercase().as_str() {
|
||||
"running" => "running",
|
||||
"exited" => "exited",
|
||||
"stopped" => "stopped",
|
||||
"created" => "created",
|
||||
"paused" => "paused",
|
||||
_ => "unknown",
|
||||
};
|
||||
|
||||
let name = c.get("Names").and_then(|v| v.as_array()).and_then(|a| a.first()).and_then(|v| v.as_str()).unwrap_or("");
|
||||
|
||||
// Determine lan_address based on container name
|
||||
let lan_address = match name {
|
||||
"bitcoin-knots" => Some("http://localhost:8334"),
|
||||
"lnd" => Some("http://localhost:8081"),
|
||||
"tailscale" => Some("http://localhost:8240"),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
serde_json::json!({
|
||||
"id": c.get("Id").and_then(|v| v.as_str()).unwrap_or(""),
|
||||
"name": name,
|
||||
"state": mapped_state,
|
||||
"image": c.get("Image").and_then(|v| v.as_str()).unwrap_or(""),
|
||||
"created": c.get("Created").and_then(|v| v.as_str()).unwrap_or(""),
|
||||
"ports": c.get("Ports").and_then(|v| v.as_array()).map(|a|
|
||||
a.iter().filter_map(|p| p.get("hostPort").and_then(|v| v.as_u64()).map(|p| p.to_string())).collect::<Vec<_>>()
|
||||
).unwrap_or_default(),
|
||||
"lan_address": lan_address,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(serde_json::json!(containers))
|
||||
}
|
||||
|
||||
pub(super) async fn handle_container_status(
|
||||
&self,
|
||||
params: Option<serde_json::Value>,
|
||||
) -> Result<serde_json::Value> {
|
||||
let orchestrator = self
|
||||
.orchestrator
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow::anyhow!("Container orchestrator not available (dev mode required)"))?;
|
||||
|
||||
let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
|
||||
let app_id = params
|
||||
.get("app_id")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| anyhow::anyhow!("Missing app_id"))?;
|
||||
|
||||
let status = orchestrator
|
||||
.get_container_status(app_id)
|
||||
.await
|
||||
.context("Failed to get container status")?;
|
||||
|
||||
Ok(serde_json::to_value(status)?)
|
||||
}
|
||||
|
||||
pub(super) async fn handle_container_logs(
|
||||
&self,
|
||||
params: Option<serde_json::Value>,
|
||||
) -> Result<serde_json::Value> {
|
||||
let orchestrator = self
|
||||
.orchestrator
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow::anyhow!("Container orchestrator not available (dev mode required)"))?;
|
||||
|
||||
let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
|
||||
let app_id = params
|
||||
.get("app_id")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| anyhow::anyhow!("Missing app_id"))?;
|
||||
let lines = params
|
||||
.get("lines")
|
||||
.and_then(|v| v.as_u64())
|
||||
.unwrap_or(100) as u32;
|
||||
|
||||
let logs = orchestrator
|
||||
.get_container_logs(app_id, lines)
|
||||
.await
|
||||
.context("Failed to get container logs")?;
|
||||
|
||||
Ok(serde_json::to_value(logs)?)
|
||||
}
|
||||
|
||||
/// Used by HTTP GET /api/container/logs (same logic as container-logs RPC).
|
||||
pub async fn get_container_logs_value(
|
||||
&self,
|
||||
app_id: &str,
|
||||
lines: u32,
|
||||
) -> Result<serde_json::Value> {
|
||||
let orchestrator = self
|
||||
.orchestrator
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow::anyhow!("Container orchestrator not available (dev mode required)"))?;
|
||||
|
||||
let logs = orchestrator
|
||||
.get_container_logs(app_id, lines)
|
||||
.await
|
||||
.context("Failed to get container logs")?;
|
||||
|
||||
Ok(serde_json::to_value(logs)?)
|
||||
}
|
||||
|
||||
pub(super) async fn handle_container_health(
|
||||
&self,
|
||||
params: Option<serde_json::Value>,
|
||||
) -> Result<serde_json::Value> {
|
||||
let orchestrator = self
|
||||
.orchestrator
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow::anyhow!("Container orchestrator not available (dev mode required)"))?;
|
||||
|
||||
// If app_id is provided, get health for that app
|
||||
if let Some(params) = params {
|
||||
if let Some(app_id) = params.get("app_id").and_then(|v| v.as_str()) {
|
||||
let health = orchestrator
|
||||
.get_health_status(app_id)
|
||||
.await
|
||||
.context("Failed to get container health")?;
|
||||
return Ok(serde_json::json!({ app_id: health }));
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise, get health for all containers
|
||||
let containers = orchestrator
|
||||
.list_containers()
|
||||
.await
|
||||
.context("Failed to list containers")?;
|
||||
|
||||
let mut health_map = serde_json::Map::new();
|
||||
for container in containers {
|
||||
if let Some(app_id) = container.name.strip_prefix("archipelago-") {
|
||||
if let Some(app_id) = app_id.strip_suffix("-dev") {
|
||||
match orchestrator.get_health_status(app_id).await {
|
||||
Ok(health) => {
|
||||
health_map.insert(app_id.to_string(), serde_json::Value::String(health));
|
||||
}
|
||||
Err(_) => {
|
||||
health_map.insert(app_id.to_string(), serde_json::Value::String("unknown".to_string()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(serde_json::Value::Object(health_map))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user