fix: container orchestration stability, AIUI inclusion, lnd-ui port, version 1.3.0
Container stability: - Merge scan results instead of full replacement (prevents UI flapping) - Absence threshold: 3 consecutive missed scans before removing from state - container-list RPC uses cached scanner state for consistency - Increased Podman API timeout 30s → 60s (scanner + health monitor) - Keep crashed containers visible as "exited" instead of podman rm -f - Resolve host-gateway IP via ip route (podman 4.3.x compatibility) ISO build fixes: - AIUI web app inclusion: searches 5 paths + CI step to copy from build server - Claude API proxy: systemctl enable with symlink fallback - AIUI nginx: try_files =404 (was /aiui/index.html redirect loop) - Build version set to 1.3.0 Container fixes: - lnd-ui: nginx listens on 8080 (was 80, Permission denied in rootless) - first-boot: image-versions.sh sourced from correct path with validation - first-boot: host-gateway resolved to actual gateway IP Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
9d4fb805f5
commit
ee7b5980dd
@@ -11,6 +11,7 @@ use crate::state::StateManager;
|
||||
use anyhow::Result;
|
||||
use hyper::server::conn::Http;
|
||||
use hyper::service::service_fn;
|
||||
use std::collections::HashMap;
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
@@ -269,7 +270,10 @@ impl Server {
|
||||
// Brief delay for containers to stabilize after boot
|
||||
tokio::time::sleep(Duration::from_secs(3)).await;
|
||||
info!("🐳 Scanning containers...");
|
||||
if let Err(e) = scan_and_update_packages(&scanner, &state, identity_clone.as_ref()).await {
|
||||
// Tracks how many consecutive scans each container has been absent from.
|
||||
// Prevents UI flapping when podman intermittently returns incomplete results.
|
||||
let mut absence_tracker: HashMap<String, u32> = HashMap::new();
|
||||
if let Err(e) = scan_and_update_packages(&scanner, &state, identity_clone.as_ref(), &mut absence_tracker).await {
|
||||
error!("Failed to scan containers: {}", e);
|
||||
}
|
||||
|
||||
@@ -284,7 +288,7 @@ impl Server {
|
||||
continue;
|
||||
}
|
||||
scanning.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||
if let Err(e) = scan_and_update_packages(&scanner, &state, identity_clone.as_ref()).await {
|
||||
if let Err(e) = scan_and_update_packages(&scanner, &state, identity_clone.as_ref(), &mut absence_tracker).await {
|
||||
error!("Failed to update containers: {}", e);
|
||||
}
|
||||
scanning.store(false, std::sync::atomic::Ordering::Relaxed);
|
||||
@@ -458,15 +462,19 @@ async fn refresh_tor_address(state: &StateManager, identity: &NodeIdentity) -> R
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Number of consecutive absent scans before removing a container from state.
|
||||
/// 3 scans × 30s = 90 seconds of absence before removal.
|
||||
const CONTAINER_ABSENCE_THRESHOLD: u32 = 3;
|
||||
|
||||
async fn scan_and_update_packages(
|
||||
scanner: &DockerPackageScanner,
|
||||
state: &StateManager,
|
||||
identity: &NodeIdentity,
|
||||
absence_tracker: &mut HashMap<String, u32>,
|
||||
) -> Result<()> {
|
||||
let packages = scanner.scan_containers().await?;
|
||||
|
||||
|
||||
let (current_data, _) = state.get_snapshot().await;
|
||||
let packages_changed = !packages.is_empty() && current_data.package_data != packages;
|
||||
let tor_addr = docker_packages::read_tor_address("archipelago").await;
|
||||
let tor_changed = tor_addr != current_data.server_info.tor_address;
|
||||
let first_scan = !current_data.server_info.status_info.containers_scanned;
|
||||
@@ -478,17 +486,58 @@ async fn scan_and_update_packages(
|
||||
.unwrap_or(false);
|
||||
let update_changed = update_available != current_data.server_info.status_info.updated;
|
||||
|
||||
if packages_changed || tor_changed || first_scan || update_changed {
|
||||
let mut data = current_data;
|
||||
if !packages.is_empty() {
|
||||
data.package_data = packages;
|
||||
// Empty scan result = podman failure or timeout, preserve existing state
|
||||
if packages.is_empty() && !first_scan {
|
||||
if tor_changed || update_changed {
|
||||
let mut data = current_data;
|
||||
data.server_info.tor_address = tor_addr.clone();
|
||||
data.server_info.node_address = tor_addr.as_ref().map(|t| identity.node_address(t));
|
||||
data.server_info.status_info.updated = update_available;
|
||||
state.update_data(data).await;
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Merge scan results with current state instead of full replacement.
|
||||
// This prevents containers from vanishing when podman intermittently
|
||||
// returns incomplete results under heavy load.
|
||||
let mut merged = current_data.package_data.clone();
|
||||
let mut changed = false;
|
||||
|
||||
// Update/add containers found in this scan
|
||||
for (id, pkg) in &packages {
|
||||
absence_tracker.remove(id);
|
||||
if merged.get(id) != Some(pkg) {
|
||||
merged.insert(id.clone(), pkg.clone());
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Track containers in state but missing from this scan.
|
||||
// Only remove after CONTAINER_ABSENCE_THRESHOLD consecutive absent scans.
|
||||
let current_ids: Vec<String> = merged.keys().cloned().collect();
|
||||
for id in current_ids {
|
||||
if !packages.contains_key(&id) {
|
||||
let count = absence_tracker.entry(id.clone()).or_insert(0);
|
||||
*count += 1;
|
||||
if *count >= CONTAINER_ABSENCE_THRESHOLD {
|
||||
debug!("Removing {} from state after {} consecutive absent scans", id, count);
|
||||
merged.remove(&id);
|
||||
absence_tracker.remove(&id);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if changed || tor_changed || first_scan || update_changed {
|
||||
let mut data = current_data;
|
||||
data.package_data = merged;
|
||||
data.server_info.tor_address = tor_addr.clone();
|
||||
data.server_info.node_address = tor_addr.as_ref().map(|t| identity.node_address(t));
|
||||
data.server_info.status_info.containers_scanned = true;
|
||||
data.server_info.status_info.updated = update_available;
|
||||
state.update_data(data).await;
|
||||
debug!("📦 State changed (packages={}, tor={}, first_scan={}, update={}), broadcasting update", packages_changed, tor_changed, first_scan, update_changed);
|
||||
debug!("📦 State changed (packages={}, tor={}, first_scan={}, update={}), broadcasting update", changed, tor_changed, first_scan, update_changed);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
Reference in New Issue
Block a user