120 lines
3.3 KiB
Python
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),
|
|
}
|