import { useCallback, useEffect, useMemo, useState } from 'react'; import type { AgentAction, AgentView } from '@stream-control/shared'; import { api, getToken } from './api'; import { useRealtime } from './useRealtime'; import { Login } from './components/Login'; import { AgentCard } from './components/AgentCard'; import { AgentSettings } from './components/AgentSettings'; import { LogPanel } from './components/LogPanel'; interface Toast { message: string; tone: 'info' | 'error'; } export function App() { const [authenticated, setAuthenticated] = useState(() => Boolean(getToken())); const [selection, setSelection] = useState>(new Set()); const [settingsFor, setSettingsFor] = useState(null); const [toast, setToast] = useState(null); const [newAgentToken, setNewAgentToken] = useState<{ name: string; token: string } | null>(null); const onUnauthorized = useCallback(() => setAuthenticated(false), []); const { agents, logs, connected } = useRealtime(authenticated, onUnauthorized); const notify = useCallback((message: string, tone: 'info' | 'error' = 'info') => { setToast({ message, tone }); }, []); useEffect(() => { if (!toast) return; const timer = window.setTimeout(() => setToast(null), 4000); return () => window.clearTimeout(timer); }, [toast]); // La fiche ouverte doit refléter les mises à jour temps réel. const openAgent = useMemo( () => (settingsFor ? (agents.find((agent) => agent.id === settingsFor.id) ?? null) : null), [agents, settingsFor], ); const online = agents.filter((agent) => agent.online); const recording = agents.filter((agent) => agent.status.recording); const runCommand = useCallback( async (id: string, action: AgentAction, params?: Record) => { try { await api.command(id, action, params); } catch (err) { notify(err instanceof Error ? err.message : 'Commande en échec', 'error'); } }, [notify], ); const runBulk = useCallback( async (action: AgentAction) => { const targets = selection.size > 0 ? [...selection] : online.map((agent) => agent.id); if (targets.length === 0) { notify('Aucun agent sélectionné', 'error'); return; } try { const { results } = await api.bulk(targets, action); const failures = results.filter((result) => !result.ok); if (failures.length === 0) notify(`${results.length} agent(s) : commande envoyée`); else notify( `${results.length - failures.length}/${results.length} OK — ${failures[0]?.error ?? ''}`, 'error', ); } catch (err) { notify(err instanceof Error ? err.message : 'Commande groupée en échec', 'error'); } }, [notify, online, selection], ); function toggleSelect(id: string) { setSelection((current) => { const next = new Set(current); if (next.has(id)) next.delete(id); else next.add(id); return next; }); } async function addAgent() { const name = prompt("Nom du nouvel agent (ex. vm-rec-01)")?.trim(); if (!name) return; try { const result = await api.createAgent({ name }); setNewAgentToken({ name, token: result.token }); } catch (err) { notify(err instanceof Error ? err.message : 'Création impossible', 'error'); } } function logout() { api.logout(); setAuthenticated(false); } if (!authenticated) { return setAuthenticated(true)} />; } return (

Stream Control

{online.length}/{agents.length} en ligne · {recording.length} en enregistrement
{!connected && (
Flux temps réel interrompu — reconnexion en cours…
)} {newAgentToken && (
Agent « {newAgentToken.name} » créé. Jeton (affiché une seule fois) :{' '} {newAgentToken.token}
)}
{agents.length === 0 && (

Aucun agent enregistré

Crée un agent ici pour obtenir un jeton, ou démarre un agent avec le jeton d'enrôlement : il apparaîtra automatiquement.

)} {agents.map((agent) => ( ))}
{openAgent && ( setSettingsFor(null)} onCommand={runCommand} notify={notify} /> )} {toast &&
{toast.message}
}
); }