mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-31 16:49:34 +00:00
feat(web): improve WASM channel setup flow (#380)
* feat(web): improve WASM channel setup flow with stepper UI and auto-configure Streamline the WASM channel setup experience in the web gateway: - Auto-open configure modal after installing a WASM channel - Add progress stepper (Installed → Configured → Active) on channel cards - Replace generic Activate button with state-specific actions (Setup, Reconfigure, Restart) - Show "Awaiting Pairing" status for Telegram until first user is paired - Add SSE extension_status events for real-time status updates - Add gateway restart endpoint (POST /api/gateway/restart) with idempotency guard - Always mount webhook routes at startup so hot-added channels work without restart - Add pairing request polling (10s interval) on extensions tab - Track activation errors per channel with inline error display Includes review fixes: activation_error priority over active status, stepper failed state rendering, restart poll timeout, configure modal double-submit guard, and SSE sender ordering constraint documentation. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * refactor: address PR review comments - Move PairingStore construction outside .map() loop - Extract createReconfigureButton() helper to reduce duplication Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> --------- Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
abda94d44f
commit
996c6a8cc9
+70
-10
@@ -52,6 +52,14 @@ struct ChannelRuntimeState {
|
||||
telegram_owner_id: Option<i64>,
|
||||
}
|
||||
|
||||
/// Result of saving setup secrets and attempting activation.
|
||||
pub struct SetupResult {
|
||||
/// Human-readable status message.
|
||||
pub message: String,
|
||||
/// Whether the channel was successfully activated after saving secrets.
|
||||
pub activated: bool,
|
||||
}
|
||||
|
||||
/// Central manager for extension lifecycle operations.
|
||||
pub struct ExtensionManager {
|
||||
registry: ExtensionRegistry,
|
||||
@@ -82,6 +90,11 @@ pub struct ExtensionManager {
|
||||
store: Option<Arc<dyn crate::db::Database>>,
|
||||
/// Names of WASM channels that were successfully loaded at startup.
|
||||
active_channel_names: RwLock<HashSet<String>>,
|
||||
/// Last activation error for each WASM channel (ephemeral, cleared on success).
|
||||
activation_errors: RwLock<HashMap<String, String>>,
|
||||
/// SSE broadcast sender (set post-construction via `set_sse_sender()`).
|
||||
sse_sender:
|
||||
RwLock<Option<tokio::sync::broadcast::Sender<crate::channels::web::types::SseEvent>>>,
|
||||
}
|
||||
|
||||
impl ExtensionManager {
|
||||
@@ -121,6 +134,8 @@ impl ExtensionManager {
|
||||
user_id,
|
||||
store,
|
||||
active_channel_names: RwLock::new(HashSet::new()),
|
||||
activation_errors: RwLock::new(HashMap::new()),
|
||||
sse_sender: RwLock::new(None),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,6 +168,25 @@ impl ExtensionManager {
|
||||
active.extend(names);
|
||||
}
|
||||
|
||||
/// Set the SSE broadcast sender for pushing extension status events to the web UI.
|
||||
pub async fn set_sse_sender(
|
||||
&self,
|
||||
sender: tokio::sync::broadcast::Sender<crate::channels::web::types::SseEvent>,
|
||||
) {
|
||||
*self.sse_sender.write().await = Some(sender);
|
||||
}
|
||||
|
||||
/// Broadcast an extension status change to the web UI via SSE.
|
||||
async fn broadcast_extension_status(&self, name: &str, status: &str, message: Option<&str>) {
|
||||
if let Some(ref sender) = *self.sse_sender.read().await {
|
||||
let _ = sender.send(crate::channels::web::types::SseEvent::ExtensionStatus {
|
||||
extension_name: name.to_string(),
|
||||
status: status.to_string(),
|
||||
message: message.map(|m| m.to_string()),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Search for extensions. If `discover` is true, also searches online.
|
||||
pub async fn search(
|
||||
&self,
|
||||
@@ -299,6 +333,7 @@ impl ExtensionManager {
|
||||
tools,
|
||||
needs_setup: false,
|
||||
installed: true,
|
||||
activation_error: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -327,6 +362,7 @@ impl ExtensionManager {
|
||||
tools: if active { vec![name] } else { Vec::new() },
|
||||
needs_setup: false,
|
||||
installed: true,
|
||||
activation_error: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -343,10 +379,12 @@ impl ExtensionManager {
|
||||
match crate::channels::wasm::discover_channels(&self.wasm_channels_dir).await {
|
||||
Ok(channels) => {
|
||||
let active_names = self.active_channel_names.read().await;
|
||||
let errors = self.activation_errors.read().await;
|
||||
for (name, _discovered) in channels {
|
||||
let active = active_names.contains(&name);
|
||||
let (authenticated, needs_setup) =
|
||||
self.check_channel_auth_status(&name).await;
|
||||
let activation_error = errors.get(&name).cloned();
|
||||
extensions.push(InstalledExtension {
|
||||
name,
|
||||
kind: ExtensionKind::WasmChannel,
|
||||
@@ -357,6 +395,7 @@ impl ExtensionManager {
|
||||
tools: Vec::new(),
|
||||
needs_setup,
|
||||
installed: true,
|
||||
activation_error,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -392,6 +431,7 @@ impl ExtensionManager {
|
||||
tools: Vec::new(),
|
||||
needs_setup: false,
|
||||
installed: false,
|
||||
activation_error: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -2087,11 +2127,14 @@ impl ExtensionManager {
|
||||
}
|
||||
|
||||
/// Save setup secrets for an extension, validating names against the capabilities schema.
|
||||
///
|
||||
/// After saving, attempts to hot-activate the channel. Returns a [`SetupResult`]
|
||||
/// indicating whether activation succeeded (so the frontend can show appropriate UI).
|
||||
pub async fn save_setup_secrets(
|
||||
&self,
|
||||
name: &str,
|
||||
secrets: &std::collections::HashMap<String, String>,
|
||||
) -> Result<String, ExtensionError> {
|
||||
) -> Result<SetupResult, ExtensionError> {
|
||||
let kind = self.determine_installed_kind(name).await?;
|
||||
if kind != ExtensionKind::WasmChannel {
|
||||
return Err(ExtensionError::Other(
|
||||
@@ -2174,21 +2217,38 @@ impl ExtensionManager {
|
||||
|
||||
// Try to hot-activate the channel now that secrets are saved
|
||||
match self.activate_wasm_channel(name).await {
|
||||
Ok(result) => Ok(format!(
|
||||
"Configuration saved and channel '{}' activated. {}",
|
||||
name, result.message
|
||||
)),
|
||||
Ok(result) => {
|
||||
self.activation_errors.write().await.remove(name);
|
||||
self.broadcast_extension_status(name, "active", None).await;
|
||||
Ok(SetupResult {
|
||||
message: format!(
|
||||
"Configuration saved and channel '{}' activated. {}",
|
||||
name, result.message
|
||||
),
|
||||
activated: true,
|
||||
})
|
||||
}
|
||||
Err(e) => {
|
||||
let error_msg = e.to_string();
|
||||
tracing::warn!(
|
||||
channel = name,
|
||||
error = %e,
|
||||
"Saved configuration but hot-activation failed, restart may be needed"
|
||||
);
|
||||
Ok(format!(
|
||||
"Configuration saved for '{}'. \
|
||||
Automatic activation failed ({}), restart IronClaw to activate.",
|
||||
name, e
|
||||
))
|
||||
self.activation_errors
|
||||
.write()
|
||||
.await
|
||||
.insert(name.to_string(), error_msg.clone());
|
||||
self.broadcast_extension_status(name, "failed", Some(&error_msg))
|
||||
.await;
|
||||
Ok(SetupResult {
|
||||
message: format!(
|
||||
"Configuration saved for '{}'. \
|
||||
Automatic activation failed ({}), restart IronClaw to activate.",
|
||||
name, e
|
||||
),
|
||||
activated: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -203,6 +203,9 @@ pub struct InstalledExtension {
|
||||
/// Whether this extension is installed locally (false = available in registry but not installed).
|
||||
#[serde(default = "default_true")]
|
||||
pub installed: bool,
|
||||
/// Last activation error for WASM channels.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub activation_error: Option<String>,
|
||||
}
|
||||
|
||||
/// Error type for extension operations.
|
||||
|
||||
Reference in New Issue
Block a user