diff --git a/src/lib/db.js b/src/lib/db.js index 898b2df..53d8aec 100644 --- a/src/lib/db.js +++ b/src/lib/db.js @@ -338,6 +338,56 @@ export async function getRecentLlmCalls(limit = 100) { } } +// ── Reset for Smoke Testing ────────────────────────────────────────────────── + +/** + * Truncate the collections that hold AI-generated and progress data. + * + * Order matters: rows that reference `topics.id` (relations, content, + * quiz_banks) are deleted before topics themselves so we never leave + * dangling references mid-wipe. + * + * `team_members`, `settings`, and `llm_calls` are intentionally preserved. + * + * @param {{ includeHandbookState?: boolean, includeProgress?: boolean }} [opts] + * @returns {Promise>} + */ +export async function resetForSmokeTest({ + includeHandbookState = true, + includeProgress = false, +} = {}) { + const collections = [ + 'relations', + 'content', + 'quiz_banks', + 'quiz_results', + 'quiz_cache', + 'curriculum', + 'sources', + 'topics', + ]; + if (includeHandbookState) collections.push('handbook_sync_state'); + if (includeProgress) collections.push('learn_progress', 'leaderboard'); + + const results = []; + for (const name of collections) { + try { + const rows = await pb.collection(name).getFullList({ fields: 'id', requestKey: null }); + // Sequential deletes — bulk delete-many isn't in the JS SDK and parallel + // deletes on the same collection sometimes 409 in PocketBase. + let deleted = 0; + for (const r of rows) { + await pb.collection(name).delete(r.id, { requestKey: null }); + deleted++; + } + results.push({ collection: name, deleted }); + } catch (err) { + results.push({ collection: name, deleted: 0, error: err?.message || String(err) }); + } + } + return results; +} + // ── Handbook Sync State ─────────────────────────────────────────────────────── export async function getHandbookSyncStates() { diff --git a/src/pages/Admin/index.jsx b/src/pages/Admin/index.jsx index bb1f1b1..fbfcf5e 100644 --- a/src/pages/Admin/index.jsx +++ b/src/pages/Admin/index.jsx @@ -1,5 +1,5 @@ import { useState, useEffect } from 'react'; -import { Database, FileText, Settings, Users, Network, Clock, CheckCircle2, AlertCircle, Save, Info, Layers, CheckSquare, CalendarDays, Activity } from 'lucide-react'; +import { Database, FileText, Settings, Users, Network, Clock, CheckCircle2, AlertCircle, Save, Info, Layers, CheckSquare, CalendarDays, Activity, RefreshCw, AlertTriangle } from 'lucide-react'; import Card from '../../components/ui/Card'; import Tag from '../../components/ui/Tag'; import Button from '../../components/ui/Button'; @@ -30,6 +30,12 @@ const Admin = () => { const [useSimulation, setUseSimulation] = useState(false); const [saveStatus, setSaveStatus] = useState(null); + const [resetConfirmText, setResetConfirmText] = useState(''); + const [resetIncludeHandbook, setResetIncludeHandbook] = useState(true); + const [resetIncludeProgress, setResetIncludeProgress] = useState(false); + const [isResetting, setIsResetting] = useState(false); + const [resetReport, setResetReport] = useState(null); + const loadSources = async () => { const data = await db.getSources(); setSources(data); @@ -58,6 +64,23 @@ const Admin = () => { setTimeout(() => setSaveStatus(null), 3000); }; + const runSmokeReset = async () => { + setIsResetting(true); + setResetReport(null); + try { + const report = await db.resetForSmokeTest({ + includeHandbookState: resetIncludeHandbook, + includeProgress: resetIncludeProgress, + }); + setResetReport(report); + setResetConfirmText(''); + } catch (err) { + setResetReport([{ collection: '(unexpected)', deleted: 0, error: err?.message || String(err) }]); + } finally { + setIsResetting(false); + } + }; + const handleDeleteSource = async (id) => { if (confirm('Are you sure you want to delete this source? This will not delete topics already extracted.')) { await db.deleteSource(id); @@ -269,6 +292,91 @@ const Admin = () => { + + +
+
+ +
+

Danger Zone — Reset for Smoke Test

+

+ Deletes all AI-generated content so the platform can be exercised from a clean slate. Team members, settings, and LLM telemetry are preserved. +

+
+
+ +
+

Will be cleared:

+
    +
  • Sources
  • +
  • Curriculum
  • +
  • Content (per-topic learning modules)
  • +
  • Quiz banks, quiz results, quiz cache
  • +
  • Topics & relations
  • +
+
+ +
+ + +
+ +
+ + setResetConfirmText(e.target.value)} + placeholder="RESET" + className="w-full border border-bg-warm rounded-[var(--r-sm)] px-3 py-2 text-sm bg-paper font-mono" + disabled={isResetting} + /> +
+ + + + {resetReport && ( +
+

Reset complete

+ {resetReport.map((row) => ( +
+ {row.collection} + {row.error ? ( + error: {row.error} + ) : ( + {row.deleted} deleted + )} +
+ ))} +
+ )} +
+
)}