--- name: sandbox-browser-automation description: Use when you need to actually render a page (screenshot, computed styles, DOM inspection) instead of guessing from curl/static file inspection — e.g. "the CSS fix is deployed but the user still says it's not showing." Documents what's really installed in this sandbox and a live DNS-routing quirk that affects `*.palmasolutions.net` fleet domains specifically. --- # Sandbox Browser Automation Static HTTP/CSS inspection cannot prove what actually renders — clipping, cascade wins, and z-index/overflow interactions only show up in a real browser. This sandbox has Playwright fully installed; use it before concluding "the fix is deployed, must be a caching issue on the user's end." ## What's actually installed (verified 2026-08-20) - **Playwright 1.62.1**, browsers pre-downloaded at `~/.cache/ms-playwright/` (chromium + ffmpeg). No re-download needed. - **Python path (preferred — no setup required):** the `playwright` package is globally importable — `python3 -c "from playwright.sync_api import sync_playwright"` works immediately, no venv/install step. `/usr/local/bin/playwright` is this same package's CLI (`playwright codegen`, etc.). - **Node path:** no *global* npm `playwright` package — a script needs its own local `node_modules/playwright` to `require()` it. Check `/tmp/` first for a scratch dir a prior session may have already set up (e.g. past names seen: `sateulera-render`, `mobisol-render`, `st-pw-package`) before running a fresh `npm install playwright` — reusing one skips the install entirely since the browser binaries are already cached globally. `/tmp` is shared across concurrent sandboxes/sessions and not durable — don't assume a given scratch dir still exists, always `ls` first, and don't rely on one surviving into a future session. - No system `chromium`/`google-chrome` binary on `PATH` — always go through Playwright's own managed browser, not a system browser launch. Default to the Python one-shot form when you just need a screenshot or a computed-style check — it has zero setup cost. Reach for a Node script only when reusing an existing scratch project's `node_modules` is actually faster, or the task needs a JS-heavy interaction sequence. ```python from playwright.sync_api import sync_playwright with sync_playwright() as p: browser = p.chromium.launch() page = browser.new_context(viewport={"width": 375, "height": 812}, ignore_https_errors=True).new_page() page.goto("https://example.com/", wait_until="domcontentloaded", timeout=45000) page.screenshot(path="/root/.claude/jobs//tmp/out.png") browser.close() ``` Use `wait_until="domcontentloaded"` by default, not `"networkidle"` — sites with long-polling/analytics connections can hang `networkidle` well past a reasonable timeout. ## `*.palmasolutions.net` (and possibly other fleet domains) resolve differently inside this sandbox Confirmed 2026-08-20 on `sateulera-dev.palmasolutions.net`: `dig` from this box returns the real public path (madri's Caddy IP, `195.20.235.141`), but the box's own system resolver — what `curl`, Node, and Playwright actually use via `getaddrinfo` — resolves the same name **directly to the backend's Tailscale IP** (funky, `100.127.21.100`), bypassing Caddy entirely. This is likely a split-DNS/hosts entry specific to this sandbox's network setup, not present for the end user's real browser. In this case the content served both ways turned out to be byte-identical (verified via `md5sum`), so it wasn't the cause of a "still broken" report — but don't assume that holds for every fleet domain or every time. If a fix verifies correctly in Playwright from this box but the user still reports it broken, explicitly re-check via the real public path before ruling out a routing/caching difference: ```bash curl -sk --resolve :443: "https:///path" | md5sum curl -sk "https:///path" | md5sum # this box's default (possibly-shortcut) path ``` If the hashes differ, you've found a real routing/cache split, not a false alarm — investigate the reverse proxy (Caddy on madri, or nginx on proxie) rather than the origin. ## Real bug class this catches that static inspection misses A badge/element positioned with a negative offset (`top:0;left:-2px`, etc.) to overlap the corner of a **circular** `overflow:hidden` + `border-radius:50%` container gets clipped by the circle's curve even while the badge's own rectangular bounding box is still inside the container's rectangular bounds. `border-radius` and `padding` fixes on the badge itself cannot fix this — computed-style inspection via `getComputedStyle` will even report the badge's own box as unclipped (`overflow: visible` on the badge). Only a rendered screenshot (or checking the *parent's* computed `overflow`) reveals it. Fix is to drop `overflow:hidden` from the circular parent (the inner icon element usually already carries its own `border-radius:50%` and doesn't need the outer clip).