diff --git a/core/archipelago/src/container/prod_orchestrator.rs b/core/archipelago/src/container/prod_orchestrator.rs
index e488fd44..a18c3489 100644
--- a/core/archipelago/src/container/prod_orchestrator.rs
+++ b/core/archipelago/src/container/prod_orchestrator.rs
@@ -50,15 +50,6 @@ use crate::update::host_sudo;
/// so the rule is visible in one place and unit-testable.
const UI_APP_IDS: &[&str] = &["bitcoin-ui", "electrs-ui", "lnd-ui"];
const ARCHIVAL_BITCOIN_DISK_GB: u64 = 1000;
-const INDEEDHUB_BACKEND_CONTAINERS: &[&str] = &[
- "indeedhub-postgres",
- "indeedhub-redis",
- "indeedhub-minio",
- "indeedhub-relay",
- "indeedhub-api",
- "indeedhub-ffmpeg",
-];
-const INDEEDHUB_FRONTEND_READY_TIMEOUT_SECS: u64 = 90;
fn is_required_baseline_app(app_id: &str) -> bool {
matches!(
@@ -765,102 +756,6 @@ async fn restart_container_scoped_if_pasta(
}
}
-async fn patch_indeedhub_nostr_provider() {
- let _ = tokio::process::Command::new("podman")
- .args([
- "exec",
- "indeedhub",
- "sed",
- "-i",
- "/X-Frame-Options/d",
- "/etc/nginx/conf.d/default.conf",
- ])
- .output()
- .await;
-
- let provider_src = "/opt/archipelago/web-ui/nostr-provider.js";
- if tokio::fs::metadata(provider_src).await.is_ok() {
- let _ = tokio::process::Command::new("podman")
- .args([
- "cp",
- provider_src,
- "indeedhub:/usr/share/nginx/html/nostr-provider.js",
- ])
- .output()
- .await;
- }
-
- let check = tokio::process::Command::new("podman")
- .args([
- "exec",
- "indeedhub",
- "grep",
- "-q",
- "nostr-provider",
- "/etc/nginx/conf.d/default.conf",
- ])
- .output()
- .await;
- let already_patched = check.map(|o| o.status.success()).unwrap_or(false);
-
- if !already_patched {
- let cat_out = tokio::process::Command::new("podman")
- .args(["exec", "indeedhub", "cat", "/etc/nginx/conf.d/default.conf"])
- .output()
- .await;
- if let Ok(out) = cat_out {
- if out.status.success() {
- let conf = String::from_utf8_lossy(&out.stdout).to_string();
- let conf = conf.replace(
- "location = /sw.js {",
- "location = /nostr-provider.js {\n\
- add_header Cache-Control \"no-cache, no-store, must-revalidate\";\n\
- expires off;\n\
- }\n\n\
- location = /sw.js {",
- );
- let conf = if conf.contains("try_files") && !conf.contains("sub_filter") {
- conf.replacen(
- "try_files $uri $uri/ /index.html;",
- "try_files $uri $uri/ /index.html;\n\
- sub_filter_once on;\n\
- sub_filter '' '';",
- 1,
- )
- } else {
- conf
- };
-
- let tmp_path = "/tmp/indeedhub-nginx-patch.conf";
- if tokio::fs::write(tmp_path, &conf).await.is_ok() {
- let _ = tokio::process::Command::new("podman")
- .args(["cp", tmp_path, "indeedhub:/etc/nginx/conf.d/default.conf"])
- .output()
- .await;
- let _ = tokio::fs::remove_file(tmp_path).await;
- }
- }
- }
- }
-
- let _ = tokio::process::Command::new("podman")
- .args([
- "exec",
- "indeedhub",
- "sed",
- "-i",
- "s|proxy_set_header X-Forwarded-Prefix /api;|proxy_set_header X-Forwarded-Prefix $http_x_forwarded_prefix/api;|",
- "/etc/nginx/conf.d/default.conf",
- ])
- .output()
- .await;
-
- let _ = tokio::process::Command::new("podman")
- .args(["exec", "indeedhub", "nginx", "-s", "reload"])
- .output()
- .await;
-}
-
/// Outcome of `reconcile_all` for a single app.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ReconcileAction {
@@ -1374,13 +1269,12 @@ impl ProdContainerOrchestrator {
mode: ReconcileMode,
) -> Result {
let app_id = lm.manifest.app.id.clone();
- if app_id == "indeedhub" {
- // IndeedHub is a multi-container stack installed by the package
- // stack path. Boot reconcile must not fresh-install the catalog
- // manifest, but it does need to start/repair an already-installed
- // stack and reapply the frontend's Nostr provider patch after boot.
- return self.reconcile_indeedhub_stack(mode).await;
- }
+ // IndeedHub used to be a hardcoded orchestrator special-case
+ // (reconcile_indeedhub_stack + a dependency-DNS gate) that refused to
+ // create the frontend from its manifest. It is now fully manifest-driven
+ // (apps/indeedhub-* + apps/indeedhub): network_aliases, generated_secrets,
+ // dependencies, and the post_install nginx hook live in the manifests, so
+ // every member — frontend included — flows through the generic path here.
let lock = self.app_lock(&app_id).await;
let _guard = lock.lock().await;
@@ -2321,10 +2215,6 @@ impl ProdContainerOrchestrator {
self.ensure_btcpay_stack_dirs().await?;
Ok(Some(HookOutcome::Unchanged))
}
- "indeedhub" => {
- self.start_indeedhub_backends().await?;
- Ok(Some(HookOutcome::Unchanged))
- }
"grafana" => {
self.cleanup_stale_grafana_port().await;
Ok(Some(HookOutcome::Unchanged))
@@ -2398,272 +2288,6 @@ impl ProdContainerOrchestrator {
Ok(())
}
- async fn start_indeedhub_backends(&self) -> Result<()> {
- let _ = tokio::process::Command::new("podman")
- .args(["network", "create", "indeedhub-net"])
- .output()
- .await;
-
- for name in INDEEDHUB_BACKEND_CONTAINERS {
- let status = match self.runtime.get_container_status(name).await {
- Ok(status) => status,
- Err(_) => continue,
- };
- if !matches!(status.state, ContainerState::Running) {
- if let Err(err) = podman_user_scope(&["start", name]).await {
- tracing::warn!(
- container = %name,
- error = %err,
- "IndeedHub scoped backend start failed; falling back to runtime start"
- );
- self.runtime
- .start_container(name)
- .await
- .with_context(|| format!("start IndeedHub backend {name}"))?;
- }
- tokio::time::sleep(std::time::Duration::from_secs(2)).await;
- }
- }
- self.repair_indeedhub_network_aliases().await;
- self.wait_for_indeedhub_dependencies_ready(120).await?;
- Ok(())
- }
-
- async fn reconcile_indeedhub_stack(&self, mode: ReconcileMode) -> Result {
- let frontend_status = match self.runtime.get_container_status("indeedhub").await {
- Ok(status) => status,
- Err(_) => {
- if mode == ReconcileMode::ExistingOnly {
- return Ok(ReconcileAction::Left("absent".to_string()));
- }
- // Fresh stack creation is owned by package::stacks so we do not
- // create a single broken frontend container from the manifest.
- return Ok(ReconcileAction::Left("stack-managed".to_string()));
- }
- };
-
- self.start_indeedhub_backends().await?;
-
- let mut started = false;
- match frontend_status.state {
- ContainerState::Running => {}
- ContainerState::Stopped
- | ContainerState::Exited
- | ContainerState::Created
- | ContainerState::Stopping => {
- if let Err(err) = podman_user_scope(&["start", "indeedhub"]).await {
- tracing::warn!(
- error = %err,
- "IndeedHub scoped frontend start failed; falling back to runtime start"
- );
- self.runtime
- .start_container("indeedhub")
- .await
- .context("start IndeedHub frontend during reconcile")?;
- }
- started = true;
- }
- ContainerState::Paused => return Ok(ReconcileAction::Left("paused".to_string())),
- ContainerState::Unknown(s) => return Ok(ReconcileAction::Left(s)),
- }
-
- tokio::time::sleep(std::time::Duration::from_secs(5)).await;
- self.repair_indeedhub_network_aliases().await;
- patch_indeedhub_nostr_provider().await;
-
- let frontend_stable = wait_for_container_stable_running(
- self.runtime.as_ref(),
- "indeedhub",
- 5,
- INDEEDHUB_FRONTEND_READY_TIMEOUT_SECS,
- )
- .await;
- if frontend_stable.is_err() || !wait_for_host_port(7778, 10).await {
- tracing::warn!(
- error = ?frontend_stable.err(),
- "IndeedHub frontend did not stay reachable after reconcile; restarting"
- );
- let _ = self.runtime.stop_container("indeedhub").await;
- if let Err(err) = podman_user_scope(&["start", "indeedhub"]).await {
- tracing::warn!(
- error = %err,
- "IndeedHub scoped frontend restart failed; falling back to runtime start"
- );
- self.runtime
- .start_container("indeedhub")
- .await
- .context("restart IndeedHub frontend after failed readiness")?;
- }
- started = true;
- tokio::time::sleep(std::time::Duration::from_secs(5)).await;
- patch_indeedhub_nostr_provider().await;
- wait_for_container_stable_running(
- self.runtime.as_ref(),
- "indeedhub",
- 5,
- INDEEDHUB_FRONTEND_READY_TIMEOUT_SECS,
- )
- .await
- .context("IndeedHub frontend did not remain running after restart")?;
- if !wait_for_host_port(7778, 30).await {
- return Err(anyhow::anyhow!(
- "IndeedHub frontend did not expose host port 7778 after restart"
- ));
- }
- }
-
- if started {
- Ok(ReconcileAction::Started)
- } else {
- Ok(ReconcileAction::NoOp)
- }
- }
-
- async fn wait_for_indeedhub_dependencies_ready(&self, timeout_secs: u64) -> Result<()> {
- let deadline = std::time::Instant::now() + std::time::Duration::from_secs(timeout_secs);
- let mut last = String::from("not checked");
- loop {
- let mut all_running = true;
- for name in INDEEDHUB_BACKEND_CONTAINERS {
- match self.runtime.get_container_status(name).await {
- Ok(status) if matches!(status.state, ContainerState::Running) => {}
- Ok(status) => {
- all_running = false;
- last = format!("{name} state {:?}", status.state);
- break;
- }
- Err(err) => {
- all_running = false;
- last = format!("{name} status error: {err}");
- break;
- }
- }
- }
-
- if all_running && self.indeedhub_api_dependency_dns_ready().await {
- return Ok(());
- }
- if all_running {
- last = "indeedhub-api dependency DNS not ready".to_string();
- }
-
- if std::time::Instant::now() >= deadline {
- return Err(anyhow::anyhow!(
- "IndeedHub dependencies were not ready within {}s ({})",
- timeout_secs,
- last
- ));
- }
- tokio::time::sleep(std::time::Duration::from_secs(2)).await;
- }
- }
-
- async fn indeedhub_api_dependency_dns_ready(&self) -> bool {
- let aliases_ready = self.indeedhub_required_aliases_present().await;
- if cfg!(test) {
- return true;
- }
-
- for host in ["postgres", "redis", "minio", "relay"] {
- let Ok(Ok(output)) = tokio::time::timeout(
- std::time::Duration::from_secs(5),
- tokio::process::Command::new("podman")
- .args(["exec", "indeedhub-api", "getent", "hosts", host])
- .output(),
- )
- .await
- else {
- return aliases_ready;
- };
- if !output.status.success() {
- return aliases_ready;
- }
- }
- true
- }
-
- async fn indeedhub_required_aliases_present(&self) -> bool {
- for (container, alias) in [
- ("indeedhub-postgres", "postgres"),
- ("indeedhub-redis", "redis"),
- ("indeedhub-minio", "minio"),
- ("indeedhub-relay", "relay"),
- ("indeedhub-api", "api"),
- ("indeedhub", "indeedhub"),
- ] {
- if !self.indeedhub_alias_present(container, alias).await {
- return false;
- }
- }
- true
- }
-
- async fn repair_indeedhub_network_aliases(&self) {
- for (container, alias) in [
- ("indeedhub-postgres", "postgres"),
- ("indeedhub-redis", "redis"),
- ("indeedhub-minio", "minio"),
- ("indeedhub-relay", "relay"),
- ("indeedhub-api", "api"),
- ("indeedhub", "indeedhub"),
- ] {
- let exists = tokio::process::Command::new("podman")
- .args(["container", "exists", container])
- .status()
- .await
- .map(|s| s.success())
- .unwrap_or(false);
- if !exists {
- continue;
- }
- if self.indeedhub_alias_present(container, alias).await {
- continue;
- }
-
- let _ = tokio::process::Command::new("podman")
- .args(["network", "disconnect", "-f", "indeedhub-net", container])
- .output()
- .await;
- let _ = tokio::process::Command::new("podman")
- .args([
- "network",
- "connect",
- "--alias",
- alias,
- "indeedhub-net",
- container,
- ])
- .output()
- .await;
- }
- }
-
- async fn indeedhub_alias_present(&self, container: &str, alias: &str) -> bool {
- let output = match tokio::process::Command::new("podman")
- .args([
- "inspect",
- container,
- "--format",
- "{{json .NetworkSettings.Networks}}",
- ])
- .output()
- .await
- {
- Ok(output) if output.status.success() => output,
- _ => return false,
- };
-
- let Ok(networks) = serde_json::from_slice::(&output.stdout) else {
- return false;
- };
- networks
- .get("indeedhub-net")
- .and_then(|network| network.get("Aliases"))
- .and_then(|aliases| aliases.as_array())
- .map(|aliases| aliases.iter().any(|value| value.as_str() == Some(alias)))
- .unwrap_or(false)
- }
-
async fn cleanup_stale_grafana_port(&self) {
let _ = tokio::process::Command::new("pkill")
.args(["-f", "pasta.*3001"])