From 3101df38465b5abebf6da0635a4b11a2bbc1f6ee Mon Sep 17 00:00:00 2001 From: Aaron Nelson Date: Thu, 30 Apr 2026 21:54:39 +0000 Subject: [PATCH] corpus integrity UI: Ingest Health section in SettingsPanel, corpus API types --- components/SettingsPanel.tsx | 74 ++++++++++++++++++++++++++++++++++++ lib/api.ts | 43 +++++++++++++++++++++ 2 files changed, 117 insertions(+) diff --git a/components/SettingsPanel.tsx b/components/SettingsPanel.tsx index 8f2e02f..33284be 100644 --- a/components/SettingsPanel.tsx +++ b/components/SettingsPanel.tsx @@ -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(null); + const [reconciling, setReconciling] = useState(false); + const [retrying, setRetrying] = useState(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 ( <>
+ + {/* Ingest Health */} +
+
+ + + +
+ {corpusStatus?.last_reconciliation && ( +
+ Last check: {new Date(corpusStatus.last_reconciliation.timestamp).toLocaleString()} — {corpusStatus.last_reconciliation.gaps} gap(s), {corpusStatus.last_reconciliation.auto_queued} queued +
+ )} + + + {reconciling ? 'Running...' : 'Run check'} + + + {corpusStatus && corpusStatus.failure_count > 0 && ( +
+
+ {corpusStatus.failure_count} ingest failure{corpusStatus.failure_count !== 1 ? 's' : ''} +
+
+ {corpusStatus.failures.slice(0, 10).map((f, i) => ( +
+
+
+ {f.source} +
+
+ {f.error} {f.retry_count > 0 ? `(${f.retry_count} retr${f.retry_count === 1 ? 'y' : 'ies'})` : ''} +
+
+ retryFailure(f.source)} disabled={retrying === f.source || f.retry_count >= 2}> + {retrying === f.source ? '...' : f.retry_count >= 2 ? 'Manual' : 'Retry'} + +
+ ))} +
+
+ )} + {corpusStatus && corpusStatus.failure_count === 0 && ( +
No ingest failures
+ )} +
+ {/* Memory */}
{editingMemory ? ( diff --git a/lib/api.ts b/lib/api.ts index ab8ff49..c1338ca 100644 --- a/lib/api.ts +++ b/lib/api.ts @@ -11,6 +11,29 @@ async function request(path: string, options?: RequestInit): Promise { return res.json(); } + +export interface IngestFailure { + source: string; + filepath: string; + error: string; + retry_count: number; + first_failed_at: string; + last_failed_at: string; +} + +export interface CorpusStatus { + filesystem: number; + pgvector: number; + graphiti: number; + failures: IngestFailure[]; + failure_count: number; + last_reconciliation: { + timestamp: string; + gaps: number; + auto_queued: number; + } | null; +} + export const api = { getSettings: () => request('/settings'), updateSettings: (s: Partial) => @@ -38,6 +61,26 @@ export const api = { updateMemory: (content: string) => request<{ saved: boolean }>('/memory', { method: 'POST', body: JSON.stringify({ content }) }), getStatus: () => request('/status'), + getCorpusStatus: async (): Promise => { + const r = await fetch('/api/corpus/status'); + return r.json(); + }, + retryIngestFailure: async (source: string): Promise<{queued: boolean}> => { + const r = await fetch('/api/corpus/retry', { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({source}), + }); + return r.json(); + }, + runReconciliation: async (): Promise<{started: boolean}> => { + const r = await fetch('/api/corpus/reconcile', { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({fix: true}), + }); + return r.json(); + }, reindex: () => request<{ started: boolean }>('/reindex', { method: 'POST' }), getDreamerStatus: () => request('/dreamer/status'), runDreamer: (mode: string, task?: string) =>