# Web tabs: two fixes (planned + implemented 2026-07-28) Both found against the saved dashboard `https://..ts.net:4000` (Bio-Hacking-Dashboard). Kept because the root-cause analysis of the second one is not obvious from the resulting diff. Status: **both implemented and verified end-to-end.** The one deliberate non-change is recorded at the bottom. --- ## Bug 1: saved URLs could not be deleted from the Run dropdown ### What happened The "Web / URL" section of the Run dropdown listed every saved dashboard as a single clickable row whose only action was "open". Deleting required opening the dashboard as a tab, clicking the tab's gear, then Delete in the modal, so a URL you no longer wanted open at all could not be removed without first opening it. ### What shipped - `renderWebviewMenuItems()` (`src/web/public/webview-tabs.js`) now renders each saved URL as a `.run-mode-row--web` flex row: the open button, a gear (`showWebviewModal`), and an `x` (`deleteWebviewById`). Nested buttons are invalid HTML, hence the wrapper rather than a button inside a button. - `deleteWebview()` split into the modal entry point, the new row entry point `deleteWebviewById(id)`, and the shared `_confirmAndDeleteWebview(id)`. - Both side buttons call `event.stopPropagation()` so the click does not also open the dashboard. - The dropdown's outside-click handler (`session-ui.js`) closes when the click target is not inside `#runModeMenu`, and the row is gone by the time the delete resolves, so `deleteWebviewById` re-asserts `.active` on the menu. Verified in a browser: deleting one of several URLs leaves you looking at the rest of the list. - CSS in `styles.css` (`.run-mode-row--web`, `.run-mode-row-btn`) plus a larger touch target in `mobile.css`. The side buttons are permanently visible rather than hover-revealed, because this menu is used on touch. No server change: `DELETE /api/webviews/:id` already existed, owner-scoped, and already revoked the capability and broadcast `WebviewChanged`. --- ## Bug 2: images did not load in a proxied dashboard ### Reproduction (before the fix) ``` CAP=/open> # A) upstream direct -> 200 image/jpeg 118150 curl -sk "https://..ts.net:4000/api/hero?slug=120-minutes-in-nature" # B) through the proxy prefix -> 200 image/jpeg 118150 curl -sk "https://localhost:3000/webview/$CAP/api/hero?slug=120-minutes-in-nature" # C) what the browser ACTUALLY requested -> 404 {"errorCode":"NOT_FOUND"} curl -sk -H "Referer: https://localhost:3000/webview/$CAP/" \ "https://localhost:3000/api/hero?slug=120-minutes-in-nature" # D) same shape but NOT under /api -> 200 (referer fallback rescues it) curl -sk -H "Referer: https://localhost:3000/webview/$CAP/" "https://localhost:3000/styles.css" ``` The proxy itself was fine (B). The failure was entirely about which URL the browser ended up requesting (C). ### Root cause The dashboard builds its image markup at runtime with root-absolute URLs: `c.innerHTML = ''`, `img.src = slideSrc(...)` returning `/api/slide?owner=...`, `/api/story`, `/api/video`, and a nested `