feat: add "Reset for Smoke Test" button in admin settings
Truncates sources, curriculum, content, quiz banks/results/cache, topics and relations in dependency order so AI-generated state can be wiped between smoke runs without leaving dangling references. Handbook sync state is cleared by default (otherwise re-sync is a no-op); user progress and leaderboard are opt-in. Team members, settings, and LLM telemetry are preserved. UI lives in Admin → Settings → Danger Zone and requires typing RESET before the button enables. Per-collection deletion counts are reported. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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<Array<{collection: string, deleted: number, error?: string}>>}
|
||||
*/
|
||||
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() {
|
||||
|
||||
@@ -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 = () => {
|
||||
</div>
|
||||
</form>
|
||||
</Card>
|
||||
|
||||
<Card className="border border-red-300 bg-red-50/40 mt-8">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<AlertTriangle size={22} className="text-red-600 flex-shrink-0 mt-0.5" />
|
||||
<div>
|
||||
<h3 className="text-lg font-medium text-red-700">Danger Zone — Reset for Smoke Test</h3>
|
||||
<p className="text-sm text-fg-muted mt-1">
|
||||
Deletes all AI-generated content so the platform can be exercised from a clean slate. Team members, settings, and LLM telemetry are preserved.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-sm text-fg-muted bg-paper border border-bg-warm rounded-[var(--r-sm)] p-3">
|
||||
<p className="font-medium text-fg mb-2">Will be cleared:</p>
|
||||
<ul className="list-disc list-inside space-y-0.5">
|
||||
<li>Sources</li>
|
||||
<li>Curriculum</li>
|
||||
<li>Content (per-topic learning modules)</li>
|
||||
<li>Quiz banks, quiz results, quiz cache</li>
|
||||
<li>Topics & relations</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="flex items-center gap-2 text-sm cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={resetIncludeHandbook}
|
||||
onChange={(e) => setResetIncludeHandbook(e.target.checked)}
|
||||
className="rounded bg-bg-warm border-transparent focus:ring-0 text-teal"
|
||||
/>
|
||||
Also clear handbook sync state (so "Sync Employee Handbook" re-processes every file)
|
||||
</label>
|
||||
<label className="flex items-center gap-2 text-sm cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={resetIncludeProgress}
|
||||
onChange={(e) => setResetIncludeProgress(e.target.checked)}
|
||||
className="rounded bg-bg-warm border-transparent focus:ring-0 text-teal"
|
||||
/>
|
||||
Also clear user progress & leaderboard
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-xs text-fg-muted uppercase tracking-wider mb-1 block">
|
||||
Type <span className="font-mono text-red-700">RESET</span> to confirm
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={resetConfirmText}
|
||||
onChange={(e) => 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}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={runSmokeReset}
|
||||
disabled={resetConfirmText !== 'RESET' || isResetting}
|
||||
className="bg-red-600 hover:bg-red-700 text-white border-red-600 hover:border-red-700 flex items-center gap-2"
|
||||
>
|
||||
<RefreshCw size={16} className={isResetting ? 'animate-spin' : ''} />
|
||||
{isResetting ? 'Resetting…' : 'Reset for Smoke Test'}
|
||||
</Button>
|
||||
|
||||
{resetReport && (
|
||||
<div className="mt-2 text-sm bg-paper border border-bg-warm rounded-[var(--r-sm)] p-3 space-y-1">
|
||||
<p className="font-medium text-fg mb-1">Reset complete</p>
|
||||
{resetReport.map((row) => (
|
||||
<div key={row.collection} className="flex items-center justify-between font-mono text-xs">
|
||||
<span className="text-fg-muted">{row.collection}</span>
|
||||
{row.error ? (
|
||||
<span className="text-red-600">error: {row.error}</span>
|
||||
) : (
|
||||
<span className="text-teal">{row.deleted} deleted</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user