The setup pages build commands that get pasted into a terminal on a *different*
machine, but they built them from location.origin. Open the console by LAN IP or
over a tunnel and the enrolment one-liner told the target machine to curl an
address it may not resolve at all.
/docs and /enroll/:token are now served through a template that stamps in
config.baseUrl — PUBLIC_URL where set, the proxied request host otherwise — with
location.origin left as a fallback for the unsubstituted file. The remaining
127.0.0.1 references are the loopback VNC port on the machine being registered,
which is meant to be literal.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Registering a machine meant reading a multi-step page, installing Node, and
running three commands in the right order. Now it is one line per platform.
The hub serves scripts/install.sh and scripts/install.ps1 with its own address
and the enrolment token substituted in, so the published command carries
everything and there is nothing to fill in:
curl -fsSL https://support.freakma.com/install.sh?token=TOKEN | sh
irm https://support.freakma.com/install.ps1?token=TOKEN | iex
That output is piped straight into a shell, so the token — the only untrusted
value in either file — is refused unless it matches the base64url shape that
randomToken produces.
Each script checks for a usable runtime and stops with instructions rather than
guessing, warns when nothing is serving RFB on the loopback, installs per-user
with no root or administrator, and registers a login-scoped service: launchd on
macOS, a lingering systemd user service on Linux, a logon task on Windows. The
Windows script uses Node 22 when it is present and falls back to the bundled
executable otherwise, which is what lets one command cover both Windows 11 and
Server 2008 R2.
It registers a logon task rather than a service on purpose: services run in
session 0 and cannot draw on the interactive desktop, so the "ask first" consent
prompt would never appear.
Also adds /docs — a per-OS setup guide with service management and a
troubleshooting table — and reworks the enrolment page into OS tabs that open on
whichever platform the reader is sitting at.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Machines that cannot install Node 22 had no way to run the agent at all, which
left them stuck on direct mode — and direct mode only works when the hub can
route to the VNC port, which it often cannot.
agent.js now resolves the two Node 22 globals it uses through fallbacks: `ws`
for the control and tunnel sockets, and http/https for the single enrolment
POST. Node 22 loads neither, since `globalThis.WebSocket || require('ws')`
short-circuits. The require and the http/https references are static so the
bundler can follow them.
A new Docker stage bundles that file with a Node 12 runtime — the last line
supporting Windows 7 and Server 2008 R2 — into one self-contained .exe, served
from /download/agent.exe and linked from the enrolment page. The route answers
503 rather than 404 in a dev checkout, where the build has not run.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
express.json() raising a parse error was falling through to the generic
500 handler, which reads as a server fault for what is a bad request.
Also maps the body-size limit to 413.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The first real deploy failed with "port '8091' is already in use by
service 'ai-training-lab_ai-lab' as an ingress port". Worth noting the
port answered nothing when probed over HTTP — a Swarm ingress
reservation survives the service behind it being down, so probing is not
a way to find a free port.
5910 is VNC display :10 and sits clear of the 809x web-app block.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Reworks the console's look away from generic dark-mode defaults: the
neutral ramp carries a faint violet cast, and the signal palette now
distinguishes connection topology by colour — mint for live agents,
iris for direct connections, copper for consent-required machines.
Type contrast comes from pairing the platform mono against the platform
sans rather than a webfont, since the box this runs on has no internet.
particles.js draws a proximity-linked node field: full strength on the
login and public pages, frozen static in the console, and entirely
absent from the viewer, where every frame belongs to the remote desktop.
It caps DPR at 2, scales node count to viewport area, throttles to 30fps,
cancels rAF when the tab is hidden, and honours prefers-reduced-motion.
app.js and viewer.js are unchanged; the particle layer mounts itself and
tracks the console via MutationObserver on #app.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Browser-based remote control (noVNC) with invite links, per-user access
control, garagedoor SSO and a persisted client list.
The hub proxies RFB rather than pointing the browser at a VNC server. That
is what lets it authenticate upstream with a stored password the browser
never sees, and enforce view-only by dropping input messages on the
client->server stream instead of hiding buttons.
Machines are reachable two ways: direct TCP for LAN hosts, or an outbound
agent tunnel for anything behind NAT. Node 22's global WebSocket keeps the
agent dependency-free, and node:sqlite keeps the image free of native
builds.
Ships with an end-to-end suite that boots the real server against a fake
VNC server and a fake auth service (72 assertions), plus Gitea Actions
CI/CD to Portainer.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>