diff --git a/docs/web-ui.md b/docs/web-ui.md index f1bbea9..d5cf46d 100644 --- a/docs/web-ui.md +++ b/docs/web-ui.md @@ -68,7 +68,11 @@ node)` swaps the body; the JS builders for file previews, approval diffs, and journald logs all render into it. Markdown file previews (`.md` / `.markdown`) render through the vendored `marked` bundle (`GET /static/marked.js`) into a `.md` block; -other files stay raw in a `
`. +SVG previews (`.svg`) get a `rendered` / `source` tabbed view — +`rendered` shows the image via an `` `data:` URI (the +browser's secure static mode, so an untrusted SVG can't run +scripts), `source` shows the raw markup; other files stay raw +in a `
`. Both bind their listeners with `SO_REUSEADDR` via `tokio::net::TcpSocket` plus a retry loop on `AddrInUse` (12 tries, diff --git a/hive-c0re/assets/app.js b/hive-c0re/assets/app.js index dc291e0..c535c99 100644 --- a/hive-c0re/assets/app.js +++ b/hive-c0re/assets/app.js @@ -91,13 +91,47 @@ if (!resp.ok) throw new Error(text || ('HTTP ' + resp.status)); return text; } + // SVG file preview: a "rendered" tab (default) showing the image and + // a "source" tab with the raw markup. The image is loaded via an + //data: URI —
-loaded SVG runs in the browser's secure + // static mode (no scripts, no external fetches), so an untrusted + // SVG from an agent's state dir can't execute code in the dashboard. + function buildSvgPanel(text) { + const tabs = el('div', { class: 'diff-base-tabs' }); + const host = el('div', { class: 'svg-host' }); + function show(mode) { + for (const b of tabs.children) { + b.classList.toggle('active', b.dataset.mode === mode); + } + if (mode === 'source') { + host.replaceChildren(el('pre', { class: 'path-preview-body' }, text)); + return; + } + const img = el('img', { class: 'svg-render', alt: 'SVG preview' }); + img.addEventListener('error', () => { + host.replaceChildren(el('div', { class: 'meta' }, + '(could not render — see the source tab)')); + }); + img.src = 'data:image/svg+xml,' + encodeURIComponent(text); + host.replaceChildren(img); + } + for (const [mode, label] of [['rendered', 'rendered'], ['source', 'source']]) { + const b = el('button', + { type: 'button', class: 'diff-base-tab', 'data-mode': mode }, label); + b.addEventListener('click', () => show(mode)); + tabs.append(b); + } + show('rendered'); + return el('div', {}, tabs, host); + } // Lazy-load `path` from /api/state-file into the side panel. - // Markdown files render through `marked` into a `.md` block; every - // other file stays raw text in a
. + // Markdown → `marked` into a `.md` block; SVG → rendered/source + // tabbed view; every other file stays raw text in a. async function openFilePanel(path) { const isMd = /\.(md|markdown)$/i.test(path); - const view = isMd - ? el('div', { class: 'md' }) + const isSvg = /\.svg$/i.test(path); + const view = isMd ? el('div', { class: 'md' }) + : isSvg ? el('div') : el('pre', { class: 'path-preview-body' }); view.textContent = '(fetching…)'; Panel.open('↳ ' + path, view); @@ -106,6 +140,8 @@ if (isMd && window.marked && typeof window.marked.parse === 'function') { marked.setOptions({ breaks: true, gfm: true }); view.innerHTML = marked.parse(text); + } else if (isSvg) { + view.replaceChildren(buildSvgPanel(text)); } else { view.textContent = text; } diff --git a/hive-c0re/assets/dashboard.css b/hive-c0re/assets/dashboard.css index 0eca735..46c55f8 100644 --- a/hive-c0re/assets/dashboard.css +++ b/hive-c0re/assets/dashboard.css @@ -327,6 +327,18 @@ code { border-color: var(--purple); background: rgba(203, 166, 247, 0.08); } +/* SVG file preview (issue #188) */ +.svg-host { margin-top: 0.5em; } +.svg-render { + display: block; + max-width: 100%; + height: auto; + margin: 0 auto; + border: 1px solid var(--border); + border-radius: 4px; + /* checkerboard so transparent regions of the SVG read clearly */ + background: repeating-conic-gradient(#313244 0% 25%, #1e1e2e 0% 50%) 50% / 18px 18px; +} .approval-tabs { display: flex; gap: 0.4em;