diff --git a/core/archipelago/src/crash_recovery.rs b/core/archipelago/src/crash_recovery.rs index 90825dff..90d8fda4 100644 --- a/core/archipelago/src/crash_recovery.rs +++ b/core/archipelago/src/crash_recovery.rs @@ -415,7 +415,7 @@ async fn start_stopped_app_stacks(data_dir: &Path) -> RecoveryReport { }; for stack in stack_recovery_specs() { - if !stack_has_any_container(stack).await { + if !stack_anchor_container_exists(stack).await { continue; } @@ -619,6 +619,11 @@ struct StackRecoverySpec { network: &'static str, aliases: &'static [(&'static str, &'static str)], containers: &'static [&'static str], + /// The stack's core dependency (its DB / server container) — every other + /// member depends on this being present. Used to distinguish "a genuinely + /// installed stack has a crashed member" from "orphan debris from a + /// partial/failed install" (see `stack_anchor_container_exists`). + anchor: &'static str, } fn stack_recovery_specs() -> &'static [StackRecoverySpec] { @@ -632,6 +637,7 @@ fn stack_recovery_specs() -> &'static [StackRecoverySpec] { ("immich_server", "immich_server"), ], containers: &["immich_postgres", "immich_redis", "immich_server"], + anchor: "immich_postgres", }, StackRecoverySpec { name: "indeedhub", @@ -653,6 +659,7 @@ fn stack_recovery_specs() -> &'static [StackRecoverySpec] { "indeedhub-ffmpeg", "indeedhub", ], + anchor: "indeedhub-postgres", }, StackRecoverySpec { name: "netbird", @@ -663,17 +670,20 @@ fn stack_recovery_specs() -> &'static [StackRecoverySpec] { ("netbird", "netbird"), ], containers: &["netbird-server", "netbird-dashboard", "netbird"], + anchor: "netbird-server", }, ] } -async fn stack_has_any_container(stack: &StackRecoverySpec) -> bool { - for container in stack.containers { - if container_state(container).await.is_some() { - return true; - } - } - false +/// Whether the stack's core dependency container exists at all (running or +/// not — existence, not health, is what matters here). `false` means any +/// other stack member still lying around is orphan debris from a partial or +/// already-uninstalled install, not a legitimately-installed-but-crashed +/// stack — blindly restarting those siblings just crash-loops them forever +/// against a dependency that was never created (indeedhub-api on `.116`, +/// 2026-07-01: retried every 120s against a nonexistent indeedhub-postgres). +async fn stack_anchor_container_exists(stack: &StackRecoverySpec) -> bool { + container_state(stack.anchor).await.is_some() } async fn repair_stack_network_aliases(stack: &StackRecoverySpec) { @@ -1059,4 +1069,27 @@ mod tests { true )); } + + #[test] + fn stack_recovery_anchor_is_the_stacks_own_core_dependency() { + // Every stack's anchor must be one of its own containers (typically + // the DB/server the rest depend on) — a typo here would silently + // disable orphan-debris protection for that stack. + for stack in stack_recovery_specs() { + assert!( + stack.containers.contains(&stack.anchor), + "{}: anchor {} not among its own containers", + stack.name, + stack.anchor + ); + } + assert_eq!( + stack_recovery_specs() + .iter() + .find(|s| s.name == "indeedhub") + .unwrap() + .anchor, + "indeedhub-postgres" + ); + } } diff --git a/neode-ui/src/views/appSession/AppSessionFrame.vue b/neode-ui/src/views/appSession/AppSessionFrame.vue index e58293e0..fcb95d49 100644 --- a/neode-ui/src/views/appSession/AppSessionFrame.vue +++ b/neode-ui/src/views/appSession/AppSessionFrame.vue @@ -1,7 +1,12 @@