Add library purge functionality in backend and frontend

This commit is contained in:
2026-03-20 10:33:35 +01:00
parent b13f670798
commit fecd101fd0
3 changed files with 119 additions and 1 deletions

View File

@@ -984,6 +984,48 @@ def create_library(req: CreateLibraryRequest):
return library_payload(data)
@router.post("/libraries/purge")
def purge_libraries():
active_jobs = [
job for job in JOBS.values()
if job["status"] in {"queued", "running"}
]
if active_jobs:
active_slugs = sorted({str(job.get("slug") or "") for job in active_jobs if job.get("slug")})
detail = "Cannot purge databases while library sync jobs are still running."
if active_slugs:
detail = f"{detail} Active databases: {', '.join(active_slugs)}."
raise HTTPException(status_code=409, detail=detail)
removed: List[str] = []
failures: List[str] = []
for path in list(LIB_ROOT.iterdir()):
if not path.is_dir():
continue
try:
shutil.rmtree(path)
removed.append(path.name)
except Exception as exc:
failures.append(f"{path.name}: {type(exc).__name__}: {exc}")
LIB_ROOT.mkdir(parents=True, exist_ok=True)
JOBS.clear()
LIB_LOCKS.clear()
if failures:
preview = "; ".join(failures[:3])
if len(failures) > 3:
preview = f"{preview}; ..."
raise HTTPException(status_code=500, detail=f"Failed to purge some databases. {preview}")
return {
"ok": True,
"count": len(removed),
"removed": removed,
}
@router.get("/libraries/{slug}")
def get_library(slug: str):
return library_payload(read_library(slug))

View File

@@ -808,6 +808,17 @@ async function regenerateFromIndex(index, overrideUserText = null) {
}
}
async function handleLibrariesPurged() {
setLibraries([])
setLibraryJobs([])
setActiveLibrarySlug(null)
setEditingLibrarySlug(null)
setIsDbPickerOpen(false)
setChatLibraryBySession({})
await refreshLibraries()
await refreshLibraryJobs()
}
// Load chat sessions from backend on initial render
useEffect(() => {
if (!backendApiUrl) return;

View File

@@ -29,7 +29,7 @@ function getStatusTone(state) {
return 'neutral';
}
export default function GeneralSettings({ onModelChange, onStreamOutputChange }) {
export default function GeneralSettings({ onModelChange, onStreamOutputChange, onLibrariesPurged }) {
const [backendApiUrl, setBackendApiUrl] = useState('');
const [ollamaApiUrl, setOllamaApiUrl] = useState('');
const [models, setModels] = useState([]);
@@ -37,6 +37,8 @@ export default function GeneralSettings({ onModelChange, onStreamOutputChange })
const [streamOutput, setStreamOutput] = useState(false);
const [updateStatus, setUpdateStatus] = useState(DEFAULT_UPDATE_STATUS);
const [isCheckingForUpdates, setIsCheckingForUpdates] = useState(false);
const [isPurgingLibraries, setIsPurgingLibraries] = useState(false);
const [libraryPurgeStatus, setLibraryPurgeStatus] = useState({ tone: 'neutral', message: '' });
useEffect(() => {
let cancelled = false;
@@ -127,6 +129,48 @@ export default function GeneralSettings({ onModelChange, onStreamOutputChange })
}
};
const handlePurgeLibraries = async () => {
const confirmed = window.confirm(
'Delete all Heimgeist databases, staged files, and indexes from local storage? Chat history will be kept.'
);
if (!confirmed) {
return;
}
setIsPurgingLibraries(true);
setLibraryPurgeStatus({ tone: 'neutral', message: '' });
try {
const response = await fetch(`${backendApiUrl}/libraries/purge`, {
method: 'POST',
});
const data = await response.json().catch(() => null);
if (!response.ok) {
throw new Error(data?.detail || `HTTP ${response.status}`);
}
const count = Number(data?.count) || 0;
setLibraryPurgeStatus({
tone: 'success',
message: count > 0
? `Removed ${count} database${count === 1 ? '' : 's'} from local storage.`
: 'No local databases were found to remove.',
});
if (onLibrariesPurged) {
await Promise.resolve(onLibrariesPurged());
}
} catch (error) {
setLibraryPurgeStatus({
tone: 'error',
message: `Database purge failed: ${error.message || String(error)}`,
});
} finally {
setIsPurgingLibraries(false);
}
};
const updateCheckedAtLabel = updateStatus.checkedAt
? new Date(updateStatus.checkedAt).toLocaleString()
: null;
@@ -205,6 +249,27 @@ export default function GeneralSettings({ onModelChange, onStreamOutputChange })
</div>
)}
</div>
<div className="setting-section danger-zone">
<h3>Purge Databases</h3>
<div className="setting-control-row">
<button
type="button"
className="button danger"
onClick={handlePurgeLibraries}
disabled={isPurgingLibraries || !backendApiUrl}
>
{isPurgingLibraries ? 'Purging...' : 'Delete All Databases'}
</button>
</div>
<p className="setting-description">
Removes every local Heimgeist database, including staged files, corpora, and indexes. This is meant as a recovery action when the DB panel becomes unusable. Chat history stays intact.
</p>
{libraryPurgeStatus.message && (
<p className={`setting-status ${libraryPurgeStatus.tone}`}>
{libraryPurgeStatus.message}
</p>
)}
</div>
</div>
);
}