Add library purge functionality in backend and frontend
This commit is contained in:
@@ -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))
|
||||
|
||||
11
src/App.jsx
11
src/App.jsx
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user