From efe434ede346ace578888ffd5f3e74db10c89a5b Mon Sep 17 00:00:00 2001 From: dave Date: Tue, 14 Apr 2026 11:24:12 +0000 Subject: [PATCH] huskies: merge 565_story_gateway_web_ui_shell_with_project_switcher --- server/src/gateway.rs | 239 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 238 insertions(+), 1 deletion(-) diff --git a/server/src/gateway.rs b/server/src/gateway.rs index 2d49a962..0c010ea0 100644 --- a/server/src/gateway.rs +++ b/server/src/gateway.rs @@ -9,7 +9,7 @@ use poem::EndpointExt; use poem::handler; use poem::http::StatusCode; -use poem::web::Data; +use poem::web::{Data, Json}; use poem::{Body, Request, Response}; use reqwest::Client; use serde::{Deserialize, Serialize}; @@ -545,6 +545,240 @@ pub async fn gateway_health_handler(state: Data<&Arc>) -> Response .body(Body::from(serde_json::to_vec(&body).unwrap_or_default())) } +// ── Gateway Web UI ─────────────────────────────────────────────────── + +/// Self-contained HTML page for the gateway web UI. Fetches project list from +/// `/api/gateway` and switches projects via `POST /api/gateway/switch`, which +/// internally calls the `switch_project` MCP tool logic. +const GATEWAY_UI_HTML: &str = r#" + + + + +Huskies Gateway + + + +
+
+ +
+

Huskies Gateway

+
Multi-project orchestration
+
+
+ + + +
+
+ + + +"#; + +/// Serve the gateway web UI HTML page at `GET /`. +#[handler] +pub async fn gateway_index_handler() -> Response { + Response::builder() + .status(StatusCode::OK) + .header("Content-Type", "text/html; charset=utf-8") + .body(Body::from(GATEWAY_UI_HTML)) +} + +/// `GET /api/gateway` — returns the list of registered projects and the active project. +#[handler] +pub async fn gateway_api_handler(state: Data<&Arc>) -> Response { + let active = state.active_project.read().await.clone(); + let projects: Vec = state + .config + .projects + .iter() + .map(|(name, entry)| { + json!({ + "name": name, + "url": entry.url, + }) + }) + .collect(); + + let body = json!({ "active": active, "projects": projects }); + Response::builder() + .status(StatusCode::OK) + .header("Content-Type", "application/json") + .body(Body::from(serde_json::to_vec(&body).unwrap_or_default())) +} + +/// Request body for `POST /api/gateway/switch`. +#[derive(Deserialize)] +struct SwitchRequest { + project: String, +} + +/// `POST /api/gateway/switch` — switch the active project by calling the +/// `switch_project` MCP tool logic, then return `{"ok": true}` or `{"ok": false, "error": "..."}`. +#[handler] +pub async fn gateway_switch_handler( + state: Data<&Arc>, + body: Json, +) -> Response { + let params = json!({ "arguments": { "project": body.project } }); + let resp = handle_switch_project(¶ms, &state).await; + + let (ok, error) = if resp.result.is_some() { + (true, None) + } else { + let msg = resp + .error + .as_ref() + .map(|e| e.message.clone()) + .unwrap_or_else(|| "unknown error".to_string()); + (false, Some(msg)) + }; + + let body_val = if ok { + json!({ "ok": true }) + } else { + json!({ "ok": false, "error": error }) + }; + + let status = if ok { + StatusCode::OK + } else { + StatusCode::BAD_REQUEST + }; + + Response::builder() + .status(status) + .header("Content-Type", "application/json") + .body(Body::from( + serde_json::to_vec(&body_val).unwrap_or_default(), + )) +} + // ── Gateway server startup ─────────────────────────────────────────── /// Start the gateway HTTP server. This is the entry point when `--gateway` is used. @@ -588,6 +822,9 @@ pub async fn run(config_path: &Path, port: u16) -> Result<(), std::io::Error> { ); let route = poem::Route::new() + .at("/", poem::get(gateway_index_handler)) + .at("/api/gateway", poem::get(gateway_api_handler)) + .at("/api/gateway/switch", poem::post(gateway_switch_handler)) .at( "/mcp", poem::post(gateway_mcp_post_handler).get(gateway_mcp_get_handler),