corpus integrity UI: Ingest Health section in SettingsPanel, corpus API types
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useStore } from '@/lib/store';
|
||||
import { api, auth } from '@/lib/api';
|
||||
import type { CorpusStatus } from '@/lib/api';
|
||||
import type { Status, DreamerStatus } from '@/lib/api';
|
||||
|
||||
export default function SettingsPanel() {
|
||||
@@ -17,6 +18,9 @@ export default function SettingsPanel() {
|
||||
const [dreaming, setDreaming] = useState(false);
|
||||
const [dreamStarted, setDreamStarted] = useState(false);
|
||||
const [captures, setCaptures] = useState<{name: string}[]>([]);
|
||||
const [corpusStatus, setCorpusStatus] = useState<CorpusStatus | null>(null);
|
||||
const [reconciling, setReconciling] = useState(false);
|
||||
const [retrying, setRetrying] = useState<string | null>(null);
|
||||
|
||||
function formatDreamerTime(raw: string): string {
|
||||
if (!raw || raw === 'never' || raw === '—') return raw;
|
||||
@@ -39,6 +43,7 @@ export default function SettingsPanel() {
|
||||
api.getDreamerStatus().then(setDreamerStatus).catch(console.error);
|
||||
fetch('/api/captures').then(r => r.json()).then(d => setCaptures(d.captures || [])).catch(() => {});
|
||||
api.getMemory().then(d => setMemory(d.content)).catch(console.error);
|
||||
api.getCorpusStatus().then(setCorpusStatus).catch(console.error);
|
||||
}, [settingsOpen]);
|
||||
|
||||
async function updateSetting(key: string, value: unknown) {
|
||||
@@ -110,6 +115,24 @@ export default function SettingsPanel() {
|
||||
useStore.getState().setMessages([]);
|
||||
}
|
||||
|
||||
async function runReconcile() {
|
||||
setReconciling(true);
|
||||
await api.runReconciliation();
|
||||
setTimeout(() => {
|
||||
api.getCorpusStatus().then(setCorpusStatus).catch(console.error);
|
||||
setReconciling(false);
|
||||
}, 6000);
|
||||
}
|
||||
|
||||
async function retryFailure(source: string) {
|
||||
setRetrying(source);
|
||||
await api.retryIngestFailure(source);
|
||||
setTimeout(() => {
|
||||
api.getCorpusStatus().then(setCorpusStatus).catch(console.error);
|
||||
setRetrying(null);
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
@@ -183,6 +206,57 @@ export default function SettingsPanel() {
|
||||
</Row>
|
||||
</Section>
|
||||
|
||||
|
||||
{/* Ingest Health */}
|
||||
<Section title="Ingest Health">
|
||||
<div className="grid grid-cols-3 gap-2 mb-3">
|
||||
<StatCard number={(corpusStatus?.filesystem || 0).toLocaleString()} label="on disk" />
|
||||
<StatCard number={(corpusStatus?.pgvector || 0).toLocaleString()} label="pgvector" />
|
||||
<StatCard number={(corpusStatus?.graphiti || 0).toLocaleString()} label="graphiti" />
|
||||
</div>
|
||||
{corpusStatus?.last_reconciliation && (
|
||||
<div className="text-xs mb-2" style={{ color: 'var(--text3)' }}>
|
||||
Last check: {new Date(corpusStatus.last_reconciliation.timestamp).toLocaleString()} — {corpusStatus.last_reconciliation.gaps} gap(s), {corpusStatus.last_reconciliation.auto_queued} queued
|
||||
</div>
|
||||
)}
|
||||
<Row label="Check corpus" desc="Find and re-queue missing files">
|
||||
<SBtn primary onClick={runReconcile} disabled={reconciling}>
|
||||
{reconciling ? 'Running...' : 'Run check'}
|
||||
</SBtn>
|
||||
</Row>
|
||||
{corpusStatus && corpusStatus.failure_count > 0 && (
|
||||
<div className="mt-3">
|
||||
<div className="text-xs mb-2" style={{ color: 'var(--text3)', textTransform: 'uppercase', letterSpacing: '0.06em' }}>
|
||||
{corpusStatus.failure_count} ingest failure{corpusStatus.failure_count !== 1 ? 's' : ''}
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
|
||||
{corpusStatus.failures.slice(0, 10).map((f, i) => (
|
||||
<div key={i} style={{
|
||||
display: 'flex', alignItems: 'flex-start', gap: '8px',
|
||||
padding: '6px 8px', background: 'var(--bg2)',
|
||||
borderRadius: '6px', border: '1px solid var(--border)',
|
||||
}}>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{ fontSize: '11px', color: 'var(--text2)', fontFamily: 'var(--font-mono)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||||
{f.source}
|
||||
</div>
|
||||
<div style={{ fontSize: '10px', color: '#a32d2d', marginTop: '2px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||||
{f.error} {f.retry_count > 0 ? `(${f.retry_count} retr${f.retry_count === 1 ? 'y' : 'ies'})` : ''}
|
||||
</div>
|
||||
</div>
|
||||
<SBtn onClick={() => retryFailure(f.source)} disabled={retrying === f.source || f.retry_count >= 2}>
|
||||
{retrying === f.source ? '...' : f.retry_count >= 2 ? 'Manual' : 'Retry'}
|
||||
</SBtn>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{corpusStatus && corpusStatus.failure_count === 0 && (
|
||||
<div className="text-xs mt-1" style={{ color: 'var(--text3)' }}>No ingest failures</div>
|
||||
)}
|
||||
</Section>
|
||||
|
||||
{/* Memory */}
|
||||
<Section title="Memory">
|
||||
{editingMemory ? (
|
||||
|
||||
Reference in New Issue
Block a user