dashboard file preview: markdown tabs + raster image rendering

Follow-up to #188. Two additions to the side-panel file preview:

- Markdown files get a rendered/plain tabbed view (was: always
  rendered, no way to see source) — same tab pattern as SVG.
- Raster images (png/jpg/gif/webp/bmp/ico/avif) render as an
  <img>. /api/state-file previously from_utf8_lossy-stringified
  every file and served text/plain, which corrupts binary; it
  now serves image files as raw bytes with their real
  content-type (over-cap images are rejected, not truncated —
  a clipped binary is corrupt).

buildSvgPanel generalised to buildTabbedPreview, shared by SVG +
markdown. .svg-host/.svg-render renamed .preview-host/.img-preview
since they now back images + md too.

closes #192
This commit is contained in:
iris 2026-05-21 21:49:15 +02:00
parent 0884a54960
commit f42ba9b561
4 changed files with 107 additions and 42 deletions

View file

@ -65,14 +65,21 @@ swipes in from the right — a singleton `#side-panel` with a
titled header, a close button, and a scrollable body. Closes on
the button, a backdrop click, or `Escape`. `Panel.open(title,
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;
SVG previews (`.svg`) get a `rendered` / `source` tabbed view —
`rendered` shows the image via an `<img>` `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 `<pre>`.
approval diffs, and journald logs all render into it. File
previews are type-aware:
- **Markdown** (`.md` / `.markdown`) — a `rendered` / `plain`
tabbed view: `rendered` (default) is the vendored `marked`
bundle (`GET /static/marked.js`), `plain` is the raw source.
- **SVG** (`.svg`) — a `rendered` / `source` tabbed view;
`rendered` shows the image via an `<img>` `data:` URI (the
browser's secure static mode, so an untrusted SVG can't run
scripts), `source` shows the raw markup.
- **Raster images** (`.png` / `.jpg` / `.gif` / `.webp` /
`.bmp` / `.ico` / `.avif`) — render as an `<img>` pointed at
`/api/state-file`, which serves them as binary with their
real content-type (text files stay UTF-8-lossy `text/plain`).
- **Everything else** — raw text in a `<pre>`.
Both bind their listeners with `SO_REUSEADDR` via
`tokio::net::TcpSocket` plus a retry loop on `AddrInUse` (12 tries,