Author SHA1 Message Date
Mitchell-JohnsonandClaude Opus 5 363ff02bda Drop the external demo: the Widgets tab is the demo
The hamlab-demo site, its Widgets-tab button, the demo_url config key and the make publish
target that fed it are all gone, along with the public read-only token it needed. The
example stays in examples/external-demo as something to read. The installer one-liner
already comes from Gitea releases.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-23 20:17:22 -07:00
Mitchell-JohnsonandClaude Opus 5 b5454c1022 Public downloads: the installer and the updater come from Gitea releases, no token
REQUIRE_SIGNIN_VIEW is off and mitchell/hamlab is public, so anyone can clone it or pull
a release. The other five repos are individually private and 404 to anonymous. Gitea
resolves releases/download/latest/<asset>, so the install one-liner and HAMLAB_URL point
at the newest release and never need changing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-23 18:59:42 -07:00
10 changed files with 38 additions and 60 deletions
+2 -15
View File
@@ -2,7 +2,6 @@
VERSION ?= $(shell git describe --tags --always 2>/dev/null || echo dev) VERSION ?= $(shell git describe --tags --always 2>/dev/null || echo dev)
GO ?= go GO ?= go
PREFIX ?= /usr/local/bin PREFIX ?= /usr/local/bin
PUBLISH ?= /home/core/apps/hamlab-demo/site
# every architecture a release carries; arm is ARMv6 hard-float, so it covers every Raspberry Pi # every architecture a release carries; arm is ARMv6 hard-float, so it covers every Raspberry Pi
ARCHES ?= amd64 arm64 arm 386 riscv64 ARCHES ?= amd64 arm64 arm 386 riscv64
@@ -10,7 +9,7 @@ LDFLAGS = -s -w -X main.version=$(VERSION)
# static, no cgo: the same file runs on glibc, musl (Alpine) and anything else with a Linux kernel # static, no cgo: the same file runs on glibc, musl (Alpine) and anything else with a Linux kernel
GOBUILD = CGO_ENABLED=0 GOOS=linux GOARM=6 $(GO) build -trimpath -ldflags "$(LDFLAGS)" GOBUILD = CGO_ENABLED=0 GOOS=linux GOARM=6 $(GO) build -trimpath -ldflags "$(LDFLAGS)"
.PHONY: all web build go dev cross release publish gitea-release deploy push-nodes push-nodes-force test clean .PHONY: all web build go dev cross release gitea-release deploy push-nodes push-nodes-force test clean
all: build all: build
@@ -61,9 +60,7 @@ gitea-release: release
deploy: build cross deploy: build cross
./bin/hamlab install --prefix $(PREFIX) ./bin/hamlab install --prefix $(PREFIX)
@sleep 1; $(PREFIX)/hamlab push-nodes || true @sleep 1; $(PREFIX)/hamlab push-nodes || true
@# The public installer must never lag the fleet: it would offer everyone an older build, and @echo "deployed $(VERSION); 'make gitea-release' publishes it for everyone else"
@# "upgrade" a current machine backwards. Publish only where this hub serves the download site.
@if [ -d $(PUBLISH) ]; then $(MAKE) --no-print-directory publish; fi
# push the installed binary to every managed machine (what deploy does last) # push the installed binary to every managed machine (what deploy does last)
push-nodes: push-nodes:
@@ -75,13 +72,3 @@ push-nodes-force:
clean: clean:
rm -rf bin dist internal/web/dist web/dist rm -rf bin dist internal/web/dist web/dist
# Put the release where `curl -fsSL https://hamlab-demo.bigham.dev/install.sh | sh` finds it:
# the static site beside the demo page (nginx, public through NPM). Binaries first, VERSION
# last, so nobody is ever pointed at files that aren't there yet.
publish: release
mkdir -p $(PUBLISH)/dl
cp dist/hamlab-linux-* dist/SHA256SUMS $(PUBLISH)/dl/
cp dist/install.sh $(PUBLISH)/dl/install.sh && cp dist/install.sh $(PUBLISH)/install.sh
cp dist/VERSION $(PUBLISH)/dl/VERSION
@echo "published $(VERSION): curl -fsSL https://hamlab-demo.bigham.dev/install.sh | sh"
+16 -23
View File
@@ -186,7 +186,12 @@ something long in one, switch away and back, refresh — it's all still there.
a URL), status sensors (services, updates, ZFS, SMART), a widget library, custom widgets a URL), status sensors (services, updates, ZFS, SMART), a widget library, custom widgets
on the Dashboard tab (briefly renamed "Monitors" 2026-09-19, back to "Dashboard" on the Dashboard tab (briefly renamed "Monitors" 2026-09-19, back to "Dashboard"
2026-09-20 at Mitchell's request; the view id and URL were always `dashboard`), and a 2026-09-20 at Mitchell's request; the view id and URL were always `dashboard`), and a
frameable `/embed/widget` for Glance and friends. The stock Dashboard modules and the Disks frameable `/embed/widget` for Glance and friends. (An external demo page was deployed at
`hamlab-demo.bigham.dev` and linked from the Widgets tab 2026-09-19; **both removed
2026-09-23** at Mitchell's request — the Widgets tab shows the same thing against the
machine you are signed in to, so the second copy earned nothing and cost a public
read-only token, which was revoked with it. `examples/external-demo/` stays as an example
to read.) The stock Dashboard modules and the Disks
tab read the sensor API too (`sampleAdapter.ts`); `/api/metrics` is retired. tab read the sensor API too (`sampleAdapter.ts`); `/api/metrics` is retired.
Also per-service and per-container sensors (memory and CPU from cgroups), tokens limited Also per-service and per-container sensors (memory and CPU from cgroups), tokens limited
to ID patterns, and `/api/fleet/sensors/query` for the whole fleet in one request. to ID patterns, and `/api/fleet/sensors/query` for the whole fleet in one request.
@@ -398,6 +403,16 @@ something long in one, switch away and back, refresh — it's all still there.
**not** part of `make deploy`, because deploy pushes this build to the machines this hub **not** part of `make deploy`, because deploy pushes this build to the machines this hub
owns while publishing offers it to every install anywhere, and one tag per deploy would owns while publishing offers it to every install anywhere, and one tag per deploy would
be noise. Re-publishing a version needs `--force`. be noise. Re-publishing a version needs `--force`.
**Public since 2026-09-23**, at Mitchell's instruction and after the trade was put to him
twice: Gitea's instance-wide `REQUIRE_SIGNIN_VIEW` is off and `mitchell/hamlab` is marked
public, so anyone can browse it, clone it and download a release with no account. The
other five repos there are each marked private and are invisible — an anonymous request
for one is a 404, not a login prompt — registration is still disabled, and new repos
still default to private. No token is needed by anything any more (the read-only one was
revoked; only `hamlab-publish`, which creates releases, remains). Gitea resolves
`releases/download/latest/<asset>`, so `install.sh` and `HAMLAB_URL` now point straight
at the newest release and never need changing. What is public is the source, the history,
`BUGS.md` and this file — which describes the lab and names the console's hostname.
- **Auth hardening** — WebAuthn passkeys (phone Face ID), TOTP fallback, optional NPM - **Auth hardening** — WebAuthn passkeys (phone Face ID), TOTP fallback, optional NPM
access list restricting to WireGuard/LAN. access list restricting to WireGuard/LAN.
- **Files tab** — **shipped 2026-09-18** (`internal/files`, `Files.tsx`): browse from - **Files tab** — **shipped 2026-09-18** (`internal/files`, `Files.tsx`): browse from
@@ -426,28 +441,6 @@ something long in one, switch away and back, refresh — it's all still there.
## To do ## To do
- **Somewhere public to download from** — asked for 2026-09-22, half done. The **updater**
shipped 2026-09-23 (above) and reads releases from Gitea, but `git.bigham.dev` runs with
`REQUIRE_SIGNIN_VIEW=true`, so those releases are reachable only with a token. That is
fine for this fleet — each machine carries a read-only `read:repository` token — and no
use at all to a stranger running the `install.sh` one-liner, who still gets the rolling
copy on `hamlab-demo.bigham.dev`. What is left is deciding where a public download
actually lives:
- **Lower the wall on Gitea.** `REQUIRE_SIGNIN_VIEW=false` and per-repo visibility does
the rest: every repo there is private and stays hidden, and only a repo marked public
is visible. That is how Gitea is meant to be configured, and it is the one change that
makes "releases on my Gitea" literally true. It is an instance-wide posture change on
a public hostname, so it is Mitchell's call — the same trade was declined once before
(`docs/docker.md`, the demo site's `install.sh` is hosted outside Gitea for exactly
this reason).
- **Or keep the wall and keep the demo site** as the public face: `make publish` already
writes the rolling `dl/` directory there, and a generated index page (version, built
when, a row per architecture with size and checksum, the one-line install, and that
the binaries are unsigned and served from his own box) would finish the "page" half
without touching Gitea at all.
- Either way the release notes and version history live on Gitea, and the updater does
not care which of the two it is pointed at.
- **Deprecate copyparty** — asked for 2026-09-23, not started. `copyparty` on `grid` - **Deprecate copyparty** — asked for 2026-09-23, not started. `copyparty` on `grid`
(`copyparty.service`, `:3923`, `/etc/copyparty.conf`, serving all of `/tank` as user (`copyparty.service`, `:3923`, `/etc/copyparty.conf`, serving all of `/tank` as user
`core` with one account behind NPM) overlaps the **Files** tab almost entirely. The aim `core` with one account behind NPM) overlaps the **Files** tab almost entirely. The aim
+5 -4
View File
@@ -20,15 +20,16 @@ sudo ./hamlab-linux-amd64 install # that's the whole install
or, from wherever `make release` output is served: or, from wherever `make release` output is served:
```sh ```sh
curl -fsSL https://hamlab-demo.bigham.dev/install.sh | sh # asks: user, address, HTTPS, firewall, password curl -fsSL https://git.bigham.dev/mitchell/hamlab/releases/download/latest/install.sh | sh # asks: user, address, HTTPS, firewall, password
curl -fsSL https://hamlab-demo.bigham.dev/install.sh | sh -s -- --yes # asks nothing, all defaults curl -fsSL https://git.bigham.dev/mitchell/hamlab/releases/download/latest/install.sh | sh -s -- --yes # asks nothing, all defaults
curl -fsSL https://hamlab-demo.bigham.dev/install.sh | sh -s -- --listen 0.0.0.0:8100 --tls auto # flags go to `hamlab install` curl -fsSL https://git.bigham.dev/mitchell/hamlab/releases/download/latest/install.sh | sh -s -- --listen 0.0.0.0:8100 --tls auto # flags go to `hamlab install`
``` ```
`install.sh` is a wizard when there is a terminal (it reads answers from `/dev/tty`, so it `install.sh` is a wizard when there is a terminal (it reads answers from `/dev/tty`, so it
works piped from curl) and silent otherwise. Either way it picks the build for `uname -m`, works piped from curl) and silent otherwise. Either way it picks the build for `uname -m`,
checks it against `SHA256SUMS` and runs `hamlab install`; re-running it upgrades in place. checks it against `SHA256SUMS` and runs `hamlab install`; re-running it upgrades in place.
`make publish` puts a release where that URL serves it (`HAMLAB_URL` points it elsewhere); run it beside the `hamlab-linux-*` files and it installs offline. `latest` is an alias Gitea resolves to the newest release, so that URL never changes.
`make gitea-release` publishes a release there (`HAMLAB_URL` points the installer elsewhere); run the script beside the `hamlab-linux-*` files and it installs offline.
`hamlab install`: `hamlab install`:
- copies the binary to `/usr/local/bin` (`--prefix`), atomically; - copies the binary to `/usr/local/bin` (`--prefix`), atomically;
+6 -6
View File
@@ -12,10 +12,10 @@ window.HAMLAB = { base: 'http://10.0.0.234:8100', token: '<read-only sensor toke
``` ```
Anyone who can load the page can read the token. If the page is public, limit the token to Anyone who can load the page can read the token. If the page is public, limit the token to
patterns that name nothing you'd mind the world reading (the `hamlab-demo` one shows patterns that name nothing you'd mind the world reading — hardware numbers and counts,
hardware numbers and counts, not container or service names). Deployed on `core` as the `hamlab-demo` stack — not container or service names.
see Hamlet `docs/docker.md`. Update it with
`cp examples/external-demo/index.html /home/core/apps/hamlab-demo/site/`.
To link it from the Widgets tab, set `"demo_url": "http://…"` in the hub's `config.json` This is an example to read and copy, not something that runs anywhere. It was deployed on
and restart Ham Lab. `core` as the `hamlab-demo` stack and linked from the Widgets tab until 2026-09-23, when
both were removed: the Widgets tab shows the same widgets against the machine you are
already signed in to, so a second public copy earned nothing and cost a public token.
+4 -2
View File
@@ -1,7 +1,7 @@
#!/bin/sh #!/bin/sh
# Ham Lab installer. # Ham Lab installer.
# #
# curl -fsSL https://hamlab-demo.bigham.dev/install.sh | sh # curl -fsSL https://git.bigham.dev/mitchell/hamlab/releases/download/latest/install.sh | sh
# #
# With a terminal it walks you through it: who the console runs as, where it listens, # With a terminal it walks you through it: who the console runs as, where it listens,
# HTTPS, the firewall, the password — then downloads the build for this machine, checks # HTTPS, the firewall, the password — then downloads the build for this machine, checks
@@ -15,6 +15,8 @@
# ./install.sh # beside the hamlab-linux-* files: installs offline # ./install.sh # beside the hamlab-linux-* files: installs offline
# #
# HAMLAB_URL where hamlab-linux-<arch>, SHA256SUMS and VERSION are served from — # HAMLAB_URL where hamlab-linux-<arch>, SHA256SUMS and VERSION are served from —
# by default the newest release on Gitea; "latest" is an alias Gitea
# resolves to the most recent release, so the URL never needs changing —
# any static file host, or a Gitea/Forgejo release's download URL # any static file host, or a Gitea/Forgejo release's download URL
# HAMLAB_TOKEN access token, when that place is a private Gitea/Forgejo repo # HAMLAB_TOKEN access token, when that place is a private Gitea/Forgejo repo
# NO_COLOR plain output # NO_COLOR plain output
@@ -22,7 +24,7 @@
# POSIX sh: runs under dash, busybox ash and bash alike. # POSIX sh: runs under dash, busybox ash and bash alike.
set -eu set -eu
HAMLAB_URL="${HAMLAB_URL:-https://hamlab-demo.bigham.dev/dl}" HAMLAB_URL="${HAMLAB_URL:-https://git.bigham.dev/mitchell/hamlab/releases/download/latest}"
BUILT_FOR="@VERSION@" # filled in by `make release` BUILT_FOR="@VERSION@" # filled in by `make release`
# ─────────────────────────────────────────────────────────────── looks ── # ─────────────────────────────────────────────────────────────── looks ──
+2 -3
View File
@@ -1013,9 +1013,8 @@ func (s *Server) me(w http.ResponseWriter, r *http.Request) {
} }
_, rebootErr := os.Stat("/run/reboot-required") _, rebootErr := os.Stat("/run/reboot-required")
updateVersion, _ := s.behind() updateVersion, _ := s.behind()
var demoURL, addr string var addr string
s.cfg.View(func(c *config.Config) { s.cfg.View(func(c *config.Config) {
demoURL = c.DemoURL
if h, _, err := net.SplitHostPort(c.Listen); err == nil && h != "" && h != "0.0.0.0" && h != "::" { if h, _, err := net.SplitHostPort(c.Listen); err == nil && h != "" && h != "0.0.0.0" && h != "::" {
addr = h // the address this console listens on, for the machine menu addr = h // the address this console listens on, for the machine menu
} }
@@ -1023,7 +1022,7 @@ func (s *Server) me(w http.ResponseWriter, r *http.Request) {
jsonOK(w, 200, map[string]any{ jsonOK(w, 200, map[string]any{
"host": s.hostName(), "user": s.user, "account": actorOf(r).User, "version": s.version, "host": s.hostName(), "user": s.user, "account": actorOf(r).User, "version": s.version,
"load": load, "uptime": uptime, "service_started": s.started, "load": load, "uptime": uptime, "service_started": s.started,
"reboot_needed": rebootErr == nil, "fingerprint": s.fingerprint, "arch": runtime.GOARCH, "demo_url": demoURL, "addr": addr, "reboot_needed": rebootErr == nil, "fingerprint": s.fingerprint, "arch": runtime.GOARCH, "addr": addr,
"update_available": updateVersion, "update_available": updateVersion,
}) })
} }
-3
View File
@@ -70,9 +70,6 @@ type Config struct {
TLS string `json:"tls,omitempty"` // "auto" → self-signed cert in the data dir; "" → plain HTTP TLS string `json:"tls,omitempty"` // "auto" → self-signed cert in the data dir; "" → plain HTTP
Tokens []Token `json:"tokens,omitempty"` Tokens []Token `json:"tokens,omitempty"`
Nodes []Node `json:"nodes,omitempty"` Nodes []Node `json:"nodes,omitempty"`
// DemoURL, when set, is linked from the Widgets tab: a page outside Ham Lab that
// shows the sensor API being used (examples/external-demo).
DemoURL string `json:"demo_url,omitempty"`
// Updates: where a newer build comes from. See UpdateSource. // Updates: where a newer build comes from. See UpdateSource.
Updates UpdateSource `json:"update,omitempty"` Updates UpdateSource `json:"update,omitempty"`
+1 -2
View File
@@ -31,7 +31,7 @@ function Tile({ spec, data, note }: { spec: WidgetSpec; data: WidgetData; note:
) )
} }
export default function Gallery({ demoUrl }: { demoUrl?: string }) { export default function Gallery() {
const [cat, setCat] = useState<Sensor[] | null>(null) const [cat, setCat] = useState<Sensor[] | null>(null)
const [err, setErr] = useState('') const [err, setErr] = useState('')
const rate = useRefreshMs() const rate = useRefreshMs()
@@ -61,7 +61,6 @@ export default function Gallery({ demoUrl }: { demoUrl?: string }) {
<span className="hint">{needle ? `${shown.length} of ${sensors.length}` : sensors.length} sensors on this machine · live · tap a tile to copy its ID</span> <span className="hint">{needle ? `${shown.length} of ${sensors.length}` : sensors.length} sensors on this machine · live · tap a tile to copy its ID</span>
<input className="gallery-find" value={find} onChange={(e) => setFind(e.target.value)} placeholder="find — gitea, temp, enp3s0…" spellCheck={false} autoCapitalize="off" /> <input className="gallery-find" value={find} onChange={(e) => setFind(e.target.value)} placeholder="find — gitea, temp, enp3s0…" spellCheck={false} autoCapitalize="off" />
<span className="spacer" /> <span className="spacer" />
{demoUrl && <a className="btn" href={demoUrl} target="_blank" rel="noreferrer" title="a page outside Ham Lab, built on the same sensor API">External demo ↗</a>}
</div> </div>
{!needle && <h2 className="section">Widget types</h2>} {!needle && <h2 className="section">Widget types</h2>}
+1 -1
View File
@@ -269,7 +269,7 @@ export default function Shell({ me: hubMe, onLoggedOut }: { me: Me; onLoggedOut:
{view === 'space' && <Space key={machine ?? ''} />} {view === 'space' && <Space key={machine ?? ''} />}
{view === 'machines' && <Machines hub={hubMe} onChanged={loadMachines} onOpen={(n) => { switchMachine(n); setView('dashboard') }} />} {view === 'machines' && <Machines hub={hubMe} onChanged={loadMachines} onOpen={(n) => { switchMachine(n); setView('dashboard') }} />}
{view === 'dashboard' && <Dashboard />} {view === 'dashboard' && <Dashboard />}
{view === 'widgets' && <Gallery demoUrl={hubMe.demo_url} />} {view === 'widgets' && <Gallery />}
{view === 'docker' && <Docker />} {view === 'docker' && <Docker />}
{view === 'alerts' && <Alerts machines={machines.map((m) => m.name)} titleOf={(n) => titleOf(n === hubMe.host ? null : n, machines, hubMe.host)} />} {view === 'alerts' && <Alerts machines={machines.map((m) => m.name)} titleOf={(n) => titleOf(n === hubMe.host ? null : n, machines, hubMe.host)} />}
{view === 'top' && <Top />} {view === 'top' && <Top />}
+1 -1
View File
@@ -55,7 +55,7 @@ async function req<T>(method: string, path: string, body?: unknown, local = fals
} }
export interface UpdateStatus { current: string; latest: string; arch: string; behind: boolean; checking: boolean; installing: boolean; enabled: boolean; auto: boolean; url: string; notes?: string; published?: string; checked?: string; error?: string } export interface UpdateStatus { current: string; latest: string; arch: string; behind: boolean; checking: boolean; installing: boolean; enabled: boolean; auto: boolean; url: string; notes?: string; published?: string; checked?: string; error?: string }
export interface Me { demo_url?: string; addr?: string; host: string; user: string; account?: string; update_available?: string; version: string; load: string; uptime: number; reboot_needed?: boolean; fingerprint?: string; arch?: string } export interface Me { addr?: string; host: string; user: string; account?: string; update_available?: string; version: string; load: string; uptime: number; reboot_needed?: boolean; fingerprint?: string; arch?: string }
export interface PushResult { name: string; ok: boolean; version?: string; error?: string } export interface PushResult { name: string; ok: boolean; version?: string; error?: string }
// What a machine is called on screen: the hostname it reports, which follows a rename within // What a machine is called on screen: the hostname it reports, which follows a rename within
// a poll. The name it was added under stays its identity — the URL, its accent colour, and // a poll. The name it was added under stays its identity — the URL, its accent colour, and