Commit Graph
100 Commits
Author SHA1 Message Date
Mitchell-JohnsonandClaude Opus 5.5 5edad2de6a Files viewer: previous/next — buttons, ← →, swipe on pictures; steps through media when media is open; audio autoplays
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 01:27:26 -07:00
Mitchell-JohnsonandClaude Opus 5.5 829617c932 ZFS tab: landing tier checks — stale files, verify passes against the mover's ledger, verify schedules
A verify pass reads the pool's copy of every file the mover moved back against the SHA-256 the
mover took of the landing copy (its ledger, named in its status file): match, mismatch, edited
since, gone, waiting, unreadable, unhashed. 'New moves' checks what moved since the last pass,
'everything' the whole ledger. Passes run as 'hamlab landing-verify' in their own session — the
same command a schedule's line in /etc/cron.d/hamlab-landing runs — so a console restart does
not cut one short. Stale files: anything waiting on the landing branch over an hour, listed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 23:16:55 -07:00
Mitchell-JohnsonandClaude Opus 5.5 f518df239f SENSORS: the ZFS write path's sensors
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 22:41:23 -07:00
Mitchell-JohnsonandClaude Opus 5.5 41cb734f7f ZFS tab: show a mover's rsync limit only when there is one, in the units rsync means (60M = 60 MiB/s)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 22:39:33 -07:00
Mitchell-JohnsonandClaude Opus 5.5 0698f07415 ZFS tab: the write path's details one per row, wrapping instead of cut off
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 22:37:22 -07:00
Mitchell-JohnsonandClaude Opus 5.5 b2528afe2a ZFS tab: snapshot schedules full width too — every box on the tab spans the page
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 22:34:55 -07:00
Mitchell-JohnsonandClaude Opus 5.5 50768e2d94 ZFS tab: pools, caching tiers and writes full width; write and drain rates measured between real updates
A txg records its bytes only when it syncs and the mover reports every 2 s, so a per-sample delta
read 0 most of the time. Rates are now taken over the gap between syncs (or mover updates) and held
until the next one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 22:34:11 -07:00
Mitchell-JohnsonandClaude Opus 5.5 86cfcd2bad ZFS tab: Writes — the RAM write buffer, each pool's write rate, and a landing tier with its mover
Live from arcstats (dirty), dmu_tx (throttled writes), each pool's txgs (bytes synced to disk)
and objset kstats (bytes from programs). A landing tier — a mergerfs pool whose first branch is
fast storage and whose last is a ZFS dataset — is detected from mergerfs's branch xattr; a mover
status file in /run/landing-mover/ adds its queue, the file in flight and totals. Sensors
zfs.write.*, zfs.<pool>.write_bps|logical_bps and zfs.<pool>.landing.* carry the history.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 22:28:40 -07:00
Mitchell-JohnsonandClaude Opus 5.5 1a9b31ea0a PLAN: handle fuse.mergerfs (grid's landing pool) in Storage
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 21:01:59 -07:00
Mitchell-JohnsonandClaude Opus 5.5 695e4c4f57 e2e-player: the volume slider is only there wider than a phone
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 15:49:00 -07:00
Mitchell-JohnsonandClaude Opus 5.5 432ddc0df4 files: the player's volume — mute, a level slider, remembered (as Ham Share's)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 15:43:05 -07:00
Mitchell-JohnsonandClaude Opus 5.5 1c5cd2b0de No theme touches playback, any theme; the header clears the iOS status-bar blur
Every theme's full-window overlays are off while a video is open, the picture
is never filtered or blended, the player's lettering is plain. --blur, as in To
Do and Notes: the header (the top row of the app), dialogs, the full-screen
player's note and an expanded KVM start below iOS 26's scroll-edge blur in a
Home Screen app; zero on desktop and in a Safari tab.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 00:05:04 -07:00
Mitchell-JohnsonandClaude Opus 5.5 01e0da9022 files: no theme touches playback — Retro's scanlines off while a video is open, plain font
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 00:02:15 -07:00
Mitchell-JohnsonandClaude Opus 5.5 dbf89cd5e2 files: on a phone the player's time sits above the seek bar
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 23:52:35 -07:00
Mitchell-JohnsonandClaude Opus 5.5 033343793f files: the player's −5 s and +5 s buttons, as Ham Share's
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 23:48:37 -07:00
Mitchell-JohnsonandClaude Opus 5.5 f2997a04c8 Settings, PLAN: video previews follow the Transcode device
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 16:09:28 -07:00
Mitchell-JohnsonandClaude Opus 5.5 fac8a3a180 files: the previews button says where video previews are made
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 16:08:45 -07:00
Mitchell-JohnsonandClaude Opus 5.5 d5c16540a1 files: video previews are made on the Transcode device too
The setting's device makes every video preview: on the file's own machine
when it is there (the hub tells it by header), else on the device's machine,
which reads the file through the hub by a one-file token, and the preview is
kept in the file's machine's own cache (PUT /api/files/thumb, refused if the
file changed). Images and covers stay on the file's machine's CPU. If the
device's machine can't, the file's makes it as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 16:07:00 -07:00
Mitchell-JohnsonandClaude Opus 5.5 1464e1f7a0 files: seek bar — the ball goes where the seek went and waits there; the seek is on release
Same fix as Ham Share's: a trailing change event read the bar's old position
back as a drag in progress.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 15:11:31 -07:00
Mitchell-JohnsonandClaude Opus 5.5 d1b3ef7efd PLAN: the Ham Share player and the Transcode device setting; e2e-player.mjs
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 15:00:32 -07:00
Mitchell-JohnsonandClaude Opus 5.5 8c69826547 files: the player turns a phone's picture upright on the GPU, and shows HLG as it is
tonemap_vaapi refuses HLG, and the hardware path never honoured a rotation
(ffmpeg's autorotate is for frames in memory). HDR10 is still tone-mapped,
on the card and, failing that, in software before the upload.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 14:56:16 -07:00
Mitchell-JohnsonandClaude Opus 5.5 fc4126a0ed files: Ham Share's player — Auto quality, any by hand, or the file; 360 view; Transcode device setting
Every video plays YouTube's way: hls.js reads the connection and picks the
quality, or one is picked by hand, or Original plays the file as it is. The
qualities (a ladder by size, H.264) are made on the fly in 3 s pieces from
wherever the viewer seeks, one ffmpeg per play, paused when far enough ahead.
360 videos open in a look-around view (drag, pinch or scroll to zoom from
8 to 300 degrees, phone motion).

Settings -> Transcode device picks the machine and device that makes them,
for every machine's files: a machine transcoding another's file reads it
through the hub by a token for that one file. Replaces the codec/size picker
and the single-quality re-encoder.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 14:52:35 -07:00
Mitchell-JohnsonandClaude Opus 5.5 f5f1cd79ba Top: curves and canvases update before paint, in the numbers' frame
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 01:16:28 -07:00
Mitchell-JohnsonandClaude Opus 5.5 70bebcb18b Top: one beat — graphs, numbers and the process table change on the same tick
Graphs stepped once a history point (1 s at a 5 min window) while the numbers changed every
tick; they now slide a fraction of a column each tick. The process table (and the GPU boxes,
MHz and watts it carries) was polled on its own timer; it is now read per sample and committed
with it. GPU clients are walked every sample, and the 1–2 s floors on the gallery, Network,
Docker and ZFS ARC live views are gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 01:13:19 -07:00
Mitchell-JohnsonandClaude Fable 5.1 8fc71496f0 metrics: NVIDIA GPUs via nvidia-smi — device busy, VRAM, clocks, temp, power and the pmon process table
The proprietary driver's DRM card says DRIVER=nvidia, which the driver list did not
know, so the card was never enumerated; and it exposes no sysfs busy counter or
fdinfo engine totals. Read it with nvidia-smi instead (--query-gpu per sample,
pmon -c 1 -s um for clients), by PCI address. Parsers unit-tested, including an
older pmon column layout and [N/A] fields.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-23 00:29:46 -07:00
Mitchell-JohnsonandClaude Opus 5.5 450a9d3327 files: seek anywhere in a re-encode; it restarts from there
A re-encode's playlist is now the whole film on a fixed 4 s grid. A segment
that isn't on disk and isn't coming soon moves the encode there; runs stop
short of segments already made, and gaps are filled after. Every run is cut
the same way (-ss, -output_ts_offset start+10, frag_discont), so segments
from different runs join. Repackaging keeps its growing playlist.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 13:08:52 -07:00
Mitchell-JohnsonandClaude Opus 5.5 ff1373e838 docker: an update clears "update available" without a page reload
The registry's answer was cached and served as-is, so the stack view and the
machine-wide list kept calling an image behind after an update had pulled it.
The cache now keeps the digest the registry named, and an image marked behind
is re-checked locally on every read: once this machine holds that digest it
is current, at no registry cost. The page re-reads the machine-wide answer
after a stack task, a queued update, or a schedule run.

Also: stack keys can no longer collide with the machine key, and the dead
"/"+stack forget in the schedule runner is gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 12:45:47 -07:00
Mitchell-JohnsonandClaude Opus 5.5 d70ddb6d67 Cut virtualization from the app: out of scope
Ham Lab is meant to be lightweight; VMs, passthrough and their consoles
are too much. Removes the Virtualization tab, the incus package, the
/api/virt routes, the instance.* sensors and their alert default, the
virt-export command, the VGA console and the vendored spice-html5, and
VIRTUALIZATION.md. A saved instance-down rule is dropped on load.

Everything removed is kept, as it was, on the virtualization branch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 00:47:48 -07:00
Mitchell-JohnsonandClaude Fable 5.1 a2674bea6a thumbs: a GPU-access run already in flight refuses a second (409)
The sweep's battery hit the route three times; the second and third runs only collided
with the first's restart timer.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-13 18:00:39 -07:00
Mitchell-JohnsonandClaude Fable 5.1 134bbc991e PLAN.md: the whole-console sweep
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-13 17:56:16 -07:00
Mitchell-JohnsonandClaude Fable 5.1 285278abdb A sweep of the whole console: status codes, a data race, atomic writes, dead code
From the race detector, staticcheck, a garbage-in battery over every safe route and a
headless pass through every tab on two machines. Fixed:
- docker inspect/health of a container that isn't there answered 500; now 404, and a
  daemon that is down 503 (dockerErr, with a test).
- /api/virt/instances/{name} on a machine without Incus answered 502 "incus not found";
  it now says Incus isn't ready, like its siblings.
- Deleting a ZFS snapshot with a bad name answered 500; a bad name is 400, a snapshot
  that doesn't exist 404.
- The task list read a task's Running flag without the task's lock while the waiter
  goroutine cleared it: a data race, now read under the lock.
- The Virtualization export config and run record were written in place; a crash
  mid-write would have left a half file. Atomic now, like the config and state files.
- An unused function in kvm.go, one in incus/subnet.go, an unused field in files/move.go
  and a capitalised error string, from staticcheck.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-13 17:52:18 -07:00
Mitchell-JohnsonandClaude Fable 5.1 05161bf7ef PLAN.md: the Files edge sweep
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-11 20:32:34 -07:00
Mitchell-JohnsonandClaude Fable 5.1 1038c010fa Files: a sweep of the edges
Found by walking every handler and the UI with odd inputs, then a battery of requests
against the live hub. Fixed:
- A save over the 8 MiB cap was cut short in place: the file was truncated, then the
  first 8 MiB written, and the caller told 204. Now the body is read whole first and a
  body over the cap is a 413 with the file untouched.
- A link to a file showed the link's own time; now the file's, so the row reads right
  and the preview key follows the file.
- Reading a link to nothing answered 200 with an empty body (the open failed after the
  headers in root mode); now a 404 that names the target.
- A play request with an unknown codec or size was answered by inspecting the file
  first, so an audio-only file said "no video stream" instead of what was wrong; the
  choice is checked before any work, and is a 400.
- A stream that ended in failure was handed back to the next identical request for ten
  minutes; a failed one is now dropped and a fresh one made.
- A player that left while its stream was still finding a first segment made the loop
  try the next recipe; it stops. A stream closed mid-start is not started again.
- Ending a root-mode ffmpeg could wait forever: the daemon cannot signal root's
  processes, so the wait is bounded and the helper's own grace does the rest.
- A page going away (a phone closing the tab) releases its stream by a beacon, rather
  than ten minutes later by the sweeper.
- The path box resolves like a shell — relative to the folder shown, . and .. applied,
  trailing slashes dropped — in the tab and in the Move picker, with a test. A typed path
  that is a file opens it; a remembered folder that is gone falls back to / once.
- A dropped folder arrived as a nameless empty file; it is refused with a word. The
  upload toast counts what was actually uploaded.
- The viewer names a device, pipe or socket, and a dangling link, instead of calling
  them binary files.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-11 20:31:10 -07:00
Mitchell-JohnsonandClaude Fable 5.1 d200e5dc2e PLAN.md: re-encoding for the browser, on the machine's GPU
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-11 17:46:35 -07:00
Mitchell-JohnsonandClaude Fable 5.1 f3c547adfa Files: re-encode a video the browser can't play, on the machine's own GPU, and play that
The viewer tries the file as it is first. When the browser gives up — Safari and an MKV,
HEVC without a hardware decoder — or on a tap, a picker: codec (H.264, HEVC, AV1, or the
original codec repackaged) and size (original, 1080p, 720p, 480p), each codec labelled
with the engine this machine would use. POST /api/files/play runs ffmpeg into an HLS
event playlist of fragmented-MP4 segments under <data dir>/transcode/<id>/; Safari plays
HLS itself and everything else goes through hls.js, loaded only then. The recipes are
tried best first: a card on the bus before the integrated GPU, decoding on the same card
before decoding on the CPU and uploading, the software encoders last; a recipe that
produces no first segment is dropped for the next. The seekable range grows as ffmpeg
gets ahead; status says how far. Streams are reused for the same request, closed when
the player goes, swept after ten idle minutes, and the folder is emptied at start. Root
mode runs ffmpeg as root through a helper that ends when the daemon lets go of it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-11 17:42:25 -07:00
Mitchell-JohnsonandClaude Fable 5.1 aa4ae57c22 gofmt after the rename
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-11 16:54:52 -07:00
Mitchell-JohnsonandClaude Fable 5.1 af189b1fa1 internal/thumbs is internal/media, with Inspect: what a file holds, from ffprobe
The package was named for its first job; it now also decides what a file is for the
Files tab (image, video, audio) and reads its streams, which the viewer will need. Inspect
is the richer probe — codec, profile, pixel format, size, frame rate, HDR transfer, the
audio beside it, container and duration — run as root through sudo when asked.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-11 16:54:15 -07:00
Mitchell-JohnsonandClaude Fable 5.1 beec9269fe PLAN.md: video and audio playback in the Files viewer, as the file is
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-11 16:43:03 -07:00
Mitchell-JohnsonandClaude Fable 5.1 d7f56ee0b3 Files: a seek costs nothing; the backend reopens only when a read wants somewhere else
ServeContent seeks to the end for the size before it seeks to the range, which was
throwing away the stream Check had just opened.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-11 16:41:54 -07:00
Mitchell-JohnsonandClaude Fable 5.1 a9bcb88212 Files: play video and audio in the viewer, as the file is; reads answer Range requests
The viewer hands a video or audio file to the browser untouched — container, codec and
all — and the browser plays what it can; when it can't, the viewer says so and offers the
download. What the server adds is the part a player needs: /api/files/read now goes
through http.ServeContent for a real file of known size, so Range requests (seeking) and
HEAD work, and a download can resume. The root backend's helper takes an offset for that,
and a Seeker opens the backend once, at the range asked for, instead of streaming from
the top. Audio files are a preview kind too: their cover art is the thumbnail.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-11 16:40:42 -07:00
Mitchell-JohnsonandClaude Fable 5.1 c24f6f5cde Files: a move of something that isn't there is a 404, not a task that fails on arrival
The kernel answers EXDEV for two paths on different filesystems before it looks for the
source, so the cross-filesystem path was taken for a missing file.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-11 16:30:04 -07:00
Mitchell-JohnsonandClaude Fable 5.1 b6d72cce9f PLAN.md: Move… is a folder picker; moves across filesystems copy then remove
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-11 16:29:27 -07:00
Mitchell-JohnsonandClaude Fable 5.1 fb3b8e4b4c Files: Move… opens a folder picker, and a move across filesystems copies then removes
The picker walks the machine like the tab does — back, forward, up, breadcrumbs, a path
box, a new folder — and "Move here" puts the file or folder in the folder being looked
at; it says why it can't when it can't (already here, into itself, a name clash). POST
/api/files/move is a rename when the two ends share a filesystem and otherwise a task:
on a NAS every move between datasets is a different filesystem, and a rename there is
EXDEV. The copy is built under a hidden temporary name beside its destination, renamed
into place once complete, and only then is the original removed, so a failure or a
cancel leaves the original as it was. Modes, times, symlinks and, as root, owners are kept.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-11 16:27:55 -07:00
Mitchell-JohnsonandClaude Fable 5.1 e5d3fb2afd PLAN.md: previews are cached in thumbs.db
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-11 16:05:17 -07:00
Mitchell-JohnsonandClaude Fable 5.1 e198471f8c Files previews: the cache is one database file, <data dir>/thumbs.db
bbolt instead of a tree of hash-named JPEGs: pure Go, so the binary stays static; one
file per machine; an entry carries where the preview came from (engine, codec, size,
when) beside the picture, and a count rides along so the tab can say what it holds
without walking anything. A miss never creates the file, so a machine nobody browses
with previews on never has one; a cache that cannot be opened is logged and previews
are still made and served, just not kept.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-11 16:04:12 -07:00
Mitchell-JohnsonandClaude Fable 5.1 51c718e8e3 PLAN.md: previews on the Files tab, shipped as v0.1.0-200
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-11 15:25:55 -07:00
Mitchell-JohnsonandClaude Fable 5.1 f8c9479e93 Files: previews — a toggle that shows a thumbnail per image and video, made by ffmpeg on the machine's GPU where it has one, cached under the data dir
A previews button on the Files tab turns the icon column into a 256 px thumbnail for
every image and video row, loaded as rows scroll near. internal/thumbs probes what the
machine can do rather than assuming it: ffmpeg and ffprobe, every /dev/dri/renderD*
opened and exercised, vainfo's decode profiles per node where it exists. A video goes to
the first GPU that lists its codec, with ffmpeg's own software fallback behind it and a
CPU retry behind that; images decode on the CPU, JPEGs at a reduced DCT size. Previews
are kept under <data dir>/thumbs/ keyed by path, size and mtime, with undecodable files
remembered too. Root mode makes them through the same root helper the file reads use.
Where ffmpeg is missing the tab offers to install it; where a render node exists but is
closed to the daemon's user, it offers a drop-in adding the group and a restart.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-11 15:20:27 -07:00
Mitchell-JohnsonandClaude Fable 5.1 e7034cd478 VIRTUALIZATION.md: grid's Incus removed for a fresh start; status
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-11 15:02:52 -07:00
Mitchell-JohnsonandClaude Opus 5.5 e068fbdb2f PLAN.md: copyparty retired ahead of the Files gaps; they stay as Files polish
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 14:54:27 -07:00
Mitchell-JohnsonandClaude Fable 5.1 faf444d8ce VIRTUALIZATION.md: storage decision revised — a Disk location picker, plain files on grid's root disk
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-11 14:36:06 -07:00
Mitchell-JohnsonandClaude Fable 5.1 ae72e45bf5 Virtualization: a disk-location picker on both create forms, locations by place, add or remove a location
Pools are shown by where their bytes land (plain files on this disk, a ZFS pool inside one
file, a dataset…) rather than by driver name, which read as the NAS pool. A location can be
added as a plain-files pool at any folder on the machine, and removed while nothing uses it.
The driver order at Install is as it was; the labels say what each one really is.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-11 14:33:10 -07:00
Mitchell-JohnsonandClaude Opus 5.5 a9b4d7969e PLAN.md: CPU power on the Processes tab, shipped as v0.1.0-194
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 00:00:05 -07:00
Mitchell-JohnsonandClaude Opus 5.5 bfedf622be Processes: CPU power beside the clock, from the packages' energy counters
New sensor cpu.power: watts from the RAPL energy counters (Intel, and AMD
Zen through the same driver), summed over every package and averaged into
history. Parts of a package, psys and the MMIO duplicate are left out.

The counters are root-only, so a daemon that isn't root reads them through
`hamlab cpu-energy`, a root helper started once with sudo or doas and asked
over a pipe each sample, the same shape as the GPU client walk's helper.
It reads only the counters it finds itself. No counters or no root means
no reading, and the CPU box shows nothing extra.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 23:58:35 -07:00
Mitchell-JohnsonandClaude Opus 5.5 d5680e8e41 VGA console: stop importing spice-html5's demo stylesheet, which restyled every button
spice.css sets body, *, button and canvas for the whole document. Bundled
into the App chunk since 43ebacd (deployed as v0.1.0-188), it turned every
button into a gradient pill, shifted 15% right and 8% up with a 150px
minimum width, broke the rail and header layout, and bordered every graph
canvas. The console is already styled by the .vga rules in styles.css.

The file is removed from the vendored copy, and a new web test fails on any
stylesheet import outside an allow-list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 15:19:20 -07:00
Mitchell-JohnsonandClaude Opus 5.5 ea84ee40c7 PLAN.md: the 2375 item deployed as v0.1.0-191 and checked on every machine
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 15:00:19 -07:00
Mitchell-JohnsonandClaude Opus 5.5 24a9979432 Docker updates: a machine without Docker answers empty, not 500
The hub used to answer this for its nodes and hid the node's 500. Now the
node answers itself, so it needs the same guard the stack list has.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 14:58:13 -07:00
Mitchell-JohnsonandClaude Opus 5.5 c27d9f0ada Stacks: one machine, one set of files; default home is <data dir>/stacks
The hub no longer probes a node's Docker on tcp :2375, holds compose files
for a node, or runs compose against another machine's Docker. hubStacks,
nodeDocker, taskOnHub and the hub-side /api/nodes/{node}/docker/... routes
are gone; the generic node proxy hands those requests to the node's own
Ham Lab. Stack.remote / held_by and compose.Spec.Remote are removed.

New stacks default to <data dir>/stacks on every machine. Folders with two
or more deployed stacks are still offered, after it, never as default.
The ~/stacks fallback is gone. A missing home is skipped without a sudo find.

A schedule saved earlier with a tcp target is refused with a message
instead of being run against this machine's Docker.

PORTABILITY.md 3.1, 3.2, 3.3 and 9.2 done; 3.5 checked, no change needed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 14:53:10 -07:00
Mitchell-JohnsonandClaude Fable 5.1 a0ce3e5eb9 VIRTUALIZATION.md: phase 2 deployed as v0.1.0-188 and verified live; what is left
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-09 22:34:05 -07:00
Mitchell-JohnsonandClaude Fable 5.1 80a5b4b464 VGA console: one console operation per VM, a data websocket per SPICE channel
spice-html5 opens a websocket per channel (main, display, inputs, cursor, playback), and
each one was making Ham Lab open a new console operation, which Incus refuses as already
connected. Now the first channel opens the operation and holds its control websocket; every
further channel is one more data websocket on the same operation, which Incus allows; the
operation is let go when the last channel closes. A stale operation from a previous Ham Lab
process is taken over with force.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-09 22:32:08 -07:00
Mitchell-JohnsonandClaude Fable 5.1 a5ebfdc9ce VGA console: agree to spice-html5's binary subprotocol; label layout and two labels in the detail pane
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-09 22:28:00 -07:00
Mitchell-JohnsonandClaude Fable 5.1 43ebacdd92 Virtualization phase 2: ISOs, New VM from ISO, the VGA console in the browser, nightly exports, mounts, instance sensors
- Library: an ISO shelf in virt/isos: upload (the Files uploader), fetch by URL with an
  optional sha256, import a file from the machine by copying it in, rename, delete (refused
  while attached), attach to a VM as a CD-ROM live or stopped with boot-first, eject, and
  New VM from ISO: an empty VM with the ISO as first boot device, Secure Boot on or off.
- The VGA console: Ham Lab asks Incus for the VM's SPICE console over its unix socket and
  bridges the display websocket to the browser, where spice-html5 (vendored from upstream
  b372aab, LGPL, loaded on demand) draws it; Ctrl+Alt+Del, fit-to-window.
- Exports: every instance as a tarball in virt/exports/<name>/, kept to a count, as a cron
  line in /etc/cron.d/hamlab-virt calling hamlab virt-export, settings and the last run
  as files beside the data; Export now per instance or for all.
- Disks tab per instance: the root disk, attached ISOs with Eject, bind mounts in and out
  (shift for containers, virtiofs for VMs).
- instance.* sensors (running, down, state, cpu, mem) and the VM-or-container-down alert
  default, offered once to installs seeded before it existed.
- cloud-init: Use my SSH keys writes a cloud-config from the account's public keys.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-09 22:21:57 -07:00
Mitchell-JohnsonandClaude Fable 5.1 66498a58c5 VIRTUALIZATION.md: phase 1 deployed as v0.1.0-184, Install verified live on grid
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-09 15:31:06 -07:00
Mitchell-JohnsonandClaude Fable 5.1 b790b0c31c virt: the description is an instance field, not a config key — set it with a PATCH
Found on the first live test on grid: incus config set refused 'description' and the whole
limits save with it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-09 15:30:10 -07:00
Mitchell-JohnsonandClaude Fable 5.1 38675ce9af Portability audit: every lab-specific design choice, listed for generalisation
PORTABILITY.md sweeps the whole repo for assumptions tied to this lab's hosts,
network, hardware, distro, init system, orchestrator history and privilege
posture. Nothing is changed yet; this is the list to work from, graded A/B/C
with the general form of each.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-09 15:29:12 -07:00
Mitchell-JohnsonandClaude Fable 5.1 dfe2ca359a incus: syntax-check the generated install script for each machine shape; refusals; create args
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-09 14:16:05 -07:00
Mitchell-JohnsonandClaude Fable 5.1 289aa4aee3 VIRTUALIZATION.md: phase 1 built as v0.1.0-180, awaiting deploy
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-09 14:13:54 -07:00
Mitchell-JohnsonandClaude Fable 5.1 240129c83f Virtualization tab, phase 1: Incus per machine — detect, install, instances, consoles, snapshots, images
The tab reports one of four states for the selected machine (ready, repair, installable,
unavailable) from what it finds — /dev/kvm, the package manager's incus candidate, the
pool drivers the kernel has — and acts on that machine only. Install is one followed task:
packages, a pool whose backing file sits in Ham Lab's own virt/ folder (zfs pool on a file,
btrfs on a loop mount, or a plain dir), a bridge on a subnet picked clear of the machine's
routes and Docker's pools (never 10.x on a /8 LAN), the DOCKER-USER rules that keep
Docker's FORWARD drop off the bridge, the incus-admin group for Ham Lab's account, and a
restart. Instances list with live CPU and memory, create from the images: catalogue or a
local image (container or VM, limits, cloud-init), start/stop/restart/force-stop/rename/
delete, console and shell as terminal tabs, snapshots with Incus's own schedule and expiry,
limits editing. Images: local store, catalogue search and download (exported into the
library too), import a tarball or a VM disk from a path, publish an instance.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-09 14:09:56 -07:00
Mitchell-JohnsonandClaude Fable 5.1 af53b45b67 Virtualization plan: every artefact under Ham Lab's data directory, one virt/ tree per machine
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-09 13:48:31 -07:00
Mitchell-JohnsonandClaude Fable 5.1 e772f7fae2 Virtualization plan: a Library for ISOs and images; VGA console moves to phase 2 with it
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-09 13:36:27 -07:00
Mitchell-JohnsonandClaude Fable 5.1 0fa6ab98e2 Virtualization plan: capability-gated per machine, never policy-gated; Install or report
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-09 12:56:18 -07:00
Mitchell-JohnsonandClaude Fable 5.1 16e85e1e42 Virtualization tab: scoping plan — Incus on grid, what is and isn't feasible
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-09 12:11:33 -07:00
Mitchell-JohnsonandClaude Opus 5.5 1fbf345166 BUGS: bug bash 6 and its quality-of-life builds deployed as v0.1.0-174
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 15:11:29 -07:00
Mitchell-JohnsonandClaude Opus 5.5 07645089d7 Version builds from the base tag, not from a published release's tag
git describe had started picking the nearest release tag (v0.1.0-137-gc129f65,
fetched from Gitea), so a build came out as v0.1.0-137-gc129f65-36-g7d0b87b,
which the version compare reads as build 137: older than the 169 it replaces.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 15:06:48 -07:00
Mitchell-JohnsonandClaude Opus 5.5 7d0b87bc45 A GPU with nothing measurable is one line, not half the Processes page
The Mac's Asahi GPU exposes no busy counter, clock, sensor or memory reading, so
its box was a graph that could never draw beside a column of dashes. A GPU with
nothing to read now gets one line (driver, "nothing measurable" or the stall,
and whether a process has it open), its row shrinks to fit, and the Gpu% column
and gpu sort are dropped when no GPU on the machine can light them. QoL #3 from
bug bash 6 (BUGS.md).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 19:31:25 -07:00
Mitchell-JohnsonandClaude Fable 5.1 f3ca3d8f73 Idle sessions sign themselves out; a KVM's login is remembered; the compose editor knows YAML
A session unused for seven days is dropped, the Sessions list marks a day's idleness and
can sign all of those out at once. The hub keeps a KVM's own cookies from any browser that
has them and lends them to one that hasn't, on the wire and back to the page. The stack
editor and the new-stack dialog use CodeEditor: line numbers, YAML and .env colour under a
real textarea, Tab/Shift+Tab/Enter indentation, and a key bar on touch screens.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-06 01:20:27 -07:00
Mitchell-JohnsonandClaude Fable 5.1 faaa6aaf9d The phone's ZFS table headers no longer collide; the bash notes that deploy is still owed
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-03 11:34:31 -07:00
Mitchell-JohnsonandClaude Fable 5.1 e0e2562f0a Bug bash 6: the phone fits ZFS, Usage and journal lines; a snapshot's default name is local time; twelve smaller faults
Found by a headless pass over every page on every machine at desktop and phone widths, then
every interaction on the hub. BUGS.md has the list, what was checked and found fine, and the
quality-of-life notes to discuss.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-03 11:28:44 -07:00
Mitchell-JohnsonandClaude Fable 5.1 3a58e265c6 A GPU is named from an embedded PCI ID table, the same on every machine
The router has no lspci, so its GeminiLake chip came up as 'Intel GPU
3185' beside the identical chip's 'Intel UHD Graphics 600' on hamtv.
A trimmed pci.ids — the graphics devices of Intel, AMD and NVIDIA, and the
virtual GPUs, 115 KB — rides in the binary and is consulted first; lspci
is the fallback for a card newer than the table. scripts/pci-ids-gpu.sh
regenerates it from the hub's copy.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-02 23:48:05 -07:00
Mitchell-JohnsonandClaude Fable 5.1 b5cecce435 Never read sysfs through Go's poller, and a GPU that stops answering is skipped
On grid the sampler sat 42 minutes in runtime_pollWait on the Arc A380's
hwmon fan1_input: kernfs accepts epoll, so os.ReadFile made the fd
non-blocking, i915 answered EAGAIN once, and the poller waited for a
readiness kernfs only signals on sysfs_notify. Every graph on the machine
froze while the API carried on.

