Commit Graph
31 Commits
Author SHA1 Message Date
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 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 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 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 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 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 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 f17a365ea1 stacks: remember where a container's bind mounts pointed
Every stack on core and grid is labelled with a path inside Dockhand's
filesystem — /app/data/stacks/… — because Dockhand ran compose from inside its
own container. Ham Lab resolves those by walking the bind mounts of running
containers, which works right up until the container doing the mounting is
removed. Retiring Dockhand would, in one step, make seventeen stacks on core
and five on grid unfindable.

So the mounts are written down as they are seen, and a path no live container
explains is looked for in what was seen before. A remembered mount resolves
with no container to run in, which is the right answer: compose runs on the
host, against the host's own copy of the file.

The same problem bites a scheduled update, which captures how to invoke
compose when it is made so the cron job never has to rediscover the world at
3am. repoint() checks the named container is still running and falls back the
same way if it is not — and pointedly leaves the spec alone if nothing
explains the path, so it fails loudly rather than deploying a guess.

Out in cron the record is read and never added to: the daemon owns state.json
and would clobber anything written from there.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-24 20:19:54 -07:00
Mitchell-JohnsonandClaude Opus 5 192504ead3 docker: the eight things Dockhand did that Ham Lab did not
Dockhand is being retired, so the Docker tab has to cover what it actually
earned its keep doing. Eight pieces, all per container or per machine:

- Stats: CPU, memory and IO over time, from sensors already collected.
- Health: the container's own healthcheck, with the last few probe outputs —
  which is where a failing check says why.
- Events: `docker events` keeps a minute of backlog and nothing more, so Ham
  Lab follows the stream and keeps a ring of its own. A restart at 4am is
  still there at 9.
- Shell: `docker exec` into a container, in the terminal already here.
- Limits: memory, CPU and restart policy changed live, without hand-editing
  the compose file for a number you want to try.
- Updates: what the registry has that this machine does not, for every image
  at once rather than one stack at a time.
- The same across every machine, asked of the hub — which is the only thing
  that can reach them all.
- Schedules: a real line in /etc/cron.d/hamlab-docker calling the binary,
  in the same shape as the ZFS snapshot schedules. Dockhand updated DNS and
  ingress nightly from a ticker inside a web app; a cron line keeps running
  when the console does not, and is visible to anyone reading the machine the
  normal way.

internal/compose is new and shared, so a deploy from the console and a deploy
from cron invoke compose identically — including the case where compose has to
run inside the orchestrator's own container, where the paths are that
container's and not the host's.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-24 20:11:36 -07:00
Mitchell-JohnsonandClaude Opus 5 09b7558d44 Snapshot schedules are real cron jobs, written and owned by Ham Lab
A schedule is now a line in /etc/cron.d/hamlab-zfs, which Ham Lab owns and rewrites whole
whenever the schedules change; the line calls `hamlab zfs-snapshot <id>` as the account Ham
Lab runs as, reaching zfs through sudo exactly as the daemon does. The daemon's own ticker
is gone. Snapshots are backup-shaped and should not stop because a web console is down,
being upgraded, or uninstalled — and a cron line is greppable by anyone looking at the
machine the normal way, which a schedule hidden inside a daemon is not.

A cron expression is now the single description of when a schedule fires: the four common
shapes render to one, a custom schedule states it outright, and the next run is read back
with the same evaluator the Jobs tab uses on everyone else's lines. So the page, the file
and the prediction cannot disagree.

Schedules gain a custom cron expression and an optional snapshot name prefix. What a run
did moves out of the policy into zfs-runs/<id>.json beside the config, because the cron job
is a separate process and must never write state.json behind the daemon's back.

The detected external job can be adopted: its schedule, retention and dataset are read out
of the cron line into a new Ham Lab schedule, ready to check rather than retype.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-24 13:09:43 -07:00
Mitchell-JohnsonandClaude Fable 5.1 53d1da08a5 Space keeps its scans and refreshes them: zfs diff, folder mtimes, resume after cancel
A scan is saved beside the config and read back after a restart. The next scan starts
from it: on a pool with snapshots zfs diff names what changed and only those folders are
read; elsewhere a folder whose mtime is unchanged keeps its list and only its kept files
are stat'ed again. A cancelled scan keeps what it walked and Resume reads the rest.
Forget drops the saved result. The treemap is every file a tile, grouped by folder, with
no nested boxes: the folder chains of a Linux system made a staircase of empty headers.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-23 00:43:30 -07:00
Mitchell-JohnsonandClaude Fable 5.1 fc5236471e Space: what is using a disk, WizTree-style
Pick a disk, not a partition; the scan walks every filesystem it backs, as root when
the host allows, in a helper process that streams the tree back. A tree of folders
largest-first over a treemap of the selected folder, nested to the file and coloured by
kind, plus the thousand largest files. The rail no longer offers the hub-only pages
while a remote machine is being looked at.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-22 20:18:36 -07:00
Mitchell-JohnsonandClaude Fable 5.1 2b54a06821 Users, with their own settings, and a list of where each is signed in
The login is a user name and a password. The old single password becomes the account
named after the OS user on first boot, along with its sessions and the machine-wide look.
Settings gains Users (add, remove, reset a password — your own password confirms) and
Sessions (device, IP, last seen; revoke one or sign out everywhere else). Theme, accents
and the dashboard layout follow the account; the refresh rate, graph history and alerts
stay the machine's. Pairing a machine takes a user on it. Log lines name the actor.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-22 18:20:30 -07:00
Mitchell-JohnsonandClaude Fable 5.1 488d929733 Portable install: the binary installs itself; static builds on five architectures; root via sudo, doas or uid 0; every package manager; older systemd; no lsblk
- hamlab install / uninstall (internal/setup): systemd or OpenRC, elevates through
  sudo or doas, runs as the invoking user, sets or generates the password, names only
  journal groups that exist, waits for /healthz, reports an active firewalld/ufw.
  --purge and the data chown never touch a directory that isn't Ham Lab's alone.
