mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-11-15 11:12:16 -05:00
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
// Discord Replay Command
|
// Discord Replay Command
|
||||||
// Purpose: Handles replay capture requests from Discord through the shared replay job/status workflow.
|
// Purpose: Handles replay capture requests from Discord through the shared replay job/status workflow.
|
||||||
// Scope: Resolves sources, enforces cooldowns, reports job progress, uploads video, and broadcasts media URLs.
|
// Scope: Resolves sources, enforces replay availability, reports job progress, uploads video, and broadcasts media URLs.
|
||||||
const { AttachmentBuilder } = require('discord.js');
|
const { AttachmentBuilder } = require('discord.js');
|
||||||
const io = require('../../../globals/io');
|
const io = require('../../../globals/io');
|
||||||
const { hostReplay } = require('../../replayMediaService');
|
const { hostReplay } = require('../../replayMediaService');
|
||||||
@@ -60,12 +60,6 @@ function createReplayCommand({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const attempt = tryTriggerReplay({ by: message.author?.id || null, source: 'discord' });
|
|
||||||
if (!attempt.ok) {
|
|
||||||
await replyDenied(message, `Replay denied: cooldown active. Try again in ${Math.ceil(attempt.remainingMs / 1000)}s.`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const requester = message.member?.nickname || message.author?.globalName || message.author?.username || 'Discord';
|
const requester = message.member?.nickname || message.author?.globalName || message.author?.username || 'Discord';
|
||||||
const job = createReplayJob({
|
const job = createReplayJob({
|
||||||
id: buildReplayJobId('discord'),
|
id: buildReplayJobId('discord'),
|
||||||
@@ -74,17 +68,23 @@ function createReplayCommand({
|
|||||||
sources: resolved.sources || [],
|
sources: resolved.sources || [],
|
||||||
includeSidebar: true,
|
includeSidebar: true,
|
||||||
});
|
});
|
||||||
jobStatus.emit(job, 'accepted', { message: buildAcceptedMessage(job) });
|
const attempt = tryTriggerReplay({ jobId: job.id, by: { source: 'discord', requester } });
|
||||||
|
if (!attempt.ok) {
|
||||||
const progressMessage = await message.reply({
|
await replyDenied(message, `Replay denied: ${attempt.error}`);
|
||||||
content: sanitizeMentions(buildAcceptedMessage(job)),
|
return;
|
||||||
allowedMentions: DEFAULT_ALLOWED_MENTIONS,
|
}
|
||||||
});
|
|
||||||
const stopTyping = startDiscordTypingLoop(message.channel, logger, 'discord replay command');
|
|
||||||
|
|
||||||
|
let progressMessage = null;
|
||||||
|
let stopTyping = () => {};
|
||||||
let builtReplay = null;
|
let builtReplay = null;
|
||||||
let deliveredMedia = null;
|
let deliveredMedia = null;
|
||||||
try {
|
try {
|
||||||
|
jobStatus.emit(job, 'accepted', { message: buildAcceptedMessage(job) });
|
||||||
|
progressMessage = await message.reply({
|
||||||
|
content: sanitizeMentions(buildAcceptedMessage(job)),
|
||||||
|
allowedMentions: DEFAULT_ALLOWED_MENTIONS,
|
||||||
|
});
|
||||||
|
stopTyping = startDiscordTypingLoop(message.channel, logger, 'discord replay command');
|
||||||
jobStatus.emit(job, 'building', { message: buildStatusMessage(job, 'building') });
|
jobStatus.emit(job, 'building', { message: buildStatusMessage(job, 'building') });
|
||||||
if (progressMessage?.edit) {
|
if (progressMessage?.edit) {
|
||||||
await progressMessage.edit({ content: sanitizeMentions(buildStatusMessage(job, 'building')), allowedMentions: DEFAULT_ALLOWED_MENTIONS });
|
await progressMessage.edit({ content: sanitizeMentions(buildStatusMessage(job, 'building')), allowedMentions: DEFAULT_ALLOWED_MENTIONS });
|
||||||
|
|||||||
@@ -35,9 +35,9 @@ async function deliverReplay(payload = {}) {
|
|||||||
requestedBy: payload.requestedBy,
|
requestedBy: payload.requestedBy,
|
||||||
});
|
});
|
||||||
|
|
||||||
jobStatus.emit(job, 'accepted', { message: buildAcceptedMessage(job) });
|
|
||||||
let providerContext = null;
|
let providerContext = null;
|
||||||
try {
|
try {
|
||||||
|
jobStatus.emit(job, 'accepted', { message: buildAcceptedMessage(job) });
|
||||||
let providerError = null;
|
let providerError = null;
|
||||||
if (preferredDeliveryProvider?.begin) {
|
if (preferredDeliveryProvider?.begin) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// Purpose: Provides transport-neutral replay jobs, source lookup, status events, and user-facing progress text.
|
// Purpose: Provides transport-neutral replay jobs, source lookup, status events, and user-facing progress text.
|
||||||
// Scope: Keeps Discord-command and web-triggered replay delivery on the same core status pipeline.
|
// Scope: Keeps Discord-command and web-triggered replay delivery on the same core status pipeline.
|
||||||
const Fuse = require('fuse.js');
|
const Fuse = require('fuse.js');
|
||||||
|
const { updateReplayStatus } = require('../replayEngineV2/cooldown');
|
||||||
|
|
||||||
const DEFAULT_ALLOWED_MENTIONS = { parse: [], repliedUser: false };
|
const DEFAULT_ALLOWED_MENTIONS = { parse: [], repliedUser: false };
|
||||||
const FUZZY_THRESHOLD = 0.42;
|
const FUZZY_THRESHOLD = 0.42;
|
||||||
@@ -180,6 +181,22 @@ function createJobStatusEmitter({ io, logger, sanitizeMentions }) {
|
|||||||
|
|
||||||
function emit(job, status, extra = {}) {
|
function emit(job, status, extra = {}) {
|
||||||
const payload = buildPayload(job, status, extra);
|
const payload = buildPayload(job, status, extra);
|
||||||
|
updateReplayStatus(payload);
|
||||||
|
if (status === 'accepted') {
|
||||||
|
// Sound once per participating rover; a disconnected rover must not abort video creation.
|
||||||
|
const { issueCommand } = require('../commandService');
|
||||||
|
const roverIds = new Set(job.sources.filter((entry) => entry.type === 'rover').map((entry) => entry.id));
|
||||||
|
for (const roverId of roverIds) {
|
||||||
|
try {
|
||||||
|
issueCommand(roverId, {
|
||||||
|
type: 'song',
|
||||||
|
song: { notes: [{ note: 72, duration: 8 }, { note: 76, duration: 8 }, { note: 79, duration: 16 }] },
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
logger?.warn?.('Unable to play replay tune', { roverId, error: err.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
io.emit('replay:status', payload);
|
io.emit('replay:status', payload);
|
||||||
if (status === 'ready' && extra.media) io.emit('replay:ready', extra.media);
|
if (status === 'ready' && extra.media) io.emit('replay:ready', extra.media);
|
||||||
if (status === 'failed') io.emit('replay:failed', payload);
|
if (status === 'failed') io.emit('replay:failed', payload);
|
||||||
|
|||||||
@@ -4,40 +4,61 @@
|
|||||||
const EventEmitter = require('events');
|
const EventEmitter = require('events');
|
||||||
|
|
||||||
const COOLDOWN_MS = 10 * 1000;
|
const COOLDOWN_MS = 10 * 1000;
|
||||||
|
const MAX_CONCURRENT_JOBS = 3;
|
||||||
const replayEvents = new EventEmitter();
|
const replayEvents = new EventEmitter();
|
||||||
|
const activeJobs = new Map();
|
||||||
|
let status = null;
|
||||||
let lastTriggeredAt = null;
|
let lastTriggeredAt = null;
|
||||||
let lastTriggeredBy = null;
|
let lastTriggeredBy = null;
|
||||||
|
|
||||||
function getRemainingMs(now = Date.now()) {
|
|
||||||
if (!lastTriggeredAt) return 0;
|
|
||||||
return Math.max(0, COOLDOWN_MS - (now - lastTriggeredAt));
|
|
||||||
}
|
|
||||||
|
|
||||||
function getReplayState() {
|
function getReplayState() {
|
||||||
const remainingMs = getRemainingMs();
|
const remainingMs = lastTriggeredAt === null ? 0 : Math.max(0, COOLDOWN_MS - (Date.now() - lastTriggeredAt));
|
||||||
return {
|
return {
|
||||||
cooldownMs: COOLDOWN_MS,
|
cooldownMs: COOLDOWN_MS,
|
||||||
lastTriggeredAt,
|
lastTriggeredAt,
|
||||||
lastTriggeredBy,
|
lastTriggeredBy,
|
||||||
remainingMs,
|
remainingMs,
|
||||||
available: remainingMs === 0,
|
atJobLimit: activeJobs.size >= MAX_CONCURRENT_JOBS,
|
||||||
|
activeSourceKeys: [...new Set(Array.from(activeJobs.values()).flatMap((sources) =>
|
||||||
|
sources.map((source) => `${source.type}:${source.id}`),
|
||||||
|
))],
|
||||||
|
status,
|
||||||
|
available: remainingMs === 0 && activeJobs.size < MAX_CONCURRENT_JOBS,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function tryTriggerReplay(by = null) {
|
function emitUpdate() {
|
||||||
const remainingMs = getRemainingMs();
|
replayEvents.emit('update', { state: getReplayState(), by: lastTriggeredBy });
|
||||||
if (remainingMs > 0) {
|
|
||||||
return { ok: false, remainingMs, state: getReplayState() };
|
|
||||||
}
|
|
||||||
lastTriggeredAt = Date.now();
|
|
||||||
lastTriggeredBy = by || null;
|
|
||||||
const state = getReplayState();
|
|
||||||
replayEvents.emit('update', { state, by: lastTriggeredBy });
|
|
||||||
return { ok: true, state };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
function tryTriggerReplay({ jobId, by = null } = {}) {
|
||||||
tryTriggerReplay,
|
const state = getReplayState();
|
||||||
getReplayState,
|
if (state.atJobLimit) {
|
||||||
replayEvents,
|
return { ok: false, error: 'Replay limit reached: 3 jobs are already being made', state };
|
||||||
};
|
}
|
||||||
|
if (state.remainingMs > 0) {
|
||||||
|
return { ok: false, error: `Replay cooldown active. Try again in ${Math.ceil(state.remainingMs / 1000)}s.`, remainingMs: state.remainingMs, state };
|
||||||
|
}
|
||||||
|
if (!jobId) throw new Error('Replay job id required');
|
||||||
|
activeJobs.set(jobId, []);
|
||||||
|
status = { jobId, status: 'accepted' };
|
||||||
|
lastTriggeredAt = Date.now();
|
||||||
|
lastTriggeredBy = by;
|
||||||
|
emitUpdate();
|
||||||
|
return { ok: true, state: getReplayState() };
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateReplayStatus(payload) {
|
||||||
|
if (!activeJobs.has(payload.jobId)) return;
|
||||||
|
if (payload.jobId === status?.jobId) {
|
||||||
|
status = { jobId: payload.jobId, status: payload.status, title: payload.title, message: payload.message };
|
||||||
|
}
|
||||||
|
if (payload.status === 'ready' || payload.status === 'failed') {
|
||||||
|
activeJobs.delete(payload.jobId);
|
||||||
|
} else {
|
||||||
|
activeJobs.set(payload.jobId, payload.sources || []);
|
||||||
|
}
|
||||||
|
emitUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { tryTriggerReplay, getReplayState, replayEvents, updateReplayStatus };
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// Replay Socket Hooks
|
// Replay Socket Hooks
|
||||||
// Purpose: Registers web socket replay-trigger handler that publishes replay requests.
|
// Purpose: Registers web socket replay-trigger handler that publishes replay requests.
|
||||||
// Scope: Applies replay mode/cooldown/source validation for socket-triggered replay requests.
|
// Scope: Applies replay mode/availability/source validation for socket-triggered replay requests.
|
||||||
const io = require('../../globals/io');
|
const io = require('../../globals/io');
|
||||||
const logger = require('../../globals/logger').child('replaySocket');
|
const logger = require('../../globals/logger').child('replaySocket');
|
||||||
const { getMode, MODES } = require('../modeManager');
|
const { getMode, MODES } = require('../modeManager');
|
||||||
@@ -45,9 +45,9 @@ function registerReplaySocketHooks({ tryTriggerReplay, validateSources, getDefau
|
|||||||
const title = normalizeReplayTitle(payload?.title);
|
const title = normalizeReplayTitle(payload?.title);
|
||||||
const includeSidebar = normalizeIncludeSidebar(payload?.includeSidebar);
|
const includeSidebar = normalizeIncludeSidebar(payload?.includeSidebar);
|
||||||
const jobId = buildReplayJobId('web');
|
const jobId = buildReplayJobId('web');
|
||||||
const attempt = tryTriggerReplay({ by: { source: 'web', requester } });
|
const attempt = tryTriggerReplay({ jobId, by: { source: 'web', requester } });
|
||||||
if (!attempt.ok) {
|
if (!attempt.ok) {
|
||||||
cb({ error: 'Replay cooldown active', remainingMs: attempt.remainingMs, state: attempt.state });
|
cb({ error: attempt.error, remainingMs: attempt.remainingMs, state: attempt.state });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -459,7 +459,7 @@ balanceBoardEvents.on('change', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
replayEvents.on('update', () => {
|
replayEvents.on('update', () => {
|
||||||
logger.info('Replay cooldown updated; syncing all clients');
|
logger.info('Replay state updated; syncing all clients');
|
||||||
syncAll();
|
syncAll();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export default function ReplaySourcesPanel({
|
|||||||
const assignmentRoverId = useSessionSelector((state) => state.session?.assignment?.roverId ?? null);
|
const assignmentRoverId = useSessionSelector((state) => state.session?.assignment?.roverId ?? null);
|
||||||
const roster = useSessionSelector((state) => state.session?.roster ?? []);
|
const roster = useSessionSelector((state) => state.session?.roster ?? []);
|
||||||
const replayState = useSessionSelector((state) => state.session?.replay || null);
|
const replayState = useSessionSelector((state) => state.session?.replay || null);
|
||||||
const replayStatus = useSessionSelector((state) => state.replayStatus);
|
const replayStatus = replayState?.status;
|
||||||
const socket = useSocket();
|
const socket = useSocket();
|
||||||
const { triggerReplay } = useSessionActions();
|
const { triggerReplay } = useSessionActions();
|
||||||
const sources = useMemo(() => normalizeSources(replaySources || []), [replaySources]);
|
const sources = useMemo(() => normalizeSources(replaySources || []), [replaySources]);
|
||||||
@@ -161,48 +161,25 @@ export default function ReplaySourcesPanel({
|
|||||||
};
|
};
|
||||||
}, [sources]);
|
}, [sources]);
|
||||||
|
|
||||||
const hasReplayCooldown = Boolean(replayState?.lastTriggeredAt && replayState?.cooldownMs);
|
const cooldownNow = useSharedClock(1000, Boolean(replayState?.lastTriggeredAt));
|
||||||
const replayCooldownEndsAt = hasReplayCooldown
|
const remainingMs = replayState?.lastTriggeredAt
|
||||||
? replayState.lastTriggeredAt + replayState.cooldownMs
|
? Math.max(0, replayState.lastTriggeredAt + replayState.cooldownMs - cooldownNow)
|
||||||
: 0;
|
: 0;
|
||||||
const cooldownNow = useSharedClock(1000, hasReplayCooldown);
|
const atJobLimit = replayState?.atJobLimit === true;
|
||||||
const remainingMs = useMemo(() => {
|
const replaySourceKeys = replayState?.activeSourceKeys;
|
||||||
if (!hasReplayCooldown) return 0;
|
const activeSourceKeys = useMemo(() => new Set(replaySourceKeys || []), [replaySourceKeys]);
|
||||||
/*
|
const replayDisabled = busy || mode === 'lockdown' || remainingMs > 0 || atJobLimit || !selected.length;
|
||||||
The button only shows whole seconds, so a shared one-second clock gives the
|
|
||||||
same useful information without each mounted replay panel owning a 250ms
|
|
||||||
interval. The exact server cooldown still decides whether the action is
|
|
||||||
accepted; this value is only the local disabled-state/display estimate.
|
|
||||||
*/
|
|
||||||
const next = replayCooldownEndsAt - cooldownNow;
|
|
||||||
return Math.max(0, next);
|
|
||||||
}, [cooldownNow, hasReplayCooldown, replayCooldownEndsAt]);
|
|
||||||
|
|
||||||
const replayDisabled = busy || mode === 'lockdown' || remainingMs > 0 || !selected.length;
|
|
||||||
const selectedSet = useMemo(() => {
|
const selectedSet = useMemo(() => {
|
||||||
// Checkbox rendering asks the same membership question for every source.
|
// Checkbox rendering asks the same membership question for every source.
|
||||||
// A Set avoids repeated linear scans and, more importantly, gives memoized
|
// A Set avoids repeated linear scans and, more importantly, gives memoized
|
||||||
// child lists a stable value while the selected keys have not changed.
|
// child lists a stable value while the selected keys have not changed.
|
||||||
return new Set(selected);
|
return new Set(selected);
|
||||||
}, [selected]);
|
}, [selected]);
|
||||||
const replayStatusText = useMemo(() => {
|
const replayStatusText = replayStatus?.status
|
||||||
if (!replayStatus?.status) return null;
|
? replayStatus.status === 'failed' && replayStatus.message
|
||||||
const titleText = replayStatus.title ? `: ${replayStatus.title}` : '';
|
? replayStatus.message
|
||||||
switch (replayStatus.status) {
|
: `Replay ${replayStatus.status}${replayStatus.title ? `: ${replayStatus.title}` : ''}`
|
||||||
case 'accepted':
|
: null;
|
||||||
return `Replay accepted${titleText}`;
|
|
||||||
case 'building':
|
|
||||||
return `Replay building${titleText}`;
|
|
||||||
case 'uploading':
|
|
||||||
return `Replay uploading${titleText}`;
|
|
||||||
case 'ready':
|
|
||||||
return `Replay ready${titleText}`;
|
|
||||||
case 'failed':
|
|
||||||
return replayStatus.message || `Replay failed${titleText}`;
|
|
||||||
default:
|
|
||||||
return replayStatus.message || `Replay ${replayStatus.status}${titleText}`;
|
|
||||||
}
|
|
||||||
}, [replayStatus]);
|
|
||||||
const toggleKey = useCallback((key) => {
|
const toggleKey = useCallback((key) => {
|
||||||
setSelected((prev) => {
|
setSelected((prev) => {
|
||||||
return prev.includes(key) ? prev.filter((value) => value !== key) : [...prev, key];
|
return prev.includes(key) ? prev.filter((value) => value !== key) : [...prev, key];
|
||||||
@@ -250,13 +227,13 @@ export default function ReplaySourcesPanel({
|
|||||||
current desktop sidebar. They only collapse when the card becomes
|
current desktop sidebar. They only collapse when the card becomes
|
||||||
narrower than the supported new-drive desktop column. */}
|
narrower than the supported new-drive desktop column. */}
|
||||||
<div className={`grid grid-cols-2 gap-0.5 @max-[15rem]:grid-cols-1 ${listWrapClass}`}>
|
<div className={`grid grid-cols-2 gap-0.5 @max-[15rem]:grid-cols-1 ${listWrapClass}`}>
|
||||||
<GroupList title="Rovers" items={grouped.rovers} selectedSet={selectedSet} onToggle={toggleKey} />
|
<GroupList title="Rovers" items={grouped.rovers} selectedSet={selectedSet} activeSourceKeys={activeSourceKeys} onToggle={toggleKey} />
|
||||||
<GroupList title="Room Cams" items={grouped.rooms} selectedSet={selectedSet} onToggle={toggleKey} />
|
<GroupList title="Room Cams" items={grouped.rooms} selectedSet={selectedSet} activeSourceKeys={activeSourceKeys} onToggle={toggleKey} />
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
{error ? <div className="text-xs text-amber-400">{error}</div> : null}
|
{error ? <div className="text-xs text-amber-400">{error}</div> : null}
|
||||||
{replayStatusText ? (
|
{replayStatusText ? (
|
||||||
<div className={`text-xs ${replayStatus?.status === 'failed' ? 'text-amber-400' : 'text-emerald-300'}`}>
|
<div className={`text-xs ${replayStatus.status === 'failed' ? 'text-amber-400' : 'text-emerald-300'}`}>
|
||||||
{replayStatusText}
|
{replayStatusText}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -279,11 +256,6 @@ export default function ReplaySourcesPanel({
|
|||||||
saveSettings((current) => ({ ...(current || {}), [titleSettingKey]: next }));
|
saveSettings((current) => ({ ...(current || {}), [titleSettingKey]: next }));
|
||||||
}}
|
}}
|
||||||
onKeyDown={(event) => {
|
onKeyDown={(event) => {
|
||||||
// Enter is the keyboard equivalent of clicking Replay. Ignore
|
|
||||||
// composition events so confirming an IME candidate cannot
|
|
||||||
// accidentally submit a replay before the title is complete.
|
|
||||||
// handleReplay remains the single authority for cooldown,
|
|
||||||
// lockdown, busy, and empty-source checks.
|
|
||||||
if (event.key !== 'Enter' || event.nativeEvent?.isComposing) return;
|
if (event.key !== 'Enter' || event.nativeEvent?.isComposing) return;
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
handleReplay();
|
handleReplay();
|
||||||
@@ -314,7 +286,7 @@ export default function ReplaySourcesPanel({
|
|||||||
onClick={handleReplay}
|
onClick={handleReplay}
|
||||||
disabled={replayDisabled}
|
disabled={replayDisabled}
|
||||||
>
|
>
|
||||||
{remainingMs > 0 ? `Replay (${Math.ceil(remainingMs / 1000)}s)` : busy ? 'Replay…' : 'Replay'}
|
{atJobLimit ? 'Replay (limit reached)' : remainingMs > 0 ? `Replay (${Math.ceil(remainingMs / 1000)}s)` : busy ? 'Replay…' : 'Replay'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -323,14 +295,14 @@ export default function ReplaySourcesPanel({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const GroupList = React.memo(function GroupList({ title, items, selectedSet, onToggle }) {
|
const GroupList = React.memo(function GroupList({ title, items, selectedSet, activeSourceKeys, onToggle }) {
|
||||||
if (!items.length) return null;
|
if (!items.length) return null;
|
||||||
return (
|
return (
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
<div className="panel-muted text-xs">{title}</div>
|
<div className="panel-muted text-xs">{title}</div>
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
{items.map((item) => (
|
{items.map((item) => (
|
||||||
<label key={item.key} className="surface flex items-center gap-0.5 text-xs">
|
<label key={item.key} className={`surface flex items-center gap-0.5 text-xs ${activeSourceKeys.has(item.key) ? 'animate-pulse' : ''}`}>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={selectedSet.has(item.key)}
|
checked={selectedSet.has(item.key)}
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ const INITIAL_STATE = {
|
|||||||
overseerMemory: null,
|
overseerMemory: null,
|
||||||
alerts: [],
|
alerts: [],
|
||||||
latestReplay: null,
|
latestReplay: null,
|
||||||
replayStatus: null,
|
|
||||||
duplicateIdentityBlock: null,
|
duplicateIdentityBlock: null,
|
||||||
roverRemovalNotice: null,
|
roverRemovalNotice: null,
|
||||||
};
|
};
|
||||||
@@ -199,23 +198,6 @@ export function SessionProvider({ children }) {
|
|||||||
],
|
],
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
function handleReplayStatus(payload = {}) {
|
|
||||||
if (!payload?.status) return;
|
|
||||||
setState((prev) => ({
|
|
||||||
...prev,
|
|
||||||
/*
|
|
||||||
Replay creation is serialized by the server's cooldown and workflow,
|
|
||||||
so the browser needs one current progress value rather than a map of
|
|
||||||
jobs. The server remains authoritative for job identity and execution;
|
|
||||||
this scalar exists only to present accepted/building/uploading/ready/
|
|
||||||
failed progress in the web UI.
|
|
||||||
*/
|
|
||||||
replayStatus: {
|
|
||||||
...payload,
|
|
||||||
receivedAt: Date.now(),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
function handleReplayReady(payload = {}) {
|
function handleReplayReady(payload = {}) {
|
||||||
if (!payload?.jobId || !payload?.url) return;
|
if (!payload?.jobId || !payload?.url) return;
|
||||||
setState((prev) => ({
|
setState((prev) => ({
|
||||||
@@ -316,7 +298,6 @@ export function SessionProvider({ children }) {
|
|||||||
socket.on('overseer:state', handleOverseerState);
|
socket.on('overseer:state', handleOverseerState);
|
||||||
socket.on('overseer:memory', handleOverseerMemory);
|
socket.on('overseer:memory', handleOverseerMemory);
|
||||||
socket.on('alert:new', handleAlertNew);
|
socket.on('alert:new', handleAlertNew);
|
||||||
socket.on('replay:status', handleReplayStatus);
|
|
||||||
socket.on('replay:ready', handleReplayReady);
|
socket.on('replay:ready', handleReplayReady);
|
||||||
socket.on('replay:failed', handleReplayFailed);
|
socket.on('replay:failed', handleReplayFailed);
|
||||||
socket.on('session:duplicateIdentity', handleDuplicateIdentity);
|
socket.on('session:duplicateIdentity', handleDuplicateIdentity);
|
||||||
@@ -331,7 +312,6 @@ export function SessionProvider({ children }) {
|
|||||||
socket.off('overseer:state', handleOverseerState);
|
socket.off('overseer:state', handleOverseerState);
|
||||||
socket.off('overseer:memory', handleOverseerMemory);
|
socket.off('overseer:memory', handleOverseerMemory);
|
||||||
socket.off('alert:new', handleAlertNew);
|
socket.off('alert:new', handleAlertNew);
|
||||||
socket.off('replay:status', handleReplayStatus);
|
|
||||||
socket.off('replay:ready', handleReplayReady);
|
socket.off('replay:ready', handleReplayReady);
|
||||||
socket.off('replay:failed', handleReplayFailed);
|
socket.off('replay:failed', handleReplayFailed);
|
||||||
socket.off('session:duplicateIdentity', handleDuplicateIdentity);
|
socket.off('session:duplicateIdentity', handleDuplicateIdentity);
|
||||||
|
|||||||
Reference in New Issue
Block a user