optional sidebar

This commit is contained in:
legop3
2026-04-28 13:13:42 -04:00
parent 928c1112a1
commit b0cd29c075
8 changed files with 112 additions and 35 deletions
+1
View File
@@ -0,0 +1 @@
# RULES FOR LOCKDOWN AUDIT
+27
View File
@@ -0,0 +1,27 @@
# REFACTOR RULES
## server backend
- there are a lot of services and some have gotten huge, like way too large to manage manually
- all services should be converted into folders, with their files inside the folders
- large functions of the service should be split off into more, smaller files within the service's folder
- keep files relatively small and very clear and concise in their function. with titles commented at the top.
## webui frontend
- similar situation to the server's service file bloat, but with large jsx component files
- split up large jsx components and backing js files into folders containing smaller files
- use same clear concise functional format, with title comments
# REFACTOR TRACKING
## server backend
### COMPLETED SERVICES
-
### LARGE CHANGES
-
## webui frontend
### COMPLETED COMPONENTS
-
### LARGE CHANGES
-
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -11,7 +11,7 @@
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" /> <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="Multi Roomba Rover" /> <meta name="apple-mobile-web-app-title" content="Multi Roomba Rover" />
<title>Multi Roomba Rover</title> <title>Multi Roomba Rover</title>
<script type="module" crossorigin src="/assets/index-lJfgnUIc.js"></script> <script type="module" crossorigin src="/assets/index-bndWXjZU.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DVTOmRBl.css"> <link rel="stylesheet" crossorigin href="/assets/index-DVTOmRBl.css">
</head> </head>
<body> <body>
+9 -2
View File
@@ -450,7 +450,7 @@ function buildReplayCaption({ requester, usedSources = [], missingSources = [],
return lines.join('\n'); return lines.join('\n');
} }
async function sendReplayToChannel(channelId, requester, sources = [], explicitTitle = '') { async function sendReplayToChannel(channelId, requester, sources = [], explicitTitle = '', includeSidebar = true) {
if (!channelId) { if (!channelId) {
throw new Error('Replay channel not configured'); throw new Error('Replay channel not configured');
} }
@@ -459,6 +459,7 @@ async function sendReplayToChannel(channelId, requester, sources = [], explicitT
sources, sources,
title: resolvedTitle, title: resolvedTitle,
requester, requester,
includeSidebar,
}); });
const filenameBase = sanitizeReplayTitleForFilename(resolvedTitle); const filenameBase = sanitizeReplayTitleForFilename(resolvedTitle);
const attachment = new AttachmentBuilder(buffer, { name: `${filenameBase}.mp4` }); const attachment = new AttachmentBuilder(buffer, { name: `${filenameBase}.mp4` });
@@ -1619,7 +1620,13 @@ function handleBusEvent(event) {
schedulePresenceRotation(); schedulePresenceRotation();
break; break;
case 'replay.requested': case 'replay.requested':
sendReplayToChannel(payload?.channelId, payload?.requester, payload?.sources || [], payload?.title || '').catch((err) => { sendReplayToChannel(
payload?.channelId,
payload?.requester,
payload?.sources || [],
payload?.title || '',
payload?.includeSidebar !== false,
).catch((err) => {
logger.warn('Replay send failed', err.message); logger.warn('Replay send failed', err.message);
}); });
break; break;
+29 -21
View File
@@ -724,7 +724,7 @@ async function probeMaxFrameSize(paths) {
return { maxWidth, maxHeight }; return { maxWidth, maxHeight };
} }
async function buildReplayVideo({ sources = [], title = '', requester = '' } = {}) { async function buildReplayVideo({ sources = [], title = '', requester = '', includeSidebar = true } = {}) {
if (!Array.isArray(sources) || !sources.length) { if (!Array.isArray(sources) || !sources.length) {
throw new Error('No replay sources selected'); throw new Error('No replay sources selected');
} }
@@ -843,22 +843,6 @@ async function buildReplayVideo({ sources = [], title = '', requester = '' } = {
tileWidth = clampEven(tileWidth); tileWidth = clampEven(tileWidth);
tileHeight = clampEven(tileHeight); tileHeight = clampEven(tileHeight);
const selectedRoverIds = usedSources
.filter((entry) => entry?.type === 'rover')
.map((entry) => String(entry.id));
const driverBatteryLines = buildDriverBatterySnapshot(selectedRoverIds);
const chatEvents = buildChatEventsForWindow(tStart, tEnd);
const durationSec = BUILD_DURATION_MS / 1000;
const sidebarPath = await renderSidebarVideo({
tmpDir,
title: resolvedTitle,
durationSec,
height: clampEven(tileHeight * layout.rows),
windowStartMs: tStart,
driverBatteryLines,
chatEvents,
});
const inputArgs = []; const inputArgs = [];
const filterParts = []; const filterParts = [];
const layoutParts = []; const layoutParts = [];
@@ -872,9 +856,29 @@ async function buildReplayVideo({ sources = [], title = '', requester = '' } = {
layoutParts.push(`${x}_${y}`); layoutParts.push(`${x}_${y}`);
} }
inputArgs.push('-i', sidebarPath); let audioInputStart = normalizedVideos.length;
const sidebarInputIndex = normalizedVideos.length; let sidebarInputIndex = -1;
const audioInputStart = normalizedVideos.length + 1; if (includeSidebar) {
const selectedRoverIds = usedSources
.filter((entry) => entry?.type === 'rover')
.map((entry) => String(entry.id));
const driverBatteryLines = buildDriverBatterySnapshot(selectedRoverIds);
const chatEvents = buildChatEventsForWindow(tStart, tEnd);
const durationSec = BUILD_DURATION_MS / 1000;
const sidebarPath = await renderSidebarVideo({
tmpDir,
title: resolvedTitle,
durationSec,
height: clampEven(tileHeight * layout.rows),
windowStartMs: tStart,
driverBatteryLines,
chatEvents,
});
inputArgs.push('-i', sidebarPath);
sidebarInputIndex = normalizedVideos.length;
audioInputStart = normalizedVideos.length + 1;
}
for (const audioPath of normalizedAudios) { for (const audioPath of normalizedAudios) {
inputArgs.push('-i', audioPath); inputArgs.push('-i', audioPath);
} }
@@ -887,7 +891,11 @@ async function buildReplayVideo({ sources = [], title = '', requester = '' } = {
`xstack=inputs=${normalizedVideos.length}:layout=${layoutParts.join('|')}:fill=black[vgrid]`, `xstack=inputs=${normalizedVideos.length}:layout=${layoutParts.join('|')}:fill=black[vgrid]`,
); );
} }
filterParts.push(`[vgrid][${sidebarInputIndex}:v]hstack=inputs=2[vout]`); if (includeSidebar) {
filterParts.push(`[vgrid][${sidebarInputIndex}:v]hstack=inputs=2[vout]`);
} else {
filterParts.push('[vgrid]null[vout]');
}
if (normalizedAudios.length) { if (normalizedAudios.length) {
const audioRefs = normalizedAudios.map((_, idx) => `[${audioInputStart + idx}:a]`).join(''); const audioRefs = normalizedAudios.map((_, idx) => `[${audioInputStart + idx}:a]`).join('');
@@ -20,6 +20,11 @@ function normalizeReplayTitle(value) {
return value.trim().slice(0, 120); return value.trim().slice(0, 120);
} }
function normalizeIncludeSidebar(value) {
if (typeof value === 'boolean') return value;
return true;
}
io.on('connection', (socket) => { io.on('connection', (socket) => {
socket.on('replay:trigger', (payload = {}, cb = () => {}) => { socket.on('replay:trigger', (payload = {}, cb = () => {}) => {
if (getMode() === MODES.LOCKDOWN) { if (getMode() === MODES.LOCKDOWN) {
@@ -43,6 +48,7 @@ io.on('connection', (socket) => {
} }
const requester = buildRequesterLabel(socket); const requester = buildRequesterLabel(socket);
const title = normalizeReplayTitle(payload?.title); const title = normalizeReplayTitle(payload?.title);
const includeSidebar = normalizeIncludeSidebar(payload?.includeSidebar);
const attempt = tryTriggerReplay({ by: { source: 'web', requester } }); const attempt = tryTriggerReplay({ by: { source: 'web', requester } });
if (!attempt.ok) { if (!attempt.ok) {
cb({ error: 'Replay cooldown active', remainingMs: attempt.remainingMs, state: attempt.state }); cb({ error: 'Replay cooldown active', remainingMs: attempt.remainingMs, state: attempt.state });
@@ -55,6 +61,7 @@ io.on('connection', (socket) => {
channelId, channelId,
requester, requester,
title, title,
includeSidebar,
sources, sources,
requestedBy: { socketId: socket.id }, requestedBy: { socketId: socket.id },
}, },
+28 -1
View File
@@ -28,6 +28,7 @@ export default function ReplaySourcesPanel({ panelId = 'replay-sources', fillHei
const [success, setSuccess] = useState(null); const [success, setSuccess] = useState(null);
const [title, setTitle] = useState(''); const [title, setTitle] = useState('');
const [titleDirty, setTitleDirty] = useState(false); const [titleDirty, setTitleDirty] = useState(false);
const [includeSidebar, setIncludeSidebar] = useState(true);
const replayState = session?.replay || null; const replayState = session?.replay || null;
const [remainingMs, setRemainingMs] = useState(0); const [remainingMs, setRemainingMs] = useState(0);
@@ -61,6 +62,15 @@ export default function ReplaySourcesPanel({ panelId = 'replay-sources', fillHei
setSelected(defaults); setSelected(defaults);
}, [settings?.[panelId], defaults, panelId]); }, [settings?.[panelId], defaults, panelId]);
useEffect(() => {
const saved = settings?.[`${panelId}:includeSidebar`];
if (typeof saved === 'boolean') {
setIncludeSidebar(saved);
return;
}
setIncludeSidebar(true);
}, [settings?.[`${panelId}:includeSidebar`], panelId]);
useEffect(() => { useEffect(() => {
if (!titleDirty) { if (!titleDirty) {
setTitle(defaultTitle); setTitle(defaultTitle);
@@ -115,7 +125,7 @@ export default function ReplaySourcesPanel({ panelId = 'replay-sources', fillHei
return { type, id }; return { type, id };
}); });
const resolvedTitle = String(title || '').trim() || defaultTitle; const resolvedTitle = String(title || '').trim() || defaultTitle;
await triggerReplay(payload, resolvedTitle); await triggerReplay({ sources: payload, title: resolvedTitle, includeSidebar });
setSuccess('Replay sent. Check the Discord replay channel.'); setSuccess('Replay sent. Check the Discord replay channel.');
} catch (err) { } catch (err) {
setError(err.message); setError(err.message);
@@ -138,7 +148,11 @@ export default function ReplaySourcesPanel({ panelId = 'replay-sources', fillHei
<GroupList title="Room Cams" items={grouped.rooms} selected={selected} onToggle={toggleKey} /> <GroupList title="Room Cams" items={grouped.rooms} selected={selected} onToggle={toggleKey} />
</div> </div>
<div className="space-y-0.5"> <div className="space-y-0.5">
<label className="panel-muted block text-xs" htmlFor={`${panelId}-title`}>
Replay title
</label>
<input <input
id={`${panelId}-title`}
type="text" type="text"
className="field-input w-full text-xs" className="field-input w-full text-xs"
value={title} value={title}
@@ -149,6 +163,19 @@ export default function ReplaySourcesPanel({ panelId = 'replay-sources', fillHei
placeholder={defaultTitle} placeholder={defaultTitle}
maxLength={120} maxLength={120}
/> />
<label className="surface flex items-center gap-0.5 text-xs">
<input
type="checkbox"
checked={includeSidebar}
onChange={(event) => {
const next = Boolean(event.target.checked);
setIncludeSidebar(next);
saveSettings((current) => ({ ...(current || {}), [`${panelId}:includeSidebar`]: next }));
}}
className="accent-emerald-400"
/>
<span>Include replay sidebar</span>
</label>
<button <button
type="button" type="button"
className="button-dark w-full text-xs disabled:opacity-40" className="button-dark w-full text-xs disabled:opacity-40"