refactor(maestro): flatten web UI to a single host

Session tree nests by working directory only; the daemon chips in the
header become a session tally, and the spawn form loses its host picker.
API calls move from /api/daemons/:id/* to /api/dirs, /api/recent-dirs,
/api/gitroot, and /api/info.
This commit is contained in:
Miguel Palhas
2026-08-22 10:14:10 +01:00
parent 6790f41652
commit 1bb464be4b
14 changed files with 155 additions and 467 deletions
+11 -9
View File
@@ -207,12 +207,12 @@ pub async fn pump_ws(mut client: WebSocket, upstream_url: String) {
Ok(pair) => pair,
Err(e) => {
tracing::warn!(url = %upstream_url, error = %e, "proxy: upstream WS connect failed");
// The daemon rejects broker attaches it can't route (no shared
// perimeter, peer down, unknown session — NAP-247) with a structured
// HTTP error *before* the upgrade. We've already upgraded the browser
// socket, so a bare drop reaches it as an opaque 1006 close — the
// attach pane just flashes and vanishes with no reason. Relay the
// daemon's reason as an application close frame the UI can show (#39).
// The daemon rejects an attach it can't serve (unknown session)
// with a structured HTTP error *before* the upgrade. We've already
// upgraded the browser socket, so a bare drop reaches it as an
// opaque 1006 close — the attach pane just flashes and vanishes with
// no reason. Relay the daemon's reason as an application close frame
// the UI can show (#39).
let (code, reason) = upstream_close_reason(&e);
let _ = client
.send(AxMessage::Close(Some(axum::extract::ws::CloseFrame {
@@ -397,14 +397,16 @@ mod tests {
#[test]
fn upstream_close_reason_extracts_daemon_message() {
use tokio_tungstenite::tungstenite::{http, Error as WsError};
let body = br#"{"error":{"code":"no_perimeter_candidate","message":"peer hasn't advertised a perimeter; T2 unavailable"}}"#.to_vec();
let body =
br#"{"error":{"code":"unknown_session","message":"no such session on this daemon"}}"#
.to_vec();
let resp = http::Response::builder()
.status(502)
.status(404)
.body(Some(body))
.unwrap();
let (code, reason) = upstream_close_reason(&WsError::Http(resp));
assert_eq!(code, CLOSE_UPSTREAM_REJECTED);
assert_eq!(reason, "peer hasn't advertised a perimeter; T2 unavailable");
assert_eq!(reason, "no such session on this daemon");
}
/// A non-HTTP failure (e.g. TCP refused — peer down) still yields a usable
+3 -3
View File
@@ -20,8 +20,8 @@ async fn spawn_mock_daemon() -> (SocketAddr, oneshot::Sender<()>) {
async fn list_sessions() -> Json<serde_json::Value> {
Json(serde_json::json!([{"id": "session-1"}]))
}
async fn web_info() -> Json<serde_json::Value> {
Json(serde_json::json!({"daemon_id": "mock", "hostname": "mock-host"}))
async fn daemon_info() -> Json<serde_json::Value> {
Json(serde_json::json!({"hostname": "mock-host"}))
}
async fn echo_ws(ws: WebSocketUpgrade) -> impl IntoResponse {
ws.on_upgrade(echo_handler)
@@ -43,7 +43,7 @@ async fn spawn_mock_daemon() -> (SocketAddr, oneshot::Sender<()>) {
let app = Router::new()
.route("/api/sessions", get(list_sessions))
.route("/api/web/info", get(web_info))
.route("/api/info", get(daemon_info))
.route("/api/attach/:id", any(echo_ws));
let listener = TcpListener::bind(SocketAddr::from((Ipv4Addr::LOCALHOST, 0)))
.await