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
9 changed files with 21 additions and 35 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"
+7 -3
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.
@@ -406,8 +411,7 @@ something long in one, switch away and back, refresh — it's all still there.
still default to private. No token is needed by anything any more (the read-only one was 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 revoked; only `hamlab-publish`, which creates releases, remains). Gitea resolves
`releases/download/latest/<asset>`, so `install.sh` and `HAMLAB_URL` now point straight `releases/download/latest/<asset>`, so `install.sh` and `HAMLAB_URL` now point straight
at the newest release and never need changing; the rolling copy on at the newest release and never need changing. What is public is the source, the history,
`hamlab-demo.bigham.dev` stays as a mirror. What is public is the source, the history,
`BUGS.md` and this file — which describes the lab and names the console's hostname. `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.
+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.
+1 -1
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
+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