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
This commit is contained in:
+11
-3
@@ -2154,12 +2154,17 @@ async function createNewSession(name, remoteId) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Kill a tmux session by name via DELETE /api/sessions/{name}.
|
* 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.
|
* Shows a confirmation dialog before killing. Refreshes the session list on success.
|
||||||
* @param {string} name - The session name to kill.
|
* @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;
|
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() {
|
.then(function() {
|
||||||
showToast('Session \'' + name + '\' killed');
|
showToast('Session \'' + name + '\' killed');
|
||||||
// If we deleted the session we're currently viewing, return to dashboard
|
// If we deleted the session we're currently viewing, return to dashboard
|
||||||
@@ -2184,7 +2189,10 @@ function bindStaticEventListeners() {
|
|||||||
if (!deleteBtn) return;
|
if (!deleteBtn) return;
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
var name = deleteBtn.dataset.session;
|
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);
|
on($('back-btn'), 'click', closeSession);
|
||||||
|
|||||||
@@ -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)
|
# Static file serving — MUST come after all API routes (first-match-wins)
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user