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 { useEffect, useState } from 'react';
|
||||||
import { useStore } from '@/lib/store';
|
import { useStore } from '@/lib/store';
|
||||||
import { api, auth } from '@/lib/api';
|
import { api, auth } from '@/lib/api';
|
||||||
|
import type { CorpusStatus } from '@/lib/api';
|
||||||
import type { Status, DreamerStatus } from '@/lib/api';
|
import type { Status, DreamerStatus } from '@/lib/api';
|
||||||
|
|
||||||
export default function SettingsPanel() {
|
export default function SettingsPanel() {
|
||||||
@@ -17,6 +18,9 @@ export default function SettingsPanel() {
|
|||||||
const [dreaming, setDreaming] = useState(false);
|
const [dreaming, setDreaming] = useState(false);
|
||||||
const [dreamStarted, setDreamStarted] = useState(false);
|
const [dreamStarted, setDreamStarted] = useState(false);
|
||||||
const [captures, setCaptures] = useState<{name: string}[]>([]);
|
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 {
|
function formatDreamerTime(raw: string): string {
|
||||||
if (!raw || raw === 'never' || raw === '—') return raw;
|
if (!raw || raw === 'never' || raw === '—') return raw;
|
||||||
@@ -39,6 +43,7 @@ export default function SettingsPanel() {
|
|||||||
api.getDreamerStatus().then(setDreamerStatus).catch(console.error);
|
api.getDreamerStatus().then(setDreamerStatus).catch(console.error);
|
||||||
fetch('/api/captures').then(r => r.json()).then(d => setCaptures(d.captures || [])).catch(() => {});
|
fetch('/api/captures').then(r => r.json()).then(d => setCaptures(d.captures || [])).catch(() => {});
|
||||||
api.getMemory().then(d => setMemory(d.content)).catch(console.error);
|
api.getMemory().then(d => setMemory(d.content)).catch(console.error);
|
||||||
|
api.getCorpusStatus().then(setCorpusStatus).catch(console.error);
|
||||||
}, [settingsOpen]);
|
}, [settingsOpen]);
|
||||||
|
|
||||||
async function updateSetting(key: string, value: unknown) {
|
async function updateSetting(key: string, value: unknown) {
|
||||||
@@ -110,6 +115,24 @@ export default function SettingsPanel() {
|
|||||||
useStore.getState().setMessages([]);
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
@@ -183,6 +206,57 @@ export default function SettingsPanel() {
|
|||||||
</Row>
|
</Row>
|
||||||
</Section>
|
</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 */}
|
{/* Memory */}
|
||||||
<Section title="Memory">
|
<Section title="Memory">
|
||||||
{editingMemory ? (
|
{editingMemory ? (
|
||||||
|
|||||||
+43
@@ -11,6 +11,29 @@ async function request<T>(path: string, options?: RequestInit): Promise<T> {
|
|||||||
return res.json();
|
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 = {
|
export const api = {
|
||||||
getSettings: () => request<Settings>('/settings'),
|
getSettings: () => request<Settings>('/settings'),
|
||||||
updateSettings: (s: Partial<Settings>) =>
|
updateSettings: (s: Partial<Settings>) =>
|
||||||
@@ -38,6 +61,26 @@ export const api = {
|
|||||||
updateMemory: (content: string) =>
|
updateMemory: (content: string) =>
|
||||||
request<{ saved: boolean }>('/memory', { method: 'POST', body: JSON.stringify({ content }) }),
|
request<{ saved: boolean }>('/memory', { method: 'POST', body: JSON.stringify({ content }) }),
|
||||||
getStatus: () => request<Status>('/status'),
|
getStatus: () => request<Status>('/status'),
|
||||||
|
getCorpusStatus: async (): Promise<CorpusStatus> => {
|
||||||
|
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' }),
|
reindex: () => request<{ started: boolean }>('/reindex', { method: 'POST' }),
|
||||||
getDreamerStatus: () => request<DreamerStatus>('/dreamer/status'),
|
getDreamerStatus: () => request<DreamerStatus>('/dreamer/status'),
|
||||||
runDreamer: (mode: string, task?: string) =>
|
runDreamer: (mode: string, task?: string) =>
|
||||||
|
|||||||
Reference in New Issue
Block a user