feat(dashboard): close legacy parity gaps
This commit is contained in:
1 parent
70d62b2da3
commit
0fecd426f4
21 files changed
+397
-28
No files matched your search
@@ -24,7 +24,8 @@ export type LiveConsoleAction =
|
||||
| { type: 'set-paused'; paused: boolean }
|
||||
| { type: 'set-auto-scroll'; enabled: boolean }
|
||||
| { type: 'set-level'; level: ConsoleLevelFilter }
|
||||
| { type: 'set-search'; search: string };
|
||||
| { type: 'set-search'; search: string }
|
||||
| { type: 'clear' };
|
||||
|
||||
function boundedAppend(
|
||||
entries: readonly ConsoleEntry[],
|
||||
@@ -89,6 +90,8 @@ export function reduceLiveConsoleState(
|
||||
return { ...state, level: action.level };
|
||||
case 'set-search':
|
||||
return { ...state, search: action.search };
|
||||
case 'clear':
|
||||
return { ...state, entries: [], pendingEntries: [] };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@ import { z } from 'zod';
|
||||
import {
|
||||
ActivityResponseSchema,
|
||||
ChatSyncStatusSchema,
|
||||
ChatSyncTriggerResponseSchema,
|
||||
FetchAllChatsResponseSchema,
|
||||
} from '../../../../../packages/api-contracts/src/index.js';
|
||||
|
||||
export const RawLogEntrySchema = z.strictObject({
|
||||
@@ -32,8 +34,8 @@ async function parseResponse<T>(
|
||||
}
|
||||
|
||||
export function createOperationsApi(fetchImplementation?: typeof fetch) {
|
||||
function request(path: string): Promise<Response> {
|
||||
return (fetchImplementation ?? globalThis.fetch)(path);
|
||||
function request(path: string, init?: RequestInit): Promise<Response> {
|
||||
return (fetchImplementation ?? globalThis.fetch)(path, init);
|
||||
}
|
||||
return {
|
||||
async getActivity() {
|
||||
@@ -48,6 +50,18 @@ export function createOperationsApi(fetchImplementation?: typeof fetch) {
|
||||
async getSyncStatus() {
|
||||
return parseResponse(await request('/api/chats/sync'), ChatSyncStatusSchema);
|
||||
},
|
||||
async triggerSync(kind: 'delta' | 'full') {
|
||||
const result = await parseResponse(
|
||||
await request(`/api/chats/sync/${kind}`, { method: 'POST' }),
|
||||
ChatSyncTriggerResponseSchema,
|
||||
);
|
||||
if (!('success' in result)) throw new Error(result.error);
|
||||
if (!result.success) throw new Error(result.reason);
|
||||
return result;
|
||||
},
|
||||
async fetchAllChats() {
|
||||
return parseResponse(await request('/api/chats/fetch-all', { method: 'POST' }), FetchAllChatsResponseSchema);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
type ConsoleEntry,
|
||||
type ConsoleLevelFilter,
|
||||
} from './live-console-model.js';
|
||||
import { useActivityQuery, useLogsQuery, useSyncStatusQuery } from './operations-queries.js';
|
||||
import { useActivityQuery, useFetchAllChatsMutation, useLogsQuery, useSyncStatusQuery, useTriggerChatSyncMutation } from './operations-queries.js';
|
||||
import {
|
||||
createBrowserEventSource,
|
||||
createDashboardRealtimeClient,
|
||||
@@ -77,6 +77,9 @@ export function OperationsPage({ realtimeEnabled = true }: OperationsPageProps)
|
||||
const activityQuery = useActivityQuery();
|
||||
const logsQuery = useLogsQuery();
|
||||
const syncQuery = useSyncStatusQuery();
|
||||
const syncMutation = useTriggerChatSyncMutation();
|
||||
const fetchAllMutation = useFetchAllChatsMutation();
|
||||
const [consoleClearNotice, setConsoleClearNotice] = useState(false);
|
||||
const [consoleState, dispatchConsole] = useReducer(
|
||||
reduceLiveConsoleState,
|
||||
createLiveConsoleState(500),
|
||||
@@ -132,6 +135,15 @@ export function OperationsPage({ realtimeEnabled = true }: OperationsPageProps)
|
||||
}, [queryClient, realtimeEnabled]);
|
||||
|
||||
const visibleEntries = selectVisibleConsoleEntries(consoleState);
|
||||
const syncBusy = syncQuery.data?.syncing === true || syncMutation.isPending || fetchAllMutation.isPending;
|
||||
|
||||
function confirmAndRun(kind: 'full' | 'fetch-all') {
|
||||
const confirmed = globalThis.confirm(kind === 'full'
|
||||
? 'Run a full chat cache sync? This can take significantly longer than a delta sync.'
|
||||
: 'Fetch every chat now? This is a legacy blocking operation and may take a while.');
|
||||
if (!confirmed) return;
|
||||
if (kind === 'full') syncMutation.mutate('full'); else fetchAllMutation.mutate();
|
||||
}
|
||||
useEffect(() => {
|
||||
if (
|
||||
consoleState.isAutoScrollEnabled &&
|
||||
@@ -156,17 +168,23 @@ export function OperationsPage({ realtimeEnabled = true }: OperationsPageProps)
|
||||
</section>
|
||||
<section aria-labelledby="sync-jobs-heading" className="panel feature-panel" role="region">
|
||||
<div className="panel-heading"><div><span className="eyebrow">Background work</span><h2 id="sync-jobs-heading">Sync jobs</h2></div></div>
|
||||
{syncQuery.isPending ? <p role="status">Loading sync state…</p> : syncQuery.isError ? <p role="alert">{syncQuery.error.message}</p> : <SyncSummary status={syncQuery.data} />}
|
||||
{syncQuery.isPending ? <p role="status">Loading sync state…</p> : syncQuery.isError ? <p role="alert">{syncQuery.error.message}</p> : <><SyncSummary status={syncQuery.data} /><div className="header-actions"><button disabled={syncBusy} onClick={() => syncMutation.mutate('delta')} type="button">Sync latest chats</button><button disabled={syncBusy} onClick={() => confirmAndRun('full')} type="button">Full cache sync</button><button disabled={syncBusy} onClick={() => confirmAndRun('fetch-all')} type="button">Fetch all chats</button></div></>}
|
||||
{syncMutation.isSuccess ? <p role="status">{syncMutation.data.kind === 'delta' ? 'Delta' : 'Full'} sync started. Progress is shown from server status.</p> : null}
|
||||
{fetchAllMutation.isSuccess ? <p role="status">Fetched {fetchAllMutation.data.count} chats.</p> : null}
|
||||
{syncMutation.isError ? <p role="alert">{syncMutation.error.message}</p> : null}
|
||||
{fetchAllMutation.isError ? <p role="alert">{fetchAllMutation.error.message}</p> : null}
|
||||
</section>
|
||||
</div>
|
||||
<section aria-labelledby="live-console-heading" className="panel console-panel" role="region">
|
||||
<div className="panel-heading"><div><span className="eyebrow">Raw diagnostics</span><h2 id="live-console-heading">Live console</h2></div><span className="console-count">{consoleState.entries.length} retained</span></div>
|
||||
<div className="console-controls">
|
||||
<button aria-pressed={consoleState.isPaused} className="button secondary" onClick={() => dispatchConsole({ type: 'set-paused', paused: !consoleState.isPaused })} type="button">{consoleState.isPaused ? `Resume console (${consoleState.pendingEntries.length})` : 'Pause console'}</button>
|
||||
<button className="button secondary" onClick={() => { dispatchConsole({ type: 'clear' }); setConsoleClearNotice(true); }} type="button">Clear local console</button>
|
||||
<label>Log level<select aria-label="Log level" onChange={(event) => dispatchConsole({ type: 'set-level', level: event.target.value as ConsoleLevelFilter })} value={consoleState.level}><option value="all">All levels</option><option value="debug">Debug</option><option value="info">Info</option><option value="warn">Warning</option><option value="error">Error</option></select></label>
|
||||
<label>Search logs<input aria-label="Search logs" onChange={(event) => dispatchConsole({ type: 'set-search', search: event.target.value })} placeholder="Message or metadata" type="search" value={consoleState.search} /></label>
|
||||
<label className="checkbox-control"><input checked={consoleState.isAutoScrollEnabled} onChange={(event) => dispatchConsole({ type: 'set-auto-scroll', enabled: event.target.checked })} type="checkbox" />Auto-scroll</label>
|
||||
</div>
|
||||
{consoleClearNotice ? <p role="status">Cleared from this browser view. Server logs were not deleted.</p> : null}
|
||||
<div aria-live="polite" className="live-console" role="log">
|
||||
{logsQuery.isPending ? <p>Loading logs…</p> : visibleEntries.length === 0 ? <p>No logs match the current filters.</p> : visibleEntries.map((entry) => (
|
||||
<article className={`console-entry ${entry.level}`} key={entry.id}><time dateTime={entry.occurredAt}>{formatTimestamp(entry.occurredAt)}</time><strong>{entry.level.toUpperCase()}</strong><span>{entry.message}</span></article>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { operationsApi } from './operations-api.js';
|
||||
|
||||
export const operationsQueryKeys = {
|
||||
@@ -16,5 +16,15 @@ export function useLogsQuery() {
|
||||
}
|
||||
|
||||
export function useSyncStatusQuery() {
|
||||
return useQuery({ queryKey: operationsQueryKeys.syncStatus, queryFn: operationsApi.getSyncStatus });
|
||||
return useQuery({ queryKey: operationsQueryKeys.syncStatus, queryFn: operationsApi.getSyncStatus, refetchInterval: (query) => query.state.data?.syncing ? 2_000 : false });
|
||||
}
|
||||
|
||||
export function useTriggerChatSyncMutation() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({ mutationFn: (kind: 'delta' | 'full') => operationsApi.triggerSync(kind), onSettled: () => queryClient.invalidateQueries({ queryKey: operationsQueryKeys.syncStatus }) });
|
||||
}
|
||||
|
||||
export function useFetchAllChatsMutation() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({ mutationFn: operationsApi.fetchAllChats, onSettled: () => queryClient.invalidateQueries({ queryKey: operationsQueryKeys.syncStatus }) });
|
||||
}
|
||||
Reference in new issue
Block a user