readStr now reads with plain blocking syscalls. Each GPU's sysfs reads run
under a three-second deadline; a device that does not answer is marked
stalled, logged, and left alone until restart, with the box saying so.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-02 23:41:22 -07:00
Mitchell-JohnsonandClaude Fable 5.1 a8f4655c83 A graph's canvas is out of flow, so a box can never grow itself without end
The gpu box sat in an auto-height grid row. The canvas inside it was sized
from its box and also sized the box, so every resize fed the next and the
Processes tab grew for as long as it was open. The canvas is now absolute
within its box, and the gpu row has a floor instead of auto.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-02 22:54:49 -07:00
Mitchell-JohnsonandClaude Fable 5.1 7e070bd524 The gpu box's layout rules, which the first commit lost
The desktop grid placement, the stats panel, the engine label width and
the client rows' columns were never in the stylesheet: on a phone the
caption sat on the stats head, 'enhance' ran into its meter, and a client
row came out as one word. Restored, with the graph box as tall as the
cpu's so its caption always has a line of its own.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-02 22:53:50 -07:00
Mitchell-JohnsonandClaude Fable 5.1 b44faf3ac9 The ZFS tab shows the ARC, and names every caching tier a pool has or lacks
A Cache section between the pools and the schedules. One card for the ARC:
size against its ceiling with the target and floor, sparklines of size and
hit rate from the sampler's history, what it holds (MRU/MFU/dirty, data vs
metadata, compression, the ghost lists), the prefetcher and dbuf cache, the
memory-pressure counters, and the since-boot breakdown by demand/prefetch
and data/metadata. Then a card per pool for L2ARC, SLOG, special vdev,
dedup table and spares — the devices and their fill where they exist, and
where they don't, plainly none and what that means (the ZIL writing to the
pool's own vdevs, with how much it has written).

