From 12c8f8f55097fa77c2427c475c799e982e861efe Mon Sep 17 00:00:00 2001 From: Brian Krabach Date: Wed, 8 Apr 2026 08:21:22 -0700 Subject: [PATCH] feat: add federation proxy for deleting remote sessions - Add DELETE /api/federation/{remote_id}/sessions/{session_name} endpoint to proxy session deletion to remote instances with Bearer auth (follows same pattern as existing create/connect/bell-clear proxies) - Update killSession() to accept optional remoteId parameter and route through federation delete proxy when present - Update delegated click handler to extract data-remote-id from parent tile/sidebar element and pass to killSession() for remote routing - Now users can delete sessions on remote devices from their local client Fixes: https://github.com/bkrabach/muxplex/issues (delete remote sessions) Generated with Amplifier --- muxplex/frontend/app.js | 14 +++++++++--- muxplex/main.py | 47 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 3 deletions(-) diff --git a/muxplex/frontend/app.js b/muxplex/frontend/app.js index e07c149..c1a7317 100644 --- a/muxplex/frontend/app.js +++ b/muxplex/frontend/app.js @@ -2154,12 +2154,17 @@ async function createNewSession(name, remoteId) { /** * Kill a tmux session by name via DELETE /api/sessions/{name}. + * For remote sessions, proxies through the federation delete route. * Shows a confirmation dialog before killing. Refreshes the session list on success. * @param {string} name - The session name to kill. + * @param {string} [remoteId] - Remote instance index (empty or absent for local). */ -function killSession(name) { +function killSession(name, remoteId) { if (!confirm('Kill session "' + name + '"?')) return; - api('DELETE', '/api/sessions/' + name) + var endpoint = remoteId + ? '/api/federation/' + encodeURIComponent(remoteId) + '/sessions/' + encodeURIComponent(name) + : '/api/sessions/' + encodeURIComponent(name); + api('DELETE', endpoint) .then(function() { showToast('Session \'' + name + '\' killed'); // If we deleted the session we're currently viewing, return to dashboard @@ -2184,7 +2189,10 @@ function bindStaticEventListeners() { if (!deleteBtn) return; e.stopPropagation(); var name = deleteBtn.dataset.session; - if (name) killSession(name); + // Walk up to the tile/sidebar-item to get remoteId for federation routing + var container = deleteBtn.closest('[data-remote-id]'); + var remoteId = container ? container.dataset.remoteId : ''; + if (name) killSession(name, remoteId); }); on($('back-btn'), 'click', closeSession); diff --git a/muxplex/main.py b/muxplex/main.py index 3233956..9d755e7 100644 --- a/muxplex/main.py +++ b/muxplex/main.py @@ -1248,6 +1248,53 @@ async def federation_create_session( ) +@app.delete("/api/federation/{remote_id}/sessions/{session_name}") +async def federation_delete_session( + remote_id: int, session_name: str, request: Request +) -> dict: + """Proxy a delete-session DELETE to a remote instance. + + Looks up the remote by integer index into ``remote_instances`` in settings, + sends ``DELETE {remote_url}/api/sessions/{session_name}`` with a Bearer auth + header, and returns the remote's JSON response. + + Raises HTTP 404 if ``remote_id`` is not a valid integer index, + 503 when remote is unreachable, 502 when remote returns HTTP error. + """ + settings = load_settings() + remotes = settings.get("remote_instances", []) + if remote_id < 0 or remote_id >= len(remotes): + raise HTTPException( + status_code=404, + detail=f"Remote instance '{remote_id}' not found", + ) + remote = remotes[remote_id] + remote_url: str = remote.get("url", "").rstrip("/") + remote_key: str = remote.get("key", "") + url = f"{remote_url}/api/sessions/{session_name}" + http_client: httpx.AsyncClient = request.app.state.federation_client + try: + resp = await http_client.delete( + url, + headers={"Authorization": f"Bearer {remote_key}"} if remote_key else {}, + ) + resp.raise_for_status() + return resp.json() + except httpx.HTTPStatusError as exc: + raise HTTPException( + status_code=502, + detail=f"Remote returned {exc.response.status_code}", + ) + except Exception as exc: + _log.warning( + "federation_delete_session: remote %s unreachable: %s", remote_url, exc + ) + raise HTTPException( + status_code=503, + detail=f"Remote unreachable: {remote_url} ({type(exc).__name__}: {exc})", + ) + + # --------------------------------------------------------------------------- # Static file serving — MUST come after all API routes (first-match-wins) # ---------------------------------------------------------------------------