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.
This commit is contained in:
2026-10-24 11:47:24 +00:00
parent e4940f9404
commit c72bfe71d6
4 changed files with 349 additions and 8 deletions
+108 -8
View File
@@ -42,6 +42,18 @@
# GO2RTC_BIN=~/.local/bin/go2rtc
# PROXY_DIR=../continuum-proxy
# KEEP_DATA=1 leave the seeded farm behind for inspection
#
# BROWSER=1 also drive the real web console in a real Chrome
# (../continuum-app) and check the picture on
# screen. Implies AUTH=supabase, because the app's
# client needs a real Supabase session; needs
# SUPABASE_URL, SUPABASE_SECRET_KEY and
# SUPABASE_PUBLISHABLE_KEY (source ../dev-secrets.sh).
# It creates/reuses one Supabase user,
# continuum-e2e@example.invalid, and resets its
# password each run. Port 3042 must be free.
# AUTH=supabase use that real user's token for the API phases too
# (default is the local dev-auth issuer)
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
@@ -64,10 +76,18 @@ R2_ENDPOINT="${R2_ENDPOINT:-http://localhost:9000}"
export DATABASE_URL VALKEY_URL EDGE_JWT_SECRET PORT
export R2_ACCESS_KEY_ID R2_SECRET_ACCESS_KEY R2_ENDPOINT
BROWSER="${BROWSER:-0}"
AUTH="${AUTH:-dev}"
[ "$BROWSER" = "1" ] && AUTH=supabase
APP_DIR="${APP_DIR:-$REPO_ROOT/../continuum-app}"
APP_PORT="${APP_PORT:-3042}"
CHROME_BIN="${CHROME_BIN:-/usr/bin/google-chrome}"
WORK="$(mktemp -d)"
BACKEND_PID=""
PROXY_PID=""
AUTH_PID=""
APP_PID=""
fail() { echo "FAIL: $*" >&2; exit 1; }
ok() { echo " ok: $*"; }
@@ -84,6 +104,9 @@ cleanup() {
for pid in "$PROXY_PID" "$BACKEND_PID" "$AUTH_PID"; do
[ -n "$pid" ] && kill "$pid" 2>/dev/null || true
done
# The dev server forks workers, so it is signalled as a group: it was started
# with setsid, which makes its PID the group id. `kill 0` would hit us.
[ -n "$APP_PID" ] && kill -- "-$APP_PID" 2>/dev/null || true
for _ in $(seq 1 20); do
[ -n "$PROXY_PID" ] && kill -0 "$PROXY_PID" 2>/dev/null || break
sleep 0.3
@@ -91,6 +114,7 @@ cleanup() {
for pid in "$PROXY_PID" "$BACKEND_PID" "$AUTH_PID"; do
[ -n "$pid" ] && kill -9 "$pid" 2>/dev/null || true
done
[ -n "$APP_PID" ] && kill -9 -- "-$APP_PID" 2>/dev/null || true
if [ -z "${KEEP_DATA:-}" ] && [ -n "${ORG_ID:-}" ]; then
psql_q "DELETE FROM organisations WHERE id = '$ORG_ID'" >/dev/null 2>&1 || true
fi
@@ -137,6 +161,30 @@ else
ok "could not read $CAMERA_ID's address from printers.toml; skipping the 322 pre-check"
fi
if [ "$AUTH" = "supabase" ]; then
for name in SUPABASE_URL SUPABASE_SECRET_KEY SUPABASE_PUBLISHABLE_KEY; do
[ -n "${!name:-}" ] || fail "$name is not set - source ../dev-secrets.sh (AUTH=supabase / BROWSER=1)"
done
fi
if [ "$BROWSER" = "1" ]; then
[ -x "$CHROME_BIN" ] || fail "no Chrome at $CHROME_BIN (set CHROME_BIN)"
[ -d "$APP_DIR/apps/web/node_modules" ] || fail "$APP_DIR has no installed web app - run pnpm install there"
ss -ltn 2>/dev/null | grep -q ":$APP_PORT " && fail "port $APP_PORT is already in use"
fi
# The real Supabase user, when one is needed. Captured, never echoed: the JSON
# holds a password and a live access token.
if [ "$AUTH" = "supabase" ]; then
SB_USER="$(bun scripts/e2e-supabase-user.ts)" || fail "could not provision the Supabase test user"
sb_field() { printf '%s' "$SB_USER" | python3 -c 'import json,sys; print(json.load(sys.stdin)[sys.argv[1]])' "$1"; }
USER_ID="$(sb_field id)"
USER_TOKEN="$(sb_field accessToken)"
SB_EMAIL="$(sb_field email)"
SB_PASSWORD="$(sb_field password)"
unset SB_USER
ok "signed in as the real Supabase test user $USER_ID"
fi
# --- 1. seed ---------------------------------------------------------------
echo "==> seeding an organisation and a farm claiming '$CAMERA_ID'"
@@ -154,14 +202,18 @@ ok "org $ORG_ID, farm $FARM_ID, viewer $USER_ID"
# --- 2. backend ------------------------------------------------------------
echo "==> starting the dev-auth issuer on :$AUTH_PORT and the backend on :$PORT"
bun scripts/dev-auth.ts --port "$AUTH_PORT" > "$WORK/auth.log" 2>&1 &
AUTH_PID=$!
AUTH_URL="http://localhost:$AUTH_PORT"
for _ in $(seq 1 40); do curl -sf "$AUTH_URL/auth/v1/.well-known/jwks.json" >/dev/null 2>&1 && break; sleep 0.25; done
curl -sf "$AUTH_URL/auth/v1/.well-known/jwks.json" >/dev/null 2>&1 || fail "dev-auth did not come up"
export SUPABASE_URL="$AUTH_URL"
USER_TOKEN="$(curl -sf "$AUTH_URL/token?sub=$USER_ID" | python3 -c 'import json,sys; print(json.load(sys.stdin)["access_token"])')"
if [ "$AUTH" = "dev" ]; then
echo "==> starting the dev-auth issuer on :$AUTH_PORT and the backend on :$PORT"
bun scripts/dev-auth.ts --port "$AUTH_PORT" > "$WORK/auth.log" 2>&1 &
AUTH_PID=$!
AUTH_URL="http://localhost:$AUTH_PORT"
for _ in $(seq 1 40); do curl -sf "$AUTH_URL/auth/v1/.well-known/jwks.json" >/dev/null 2>&1 && break; sleep 0.25; done
curl -sf "$AUTH_URL/auth/v1/.well-known/jwks.json" >/dev/null 2>&1 || fail "dev-auth did not come up"
export SUPABASE_URL="$AUTH_URL"
USER_TOKEN="$(curl -sf "$AUTH_URL/token?sub=$USER_ID" | python3 -c 'import json,sys; print(json.load(sys.stdin)["access_token"])')"
else
echo "==> starting the backend on :$PORT, verifying tokens against the real Supabase JWKS"
fi
bun run src/index.ts > "$WORK/backend.log" 2>&1 &
BACKEND_PID=$!
for _ in $(seq 1 60); do curl -sf "$BASE/health" >/dev/null 2>&1 && break; sleep 0.5; done
@@ -251,9 +303,57 @@ else
echo " The TURN-relayed path, which the browser is pinned to, is NOT verified by this run."
fi
# --- 5. the real web console in a real browser -------------------------------
if [ "$BROWSER" = "1" ]; then
echo "==> starting the web console on :$APP_PORT"
# setsid so the whole dev server (it forks workers) shares one process group
# that cleanup can signal; the app is told where the backend is and given the
# real Supabase project it signs in against.
setsid env -C "$APP_DIR/apps/web" \
NUXT_PUBLIC_API_BASE="$BASE" \
NUXT_PUBLIC_WS_BASE="ws://localhost:$PORT" \
SUPABASE_URL="$SUPABASE_URL" \
SUPABASE_KEY="$SUPABASE_PUBLISHABLE_KEY" \
pnpm exec nuxt dev --host 127.0.0.1 --port "$APP_PORT" > "$WORK/app.log" 2>&1 &
APP_PID=$!
for _ in $(seq 1 120); do
curl -sf -o /dev/null "http://127.0.0.1:$APP_PORT/login" 2>/dev/null && break
sleep 1
done
curl -sf -o /dev/null "http://127.0.0.1:$APP_PORT/login" \
|| { tail -20 "$WORK/app.log" >&2; fail "the web console did not start on :$APP_PORT"; }
ok "the web console is up"
echo "==> the real console in real Chrome: sign in, open the fleet, watch the camera"
BROWSER_OUT="$(env APP_URL="http://127.0.0.1:$APP_PORT" SUPABASE_E2E_EMAIL="$SB_EMAIL" \
SUPABASE_E2E_PASSWORD="$SB_PASSWORD" CHROME_BIN="$CHROME_BIN" \
timeout 240 bun scripts/e2e-camera-browser.ts 2>&1 || true)"
seen=0; bad=0
while IFS=$'\t' read -r verdict label; do
case "$verdict" in
ok) ok "$label"; seen=$((seen + 1)) ;;
FAILED) echo " FAILED: $label" >&2; seen=$((seen + 1)); bad=$((bad + 1)) ;;
esac
done < <(printf '%s\n' "$BROWSER_OUT" | grep -E "^(ok|FAILED)$(printf '\t')")
# A complete run is nine results. Fewer means the driver died part way, and a
# silent driver must not read as a pass.
if [ "$seen" -lt 9 ] && [ "$bad" -eq 0 ]; then
bad=1; echo " FAILED: the browser run produced only $seen of 9 results" >&2
fi
if [ "$bad" -ne 0 ]; then
printf '%s\n' "$BROWSER_OUT" | sed 's/^/ | /' >&2
echo "--- web console log (tail) ---" >&2; tail -15 "$WORK/app.log" >&2
fail "the real browser did not show the camera"
fi
fi
echo
echo "PASS - a real WebRTC peer received video from the printer's camera"
echo " through the backend relay, the uplink, the proxy and go2rtc."
if [ -n "${CLOUDFLARE_TURN_KEY_ID:-}" ] && [ -n "${CLOUDFLARE_TURN_API_TOKEN:-}" ]; then
echo " Both directly and pinned to relay-only through Cloudflare TURN."
fi
if [ "$BROWSER" = "1" ]; then
echo " And the real web console, in real Chrome, showed the picture."
fi