Live numbers come from the sampler, which reads arcstats every tick and
publishes zfs.arc.* sensors; the fuller picture is read on request, with
zpool list -Hpv and zfs get run without sudo and cached for thirty seconds.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-02 18:30:02 -07:00
Mitchell-JohnsonandClaude Fable 5.1 db40220365 GPU boxes on the Processes tab: busy, engines, temps, and who has the card open
Every DRM device with a real GPU driver gets a box under the cpu box, laid out
like it: the busy curve, a meter for the device and one per engine, temperature,
clock, power and fan where the card has them, and the processes holding it open
with their share. The process table gains a Gpu% column and sort.

Device busy is Intel's RC6 idle residency inverted (what intel_gpu_top shows,
and readable without root) or amdgpu's gpu_busy_percent. Per-process shares are
DRM fdinfo deltas, as nvtop reads them; other users' fdinfo needs root, so a
daemon running as a user keeps one root child (hamlab drm-clients) over a pipe
rather than paying a sudo a second. Everything is also a gpu.* sensor.

What cannot be measured is left out, not shown as zero: an Arc A380 on i915 6.12
has no temperature sensor, an integrated GPU wears the package temperature
marked *, and Asahi has neither a busy counter nor fdinfo yet.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-02 18:18:02 -07:00
Mitchell-JohnsonandClaude Opus 5 0e82719070 plan: the VPN tab edits devices, and what that cost in care
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-27 23:22:13 -07:00
Mitchell-JohnsonandClaude Opus 5 8fb21ee95e Create, rename and remove the devices that dial in
On a tunnel this machine configures — today that means OpenWrt's UCI, which is
where the server here lives — the VPN tab can add a device, rename it and take
it away. A wg-quick host is reported as unmanaged and shows no buttons, because
half-supporting a second configuration format is worse than an honest refusal.

