From 62605d57eb2cd44a2274a655db295824e16c2511 Mon Sep 17 00:00:00 2001 From: Mitchell-Johnson Date: Sat, 19 Sep 2026 08:58:36 -0700 Subject: [PATCH] Widgets tab links the external demo when config.json names one (demo_url) Co-Authored-By: Claude Fable 5.1 --- examples/external-demo/README.md | 3 +++ internal/api/server.go | 4 +++- internal/config/config.go | 3 +++ web/src/Gallery.tsx | 8 ++++++-- web/src/Shell.tsx | 2 +- web/src/api.ts | 2 +- web/src/styles.css | 1 + 7 files changed, 18 insertions(+), 5 deletions(-) diff --git a/examples/external-demo/README.md b/examples/external-demo/README.md index 52f4890..0064957 100644 --- a/examples/external-demo/README.md +++ b/examples/external-demo/README.md @@ -15,3 +15,6 @@ Anyone who can load the page can read the token, so serve it only where that's a (it can read sensors and nothing else). Deployed on `core` as the `hamlab-demo` stack — 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` +and restart Ham Lab. diff --git a/internal/api/server.go b/internal/api/server.go index 2380216..0623b22 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -736,10 +736,12 @@ func (s *Server) me(w http.ResponseWriter, _ *http.Request) { } } _, rebootErr := os.Stat("/run/reboot-required") + var demoURL string + s.cfg.View(func(c *config.Config) { demoURL = c.DemoURL }) jsonOK(w, 200, map[string]any{ "host": s.host, "user": s.user, "version": s.version, "load": load, "uptime": uptime, "service_started": s.started, - "reboot_needed": rebootErr == nil, "fingerprint": s.fingerprint, "arch": runtime.GOARCH, + "reboot_needed": rebootErr == nil, "fingerprint": s.fingerprint, "arch": runtime.GOARCH, "demo_url": demoURL, }) } diff --git a/internal/config/config.go b/internal/config/config.go index 89a3135..8bd425d 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -44,6 +44,9 @@ type Config struct { TLS string `json:"tls,omitempty"` // "auto" → self-signed cert in the data dir; "" → plain HTTP Tokens []Token `json:"tokens,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"` path string mu sync.Mutex diff --git a/web/src/Gallery.tsx b/web/src/Gallery.tsx index e2a0500..093f2df 100644 --- a/web/src/Gallery.tsx +++ b/web/src/Gallery.tsx @@ -31,7 +31,7 @@ function Tile({ spec, data, note }: { spec: WidgetSpec; data: WidgetData; note: ) } -export default function Gallery() { +export default function Gallery({ demoUrl }: { demoUrl?: string }) { const [cat, setCat] = useState(null) const [err, setErr] = useState('') const rate = useRefreshMs() @@ -54,7 +54,11 @@ export default function Gallery() { return (
-
{sensors.length} sensors on this machine · live · tap a tile to copy its ID
+
+ {sensors.length} sensors on this machine · live · tap a tile to copy its ID + + {demoUrl && External demo ↗} +

Widget types

diff --git a/web/src/Shell.tsx b/web/src/Shell.tsx index 93a2951..ad92648 100644 --- a/web/src/Shell.tsx +++ b/web/src/Shell.tsx @@ -179,7 +179,7 @@ export default function Shell({ me: hubMe, onLoggedOut }: { me: Me; onLoggedOut: {view === 'settings' && } {view === 'machines' && { switchMachine(n); setView('dashboard') }} />} {view === 'dashboard' && } - {view === 'widgets' && } + {view === 'widgets' && } {view === 'procs' && } {view === 'services' && } {view === 'cron' && } diff --git a/web/src/api.ts b/web/src/api.ts index 1d545da..2bd1a2c 100644 --- a/web/src/api.ts +++ b/web/src/api.ts @@ -51,7 +51,7 @@ async function req(method: string, path: string, body?: unknown, local = fals return r.json() } -export interface Me { host: string; user: string; version: string; load: string; uptime: number; reboot_needed?: boolean; fingerprint?: string; arch?: string } +export interface Me { demo_url?: string; host: string; user: 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 MachineStatus { name: string; url: string; fingerprint?: string; added: string; online: boolean; host?: string; version?: string; load?: string; uptime?: number; reboot_needed?: boolean; last_seen?: string; error?: string } export interface Term { id: string; name: string; running: boolean; exit_code: number; created: string; clients: number } diff --git a/web/src/styles.css b/web/src/styles.css index ab29c44..37a6320 100644 --- a/web/src/styles.css +++ b/web/src/styles.css @@ -427,3 +427,4 @@ h2.section { margin: 0; font-size: 13px; font-weight: 600; color: var(--muted); .gallery-tile .value { font-size: 18px; overflow-wrap: anywhere; } .gallery-id { margin-top: auto; padding-top: 8px; font-size: 10.5px; color: var(--muted); overflow-wrap: anywhere; } @media (max-width: 800px) { .gallery .kpis { grid-template-columns: repeat(2, minmax(0, 1fr)); } } +a.btn { text-decoration: none; display: inline-flex; align-items: center; }