This commit is contained in:
jeanotx32
2026-08-11 00:26:56 +02:00
commit 6f11b72cbb
43 changed files with 7175 additions and 0 deletions

View File

@@ -0,0 +1,161 @@
import { useState } from 'react';
import type { AgentAction, AgentView } from '@stream-control/shared';
import { api } from '../api';
interface Props {
agent: AgentView;
onClose: () => void;
onCommand: (id: string, action: AgentAction, params?: Record<string, unknown>) => Promise<void>;
notify: (message: string, tone?: 'info' | 'error') => void;
}
export function AgentSettings({ agent, onClose, onCommand, notify }: Props) {
const [name, setName] = useState(agent.name);
const [host, setHost] = useState(agent.obs.host);
const [port, setPort] = useState(String(agent.obs.port));
const [password, setPassword] = useState(agent.obs.password);
const [autoConnect, setAutoConnect] = useState(agent.autoConnectObs);
const [notes, setNotes] = useState(agent.notes ?? '');
const [directory, setDirectory] = useState(agent.status.recordDirectory ?? '');
const [token, setToken] = useState<string | null>(null);
const [busy, setBusy] = useState(false);
async function save() {
setBusy(true);
try {
await api.updateAgent(agent.id, {
name,
notes,
autoConnectObs: autoConnect,
obs: { host, port: Number(port), password },
} as Partial<AgentView>);
notify('Configuration enregistrée');
onClose();
} catch (err) {
notify(err instanceof Error ? err.message : 'Enregistrement impossible', 'error');
} finally {
setBusy(false);
}
}
async function applyDirectory() {
if (!directory.trim()) return;
await onCommand(agent.id, 'recordDirectory.set', { directory: directory.trim() });
}
async function rotate() {
if (!confirm("Régénérer le jeton ? L'agent sera déconnecté jusqu'à sa reconfiguration.")) return;
try {
const result = await api.rotateToken(agent.id);
setToken(result.token);
} catch (err) {
notify(err instanceof Error ? err.message : 'Rotation impossible', 'error');
}
}
async function remove() {
if (!confirm(`Supprimer définitivement l'agent « ${agent.name} » ?`)) return;
try {
await api.deleteAgent(agent.id);
notify('Agent supprimé');
onClose();
} catch (err) {
notify(err instanceof Error ? err.message : 'Suppression impossible', 'error');
}
}
return (
<div className="modal-backdrop" onClick={onClose}>
<div className="modal" onClick={(event) => event.stopPropagation()}>
<header className="modal-head">
<h2>Configuration · {agent.name}</h2>
<button className="icon" onClick={onClose}>
</button>
</header>
<div className="modal-body">
<label className="field">
<span>Nom affiché</span>
<input value={name} onChange={(event) => setName(event.target.value)} />
</label>
<div className="row">
<label className="field grow">
<span>Hôte obs-websocket</span>
<input value={host} onChange={(event) => setHost(event.target.value)} />
</label>
<label className="field small-field">
<span>Port</span>
<input value={port} onChange={(event) => setPort(event.target.value)} inputMode="numeric" />
</label>
</div>
<label className="field">
<span>Mot de passe obs-websocket</span>
<input
type="password"
value={password}
onChange={(event) => setPassword(event.target.value)}
placeholder="inchangé"
/>
</label>
<label className="checkbox">
<input
type="checkbox"
checked={autoConnect}
onChange={(event) => setAutoConnect(event.target.checked)}
/>
<span>Connecter OBS automatiquement au démarrage de l'agent</span>
</label>
<label className="field">
<span>Notes</span>
<textarea rows={2} value={notes} onChange={(event) => setNotes(event.target.value)} />
</label>
<div className="row">
<label className="field grow">
<span>Dossier d'enregistrement OBS</span>
<input
value={directory}
onChange={(event) => setDirectory(event.target.value)}
placeholder="D:\\records ou /srv/records"
/>
</label>
<button className="ghost align-end" onClick={() => void applyDirectory()}>
Appliquer
</button>
</div>
<div className="danger-zone">
<div>
<strong>Jeton d'agent</strong>
<p className="muted small">
Affiché une seule fois. À reporter dans le fichier <code>agent.config.json</code> de la VM.
</p>
{token && <code className="token">{token}</code>}
</div>
<button className="ghost" onClick={() => void rotate()}>
Régénérer
</button>
</div>
</div>
<footer className="modal-foot">
<button className="danger ghost" onClick={() => void remove()}>
Supprimer l'agent
</button>
<div className="spacer" />
<button className="ghost" onClick={onClose}>
Annuler
</button>
<button className="primary" disabled={busy} onClick={() => void save()}>
Enregistrer
</button>
</footer>
</div>
</div>
);
}