Adding generates the keypair and a pre-shared key on the machine, picks the
lowest free address in the tunnel's range, and hands back the complete client
profile with a QR code for the WireGuard apps. The private key in it is
generated for that one call and stored nowhere, so the profile is shown until
dismissed and then gone — the same bargain as any password this console makes.

Nothing restarts the network, which matters when the interface belongs to the
router the house is behind. `uci commit` writes the file and applies nothing;
`wg set` changes one peer on the live interface and touches nothing else.

The order of the two is chosen so they cannot disagree quietly. Adding writes
the configuration first and rolls it back if the live tunnel refuses, because a
device that works only after the next reboot is the worst of the three outcomes.
Removing does the live tunnel first, since that is the moment access actually
ends, and says plainly if the configuration half then fails.

Removing asks for the device's name to be typed back, at both ends. It is the
one action here that cannot be undone and it sits one tap from a list of devices
that all look alike.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-27 23:20:41 -07:00
Mitchell-JohnsonandClaude Opus 5 32fdbf169a Fix the blank VPN tab, and stop one page taking the console with it
On a machine with wireguard-tools installed but no tunnel up — core, which is
the machine the console opens on — the tunnel list was a nil slice, which is
null in JSON, and the page took its length. That throws, React unmounts the
whole tree, and what is left is a blank window with nothing to go on.

