Feat : STRCHA Stream status
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { useState } from 'react';
|
||||
import type { AgentAction, AgentView } from '@stream-control/shared';
|
||||
import type { AgentAction, AgentView, WatchState } from '@stream-control/shared';
|
||||
import { formatBytes, formatPercent, formatRelative, formatTimecode } from '../format';
|
||||
|
||||
interface Props {
|
||||
@@ -26,13 +26,16 @@ export function AgentCard({ agent, selected, onToggleSelect, onCommand, onOpenSe
|
||||
const busy = pending !== null;
|
||||
const obsReady = agent.online && status.obsConnected;
|
||||
|
||||
const watch = status.watch;
|
||||
const pausedLabel = watch?.autoPaused ? 'Pause · show privé' : 'En pause';
|
||||
|
||||
const state = !agent.online
|
||||
? { label: 'Hors-ligne', tone: 'offline' as const }
|
||||
: !status.obsConnected
|
||||
? { label: 'OBS déconnecté', tone: 'warn' as const }
|
||||
: status.recording
|
||||
? {
|
||||
label: status.recordPaused ? 'En pause' : 'Enregistre',
|
||||
label: status.recordPaused ? pausedLabel : 'Enregistre',
|
||||
tone: status.recordPaused ? ('warn' as const) : ('rec' as const),
|
||||
}
|
||||
: { label: 'Prêt', tone: 'ok' as const };
|
||||
@@ -65,6 +68,15 @@ export function AgentCard({ agent, selected, onToggleSelect, onCommand, onOpenSe
|
||||
<p className="error small">{status.obsError}</p>
|
||||
)}
|
||||
|
||||
{watch?.enabled && (
|
||||
<WatchStrip
|
||||
watch={watch}
|
||||
busy={busy}
|
||||
onCheck={() => void run('watch.check')}
|
||||
onFullscreen={() => void run('hotkey.fullscreen')}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="metrics">
|
||||
<Metric label="Durée" value={formatTimecode(status.recordTimecode)} mono />
|
||||
<Metric label="Fichier" value={formatBytes(status.recordBytes)} />
|
||||
@@ -150,6 +162,56 @@ export function AgentCard({ agent, selected, onToggleSelect, onCommand, onOpenSe
|
||||
);
|
||||
}
|
||||
|
||||
const WATCH_LABELS: Record<WatchState['state'], { text: string; tone: string }> = {
|
||||
public: { text: 'public', tone: 'ok' },
|
||||
private: { text: 'show privé', tone: 'rec' },
|
||||
offline: { text: 'hors-ligne', tone: 'offline' },
|
||||
unknown: { text: 'inconnu', tone: 'offline' },
|
||||
};
|
||||
|
||||
function WatchStrip({
|
||||
watch,
|
||||
busy,
|
||||
onCheck,
|
||||
onFullscreen,
|
||||
}: {
|
||||
watch: WatchState;
|
||||
busy: boolean;
|
||||
onCheck: () => void;
|
||||
onFullscreen: () => void;
|
||||
}) {
|
||||
const info = WATCH_LABELS[watch.state];
|
||||
const stale = watch.lastCheckedAt > 0 && Date.now() - watch.lastCheckedAt > 60_000;
|
||||
|
||||
return (
|
||||
<div className="watch">
|
||||
<div className="watch-head">
|
||||
<span className={`badge ${info.tone}`}>{info.text}</span>
|
||||
<span className="watch-name" title={`${watch.provider} · ${watch.username}`}>
|
||||
{watch.username || '(aucun pseudo)'}
|
||||
</span>
|
||||
<div className="spacer" />
|
||||
<button className="icon" disabled={busy} onClick={onCheck} title="Sonder maintenant">
|
||||
⟳
|
||||
</button>
|
||||
<button className="icon" disabled={busy} onClick={onFullscreen} title="Remettre en plein écran">
|
||||
⛶
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{watch.lastError ? (
|
||||
<span className="small error">Sonde en échec : {watch.lastError}</span>
|
||||
) : (
|
||||
<span className="small muted">
|
||||
{watch.rawStatus ? `statut brut « ${watch.rawStatus} »` : 'pas encore sondé'}
|
||||
{watch.autoPaused && ' · pause automatique active'}
|
||||
{stale && ' · dernière sonde ancienne'}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Metric({ label, value, mono }: { label: string; value: string; mono?: boolean }) {
|
||||
return (
|
||||
<div className="metric">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState } from 'react';
|
||||
import type { AgentAction, AgentView } from '@stream-control/shared';
|
||||
import type { AgentAction, AgentView, WatchSettings } from '@stream-control/shared';
|
||||
import { api } from '../api';
|
||||
|
||||
interface Props {
|
||||
@@ -19,6 +19,15 @@ export function AgentSettings({ agent, onClose, onCommand, notify }: Props) {
|
||||
const [directory, setDirectory] = useState(agent.status.recordDirectory ?? '');
|
||||
const [token, setToken] = useState<string | null>(null);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [watch, setWatch] = useState<WatchSettings>(agent.watch);
|
||||
|
||||
function patchWatch(patch: Partial<WatchSettings>) {
|
||||
setWatch((current) => ({ ...current, ...patch }));
|
||||
}
|
||||
|
||||
function patchFullscreen(patch: Partial<WatchSettings['fullscreen']>) {
|
||||
setWatch((current) => ({ ...current, fullscreen: { ...current.fullscreen, ...patch } }));
|
||||
}
|
||||
|
||||
async function save() {
|
||||
setBusy(true);
|
||||
@@ -28,6 +37,7 @@ export function AgentSettings({ agent, onClose, onCommand, notify }: Props) {
|
||||
notes,
|
||||
autoConnectObs: autoConnect,
|
||||
obs: { host, port: Number(port), password },
|
||||
watch,
|
||||
} as Partial<AgentView>);
|
||||
notify('Configuration enregistrée');
|
||||
onClose();
|
||||
@@ -129,6 +139,127 @@ export function AgentSettings({ agent, onClose, onCommand, notify }: Props) {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<fieldset className="group">
|
||||
<legend>Surveillance du stream</legend>
|
||||
|
||||
<label className="checkbox">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={watch.enabled}
|
||||
onChange={(event) => patchWatch({ enabled: event.target.checked })}
|
||||
/>
|
||||
<span>Mettre l'enregistrement en pause pendant les shows privés</span>
|
||||
</label>
|
||||
|
||||
<div className="row">
|
||||
<label className="field small-field">
|
||||
<span>Plateforme</span>
|
||||
<select
|
||||
value={watch.provider}
|
||||
onChange={(event) =>
|
||||
patchWatch({ provider: event.target.value as WatchSettings['provider'] })
|
||||
}
|
||||
>
|
||||
<option value="stripchat">Stripchat</option>
|
||||
</select>
|
||||
</label>
|
||||
<label className="field grow">
|
||||
<span>Pseudo du streamer</span>
|
||||
<input
|
||||
value={watch.username}
|
||||
onChange={(event) => patchWatch({ username: event.target.value })}
|
||||
placeholder="tel qu'il apparaît dans l'URL"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="row">
|
||||
<label className="field grow">
|
||||
<span>Intervalle de sonde (s)</span>
|
||||
<input
|
||||
value={String(Math.round(watch.pollIntervalMs / 1000))}
|
||||
onChange={(event) =>
|
||||
patchWatch({ pollIntervalMs: (Number(event.target.value) || 10) * 1000 })
|
||||
}
|
||||
inputMode="numeric"
|
||||
/>
|
||||
</label>
|
||||
<label className="field grow">
|
||||
<span>Lectures avant pause</span>
|
||||
<input
|
||||
value={String(watch.confirmations)}
|
||||
onChange={(event) => patchWatch({ confirmations: Number(event.target.value) || 1 })}
|
||||
inputMode="numeric"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label className="field">
|
||||
<span>Statuts considérés comme « privé »</span>
|
||||
<input
|
||||
value={watch.privateStatuses.join(', ')}
|
||||
onChange={(event) =>
|
||||
patchWatch({
|
||||
privateStatuses: event.target.value
|
||||
.split(',')
|
||||
.map((entry) => entry.trim())
|
||||
.filter(Boolean),
|
||||
})
|
||||
}
|
||||
/>
|
||||
<span className="muted small">
|
||||
Statuts renvoyés par Stripchat : public, private, p2p, groupShow, idle. Retire
|
||||
groupShow si tu veux continuer à enregistrer les shows de groupe.
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<label className="checkbox">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={watch.fullscreen.enabled}
|
||||
onChange={(event) => patchFullscreen({ enabled: event.target.checked })}
|
||||
/>
|
||||
<span>Rappeler le plein écran à la fin du show privé</span>
|
||||
</label>
|
||||
|
||||
<div className="row">
|
||||
<label className="field small-field">
|
||||
<span>Touche</span>
|
||||
<input
|
||||
value={watch.fullscreen.key}
|
||||
onChange={(event) => patchFullscreen({ key: event.target.value })}
|
||||
/>
|
||||
</label>
|
||||
<label className="field grow">
|
||||
<span>Titre de la fenêtre du lecteur</span>
|
||||
<input
|
||||
value={watch.fullscreen.windowMatch}
|
||||
onChange={(event) => patchFullscreen({ windowMatch: event.target.value })}
|
||||
placeholder="fragment du titre, ex. Stripchat"
|
||||
/>
|
||||
</label>
|
||||
<label className="field small-field">
|
||||
<span>Délai (s)</span>
|
||||
<input
|
||||
value={String(Math.round(watch.fullscreen.delayMs / 1000))}
|
||||
onChange={(event) =>
|
||||
patchFullscreen({ delayMs: (Number(event.target.value) || 0) * 1000 })
|
||||
}
|
||||
inputMode="numeric"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="row">
|
||||
<button className="ghost" onClick={() => void onCommand(agent.id, 'watch.check')}>
|
||||
Sonder maintenant
|
||||
</button>
|
||||
<button className="ghost" onClick={() => void onCommand(agent.id, 'hotkey.fullscreen')}>
|
||||
Tester le plein écran
|
||||
</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<div className="danger-zone">
|
||||
<div>
|
||||
<strong>Jeton d'agent</strong>
|
||||
|
||||
@@ -349,6 +349,45 @@ textarea:focus {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* --- Bandeau de surveillance --- */
|
||||
|
||||
.watch {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
padding: 8px 10px;
|
||||
background: var(--panel-2);
|
||||
border-radius: 8px;
|
||||
}
|
||||
.watch-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.watch-name {
|
||||
font-size: 13px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
fieldset.group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
margin: 0;
|
||||
padding: 14px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
}
|
||||
fieldset.group > legend {
|
||||
padding: 0 6px;
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.card-foot {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
Reference in New Issue
Block a user