From 9fcfc2bd0bc08d021a4a205838307792a48fb3ba Mon Sep 17 00:00:00 2001 From: diegosouzapw Date: Wed, 27 May 2026 19:24:22 -0300 Subject: [PATCH] feat(db): add inspector custom hosts + sessions CRUD modules (F2) --- src/lib/db/inspectorCustomHosts.ts | 77 +++++++++++++++++++ src/lib/db/inspectorSessions.ts | 117 +++++++++++++++++++++++++++++ 2 files changed, 194 insertions(+) create mode 100644 src/lib/db/inspectorCustomHosts.ts create mode 100644 src/lib/db/inspectorSessions.ts diff --git a/src/lib/db/inspectorCustomHosts.ts b/src/lib/db/inspectorCustomHosts.ts new file mode 100644 index 0000000000..9874def829 --- /dev/null +++ b/src/lib/db/inspectorCustomHosts.ts @@ -0,0 +1,77 @@ +/** + * Database module: InspectorCustomHosts + * CRUD operations for inspector_custom_hosts table. + */ + +import { getDbInstance } from "./core"; +import type { InspectorCustomHostRow } from "./_rowTypes"; + +// SQLite stores booleans as integers +interface InspectorCustomHostDbRow { + host: string; + enabled: number; + label: string | null; + kind: string; + added_at: string; + last_seen_at: string | null; +} + +function mapRow(row: InspectorCustomHostDbRow): InspectorCustomHostRow { + return { + host: row.host, + enabled: row.enabled === 1, + label: row.label, + kind: row.kind as "llm" | "app" | "custom", + added_at: row.added_at, + last_seen_at: row.last_seen_at, + }; +} + +export function listCustomHosts(opts?: { enabledOnly?: boolean }): InspectorCustomHostRow[] { + const db = getDbInstance(); + const enabledOnly = opts?.enabledOnly === true; + + const rows = enabledOnly + ? (db + .prepare( + "SELECT * FROM inspector_custom_hosts WHERE enabled = 1 ORDER BY host ASC" + ) + .all() as InspectorCustomHostDbRow[]) + : (db + .prepare("SELECT * FROM inspector_custom_hosts ORDER BY host ASC") + .all() as InspectorCustomHostDbRow[]); + + return rows.map(mapRow); +} + +export function addCustomHost( + host: string, + kind: "llm" | "app" | "custom" = "custom", + label?: string +): void { + const db = getDbInstance(); + const now = new Date().toISOString(); + db.prepare( + `INSERT OR IGNORE INTO inspector_custom_hosts (host, enabled, label, kind, added_at) + VALUES (?, 1, ?, ?, ?)` + ).run(host, label ?? null, kind, now); +} + +export function removeCustomHost(host: string): void { + const db = getDbInstance(); + db.prepare("DELETE FROM inspector_custom_hosts WHERE host = ?").run(host); +} + +export function toggleCustomHost(host: string, enabled: boolean): void { + const db = getDbInstance(); + db.prepare("UPDATE inspector_custom_hosts SET enabled = ? WHERE host = ?").run( + enabled ? 1 : 0, + host + ); +} + +export function touchLastSeen(host: string): void { + const db = getDbInstance(); + const now = new Date().toISOString(); + db.prepare("UPDATE inspector_custom_hosts SET last_seen_at = ? WHERE host = ?").run(now, host); +} diff --git a/src/lib/db/inspectorSessions.ts b/src/lib/db/inspectorSessions.ts new file mode 100644 index 0000000000..c0d102c99b --- /dev/null +++ b/src/lib/db/inspectorSessions.ts @@ -0,0 +1,117 @@ +/** + * Database module: InspectorSessions + * CRUD + snapshot for inspector_sessions and inspector_session_requests tables. + */ + +import { randomUUID } from "crypto"; +import { getDbInstance } from "./core"; +import type { InspectorSessionRow } from "./_rowTypes"; + +interface InspectorSessionDbRow { + id: string; + name: string | null; + started_at: string; + ended_at: string | null; + request_count: number; + profile: string | null; +} + +interface InspectorSessionRequestDbRow { + session_id: string; + seq: number; + payload: string; +} + +function mapSessionRow(row: InspectorSessionDbRow): InspectorSessionRow { + return { + id: row.id, + name: row.name, + started_at: row.started_at, + ended_at: row.ended_at, + request_count: row.request_count, + profile: row.profile as "llm" | "custom" | "all" | null, + }; +} + +export function createSession(opts?: { + name?: string; + profile?: "llm" | "custom" | "all"; +}): { id: string; started_at: string } { + const db = getDbInstance(); + const id = randomUUID(); + const started_at = new Date().toISOString(); + + db.prepare( + `INSERT INTO inspector_sessions (id, name, started_at, profile) VALUES (?, ?, ?, ?)` + ).run(id, opts?.name ?? null, started_at, opts?.profile ?? null); + + return { id, started_at }; +} + +export function stopSession(id: string): void { + const db = getDbInstance(); + const ended_at = new Date().toISOString(); + db.prepare("UPDATE inspector_sessions SET ended_at = ? WHERE id = ?").run(ended_at, id); +} + +export function renameSession(id: string, name: string): void { + const db = getDbInstance(); + db.prepare("UPDATE inspector_sessions SET name = ? WHERE id = ?").run(name, id); +} + +export function listSessions(): InspectorSessionRow[] { + const db = getDbInstance(); + const rows = db + .prepare("SELECT * FROM inspector_sessions ORDER BY started_at DESC") + .all() as InspectorSessionDbRow[]; + return rows.map(mapSessionRow); +} + +export function getSession(id: string): InspectorSessionRow | null { + const db = getDbInstance(); + const row = db + .prepare("SELECT * FROM inspector_sessions WHERE id = ?") + .get(id) as InspectorSessionDbRow | undefined; + return row ? mapSessionRow(row) : null; +} + +export function appendSessionRequest(sessionId: string, payload: string): void { + const db = getDbInstance(); + + const runTransaction = db.transaction(() => { + // Get next seq atomically within transaction + const seqRow = db + .prepare( + "SELECT COALESCE(MAX(seq), 0) + 1 AS next_seq FROM inspector_session_requests WHERE session_id = ?" + ) + .get(sessionId) as { next_seq: number }; + + const nextSeq = seqRow.next_seq; + + db.prepare( + `INSERT INTO inspector_session_requests (session_id, seq, payload) VALUES (?, ?, ?)` + ).run(sessionId, nextSeq, payload); + + db.prepare( + "UPDATE inspector_sessions SET request_count = request_count + 1 WHERE id = ?" + ).run(sessionId); + }); + + runTransaction(); +} + +export function getSessionRequests(sessionId: string): Array<{ seq: number; payload: string }> { + const db = getDbInstance(); + const rows = db + .prepare( + "SELECT seq, payload FROM inspector_session_requests WHERE session_id = ? ORDER BY seq ASC" + ) + .all(sessionId) as InspectorSessionRequestDbRow[]; + return rows.map((r) => ({ seq: r.seq, payload: r.payload })); +} + +export function deleteSession(id: string): void { + const db = getDbInstance(); + // Cascade via FK ON DELETE CASCADE for inspector_session_requests + db.prepare("DELETE FROM inspector_sessions WHERE id = ?").run(id); +}