Two fixes, because either alone leaves the failure possible. The parse returns
an empty list rather than nil, with a test that pins the marshalled shape; and
the page treats a missing list as an empty one, so a node still running an older
build cannot blank the console either.

And the reason a small mistake had such a large blast radius: nothing caught it.
Each page now renders inside a boundary, so a fault costs that page and says
what it was, while the rail, the switcher and every other tab carry on. The
terminals are deliberately left outside it — they hold live WebSockets, and
remounting them to recover from somebody else's error would drop the sessions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-27 22:18:59 -07:00
Mitchell-JohnsonandClaude Opus 5 acb83d3a4a A peer that never connected has no timestamp, and names are readable
Two faults in the VPN tab, both found by running it against the real machines.

omitempty does nothing for a zero time.Time, so a peer that has never handshaken
was serialised as the year one and the page would have shown mbp14 as last seen
on 1 January 0001 instead of "never connected". The field is a pointer now, and
a test pins the JSON.

And peer names on a wg-quick host were never going to appear: /etc/wireguard is
root-only because it holds private keys, and the file was being opened as the
daemon's own user. It now falls back to reading it as root, the same way the
kernel state beside it is already read. Only comments and public keys are taken
from it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-27 22:01:13 -07:00
Mitchell-JohnsonandClaude Opus 5 6f634bbe11 A VPN tab: the tunnels, and who is on them
Beside Network, per machine. Every WireGuard interface on the machine in the
switcher, with its address, listen port and public key, and under it the peers:
name, allowed addresses, last handshake, where they connect from, and what they
have moved.

