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-title" content="Multi Roomba Rover" />
<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">
</head>
<body>
+9 -2
View File
@@ -450,7 +450,7 @@ function buildReplayCaption({ requester, usedSources = [], missingSources = [],
return lines.join('\n');
}
async function sendReplayToChannel(channelId, requester, sources = [], explicitTitle = '') {
async function sendReplayToChannel(channelId, requester, sources = [], explicitTitle = '', includeSidebar = true) {
if (!channelId) {
throw new Error('Replay channel not configured');
}
@@ -459,6 +459,7 @@ async function sendReplayToChannel(channelId, requester, sources = [], explicitT
sources,
title: resolvedTitle,
requester,
includeSidebar,
});
const filenameBase = sanitizeReplayTitleForFilename(resolvedTitle);
const attachment = new AttachmentBuilder(buffer, { name: `${filenameBase}.mp4` });
@@ -1619,7 +1620,13 @@ function handleBusEvent(event) {
schedulePresenceRotation();
break;
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);
});
break;
+29 -21
View File
@@ -724,7 +724,7 @@ async function probeMaxFrameSize(paths) {
return { maxWidth, maxHeight };
}
async function buildReplayVideo({ sources = [], title = '', requester = '' } = {}) {
async function buildReplayVideo({ sources = [], title = '', requester = '', includeSidebar = true } = {}) {
if (!Array.isArray(sources) || !sources.length) {
throw new Error('No replay sources selected');
}
@@ -843,22 +843,6 @@ async function buildReplayVideo({ sources = [], title = '', requester = '' } = {
tileWidth = clampEven(tileWidth);
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 filterParts = [];
const layoutParts = [];
@@ -872,9 +856,29 @@ async function buildReplayVideo({ sources = [], title = '', requester = '' } = {
layoutParts.push(`${x}_${y}`);
}
inputArgs.push('-i', sidebarPath);
const sidebarInputIndex = normalizedVideos.length;
const audioInputStart = normalizedVideos.length + 1;
let audioInputStart = normalizedVideos.length;
let sidebarInputIndex = -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) {
inputArgs.push('-i', audioPath);
}
@@ -887,7 +891,11 @@ async function buildReplayVideo({ sources = [], title = '', requester = '' } = {
`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) {
const audioRefs = normalizedAudios.map((_, idx) => `[${audioInputStart + idx}:a]`).join('');
@@ -20,6 +20,11 @@ function normalizeReplayTitle(value) {
return value.trim().slice(0, 120);
}
function normalizeIncludeSidebar(value) {
if (typeof value === 'boolean') return value;
return true;
}
io.on('connection', (socket) => {
socket.on('replay:trigger', (payload = {}, cb = () => {}) => {
if (getMode() === MODES.LOCKDOWN) {
@@ -43,6 +48,7 @@ io.on('connection', (socket) => {
}
const requester = buildRequesterLabel(socket);
const title = normalizeReplayTitle(payload?.title);
const includeSidebar = normalizeIncludeSidebar(payload?.includeSidebar);
const attempt = tryTriggerReplay({ by: { source: 'web', requester } });
if (!attempt.ok) {
cb({ error: 'Replay cooldown active', remainingMs: attempt.remainingMs, state: attempt.state });
@@ -55,6 +61,7 @@ io.on('connection', (socket) => {
channelId,
requester,
title,
includeSidebar,
sources,
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 [title, setTitle] = useState('');
const [titleDirty, setTitleDirty] = useState(false);
const [includeSidebar, setIncludeSidebar] = useState(true);
const replayState = session?.replay || null;
const [remainingMs, setRemainingMs] = useState(0);
@@ -61,6 +62,15 @@ export default function ReplaySourcesPanel({ panelId = 'replay-sources', fillHei
setSelected(defaults);
}, [settings?.[panelId], defaults, panelId]);
useEffect(() => {
const saved = settings?.[`${panelId}:includeSidebar`];
if (typeof saved === 'boolean') {
setIncludeSidebar(saved);
return;
}
setIncludeSidebar(true);
}, [settings?.[`${panelId}:includeSidebar`], panelId]);
useEffect(() => {
if (!titleDirty) {
setTitle(defaultTitle);
@@ -115,7 +125,7 @@ export default function ReplaySourcesPanel({ panelId = 'replay-sources', fillHei
return { type, id };
});
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.');
} catch (err) {
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} />
</div>
<div className="space-y-0.5">
<label className="panel-muted block text-xs" htmlFor={`${panelId}-title`}>
Replay title
</label>
<input
id={`${panelId}-title`}
type="text"
className="field-input w-full text-xs"
value={title}
@@ -149,6 +163,19 @@ export default function ReplaySourcesPanel({ panelId = 'replay-sources', fillHei
placeholder={defaultTitle}
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
type="button"
className="button-dark w-full text-xs disabled:opacity-40"