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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user