Per machine is the point rather than a limitation. The tunnel devices dial into
and a tunnel a machine dials out through are different boxes here, and putting
them on one page would invite the question of which box you were looking at.

The kernel is the truth and the configuration only supplies names: wg show all
dump has the handshakes and the counters and knows nothing but public keys, so
the # comment above a peer in a wg-quick file, or a UCI section's description on
OpenWrt, is read alongside purely to label them. Whether a tunnel is dialled or
dialling is worked out from the shape of its peers, and says neither when it
cannot tell.

Online means a handshake in the last three minutes, and the tab says so in as
many words: WireGuard is silent when idle, so there is no connection to be in.

Read-only, and no secrets leave: the private key is the second column of the
dump and is read past, pre-shared keys are noted as present and never read.

The Network tab keeps listing tunnels as the interfaces they are, and hands off
to here for anything about them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-27 21:59:30 -07:00
Mitchell-JohnsonandClaude Opus 5 9c7848907b Network sits under Files by default
Where Mitchell had already dragged it. An account with its own rail order is
unaffected either way; this is the slot a fresh one gets.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-27 20:20:49 -07:00
Mitchell-JohnsonandClaude Opus 5 48d3887514 A second download source, for when the first one refuses
Cloudflare rate-limits by volume, and the block it handed out while this was
being built lasted over twenty minutes — too fragile to be the only thing behind
a button. So the download phase tries its sources in order: the anycast endpoint
first, then a plain 100 MB file on Linode's Seattle mirror, which has no opinion
about how much has been pulled today.

