7 Commits
Author SHA1 Message Date
octoturge 9e4b3e5315 Drive the first-run experience in a real browser
e2e-setup-browser.sh signs a user with no organisation into the real web
console, creates a farm, copies the gateway settings the page shows,
connects a stand-in gateway configured from exactly those settings, adds a
printer through the form, gets a failed-print alert and dismisses it, then
removes the printer. Afterwards it searches every table and the backend log
for the access code the browser typed. No printer is touched and dispatch
is off. The e2e scripts that start the real proxy also give it its own store
file in the scratch directory.
2026-10-29 15:15:06 +00:00
octoturge 519ada57f3 Give the real proxy the farm's proxy token, as the console will
The e2e scripts handed the proxy a pre-minted edge JWT, which hid that the
proxy could not exchange its configured token for one. It now does, so the
scripts configure it the way a real gateway is configured: with the farm's
edge proxy token.
2026-10-29 15:10:27 +00:00
octoturge 6a2457315e Add a switch to turn job dispatch off, and use it in the dev stack
The scheduler sends a queued job to any idle claimed printer in any farm.
The dashboard dev stack is wired to the real printers, so testing the job
upload UI there would have started a real print on the first idle one.

DISPATCH_ENABLED=false (exactly that string) disables the timer and the
nudge from POST /jobs; any other value that looks like an attempt to
disable it is warned about, because a typo that silently leaves it on is
the dangerous direction. scripts/e2e-camera.sh now always runs the backend
with it off.
2026-10-29 14:35:40 +00:00
octoturge 705336b422 Add SERVE=1: hold the whole stack up for working on the dashboard
Building a dashboard needs the backend, the proxy with its go2rtc, the
web console with hot reload and a seeded farm to stay up while it is
being edited, and e2e-camera.sh already boots exactly that. SERVE=1 skips
every check, seeds the user as owner (the dashboard has to command and
manage the org) with every printer in printers.toml, named as that file
names them, and holds until SIGTERM or Ctrl-C, when the usual cleanup
removes the farm, the processes and the sign-in file.

The password is written to a 0600 file under umask 077 and only the path
is printed. The web console binds loopback unless APP_HOST says
otherwise, because it serves a password sign-in.

Verified: signing in through the real form against the live stack shows
Live and both printers (the H2C live, the P1P honestly "offline, Never
reported" because it was switched off); SIGTERM leaves no process, port,
login file or seeded farm behind.
2026-10-29 10:17:54 +00:00
octoturge c72bfe71d6 Show the picture in the real web console, in a real browser
Every earlier camera proof stood in for the browser with a werift peer.
The question that decides whether the feature works is whether a person
looking at the fleet page sees a picture, and the web console had never
been opened.

BROWSER=1 on scripts/e2e-camera.sh now starts the real Nuxt app and
drives it in real Chrome (puppeteer-core): sign in, open the fleet page,
wait for the camera player, and check the result. Measured against the
H2C: the app's own Supabase client signs in, the header reports Live
(the telemetry socket authenticated with the real token), the player
reports connected, Chrome decodes 1680x1080 frames, playback time
advances, the frame has picture content (mean luma 88, stdev 62.8), and
the browser's own ICE stats show the selected candidate is a TURN relay.
No uncaught page errors.

The login page only offers a magic link by email, which a script cannot
follow, so the driver signs in with the app's $supabase.client and a
password just set on a throwaway user. That means AUTH=supabase: the
backend verifies against the real project's JWKS rather than the local
dev issuer, with e2e-supabase-user.ts creating or reusing
continuum-e2e@example.invalid (a reserved .invalid address) and
resetting its password each run. The JSON it prints carries a password
and a live token, so the shell captures it and never echoes it.

The frame is judged by pixel statistics and no screenshot is kept: it is
the inside of someone's workshop, and "is there a picture" does not need
one stored. The relay is read from getStats() through a wrapped
RTCPeerConnection rather than inferred from configuration. A browser run
with fewer than its nine results fails, so a driver that dies quietly
cannot read as a pass.

puppeteer-core is a devDependency for these scripts alone. Its 22
resolved packages are all Apache-2.0, BSD-3-Clause, ISC or MIT.
2026-10-24 11:47:24 +00:00
octoturge e4940f9404 Prove the TURN-relayed camera path, the one the browser actually uses
The first e2e-camera.sh connected over direct host candidates. The real
browser is pinned to iceTransportPolicy "relay", and PLAN.md recorded
go2rtc's known trap for that configuration: a relay candidate on one
side only can leave ICE stuck in `checking`.

The script now runs the peer a second time pinned the same way, with the
iceServers the backend hands out. The peer asserts its offer contains
only `typ relay` candidates, so a pass cannot mean it connected directly,
and never prints the credentials. Measured against the H2C: 937 packets
(~943 KB) arrive through Cloudflare, and the trap did not occur.

The phase is skipped with an explicit message when TURN credentials are
absent, so a run without them cannot be mistaken for one that verified
the relay. Both limits are written into the script header: viewer and
gateway share a host, so this is not two distinct NATs, and no real
browser has rendered a frame.
2026-10-24 08:06:58 +00:00
octoturge ac484a99ba Prove video flows: an e2e suite with a real WebRTC viewer
e2e-edge.sh proves the camera signalling relay round-trips, and says so
honestly - it posts a stub SDP with go2rtc absent and accepts a 503 that
came back from the gateway. But "the offer round-trips" says nothing
about whether anyone would see a picture, and it once read as more than
it was: a stray go2rtc left over from an earlier run answered the stub
with an HTTP 500 and the test still printed ok. Video had never been
shown to flow.

scripts/e2e-camera.sh runs the real backend, the real proxy and the real
go2rtc against the H2C, and scripts/e2e-camera-peer.ts plays the
browser's part with werift: createOffer, relay through the backend,
setRemoteDescription, then ICE, DTLS and SRTP, counting RTP out the far
end. Measured: 124-149 packets and ~119-147 KB of payload within a
second or two. It sends no printer command; the camera is a pull and the
MQTT subscription is passive. It refuses to start if a go2rtc is already
running, and fails if one survives cleanup.

werift is a devDependency for that one file; nothing under src/ imports
it. Its 30 resolved packages were audited against LICENSES.md: 26 MIT,
Apache-2.0, BSD-3-Clause, Unlicense, and one MPL-2.0 (mediabunny), which
the policy allows while we do not edit its files.

Smaller fixes in the same pass:

- e2e-edge.sh now unsets the TURN variables. It asserts TURN is off, but
  sourcing dev-secrets.sh turns it on, and the failure message then
  printed the live Cloudflare username and credential. The new script
  prints only the camera id and a count, never the /stream body.
- the last hardcoded `psql -h localhost` in e2e-edge.sh now uses
  DATABASE_URL, like the rest.
2026-10-23 16:01:47 +00:00