Record worker browser sessions

This commit is contained in:
2026-08-02 23:23:23 +03:30
parent 601b8ec69e
commit 66c8c19e0c
11 changed files with 384 additions and 14 deletions
+60
View File
@@ -3,10 +3,13 @@
from __future__ import annotations
import secrets
from datetime import UTC, datetime
from pathlib import Path
from typing import Annotated, Any
import speedtest # type: ignore[import-untyped]
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.responses import FileResponse
from fastapi.security import HTTPBasic, HTTPBasicCredentials
from pydantic import BaseModel
@@ -232,6 +235,63 @@ async def list_batch_runs(_: Auth) -> list[dict[str, object]]:
return await batch_runs_repo.list() # type: ignore[return-value]
# ---------------------------------------------------------------------------
# Browser-session recordings
# ---------------------------------------------------------------------------
def _recording_path(filename: str) -> Path:
if filename != Path(filename).name or Path(filename).suffix.lower() != ".mp4":
raise HTTPException(status_code=404, detail="Recording not found")
directory = Path(config.recordings_dir).resolve()
path = directory / filename
if path.is_symlink() or not path.is_file():
raise HTTPException(status_code=404, detail="Recording not found")
return path
@router.get("/api/recordings")
def list_recordings(_: Auth) -> list[dict[str, object]]:
directory = Path(config.recordings_dir)
if not directory.is_dir():
return []
rows: list[dict[str, object]] = []
for path in directory.glob("*.mp4"):
if path.is_symlink():
continue
try:
stat_result = path.stat()
except FileNotFoundError:
continue
rows.append(
{
"name": path.name,
"size_bytes": stat_result.st_size,
"created_at": datetime.fromtimestamp(
stat_result.st_mtime, tz=UTC
).isoformat(),
}
)
rows.sort(key=lambda row: str(row["created_at"]), reverse=True)
return rows
@router.get("/api/recordings/{filename}")
def download_recording(filename: str, _: Auth) -> FileResponse:
path = _recording_path(filename)
return FileResponse(path, media_type="video/mp4", filename=path.name)
@router.delete("/api/recordings/{filename}", status_code=204)
def delete_recording(filename: str, _: Auth) -> None:
path = _recording_path(filename)
try:
path.unlink()
except FileNotFoundError:
raise HTTPException(status_code=404, detail="Recording not found") from None
# ---------------------------------------------------------------------------
# Log viewer
# ---------------------------------------------------------------------------