fester/backend/api/replay.py

120 lines
3.3 KiB
Python

"""
Replay API — start/step/seek/reset replay sessions against the timeline journal.
"""
from fastapi import APIRouter
from pydantic import BaseModel
from typing import Optional, Any, Dict
from backend.analysis.timeline_store import STORE as TIMELINE_STORE
from backend.session.db import SessionDB
router = APIRouter(prefix="/replay", tags=["replay"])
# Process-wide session DB (now SQLite-backed)
SESSION_DB = SessionDB()
class StartReplayBody(BaseModel):
journal: Optional[str] = None # session id or "latest"
session_id: Optional[str] = None
# -------------------------
# START REPLAY SESSION
# -------------------------
@router.post("/start")
def start_replay(body: StartReplayBody):
"""Start a new replay session. Snapshots the current live timeline
into a new session id, so the session has its own immutable event list."""
import uuid
# Create the session first with a placeholder, then patch the source
sid = SESSION_DB.create_session(body.journal or "live")
# Snapshot the current live timeline into the session
events = TIMELINE_STORE.all()
# Log each event into the session so SESSION_DB.get_replay_stream(sid)
# returns them (and the autopsy endpoint can find them)
for event in events:
# Don't double-store — just create a reference event
SESSION_DB.log_event(sid, event.get("type", "event"), event)
return {
"session_id": sid,
"event_count": len(events),
"events_available": len(events),
}
# -------------------------
# GET EVENTS (full journal snapshot)
# -------------------------
@router.get("/events")
def get_events():
"""Return the full current journal. Used by the replay UI on load."""
return {"events": TIMELINE_STORE.all()}
# -------------------------
# GET EVENTS FOR A SPECIFIC SESSION
# -------------------------
@router.get("/events/{session_id}")
def get_session_events(session_id: str):
return {"session_id": session_id, "events": SESSION_DB.get_replay_stream(session_id)}
# -------------------------
# STEP FORWARD
# -------------------------
@router.post("/step")
def replay_step(session_id: str):
events = SESSION_DB.get_replay_stream(session_id)
return {
"session_id": session_id,
"event_count": len(events),
"events": events[:1],
}
# -------------------------
# SEEK
# -------------------------
@router.post("/seek")
def replay_seek(session_id: str, timestamp: float):
events = SESSION_DB.get_replay_stream(session_id)
best = None
for e in events:
ts = e.get("ts") or e.get("timestamp") or 0
if ts <= timestamp:
best = e
else:
break
return {"event": best, "session_id": session_id}
# -------------------------
# RESET
# -------------------------
@router.post("/reset")
def replay_reset(session_id: str):
events = SESSION_DB.get_replay_stream(session_id)
return {
"session_id": session_id,
"event": events[0] if events else None,
"event_count": len(events),
}
# -------------------------
# Convenience: replay_endpoint wrapper for api.py facade
# -------------------------
def replay_endpoint(session_id, registry=None):
"""Legacy wrapper — returns the session events for a given id."""
return {
"session_id": session_id,
"events": SESSION_DB.get_replay_stream(session_id),
}