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;
+24 -16
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,6 +843,22 @@ async function buildReplayVideo({ sources = [], title = '', requester = '' } = {
tileWidth = clampEven(tileWidth); tileWidth = clampEven(tileWidth);
tileHeight = clampEven(tileHeight); tileHeight = clampEven(tileHeight);
const inputArgs = [];
const filterParts = [];
const layoutParts = [];
for (let i = 0; i < normalizedVideos.length; i += 1) {
inputArgs.push('-i', normalizedVideos[i].path);
const sourceTitle = normalizedVideos[i]?.source?.label || normalizedVideos[i]?.source?.id || `Source ${i + 1}`;
filterParts.push(`[${i}:v]${scalePadFilter(tileWidth, tileHeight, sourceTitle)}[v${i}]`);
const x = (i % layout.cols) * tileWidth;
const y = Math.floor(i / layout.cols) * tileHeight;
layoutParts.push(`${x}_${y}`);
}
let audioInputStart = normalizedVideos.length;
let sidebarInputIndex = -1;
if (includeSidebar) {
const selectedRoverIds = usedSources const selectedRoverIds = usedSources
.filter((entry) => entry?.type === 'rover') .filter((entry) => entry?.type === 'rover')
.map((entry) => String(entry.id)); .map((entry) => String(entry.id));
@@ -858,23 +874,11 @@ async function buildReplayVideo({ sources = [], title = '', requester = '' } = {
driverBatteryLines, driverBatteryLines,
chatEvents, chatEvents,
}); });
inputArgs.push('-i', sidebarPath);
const inputArgs = []; sidebarInputIndex = normalizedVideos.length;
const filterParts = []; audioInputStart = normalizedVideos.length + 1;
const layoutParts = [];
for (let i = 0; i < normalizedVideos.length; i += 1) {
inputArgs.push('-i', normalizedVideos[i].path);
const sourceTitle = normalizedVideos[i]?.source?.label || normalizedVideos[i]?.source?.id || `Source ${i + 1}`;
filterParts.push(`[${i}:v]${scalePadFilter(tileWidth, tileHeight, sourceTitle)}[v${i}]`);
const x = (i % layout.cols) * tileWidth;
const y = Math.floor(i / layout.cols) * tileHeight;
layoutParts.push(`${x}_${y}`);
} }
inputArgs.push('-i', sidebarPath);
const sidebarInputIndex = normalizedVideos.length;
const 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]`,
); );
} }
if (includeSidebar) {
filterParts.push(`[vgrid][${sidebarInputIndex}:v]hstack=inputs=2[vout]`); 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"