Add a complete web management panel for ZeroClaw, served directly from the binary via rust-embed. The dashboard provides real-time monitoring, agent chat, configuration editing, and system diagnostics — all accessible at http://localhost:5555/ after pairing. Backend (Rust): - Add 15+ REST API endpoints under /api/* with bearer token auth - Add WebSocket agent chat at /ws/chat with query param auth - Add SSE event stream at /api/events via BroadcastObserver - Add rust-embed static file serving at /_app/* with SPA fallback - Extend AppState with tools_registry, cost_tracker, event_tx - Extract doctor::diagnose() for structured diagnostic results - Add Serialize derives to IntegrationStatus, CliCategory, DiscoveredCli Frontend (React + Vite + Tailwind CSS): - 10 dashboard pages: Dashboard, AgentChat, Tools, Cron, Integrations, Memory, Config, Cost, Logs, Doctor - WebSocket client with auto-reconnect for agent chat - SSE client (fetch-based, supports auth headers) for live events - Full EN/TR internationalization (~190 translation keys) - Dark theme with responsive layouts - Auth flow via 6-digit pairing code, token stored in localStorage Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
125 lines
3.3 KiB
TypeScript
125 lines
3.3 KiB
TypeScript
import { useState, useEffect, useRef, useCallback } from 'react';
|
|
import { SSEClient, type SSEClientOptions } from '../lib/sse';
|
|
import type { SSEEvent } from '../types/api';
|
|
|
|
export type SSEConnectionStatus = 'disconnected' | 'connecting' | 'connected';
|
|
|
|
export interface UseSSEResult {
|
|
/** Array of all events received during this session. */
|
|
events: SSEEvent[];
|
|
/** Current connection status. */
|
|
status: SSEConnectionStatus;
|
|
/** Manually connect (called automatically on mount). */
|
|
connect: () => void;
|
|
/** Manually disconnect. */
|
|
disconnect: () => void;
|
|
/** Clear the event history. */
|
|
clearEvents: () => void;
|
|
}
|
|
|
|
export interface UseSSEOptions extends SSEClientOptions {
|
|
/** If false, do not connect automatically on mount. Default true. */
|
|
autoConnect?: boolean;
|
|
/** Maximum number of events to keep in the buffer. Default 500. */
|
|
maxEvents?: number;
|
|
/** Optional filter: only keep events whose type matches. */
|
|
filterTypes?: string[];
|
|
}
|
|
|
|
/**
|
|
* React hook that wraps the SSEClient for live event streaming.
|
|
*
|
|
* Connects on mount (unless `autoConnect` is false), accumulates incoming
|
|
* events, and cleans up on unmount.
|
|
*/
|
|
export function useSSE(options: UseSSEOptions = {}): UseSSEResult {
|
|
const {
|
|
autoConnect = true,
|
|
maxEvents = 500,
|
|
filterTypes,
|
|
...sseOptions
|
|
} = options;
|
|
|
|
const clientRef = useRef<SSEClient | null>(null);
|
|
const [status, setStatus] = useState<SSEConnectionStatus>('disconnected');
|
|
const [events, setEvents] = useState<SSEEvent[]>([]);
|
|
|
|
// Keep filter in a ref so the callback doesn't need to be recreated
|
|
const filterRef = useRef(filterTypes);
|
|
filterRef.current = filterTypes;
|
|
|
|
const maxRef = useRef(maxEvents);
|
|
maxRef.current = maxEvents;
|
|
|
|
// Stable reference to the client across renders
|
|
const getClient = useCallback((): SSEClient => {
|
|
if (!clientRef.current) {
|
|
clientRef.current = new SSEClient(sseOptions);
|
|
}
|
|
return clientRef.current;
|
|
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
|
|
|
// Setup handlers and optionally connect on mount
|
|
useEffect(() => {
|
|
const client = getClient();
|
|
|
|
client.onConnect = () => {
|
|
setStatus('connected');
|
|
};
|
|
|
|
client.onEvent = (event: SSEEvent) => {
|
|
// Apply type filter if configured
|
|
if (filterRef.current && filterRef.current.length > 0) {
|
|
if (!filterRef.current.includes(event.type)) return;
|
|
}
|
|
|
|
setEvents((prev) => {
|
|
const next = [...prev, event];
|
|
// Trim to max buffer size
|
|
if (next.length > maxRef.current) {
|
|
return next.slice(next.length - maxRef.current);
|
|
}
|
|
return next;
|
|
});
|
|
};
|
|
|
|
client.onError = () => {
|
|
setStatus('disconnected');
|
|
};
|
|
|
|
if (autoConnect) {
|
|
setStatus('connecting');
|
|
client.connect();
|
|
}
|
|
|
|
return () => {
|
|
client.disconnect();
|
|
clientRef.current = null;
|
|
};
|
|
}, [getClient, autoConnect]);
|
|
|
|
const connect = useCallback(() => {
|
|
const client = getClient();
|
|
setStatus('connecting');
|
|
client.connect();
|
|
}, [getClient]);
|
|
|
|
const disconnect = useCallback(() => {
|
|
const client = getClient();
|
|
client.disconnect();
|
|
setStatus('disconnected');
|
|
}, [getClient]);
|
|
|
|
const clearEvents = useCallback(() => {
|
|
setEvents([]);
|
|
}, []);
|
|
|
|
return {
|
|
events,
|
|
status,
|
|
connect,
|
|
disconnect,
|
|
clearEvents,
|
|
};
|
|
}
|