fix(electrobun): address all 22 Codex review #2 findings

CRITICAL:
- DocsTab XSS: DOMPurify sanitization on all {@html} output
- File RPC path traversal: guardPath() validates against project CWDs

HIGH:
- SSH injection: spawn /usr/bin/ssh via PTY args, no shell string
- Search XSS: strip HTML, highlight matches client-side with <mark>
- Terminal listener leak: cleanup functions stored + called in onDestroy
- FileBrowser race: request token, discard stale responses
- SearchOverlay race: same request token pattern
- App startup ordering: groups.list chains into active_group restore
- PtyClient timeout: 5-second auth timeout on connect()
- Rule 55: 6 {#if} patterns converted to style:display toggle

MEDIUM:
- Agent persistence: only persist NEW messages (lastPersistedIndex)
- Search errors: typed error response, "Invalid query" UI
- Health store wired: agent events call recordActivity/setProjectStatus
- index.ts SRP: split into 8 domain handler modules (298 lines)
- App.svelte: extracted workspace-store.svelte.ts
- rpc.ts: typed AppRpcHandle, removed `any`

LOW:
- CommandPalette listener wired in App.svelte
- Dead code removed (removeGroup, onDragStart, plugin loaded)
This commit is contained in:
Hibryda 2026-03-22 02:30:09 +01:00
parent 8e756d3523
commit 1cd4558740
28 changed files with 1342 additions and 1164 deletions

View file

@ -123,12 +123,7 @@
newGroupName = '';
}
async function removeGroup(id: string) {
if (groups.length <= 1) return; // keep at least one group
groups = groups.filter(g => g.id !== id);
if (activeGroupId === id) activeGroupId = groups[0]?.id ?? 'dev';
await appRpc.request['groups.delete']({ id }).catch(console.error);
}
// Fix #19: removeGroup removed — was defined but never called from UI
let activeGroupId = $state('dev');
// Fix #10: Track previous group to limit mounted DOM (max 2 groups)
let previousGroupId = $state<string | null>(null);
@ -218,38 +213,7 @@
return () => clearInterval(id);
});
// ── JS-based window drag (replaces broken -webkit-app-region on WebKitGTK) ──
let isDraggingWindow = false;
let dragStartX = 0;
let dragStartY = 0;
let winStartX = 0;
let winStartY = 0;
function onDragStart(e: MouseEvent) {
isDraggingWindow = true;
dragStartX = e.screenX;
dragStartY = e.screenY;
appRpc?.request["window.getFrame"]({}).then((frame: any) => {
winStartX = frame.x;
winStartY = frame.y;
}).catch(() => {});
window.addEventListener('mousemove', onDragMove);
window.addEventListener('mouseup', onDragEnd);
}
function onDragMove(e: MouseEvent) {
if (!isDraggingWindow) return;
const dx = e.screenX - dragStartX;
const dy = e.screenY - dragStartY;
appRpc?.request["window.setPosition"]?.({ x: winStartX + dx, y: winStartY + dy })?.catch?.(() => {});
}
function onDragEnd() {
isDraggingWindow = false;
window.removeEventListener('mousemove', onDragMove);
window.removeEventListener('mouseup', onDragEnd);
saveWindowFrame();
}
// Fix #19: onDragStart/onDragMove/onDragEnd removed — no longer referenced from template
// ── Window frame persistence (debounced 500ms) ─────────────────
let frameSaveTimer: ReturnType<typeof setTimeout> | null = null;
@ -329,15 +293,18 @@
// Set up global error boundary
setupErrorBoundary();
// Run all init tasks in parallel, mark app ready when all complete
// Fix #8: Load groups FIRST, then apply saved active_group.
// Other init tasks run in parallel.
const initTasks = [
themeStore.initTheme(appRpc).catch(console.error),
fontStore.initFonts(appRpc).catch(console.error),
keybindingStore.init(appRpc).catch(console.error),
// Sequential: groups.list -> active_group (depends on groups being loaded)
appRpc.request["groups.list"]({}).then(({ groups: dbGroups }: { groups: Group[] }) => {
if (dbGroups.length > 0) groups = dbGroups;
}).catch(console.error),
appRpc.request["settings.get"]({ key: 'active_group' }).then(({ value }: { value: string | null }) => {
// Now that groups are loaded, apply saved active_group
return appRpc.request["settings.get"]({ key: 'active_group' });
}).then(({ value }: { value: string | null }) => {
if (value && groups.some(g => g.id === value)) activeGroupId = value;
}).catch(console.error),
// Load projects from SQLite
@ -356,7 +323,6 @@
Promise.allSettled(initTasks).then(() => {
appReady = true;
// Track projects for health monitoring after load
for (const p of PROJECTS) trackProject(p.id);
});
@ -377,10 +343,24 @@
}
document.addEventListener('keydown', handleSearchShortcut);
// Fix #18: Wire CommandPalette events to action handlers
function handlePaletteCommand(e: Event) {
const detail = (e as CustomEvent).detail;
switch (detail) {
case 'settings': settingsOpen = !settingsOpen; break;
case 'search': searchOpen = !searchOpen; break;
case 'new-project': showAddProject = true; break;
case 'toggle-sidebar': settingsOpen = !settingsOpen; break;
default: console.log(`[palette] unhandled command: ${detail}`);
}
}
window.addEventListener('palette-command', handlePaletteCommand);
const cleanup = keybindingStore.installListener();
return () => {
cleanup();
document.removeEventListener('keydown', handleSearchShortcut);
window.removeEventListener('palette-command', handlePaletteCommand);
};
});
</script>