atlus/backend/routers/session.py
roberts 68fe9b4435 Fix terminal session: better error handling, reliable state save
- Fix asyncio.get_event_loop() → get_running_loop() in PTY reader
- Add error logging for PTY spawn failures
- Add POST /api/session/state endpoint for sendBeacon (beforeunload)
- Use navigator.sendBeacon for reliable state save on page close
- Improve frontend error reporting when terminal creation fails

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 21:46:08 -05:00

104 lines
3.1 KiB
Python

"""Desktop session — persistent state + terminal management."""
import logging
from fastapi import APIRouter, Depends, HTTPException, Request, Query
from pydantic import BaseModel
from backend.auth import get_current_user, decode_token
from backend.sessions import manager
router = APIRouter(prefix="/api/session", tags=["session"])
log = logging.getLogger("atlus.session")
# ---------------------------------------------------------------------------
# Models
# ---------------------------------------------------------------------------
class DesktopState(BaseModel):
open_apps: list[str] = []
active_app: str | None = None
terminal_tabs: list[dict] = [] # [{terminal_id, title}]
class TerminalCreate(BaseModel):
cols: int = 120
rows: int = 30
# ---------------------------------------------------------------------------
# Session endpoints
# ---------------------------------------------------------------------------
@router.get("")
async def get_session(user: str = Depends(get_current_user)):
"""Get or create the user's desktop session."""
session = manager.get_or_create(user)
return session.to_dict()
@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)."""
manager.save_state(user, state.model_dump())
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
# ---------------------------------------------------------------------------
@router.get("/terminals")
async def list_terminals(user: str = Depends(get_current_user)):
"""List all alive terminals for the user."""
return manager.list_terminals(user)
@router.post("/terminals")
async def create_terminal(
req: TerminalCreate,
user: str = Depends(get_current_user),
):
"""Create a new persistent terminal."""
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()
@router.delete("/terminals/{terminal_id}")
async def close_terminal(
terminal_id: str,
user: str = Depends(get_current_user),
):
"""Close and kill a terminal."""
if not manager.close_terminal(user, terminal_id):
raise HTTPException(404, "Terminal not found")
return {"ok": True}