Feat : Streamers page revamp
All checks were successful
release / build (push) Successful in 27s
release / verify-windows (push) Successful in 1m26s

This commit is contained in:
jeanotx32
2026-08-12 15:28:56 +02:00
parent d8846aac9a
commit 96b364bd66
15 changed files with 1625 additions and 131 deletions

View File

@@ -13,7 +13,17 @@ import type {
} from '@stream-control/shared';
import { emptyStatus } from '@stream-control/shared';
import { config } from './config.ts';
import { agentsRepo, logsRepo, targetsRepo, type AgentRecord } from './db.ts';
import { agentsRepo, logsRepo, spansRepo, targetsRepo, type AgentRecord } from './db.ts';
/**
* Au-delà de ce silence, une capture en cours est considérée comme terminée.
*
* Généreux face aux deux secondes du cycle de statut : un agent qui redémarre
* pendant un enregistrement doit retrouver son intervalle plutôt que d'en
* ouvrir un second. Le délai ne coûte rien en justesse — l'intervalle est
* refermé à sa dernière preuve de vie, pas à l'heure du balayage.
*/
const SPAN_STALE_MS = 60_000;
interface PendingCommand {
resolve: (value: unknown) => void;
@@ -87,9 +97,44 @@ class Hub {
updateStatus(agentId: string, status: AgentStatus): void {
this.statuses.set(agentId, status);
this.markSeen(agentId);
this.trackRecording(agentId, status);
this.publishAgent(agentId);
}
/**
* Tient à jour l'intervalle de capture en cours de cette VM.
*
* Une pause OBS (show privé) n'interrompt pas l'intervalle : elle n'écrit
* rien mais ne clôt pas le fichier, et le flux public s'est de toute façon
* arrêté pendant ce temps — la frise le montre déjà comme une coupure de
* diffusion. Découper ici laisserait croire à deux captures distinctes.
*/
private trackRecording(agentId: string, status: AgentStatus): void {
const now = Date.now();
const open = spansRepo.openForAgent(agentId);
if (!status.recording) {
if (open) spansRepo.close(open.id, now);
return;
}
if (open) {
// Le profil n'est cherché que tant qu'il manque : une capture déjà
// rattachée l'est pour de bon, inutile de reposer la question toutes les
// deux secondes.
spansRepo.touch(open.id, now, open.targetId ? null : this.recordedTargetId(agentId));
} else {
spansRepo.open(agentId, this.recordedTargetId(agentId), now);
}
}
/** Le profil que cette VM capture, d'après le pseudo sur lequel sa veille est calée. */
private recordedTargetId(agentId: string): string | null {
const record = agentsRepo.get(agentId);
if (!record?.watch.username) return null;
return targetsRepo.findByUsername(record.watch.provider, record.watch.username)?.id ?? null;
}
resolveCommand(agentId: string, requestId: string, ok: boolean, data: unknown, error?: string): void {
const pending = this.connections.get(agentId)?.pending.get(requestId);
if (!pending) return;
@@ -271,6 +316,12 @@ class Hub {
/** Coupe les agents silencieux : le heartbeat n'arrive plus. */
reapStale(): void {
// Même ménage côté captures : une VM disparue en plein enregistrement
// laisse un intervalle ouvert. On le referme à sa dernière preuve de vie,
// pas à maintenant — sans quoi une VM éteinte une semaine passerait pour
// avoir enregistré une semaine.
spansRepo.sweepStale(Date.now() - SPAN_STALE_MS);
const deadline = Date.now() - config.agentTimeoutMs;
for (const [agentId, connection] of this.connections) {
if (connection.lastSeenAt < deadline) {