import { desc, eq, sql } from "drizzle-orm"; import { db } from "@/lib/db/client"; import { playSessions, sessionDevices, devices, recordings } from "@/lib/db/schema"; import { upsertDeviceByBleName } from "./devices"; import { incrementPlayCount } from "./recordings"; import type { DeviceCapabilities } from "@/lib/db/schema"; import { PAGE_SIZE, type Page } from "@/lib/pagination"; export interface StartSessionDeviceInput { slotLabel: string; bleName: string; deviceClass?: string | null; capabilities?: DeviceCapabilities; } export interface StartSessionInput { kind: "live" | "replay"; replayedRecordingId?: number; name?: string; devices: StartSessionDeviceInput[]; } export async function startPlaySession(input: StartSessionInput) { const now = Date.now(); const [session] = await db .insert(playSessions) .values({ kind: input.kind, replayedRecordingId: input.replayedRecordingId, name: input.name, status: "active", startedAt: now, }) .returning(); const sessionDeviceRows = []; for (const d of input.devices) { const device = await upsertDeviceByBleName({ bleName: d.bleName, deviceClass: d.deviceClass, capabilities: d.capabilities, }); const [sessionDevice] = await db .insert(sessionDevices) .values({ playSessionId: session.id, deviceId: device.id, slotLabel: d.slotLabel, connectedAt: now, }) .returning(); sessionDeviceRows.push(sessionDevice); } if (input.kind === "replay" && input.replayedRecordingId) { await incrementPlayCount(input.replayedRecordingId, now); } return { session, sessionDevices: sessionDeviceRows }; } export async function endPlaySession( id: number, input: { status: "completed" | "aborted" }, ) { const [existing] = await db.select().from(playSessions).where(eq(playSessions.id, id)); if (!existing) return undefined; const endedAt = Date.now(); const durationMs = endedAt - existing.startedAt; await db .update(sessionDevices) .set({ disconnectedAt: endedAt }) .where(eq(sessionDevices.playSessionId, id)); const [updated] = await db .update(playSessions) .set({ endedAt, durationMs, status: input.status }) .where(eq(playSessions.id, id)) .returning(); return updated; } export async function listPlaySessions() { return db.select().from(playSessions).orderBy(playSessions.startedAt); } export async function listPlaySessionsPage(page: number, pageSize = PAGE_SIZE): Promise> { const [{ count }] = await db.select({ count: sql`count(*)` }).from(playSessions); const items = await db .select() .from(playSessions) .orderBy(desc(playSessions.startedAt)) .limit(pageSize) .offset((page - 1) * pageSize); return { items, page, pageSize, total: count }; } /** Lightweight name-only lookup for page titles - avoids getPlaySessionDetail's device join. */ export async function getPlaySessionName(id: number): Promise { const [row] = await db.select({ name: playSessions.name }).from(playSessions).where(eq(playSessions.id, id)); return row?.name; } export async function getPlaySessionDetail(id: number) { const [session] = await db.select().from(playSessions).where(eq(playSessions.id, id)); if (!session) return undefined; const sessionDeviceRows = await db .select({ id: sessionDevices.id, slotLabel: sessionDevices.slotLabel, connectedAt: sessionDevices.connectedAt, disconnectedAt: sessionDevices.disconnectedAt, deviceId: devices.id, deviceDisplayName: devices.displayName, deviceBleName: devices.bleName, }) .from(sessionDevices) .innerJoin(devices, eq(sessionDevices.deviceId, devices.id)) .where(eq(sessionDevices.playSessionId, id)); return { session, devices: sessionDeviceRows }; } export async function deletePlaySession(id: number, options?: { cascade?: boolean }) { if (options?.cascade) { await db.delete(recordings).where(eq(recordings.sourcePlaySessionId, id)); } await db.delete(playSessions).where(eq(playSessions.id, id)); }