- install.sh: curl-able bootstrap (arch detection, SHA256SUMS, offline beside the
  binaries, noexec /tmp fallback). make release builds dist/ for amd64, arm64, arm
  (ARMv6), 386, riscv64. Every build is CGO_ENABLED=0; the native one used to link glibc.
- internal/priv replaces every literal `sudo -n`: uid 0, sudo or doas.
- Updates acts on dnf, zypper, pacman and apk as well as apt; dnf shows installed versions.
- Services falls back to the plain systemctl table before systemd 246 and says so on
  hosts without systemd. Disks falls back to /sys/block without lsblk.
- Listener binds with IP_FREEBIND, so a LAN address that isn't up at boot is no failure.
- Self-update: the new build must pass `check` against the live config, is put in
  place by atomic rename, and the daemon re-execs itself (same under any supervisor).
- State and config writes are fsynced; a corrupt state.json is set aside, not fatal.
- Terminal shutdown no longer empties the saved list, so names come back after a
  restart by design rather than by winning a race.
- Config default is ~/.config/hamlab; an existing ~/apps/hamlab is still found.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-18 23:00:20 -07:00
Mitchell-JohnsonandClaude Fable 5.1 1ee4673d7c Don't resurrect exited terminals on daemon restart
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-18 17:53:29 -07:00
Mitchell-JohnsonandClaude Fable 5.1 c1805f6ad5 Bug 14: stream journal reads instead of buffering them; soft memory limit for the GC
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-18 17:51:09 -07:00
Mitchell-JohnsonandClaude Fable 5.1 f093b322ee Bug bash fixes: hash routing + back button, hub-only pages reset on machine switch, exited terminals reap after 10 min, self-update refuses while busy unless forced, touch-sized controls, power confirm by machine name, down interfaces collapsed and empty modules hidden, load label, offline backoff, cron log newest first, procs empty message, timer timestamp parsing
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-18 17:46:16 -07:00
Mitchell-JohnsonandClaude Fable 5.1 254e0cdd58 Update from hub: node self-update endpoint, hub push per machine / all, push-nodes CLI run by make deploy, outdated badges + Update buttons on Machines
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-18 12:04:31 -07:00
Mitchell-JohnsonandClaude Fable 5.1 7dcdb5dc98 Multi-host phase 1: API tokens, auto TLS with pinned fingerprints, hub-side node manager + reverse proxy (WebSockets included), machine switcher in the rail, Machines page
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-18 11:35:29 -07:00
Mitchell-JohnsonandClaude Fable 5.1 63b020d556 Files tab: browser with breadcrumbs/sort/filter, text editor, image preview, streamed upload with progress, download, mkdir/touch/rename/move/chmod/delete, open-in-terminal; root toggle via sudo-reexecuted 'hamlab fs' helper
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-18 05:39:57 -07:00
Mitchell-JohnsonandClaude Fable 5.1 c704e87065 Disks tab: lsblk devices with partitions/usage/live IO, SMART via smartctl (standby-aware, self-tests, install button), ZFS pools/datasets/snapshots with scrub and snapshot-now
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-17 22:48:08 -07:00
Mitchell-JohnsonandClaude Fable 5.1 acb11193b9 Updates tab (apt list/refresh/upgrade/changelog, reboot flag) on a new background tasks package with live WS streaming; header power button with hostname confirmation
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-17 17:40:29 -07:00
Mitchell-JohnsonandClaude Fable 5.1 41c2f05535 Cron tab: crontabs, cron.d, user crontabs, run-parts dirs and systemd timers in one list; next/last run, run now, enable/disable, in-place editing, cron log and output file
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-17 16:21:35 -07:00
Mitchell-JohnsonandClaude Fable 5.1 bc891b1042 Services tab: systemd unit list/filter, detail with start/stop/restart/reload/enable/disable, live journal tail over WS, unit file viewer
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-17 15:35:03 -07:00
Mitchell-JohnsonandClaude Fable 5.1 eb70daf129 Dashboard customize mode: reorder/hide modules and individual rows, layout saved server-side (GET/PUT /api/prefs/dashboard)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-17 13:46:41 -07:00
Mitchell-JohnsonandClaude Fable 5.1 7053cce7fe Portability: sysfs-only sensors with thermal-zone fallback, /sys/block disk discovery, fs blacklist, pwrstat/NUT/apcupsd UPS, login shell from passwd, templated unit + install for any user, cross-compile targets
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-17 13:43:44 -07:00
Mitchell-JohnsonandClaude Fable 5.1 3c6f4d4639 Dashboard tab: CPU/mem/load, hwmon temps+fans, net, disks, filesystems, UPS with 10-min sparklines
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-17 13:37:39 -07:00
Mitchell-JohnsonandClaude Fable 5.1 d76da4b37a Rename: Hamlet -> Ham Lab (binary/module/service/config hamlab)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-17 10:40:40 -07:00
Mitchell-JohnsonandClaude Fable 5.1 436732dac0 MVP: Go daemon, password auth, persistent PTY terminals, React/xterm UI, PWA
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-17 10:25:35 -07:00