impl App { /// Rebuild only the effective routing snapshot from the latest Docker observations. /// Desired configuration and its SQLite revision remain unchanged. pub async fn reconcile_docker(self: &Arc) -> Result<(), ApiError> { let _lock = self.update_lock.lock().await; let previous = self.config.load_full(); let desired = previous.envelope.config.clone(); let desired_vhost_count = desired.vhosts.len(); let observed = self.docker.snapshot().await; let policy = self.policy.clone(); let defaults = self.startup_listeners; let active_listeners = self.active_listeners.clone(); let revision = previous.envelope.revision; let prepared = tokio::task::spawn_blocking(move || { let effective = crate::docker::effective_config(desired, &observed)?; let mut prepared = config::compile_with_active( effective, revision, &policy, defaults, Some(&previous), &active_listeners, )?; prepared .envelope .config .vhosts .truncate(desired_vhost_count); Ok::<_, anyhow::Error>(prepared) }) .await .map_err(|_| { ApiError( StatusCode::INTERNAL_SERVER_ERROR, "Docker reconcile task failed".into(), ) })? .map_err(|error| ApiError(StatusCode::UNPROCESSABLE_ENTITY, format!("{error:#}")))?; self.config.store(Arc::new(prepared)); Ok(()) } pub async fn refresh_docker( self: &Arc, ) -> Result, ApiError> { let settings = self.config.load().envelope.config.docker_agents.clone(); let (observations, changed) = self.docker.refresh(&settings, true).await; if changed { self.reconcile_docker().await?; } Ok(observations) } }