From 51c718e8e3f6beebc7a3a03006d9405822b076de Mon Sep 17 00:00:00 2001 From: Mitchell-Johnson Date: Sun, 11 Oct 2026 15:25:55 -0700 Subject: [PATCH] PLAN.md: previews on the Files tab, shipped as v0.1.0-200 Co-Authored-By: Claude Fable 5.1 --- PLAN.md | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 54 insertions(+), 2 deletions(-) diff --git a/PLAN.md b/PLAN.md index cfcaa50..173956f 100644 --- a/PLAN.md +++ b/PLAN.md @@ -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; `/`, `/etc`, `/usr`, … are never deleted or moved whole). Every write is logged with 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**, `/thumbs//.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=-` 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. - **Notifications** — push on service failure / SMART warning / UPS event. - **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 and skips what is already there; Files streams one PUT, so a phone upload that drops at 90% starts over. - - **A gallery.** Thumbnails for a folder of images and video, and play audio/video in - place — `/tank/media` and `/tank/immich` are most of what is browsed. + - **A gallery.** ~~Thumbnails for a folder of images and video~~ (the **previews** toggle, + 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 today. - *Decide when built:* whether share links (copyparty's public per-path links) are wanted