diff --git a/engine/config.yaml b/engine/config.yaml index 2aa4376..19852b6 100644 --- a/engine/config.yaml +++ b/engine/config.yaml @@ -28,6 +28,7 @@ topic_categories: - proxmox - ai - mail + - tooling # Read-only inventory sources for the generated entity lexicon (tier 2) inventory: diff --git a/engine/lint-report.md b/engine/lint-report.md index 4f859b9..130dd2f 100644 --- a/engine/lint-report.md +++ b/engine/lint-report.md @@ -1,6 +1,6 @@ # Vault Lint Report -Generated: 2026-06-29T06:00:09Z | Docs scanned: 96 | Elapsed: 0.0s +Generated: 2026-06-29T12:00:07Z | Docs scanned: 96 | Elapsed: 0.0s ## Summary diff --git a/engine/vocab.json b/engine/vocab.json index 6fd1d43..ba19bb6 100644 --- a/engine/vocab.json +++ b/engine/vocab.json @@ -39,7 +39,8 @@ "storage", "proxmox", "ai", - "mail" + "mail", + "tooling" ], "entities": [ { diff --git a/vault/.obsidian/workspace.json b/vault/.obsidian/workspace.json index 27bce49..ab1be14 100644 --- a/vault/.obsidian/workspace.json +++ b/vault/.obsidian/workspace.json @@ -199,6 +199,12 @@ }, "active": "17bd4a6166f789d0", "lastOpenFiles": [ + "runbooks/ia-cli-reference.md.tmp.3603785.daa56d288051", + "runbooks/pipeline-patterns.md.tmp.3603785.ea95256eaac1", + "runbooks/headless-browser-page-verification.md.tmp.3603785.4eb1d115aefe", + "runbooks/headless-browser-page-verification.md.tmp.3603785.f017d6928577", + "runbooks/headless-browser-page-verification.md", + "runbooks/headless-browser-page-verification.md.tmp.3603785.012d08be6da5", "docs/software/central.md.tmp.3603785.6c9205f08816", "docs/software/central.md.tmp.3603785.ecf07e484b59", "runbooks/central-deploy-cutover.md.tmp.3603785.4e7865cbcf79", @@ -206,11 +212,6 @@ "runbooks/central-deploy-cutover.md.tmp.3603785.45f414c68859", "runbooks/central-deploy-cutover.md.tmp.3603785.ae5558fd7b31", "runbooks/central-deploy-cutover.md", - "runbooks/central-deploy-cutover.md.tmp.2734058.9aafbee53297", - "docs/software/navi.md.tmp.5281.13900bd73182", - "docs/software/navi.md.tmp.5281.4b9a09bef001", - "projects/nominatim-v5-reimport.md.tmp.5281.7f47896f6abc", - "runbooks/fleet-magicdns-resolved-migration.md.tmp.5281.0b86b5de4a11", "runbooks/fleet-magicdns-resolved-migration.md", "projects/fleet-platform-baseline.md", "runbooks/toc-cortex-pve9.2-update.md", @@ -235,7 +236,6 @@ "archive/projects/mmud/mmud-prompts/mmud-prompts/01-update-planned.md", "INDEX.md", "glossary.md", - "concepts/youtube.md", "assets/echo6yellow_logo_422x422_square.png", "assets/echo6yellow_logo_422x81.png", "assets/echo6_logo.png", diff --git a/vault/runbooks/headless-browser-page-verification.md b/vault/runbooks/headless-browser-page-verification.md new file mode 100644 index 0000000..342f40a --- /dev/null +++ b/vault/runbooks/headless-browser-page-verification.md @@ -0,0 +1,130 @@ +--- +title: "Headless Browser — Visual Page Verification" +type: runbook +tags: [tooling] +related: ["[[central]]", "[[central-deploy-cutover]]"] +updated: 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 ` `"* 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. + +```bash +# 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. + +```python +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):** +```bash +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`):** +```bash +BASE="http://100.64.0.12:8000" U="admin" P="" \ + 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 ` → scrape the **session** `csrf_token` from a rendered form (post-auth CSRF is a different, session-bound token). +4. `POST ` 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. diff --git a/vault/runbooks/ia-cli-reference.md b/vault/runbooks/ia-cli-reference.md index 11a3c0e..b7fc855 100644 --- a/vault/runbooks/ia-cli-reference.md +++ b/vault/runbooks/ia-cli-reference.md @@ -2,7 +2,7 @@ title: Internet Archive CLI Reference type: runbook tags: - - auth + - media aliases: [] related: - [[ia-download-mirror]] diff --git a/vault/runbooks/pipeline-patterns.md b/vault/runbooks/pipeline-patterns.md index 3cbf969..4eb3233 100644 --- a/vault/runbooks/pipeline-patterns.md +++ b/vault/runbooks/pipeline-patterns.md @@ -1,7 +1,7 @@ --- title: "Pipeline & Wrapper Patterns" type: runbook -tags: [] +tags: [tooling] aliases: [] related: - [[meshtastic-sidecar-node]]