echo6-docs/vault/runbooks/headless-browser-page-verification.md
echo6-autocommit 5fc02ad9ea auto: docs sync 2026-06-29T18:00:06+00:00
Files changed: engine/config.yaml engine/lint-report.md engine/vocab.json vault/.obsidian/workspace.json vault/runbooks/headless-browser-page-verification.md vault/runbooks/ia-cli-reference.md vault/runbooks/pipeline-patterns.md
2026-06-29 18:00:06 +00:00

6.6 KiB

title type tags related updated
Headless Browser — Visual Page Verification runbook
tooling
central
central-deploy-cutover
2026-06-29

Headless Browser — Visual Page Verification

Drive a real headless browser (Playwright + Chromium) from cortex to log in and screenshot a deployed web page, then view the screenshot. Use this to actually look at a page after a deploy — it catches render/layout/styling bugs that curl status codes and unit tests sail right past (a route can return 200/302 and still look broken).

Works for any web UI reachable from cortex (central, navi, Authentik, PeerTube, Mailcow, etc.) — just change the base URL, path, and credentials.

Reusing this in a prompt: tell Claude "follow the headless-browser-page-verification runbook to screenshot <service> <path>" and point it at the creds. Everything needed is self-contained below.


Prerequisites

  • Runs on cortex (where Claude Code executes). The target must be reachable from cortex — most fleet UIs are on the mesh (e.g. http://100.64.0.12:8000) or via a Caddy host.
  • python3 available.
  • Credentials for auth-gated pages come from .ref/credentials (e.g. CENTRAL_OPERATOR_USER/CENTRAL_OPERATOR_PASS). Public/auth-exempt paths (/login, /health) need none.
  • Installing Playwright + Chromium is a package install — get Matt's OK first per host policy (he authorized it 2026-06-29). No apt/system-dep install is needed on cortex; the browser is a self-contained download to ~/.cache/ms-playwright.

1. One-time setup (per session)

The venv lives in the session scratchpad and is ephemeral — it does not survive across sessions, so re-run this each time.

# pick any working dir (scratchpad is fine)
python3 -m venv pwvenv
pwvenv/bin/pip install --quiet playwright
pwvenv/bin/playwright install chromium      # downloads chromium-headless-shell (~110 MB) to ~/.cache/ms-playwright

(python -c "import playwright" succeeding is enough — playwright.__version__ is intentionally absent, not an error.)


2. The screenshot script

Save as shot.py. Parameterized by env vars; logs in only if U/P are set.

import os
from playwright.sync_api import sync_playwright

base = os.environ["BASE"].rstrip("/")     # e.g. http://100.64.0.12:8000
path = os.environ.get("PATHV", "/")        # e.g. /consumers
out  = os.environ["OUT"]                    # e.g. /tmp/.../page.png
user = os.environ.get("U")                 # omit U/P for public pages
pw   = os.environ.get("P")

with sync_playwright() as p:
    b = p.chromium.launch()
    pg = b.new_context(viewport={"width": 1366, "height": 1000}).new_page()
    if user and pw:                         # form login (CSRF hidden field + cookie handled by the real browser)
        pg.goto(base + "/login", wait_until="networkidle", timeout=20000)
        pg.fill('input[name="username"]', user)
        pg.fill('input[name="password"]', pw)
        pg.click('button[type="submit"]')
        pg.wait_for_load_state("networkidle", timeout=20000)
    r = pg.goto(base + path, wait_until="networkidle", timeout=25000)
    print("HTTP", r.status if r else "?", "| url:", pg.url, "| title:", repr(pg.title()))
    pg.screenshot(path=out, full_page=True)
    print("saved", out)
    b.close()

Selector note: the login step assumes input[name="username"], input[name="password"], and a button[type="submit"] (correct for the central GUI). For a different app, open /login once (public) and adjust the selectors to match its form.


3. Run it, then look

Auth-exempt page (no login):

BASE="http://100.64.0.12:8000" PATHV="/login" OUT="$PWD/login.png" \
  pwvenv/bin/python shot.py

Authenticated page (pull creds from .ref/credentials):

BASE="http://100.64.0.12:8000" U="admin" P="<CENTRAL_OPERATOR_PASS>" \
  PATHV="/consumers" OUT="$PWD/consumers.png" \
  pwvenv/bin/python shot.py

Then view the image — use the Read tool on the .png (it renders images visually). That is the actual verification step: read the screenshot and confirm the page looks right (layout, columns, buttons, data), not just that it returned 200.

If final url came back as /login, the login failed (bad creds / wrong selectors) — fix before trusting the shot.


4. Worked example — central /consumers

  • Base: http://100.64.0.12:8000 (mesh) or http://central.echo6.mesh:8000.
  • Creds: CENTRAL_OPERATOR_USER / CENTRAL_OPERATOR_PASS in .ref/credentials.
  • /login + /health are auth-exempt; everything else 302s to /login.
  • This loop caught a real layout bug (tables overflowing their cards) that 34 passing tests + a 302 smoke check both missed — fixed and re-verified by screenshot. See central and the deploy flow in central-deploy-cutover.

Fold it into a deploy: after restarting the service, screenshot the changed page and read it before declaring the deploy verified.


5. Extension — driving authenticated actions (not just looking)

For clicking buttons / POSTing forms (e.g. bulk-deleting via an admin page so the action is audit-logged through the app), a plain authenticated HTTP session is simpler than Playwright. Pattern (stdlib urllib + http.cookiejar):

  1. GET /login → scrape the pre-auth csrf_token from the form.
  2. POST /login with username/password/csrf_token → session cookie set.
  3. GET <page> → scrape the session csrf_token from a rendered form (post-auth CSRF is a different, session-bound token).
  4. POST <action endpoint> with that csrf_token for each action.
  5. Re-GET the page and diff to verify the effect.

This routes changes through the real app (CSRF + audit log + server-side guards) instead of bypassing it.


Gotchas

  • Ephemeral venv — scratchpad doesn't persist; re-install per session.
  • full_page=True stitches the whole scrollable page into one tall PNG — good for long admin tables.
  • wait_until="networkidle" lets HTMX/JS settle before the shot; bump timeouts for slow pages.
  • Two CSRF layers (central): pre-auth token on /login is cookie-bound/itsdangerous; post-auth token is session-bound (in config.sessions, rendered into every form). Don't reuse one for the other.
  • Reachability: prefer the mesh IP / *.echo6.mesh name over LAN IPs. If cortex can't reach the target, curl it first to confirm before blaming the browser.
  • Headless-shell vs full Chromium: playwright install chromium pulls the headless shell, which is enough for screenshots; no extra system libraries were required on cortex.