diff --git a/backend/routers/session.py b/backend/routers/session.py index 11a76c1..26a3ead 100644 --- a/backend/routers/session.py +++ b/backend/routers/session.py @@ -2,10 +2,10 @@ import logging -from fastapi import APIRouter, Depends, HTTPException +from fastapi import APIRouter, Depends, HTTPException, Request, Query from pydantic import BaseModel -from backend.auth import get_current_user +from backend.auth import get_current_user, decode_token from backend.sessions import manager router = APIRouter(prefix="/api/session", tags=["session"]) @@ -41,6 +41,8 @@ async def get_session(user: str = Depends(get_current_user)): @router.put("/state") async def save_state( state: DesktopState, + request: Request, + token: str = Query(default=None), user: str = Depends(get_current_user), ): """Save desktop state (open apps, active app, terminal tab info).""" @@ -48,6 +50,25 @@ async def save_state( return {"ok": True} +@router.post("/state") +async def save_state_beacon( + request: Request, + token: str = Query(default=None), +): + """Save desktop state via sendBeacon (POST with token in query param).""" + if not token: + raise HTTPException(401, "Missing token") + try: + payload = decode_token(token) + except Exception: + raise HTTPException(401, "Invalid token") + user = payload["sub"] + body = await request.json() + state = DesktopState(**body) + manager.save_state(user, state.model_dump()) + return {"ok": True} + + # --------------------------------------------------------------------------- # Terminal endpoints # --------------------------------------------------------------------------- @@ -64,7 +85,11 @@ async def create_terminal( user: str = Depends(get_current_user), ): """Create a new persistent terminal.""" - terminal = manager.create_terminal(user, cols=req.cols, rows=req.rows) + try: + terminal = manager.create_terminal(user, cols=req.cols, rows=req.rows) + except Exception as e: + log.exception("Failed to create terminal for %s", user) + raise HTTPException(500, f"Failed to spawn terminal: {e}") return terminal.to_dict() diff --git a/backend/sessions.py b/backend/sessions.py index c2f0dcc..2159561 100644 --- a/backend/sessions.py +++ b/backend/sessions.py @@ -64,7 +64,7 @@ class ManagedTerminal: async def _read_loop(self): """Background task: read PTY output → scrollback + attached WebSockets.""" - loop = asyncio.get_event_loop() + loop = asyncio.get_running_loop() while self.alive: try: raw = await loop.run_in_executor(None, lambda: self.pty.read(4096)) @@ -237,12 +237,18 @@ class SessionManager: "PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", } - pty = PtyProcess.spawn( - [shell, "-l"], - dimensions=(rows, cols), - env=env, - cwd=home, - ) + log.info("Spawning PTY for %s: shell=%s home=%s", username, shell, home) + + try: + pty = PtyProcess.spawn( + [shell, "-l"], + dimensions=(rows, cols), + env=env, + cwd=home, + ) + except Exception: + log.exception("Failed to spawn PTY for %s", username) + raise terminal_id = str(uuid.uuid4())[:8] terminal = ManagedTerminal( diff --git a/frontend/js/apps/terminal.js b/frontend/js/apps/terminal.js index 39f884c..3b3d4e4 100644 --- a/frontend/js/apps/terminal.js +++ b/frontend/js/apps/terminal.js @@ -19,7 +19,11 @@ method: 'POST', body: { cols: 120, rows: 30 }, }); - if (!res.ok) return null; + if (!res || !res.ok) { + const err = res ? await res.text().catch(() => 'unknown') : 'no response'; + console.error('Failed to create terminal:', err); + return null; + } const data = await res.json(); return attachToTerminal(data.terminal_id, data.title); } catch (e) { diff --git a/frontend/js/atlus.js b/frontend/js/atlus.js index 2c3991e..2ce6800 100644 --- a/frontend/js/atlus.js +++ b/frontend/js/atlus.js @@ -522,9 +522,18 @@ } }); - // Save state before unload + // Save state before unload (use sendBeacon for reliability) window.addEventListener('beforeunload', () => { - _doSaveState(); + const termApp = Atlus.apps.terminal; + const terminalTabs = (termApp && termApp.getTerminalIds) + ? termApp.getTerminalIds() : []; + const state = { + open_apps: Atlus.openApps.slice(), + active_app: Atlus.activeApp, + terminal_tabs: terminalTabs, + }; + const blob = new Blob([JSON.stringify(state)], { type: 'application/json' }); + navigator.sendBeacon('/api/session/state?token=' + TOKEN, blob); }); // =====================================================================