import { apiGet, apiPost } from "@/lib/api/client"; import type { ActivityEvent, ActivityKind } from "@/lib/student-activity"; type ApiEnvelope = { success: boolean; data: T; meta: { timestamp: string; message?: string; }; }; export type ProgressSyncEvent = { event_id: string; event_type: ActivityKind; title: string; detail?: string; subject?: string; chapter_id?: string; mission_id?: string; occurred_at: string; sample: boolean; }; export type ProgressSyncResult = { synced: boolean; user_id: string; events_received: number; events_stored: number; duplicates: number; }; export type ProgressSyncSummary = { user_id: string; total_sessions: number; total_events: number; sample_events: number; total_duration_seconds: number; by_type: Partial>; last_activity_at: string | null; recent_events: ActivityEvent[]; }; const activityKinds = new Set([ "question_asked", "lesson_started", "lesson_completed", "note_saved", "quiz_attempted", "demo_class_completed", ]); function optionalString(value: unknown): string | undefined { return typeof value === "string" && value.trim() ? value : undefined; } export function toProgressSyncEvent(event: ActivityEvent): ProgressSyncEvent { return { event_id: event.id, event_type: event.kind, title: event.title, ...(event.detail ? { detail: event.detail } : {}), ...(event.subject ? { subject: event.subject } : {}), ...(event.chapterId ? { chapter_id: event.chapterId } : {}), ...(event.missionId ? { mission_id: event.missionId } : {}), occurred_at: event.at, sample: Boolean(event.sample), }; } function normalizeRemoteEvent(value: unknown): ActivityEvent | null { if (!value || typeof value !== "object" || Array.isArray(value)) return null; const row = value as Record; if ( typeof row.id !== "string" || typeof row.kind !== "string" || !activityKinds.has(row.kind as ActivityKind) || typeof row.title !== "string" || typeof row.at !== "string" || Number.isNaN(Date.parse(row.at)) ) { return null; } const kind = row.kind as ActivityKind; const sample = row.sample === true; if (kind === "demo_class_completed" && !sample) return null; return { id: row.id, kind, title: row.title, at: row.at, ...(optionalString(row.detail) ? { detail: optionalString(row.detail) } : {}), ...(optionalString(row.subject) ? { subject: optionalString(row.subject) } : {}), ...(optionalString(row.chapterId) ? { chapterId: optionalString(row.chapterId) } : {}), ...(optionalString(row.missionId) ? { missionId: optionalString(row.missionId) } : {}), ...(kind === "demo_class_completed" ? { provenance: "demo" as const, sample: true } : sample ? { sample: true } : {}), }; } export async function syncProgressEvents( events: ActivityEvent[], ): Promise { const response = await apiPost< ApiEnvelope, { events: ProgressSyncEvent[] } >("/sync/progress", { events: events.map(toProgressSyncEvent), }); return response.data; } export async function getSyncedProgress(): Promise { const response = await apiGet & { recent_events: unknown[]; }>>("/sync/progress"); return { ...response.data, recent_events: response.data.recent_events .map(normalizeRemoteEvent) .filter((event): event is ActivityEvent => event !== null), }; }