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>
79 lines
2.3 KiB
Python
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}
|