Appearance
Training Center
The Training Center is a parallel, simulated investigation environment for onboarding agents. Trainers run data-driven scenarios with trainees; trainees investigate simulated dockets with objectives, evidence unlocks, and live trainer interaction. It shares the Flask API, the Discord-OAuth browser session, and the Supabase project of the production system — there is no second API, no second database, and no second authentication system — but simulated state is fully separate from production cases.
The training frontend is the same Vite bundle as the dashboard, booted when the hostname is training.eccdps.org (or any path beginning /training).
Trainers and trainees
- Trainer: an agent with
agents.is_trainer = true, or any supervisor rank (Senior Agent +). Trainer access is checked on every request against the live agents row (_is_trainerintraining_runtime.py). Theis_trainerflag is manageable through the admin Agents tab (admin_agents, audited). - Trainee: an agent whose status is exactly
onboarding. The trainee selector (/training/trainees) returns onlystatus = onboardingagents — no role/rank filters.
Training sessions
A session pairs one trainer with one trainee. Sessions are parent containers: a session can begin with zero scenarios, and scenarios (simulated dockets) are activated into the session as training progresses.
Session lifecycle
text
pending → active ⇄ paused → completed
│ │
└──────► aborted ◄───────┘| Status | Meaning | Who transitions |
|---|---|---|
pending | Created, not started | — |
active | Investigation phase running | trainer: start, resume, restart |
paused | Timer stopped; trainee stays locked in the session shell | trainer: pause |
completed | Ended or graded (score/result set) | trainer: end, evaluate |
aborted | Prematurely ended; trainee sees the no-active state | trainer: abort |
Every lifecycle transition is a trainer control (_trainer_control: browser session + trainer role + the caller must be the session's assigned trainer). The trainee never starts, pauses, or ends a session — trainees get 403 on all trainer-control routes.
Scenario instances
Scenario definitions live in training_scenarios (title, difficulty, category, prologue, and a JSON definition holding evidence items, objectives, and condition trees). When a trainer activates a scenario into a session, the API creates a training_session_scenarios child row with its own engine state, and the child becomes the session's current scenario pointer. Activating a new scenario completes any currently active one. The trainee sees a waiting screen while the parent is active but no child is active.
Session state and engine
The scenario engine (training_engine.py) is pure evaluation logic driven by the scenario definition:
text
condition schema:
{ "type": "action", "event": "OPEN_REPORT", "target": "reporter" }
{ "type": "objective", "id": "review_report" }
{ "type": "evidence", "key": "cctv" }
{ "type": "any" / "all", "items": [ ... ] }
{ "type": "trainer" } // true only for trainer overridesTrainee actions (whitelisted in TRAINEE_ACTION_TYPES — open docket/report/ profile/timeline/evidence, note/evidence submission, status changes, investigation start/submit) are recorded as training_events rows with a client-generated client_id for idempotency, then the engine evaluates objective completion and evidence unlocks against the scenario instance's state. Engine outcomes are persisted and broadcast live.
Trainer Control Center
TrainerControlCenter.tsx provides:
- Parent lifecycle: begin (start), pause, resume, restart, reset, end, abort.
- Scenario catalog loading and activation.
- Objective complete/reset and evidence release/lock overrides.
- Information injections (supplemental report, witness statement, admin info, custom message, simulated evidence).
- Private notes and note release (trainee notes are visible to the trainer for monitoring; trainer notes start private).
- Grading: score (0–100), pass/fail, private comments, released feedback.
- Live activity feed, presence (trainee online), and troubleshooting/recovery controls (Reconnect / Full Reconciliation).
All controls call existing API endpoints; the UI never writes Supabase or forces a status directly.
Synchronization and realtime behavior
The architecture is:
text
API/database mutation → persistent state/event → best-effort Realtime broadcast
→ recipient receives event → authoritative GET/reconciliation- The API writes the authoritative row, records the persistent event, then broadcasts over the Realtime REST endpoint to the private channel
training:session:<uuid>(both parties) and personal channelstraining:user:<agent_id>(lifecycle events). - Broadcast failure is logged and never fails the already-persisted request.
- The browser joins private channels with a short-lived JWT minted by
/training/realtime-token(role=authenticated,sub=<discord_id>, default TTL 600 s) and refreshes it before expiry (setAuth). - Channel authorization is RLS on
realtime.messagesbacked bytraining_session_members(see Row Level Security). - The frontend hook (
src/training/realtime.ts) reportsidle/connecting/connected/reconnecting/disconnectedand never pretends sync is active. Detail pages perform an authoritative GET after connect and a debounced GET after event bursts, so a missed broadcast cannot corrupt UI state.
Authorization
| Route class | Gate |
|---|---|
All /training/* | Browser session only; the bot API key is rejected (403) |
| Trainer routes | Trainer role (is_trainer flag or supervisor rank) |
| Trainer controls | Must be the session's assigned trainer |
| Trainee routes | Must be the session's trainee; only their own active/paused/completed sessions |
| Trainee detail on pending/aborted | 409 NO_ACTIVE_SESSION (trainee cannot re-enter an aborted or never-started session by manipulating the id) |
| Realtime join | API-minted JWT + RLS membership policies |
Trainee frontend visibility is equally locked: TraineeShell exposes no trainer/admin navigation and routes the trainee into their own active/paused session or the no-active landing.
Relevant database policies
- Training tables are RLS-enabled with the service role as the only data path; browser JWTs receive no normal table access.
training_session_membershas a narrow authenticated SELECT policy for a user's own membership rows (the basis for Realtime authorization).- Realtime private-channel policies on
realtime.messagesgate session/user channel access; the Realtime server's claims-less authorization-probe INSERT is permitted without granting table reads (migrations 026/027). training_eventscarries a unique(session_id, client_id)index for idempotent action recording.