diff --git a/core/archipelago/src/api/rpc/container.rs b/core/archipelago/src/api/rpc/container.rs index ef642e70..49293afa 100644 --- a/core/archipelago/src/api/rpc/container.rs +++ b/core/archipelago/src/api/rpc/container.rs @@ -171,6 +171,13 @@ impl RpcHandler { // than the WebSocket-delivered package_data, which caused apps to flicker // between "installed" and "not-installed" in the UI. let (data, _) = self.state_manager.get_snapshot().await; + // Apps the user explicitly stopped must read as "stopped" even though a + // UI companion (electrs-ui, bitcoin-ui, …) keeps serving the launch port: + // launch_port_reachable() below would otherwise upgrade an exited backend + // back to "running". The reconcile guard keeps these backends down, so the + // marker is authoritative here. + let user_stopped = + crate::crash_recovery::load_user_stopped(&self.config.data_dir).await; if data.server_info.status_info.containers_scanned && !data.package_data.is_empty() { let mut containers = Vec::with_capacity(data.package_data.len()); for (id, pkg) in &data.package_data { @@ -202,7 +209,11 @@ impl RpcHandler { // Scanner backoff preserves cached package_data. Refresh stable // states so callers do not see stale `running`/`exited` after // health-monitor recovery or Quadlet --rm container removal. - if state == "running" && requires_launch_port_for_health(id) { + if user_stopped.contains(id) { + // User stopped it → authoritative "stopped". Do NOT let a + // still-running UI companion's launch port mark it running. + state = "stopped".to_string(); + } else if state == "running" && requires_launch_port_for_health(id) { if !self.cached_reachable_health(id).await?.is_some() { state = live_state_for_app(id) .await