Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
363ff02bda | ||
|
|
b5454c1022 |
@@ -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"
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
@@ -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 ──
|
||||||
|
|||||||
@@ -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,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user