Only a refusal moves to the next one; anything else would fail the same way
twice. Every result names the source it used, and the page says "from
linode-seattle" rather than "via SEA" when it was the fallback — that mirror is
in one fixed place, so somewhere else it reads low, and a figure that quietly
came from a different continent would be a lie.

Upload stays on Cloudflare, which has never been the one to refuse.

Also: the too-short-window guard was refusing to divide 512 MiB by ten
milliseconds on a link fast enough to hit the runaway budget before the first
tick. It now guards against dividing by zero and nothing more.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-27 20:15:33 -07:00
Mitchell-JohnsonandClaude Opus 5 2f59a54678 A Network tab, with an internet speed test
Per machine like every other tab: the interfaces as the kernel has them
(addresses, MAC, MTU, negotiated link speed, carrier, totals, live rates from
the sampler's own sensors), the default route and resolvers, what the internet
sees of this connection, and a speed test with the last fifty results kept.

The test runs in the daemon, not the browser, so the figure is the line of the
machine in the switcher — the router's, if you switch to gate — and it carries
on when the phone locks. It measures against Cloudflare's public speed
endpoints, so there is nothing to install on any host, which is what makes it
work the same on Debian and on OpenWrt where flash is scarce.

What the measurement does and does not count:

  - Several streams, HTTP/2 deliberately off. One stream over one connection
    measures a congestion window, and HTTP/2 would put every stream back onto
    one connection and measure the same thing.
  - The first second of each phase is thrown away as the ramp.
  - A phase ends on 64 MiB as well as on four seconds, so a fast line says what
    it is without moving a gigabyte. A minimum window keeps a very fast line
    from filling the cap before there is anything to divide by.
  - Latency is the round trip less the far end's own Server-Timing. A zero-byte
    response is mostly Cloudflare's worker thinking: left in, it reported 31 ms
    for a line the edge itself measures at 3.5. Jitter is the mean change
    between consecutive probes, so one slow probe cannot run away with it.
  - The far end rate-limits by volume. So: a 20 s cooldown counted from the last
    attempt rather than the last success, streams that retry twice, the rate
    computed over the window in which bytes were actually still arriving, and a
    plain sentence when it was refused. Coasting to the end of the clock with
    nothing arriving reported a third of the truth.

The last result is published as net.internet.* sensors, each stamped with when
the test ran, so the line can be a widget, a graph, an alert or a Prometheus
series like anything else. The public address is refreshed on its own quarter
hour, which is worth having here: it is a DHCP lease and it moves.

Tests are offline — the window, the byte cap, the refusal and the cancellation
run against a local server; the route parser's byte order is pinned by a case
that would have caught the bug it did. The live measurement is behind
HAMLAB_NET_TEST=1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-27 20:11:03 -07:00
Mitchell-JohnsonandClaude Opus 5 a0987c940f Twenty login tries before the backoff starts, not five
A guest password is typed by hand off a message on a phone. Five tries is not
many when a hyphen lands in the wrong place, and being told to wait is worse
than the brute-force risk here: past twenty the doubling lockout still applies,
the global backstop is unchanged, and the password is ~78 bits.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-27 18:04:23 -07:00
Mitchell-JohnsonandClaude Opus 5 ed64aca63b An eye on the login form, to see what is being typed
A guest password is read off a message and typed by hand, and from the login
form a mistyped character and a wrong password look identical. Revealed, the
field is a text field, so it also turns off capitalisation and autocorrect,
which a phone keyboard would otherwise offer for it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-27 18:03:26 -07:00
Mitchell-JohnsonandClaude Opus 5 179f342a2c A guest login is a full account with a clock on it
Mitchell, on the read-only version built earlier today: scratch it. He hands
these to people he trusts and revokes them otherwise, so a console with half
its tabs missing solved a problem he doesn't have.

So the allow-list, the middleware check and every "not for a guest" branch in
the UI are gone. What is left is the part that was the point: a throwaway
account that expires by itself, is checked on every request, swept once a
minute, and can be ended in one tap. A guest now gets the whole console on
every machine, terminal included.

Said once, since it is his call and he has made it: that means a guest has a
root-capable shell on every host, and can make an account or a token of its own
that outlives the guest login.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-27 17:53:55 -07:00
Mitchell-JohnsonandClaude Opus 5 8a7167bf03 A guest login: temporary, look-only, revocable
Users -> Guest logins makes a throwaway user name and password, good for half
an hour by default, to hand to somebody who wants to see the console. It is a
normal account with `guest: true` and an expiry, so login, sessions and the
saved look need no special case, and it is left out of the account list, out of
"the last account can't be removed", and out of the one account an empty user
name means.

What a guest may do is one allow-list, checked in the auth middleware, rather
than a flag ~190 handlers have to remember. The list is matched by asking the
router which handler a request would reach, so it cannot drift from the routes
it names, and it is checked against the scoped path — /api/nodes/grid/procs is
/api/procs on grid — because the hub proxies to a node with a token that is
full control of that node. The node cannot tell a guest from its owner, so the
only place it can be stopped is here, before the request leaves.

A guest gets the dashboard, containers, stacks, disks, hardware, ZFS and usage,
on this machine and on any managed one, and nothing else: no terminal (attaching
is a GET, and it is a root shell), no files, no container logs or environments,
no stack files, no alert channels, no cron, no tokens, no settings, no power.

Expiry is enforced on every request and swept once a minute, so a console left
open on a visitor's phone stops within the minute rather than at their next
click; revoking signs them out at once. The console hides what a guest cannot
use and says how long is left where the power button would be.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-27 17:23:16 -07:00
Mitchell-JohnsonandClaude Opus 5 4450f1285d Install under procd, so OpenWrt counts as a host too
The binary was already static and musl-clean; the only thing standing between
Ham Lab and an OpenWrt box was that setup.go knew systemd and OpenRC and
nothing else, so an install there ended at "nothing starts it at boot."

Adds an embedded procd init script, detected on /sbin/procd + /etc/rc.common
ahead of the OpenRC branch. Both backends write /etc/init.d/hamlab, so
uninstall tells them apart by USE_PROCD=1 rather than by path.

Verified on the new router: procd supervises the instance, S95/K10 symlinks
land, and the hub drives its Docker and stacks like any other node. The
Updates tab needed nothing — apk-tools 3 answers the same query Alpine's does.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-26 17:39:04 -07:00
Mitchell-JohnsonandClaude Opus 5 4e3c817b2a A Recreate action, and the Updates tab uses the whole window
Recreate is `up -d --force-recreate`. It exists because Deploy and Update
both leave a container alone when its definition has not changed, which is
precisely the state a container is in when Docker will not start it — pinned
to a network that has since been recreated, answering "network … not found"
to everything. siyuan landed there today and there was no way out from the
console.

The Updates tab is a page, not a list with a detail pane, so on a wide window
it was being squeezed into the right-hand half with an empty list column
taking the left — which read as a phone layout on a desktop. It now sits
under the tab strip at full width. The list-and-detail tabs are unchanged.
The rule needed :not(.has-sel) to outrank the wide-window rule that grows the
list to fill the row.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-26 13:14:18 -07:00
Mitchell-JohnsonandClaude Opus 5 94800b8207 Update from the list that says what is out of date
The Updates tab could tell you eleven images were behind and offer nothing to
do about it; updating meant knowing which stack each image belonged to and
going to find it. Now each row has Update and the header has Update all.

Updating is per stack, because that is the unit compose deals in, so a row
acts on whichever stacks use that image — postgres:15-alpine is wikijs's,
postgres:17 is vikunja's — and Update all walks the affected stacks through a
queue one at a time, with each task's output on screen, rather than
recreating half the machine at once. A stack that finishes says "updated just
now" rather than re-checking, since every check is a registry request and
Docker Hub counts them.

Also: cron expressions read as English. "daily at 3:15am", with 15 3 * * *
kept beside it. Jobs.tsx had a smaller version of this for its own list; it
now uses the shared one, which handles lists, ranges, steps, names,
weekdays/weekends, midnight and noon, and says nothing at all when it is not
certain — the caller falls back to the expression.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-26 12:49:32 -07:00
Mitchell-JohnsonandClaude Opus 5 03e0761f3e Hardware: a Motherboard card
The board, the chassis and the firmware on it, which the System card was
reducing to two lines. Maker, model, version, serial and asset tag; chassis
and where the board sits in it; the firmware's vendor, version, revision,
date, ROM size and whether the machine boots UEFI — that last one is a line
in SMBIOS's characteristics list rather than a field, so the parser now keeps
those lists too.

Expansion slots come with it, with the device found at each occupied slot's
PCI address rather than the address alone. They are only shown when the
firmware tracks occupancy at all: core's NucBox lists five PCIe x1 slots it
does not physically have, all "Available", copied out of a reference design.
grid's board reports real addresses and real occupancy, which is the case
worth printing.

A machine with no SMBIOS gets a card that says so.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-25 22:21:27 -07:00
Mitchell-JohnsonandClaude Opus 5 38039d1adc Hardware sits under KVM in the rail
Mitchell's placement. The rail order is the account's anyway — this is only
where a page starts for someone who has never dragged it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-25 22:02:56 -07:00