-- Live classroom sessions — Phase 5A §11/§12. A classroom session is a distinct concept from a -- cohort (a standing roster), an assessment session (one student's exam attempt), a Lab -- environment (disposable infrastructure), or a login session (one browser's auth) — it -- represents ONE live teaching event, e.g. "Week 3 — Kubernetes Workloads, Aug 29 6-8pm." -- -- Deliberately five tables, not one JSONB blob: classroom_sessions (the event itself), -- classroom_participants (who's in it + presence), classroom_events (append-only audit trail), -- classroom_announcements and help_requests get their own tables rather than being folded into -- classroom_events, since both need efficient direct queries ("show me the open help queue", -- "show me this classroom's announcements in order") that a generic event log would make -- awkward. classroom_events stays the audit trail (docs/PHASE5A... §25) — it does NOT duplicate -- announcement/help-request content, just records that the action happened. CREATE TABLE IF NOT EXISTS classroom_sessions ( classroom_session_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), cohort_id TEXT NOT NULL REFERENCES cohorts(cohort_id), title TEXT NOT NULL, description TEXT, status TEXT NOT NULL DEFAULT 'scheduled' CHECK (status IN ('scheduled', 'live', 'ended', 'cancelled')), scheduled_start TIMESTAMPTZ, scheduled_end TIMESTAMPTZ, started_at TIMESTAMPTZ, ended_at TIMESTAMPTZ, created_by UUID REFERENCES instructors(instructor_id), -- "Current activity" (§20) — a scenario id, not a stage id, matching how students actually -- work (one scenario at a time). NULL means the instructor hasn't set one yet. This is -- guidance only — never used to redirect a browser or auto-mark work complete (enforced in -- application code: nothing reads this to gate access or grade anything). current_scenario_id TEXT, -- Optional external video-conferencing link (§23) — a plain URL, RootNode never validates -- which provider it points at and never proxies/embeds it. meeting_url TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS idx_classroom_sessions_cohort ON classroom_sessions(cohort_id); CREATE INDEX IF NOT EXISTS idx_classroom_sessions_status ON classroom_sessions(status); -- At most one LIVE classroom per cohort at a time — an instructor starting a second live -- session for the same cohort while one is already live is almost certainly a mistake, not a -- real use case (see classroom.js's startClassroom for the app-level check this backs). CREATE UNIQUE INDEX IF NOT EXISTS idx_classroom_sessions_one_live_per_cohort ON classroom_sessions(cohort_id) WHERE status = 'live'; CREATE TABLE IF NOT EXISTS classroom_participants ( classroom_session_id UUID NOT NULL REFERENCES classroom_sessions(classroom_session_id), student_id TEXT NOT NULL REFERENCES students(student_id), joined_at TIMESTAMPTZ NOT NULL DEFAULT now(), -- Presence (§15) — updated by a throttled heartbeat while the student has this classroom -- open, never by Lab terminal activity. online/recently-active/offline is DERIVED from this -- at read time (see admin-classroom-routes.js), not stored as a separate status column, so -- there's nothing to keep in sync or go stale on its own. last_seen_at TIMESTAMPTZ, left_at TIMESTAMPTZ, PRIMARY KEY (classroom_session_id, student_id) ); CREATE INDEX IF NOT EXISTS idx_classroom_participants_student ON classroom_participants(student_id); CREATE TABLE IF NOT EXISTS classroom_events ( event_id BIGSERIAL PRIMARY KEY, classroom_session_id UUID NOT NULL REFERENCES classroom_sessions(classroom_session_id), actor_type TEXT NOT NULL CHECK (actor_type IN ('instructor', 'student', 'system')), actor_id TEXT, event_type TEXT NOT NULL, metadata JSONB, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS idx_classroom_events_session ON classroom_events(classroom_session_id, created_at); CREATE TABLE IF NOT EXISTS classroom_announcements ( announcement_id BIGSERIAL PRIMARY KEY, classroom_session_id UUID NOT NULL REFERENCES classroom_sessions(classroom_session_id), instructor_id UUID REFERENCES instructors(instructor_id), message TEXT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS idx_classroom_announcements_session ON classroom_announcements(classroom_session_id, created_at); CREATE TABLE IF NOT EXISTS help_requests ( help_request_id BIGSERIAL PRIMARY KEY, classroom_session_id UUID NOT NULL REFERENCES classroom_sessions(classroom_session_id), student_id TEXT NOT NULL REFERENCES students(student_id), category TEXT, -- free text ('lab_problem' | 'dont_understand' | 'validation_failing' | 'other' from the frontend, not a DB-enforced enum — see §18's example list) message TEXT, status TEXT NOT NULL DEFAULT 'open' CHECK (status IN ('open', 'acknowledged', 'resolved')), created_at TIMESTAMPTZ NOT NULL DEFAULT now(), acknowledged_at TIMESTAMPTZ, acknowledged_by UUID REFERENCES instructors(instructor_id), resolved_at TIMESTAMPTZ, instructor_note TEXT ); CREATE INDEX IF NOT EXISTS idx_help_requests_session_status ON help_requests(classroom_session_id, status); CREATE INDEX IF NOT EXISTS idx_help_requests_student ON help_requests(student_id);