PLAN.md: previews on the Files tab, shipped as v0.1.0-200

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Mitchell-Johnson
2026-10-11 15:25:55 -07:00
co-authored by Claude Fable 5.1
parent f8c9479e93
commit 51c718e8e3
+54 -2
View File
@@ -625,6 +625,57 @@ something long in one, switch away and back, refresh — it's all still there.
so both modes share one implementation and one set of rules (absolute paths only; so both modes share one implementation and one set of rules (absolute paths only;
`/`, `/etc`, `/usr`, … are never deleted or moved whole). Every write is logged with `/`, `/etc`, `/usr`, … are never deleted or moved whole). Every write is logged with
the caller's IP. the caller's IP.
- **Previews on the Files tab** — **shipped 2026-10-11** (v0.1.0-200, `internal/thumbs`,
`internal/api/thumbs.go`, `Thumb`/`ThumbNote` in `Files.tsx`). Asked for as a button that
toggles a file preview, "obviously not for everything", cached, "use whatever GPU is on the
machine we're looking at, and fall back to CPU", "a single small frame preview so it's
fast". A **previews** button beside `.hidden` turns the icon column into a 256 px
thumbnail for every image and video row (the listing says which: `preview: image|video`
per entry, decided by extension in one place, `thumbs.Kind`), loaded as rows scroll within
400 px of view — the list scrolls in a box of its own, so it has its own
IntersectionObserver rather than the browser's window-based lazy loading. A video thumb
carries a ▶ badge. The setting is in localStorage and follows the browser.
- **ffmpeg is the one engine**, for images and video alike; `GET /api/files/thumb` is one
ffprobe and one ffmpeg: `-ss` a tenth of the way in (30 s at most, `-noaccurate_seek`
so one keyframe is decoded), the first real video stream (cover art is skipped), one
frame, `scale` to fit 256, MJPEG q4 to a pipe. JPEGs decode at a reduced DCT size
(`-lowres`, enough halvings to stay at twice the preview) — a 24 MP photo is read as
1.5 MP. Thumbnails are the only thing Ham Lab asks a GPU to do, and it asks the way the
Processes tab reads one: **probed, never assumed.** The probe looks ffmpeg and ffprobe
up, opens every `/dev/dri/renderD*`, pushes a synthetic frame through each with VAAPI,
and reads `vainfo`'s decode profiles per node where it exists; CUDA is tried the same
way when `/dev/nvidiactl` is there (untested: nothing here has one). A video goes to the
first GPU listing its codec; ffmpeg's own software fallback sits behind that for a
profile the list did not distinguish, and a CPU retry behind a hardware run that fails
outright. Images are CPU only — one frame through libjpeg at a quarter size beats a
round trip through a GPU. Measured on `grid`: 1080p h264 0.4 s on the UHD 750 against
0.7 s on the CPU; the 8K Osmo HEVC 1.0 s against 2.9 s. `grid`'s two GPUs are listed in
node order and both decode the same set (h264, hevc, vp9, mjpeg, mpeg2 — **no AV1 on
either**, per vainfo, so AV1 is dav1d on the CPU).
- **Cached under the machine's data dir**, `<data dir>/thumbs/<xx>/<sha256>.jpg`, keyed by
path, size and mtime — a changed file gets a new preview, an unchanged one is never
decoded twice, and the URL carries `v=<mtime>-<size>` so the browser's own cache is
right too (7 days, ETag, 304). A file ffmpeg could not decode is remembered beside the
JPEGs as `.err` with ffmpeg's reason, answered as 422, and retried only when the file
changes; a timeout is not remembered. Nothing expires: a preview is a few KB, and only
files somebody scrolled past are ever made. At most 2–4 ffmpegs at once per machine
(half the cores, clamped), one per file however many rows ask, and a preview's work
runs on a clock of its own so the row that asked first leaving does not cancel it for
the rest. Each ffmpeg runs under `nice -n 10`. `hamlet`'s backup job skips the folder.
- **Root mode makes them as root** (`hamlab thumb`, the sibling of `hamlab fs`, caps on
stdin, JSON out), so a file only root can read gets a preview, and the probe runs as
root too — root opens every render node whatever groups the daemon has. Which matters:
a render node is `root:render 0660`, and on `core` and `hamtv` the `core` user is not in
`render`, so there the daemon's own previews are CPU and the tab says so, naming the GPU
and the owner, with an **Allow GPU access** button (confirms, then a task writes
`hamlab.service.d/gpu.conf` adding `render`/`video` as supplementary groups and
restarts Ham Lab three seconds later, the shape the Incus install uses for
`incus-admin`). Not pressed anywhere yet. Where ffmpeg is missing (the Mac mini, `gate`)
the note offers **Install ffmpeg with dnf/apk** through the Updates machinery
(`ffmpeg-free` on Fedora, `ffmpeg` elsewhere); the status re-probes when the task ends.
- *Not done, on purpose:* no cap on the cache and no clear button (not asked; it only
holds what was looked at); no tonemapping for HDR sources (a 10-bit HEVC frame comes out
flat); no preview in the viewer for a video (it still says "binary, download it").
- **Docker read-only view** — `docker ps`/logs; dockhand stays the source of truth. - **Docker read-only view** — `docker ps`/logs; dockhand stays the source of truth.
- **Notifications** — push on service failure / SMART warning / UPS event. - **Notifications** — push on service failure / SMART warning / UPS event.
- **Terminal extras** — search, copy-mode, font/theme, split panes, upload via - **Terminal extras** — search, copy-mode, font/theme, split panes, upload via
@@ -732,8 +783,9 @@ what it has moved.
- **Resumable uploads.** copyparty's up2k chunks a file, survives a dropped connection - **Resumable uploads.** copyparty's up2k chunks a file, survives a dropped connection
and skips what is already there; Files streams one PUT, so a phone upload that drops at and skips what is already there; Files streams one PUT, so a phone upload that drops at
90% starts over. 90% starts over.
- **A gallery.** Thumbnails for a folder of images and video, and play audio/video in - **A gallery.** ~~Thumbnails for a folder of images and video~~ (the **previews** toggle,
place — `/tank/media` and `/tank/immich` are most of what is browsed. 2026-10-11 — see the shipped entry above; a grid view is not built), and play
audio/video in place — `/tank/media` and `/tank/immich` are most of what is browsed.
- **Search** below the current folder by name, since `find` in a terminal is the fallback - **Search** below the current folder by name, since `find` in a terminal is the fallback
today. today.
- *Decide when built:* whether share links (copyparty's public per-path links) are wanted - *Decide when built:* whether share links (copyparty's public per-path links) are wanted