atlus/backend/routers/session.py
roberts 220a5234cd Add persistent/roaming desktop sessions
PTY terminals now survive browser refresh and close. Session manager
owns PTY lifecycle independently of WebSocket connections, with
background readers storing scrollback for replay on reconnect. Desktop
state (open apps, active app, terminal tabs) persists server-side and
restores automatically on login. Auth tokens moved to localStorage.

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

79 lines
2.3 KiB
Python

"""Desktop session — persistent state + terminal management."""
import logging
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from backend.auth import get_current_user
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,
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}
# ---------------------------------------------------------------------------
# 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."""
terminal = manager.create_terminal(user, cols=req.cols, rows=req.rows)
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}