Documents what's actually installed for browser automation in this sandbox (Playwright via Python, no global Node package, browsers pre-cached) and a DNS-routing quirk on *.palmasolutions.net domains, plus the circular overflow:hidden badge-clipping bug class found while debugging sateulera-dev.
5.0 KiB
name, description
| name | description |
|---|---|
| sandbox-browser-automation | 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
playwrightpackage is globally importable —python3 -c "from playwright.sync_api import sync_playwright"works immediately, no venv/install step./usr/local/bin/playwrightis this same package's CLI (playwright codegen, etc.). - Node path: no global npm
playwrightpackage — a script needs its own localnode_modules/playwrighttorequire()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 freshnpm install playwright— reusing one skips the install entirely since the browser binaries are already cached globally./tmpis shared across concurrent sandboxes/sessions and not durable — don't assume a given scratch dir still exists, alwayslsfirst, and don't rely on one surviving into a future session. - No system
chromium/google-chromebinary onPATH— 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.
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/<job>/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:
curl -sk --resolve <domain>:443:<public-ip-from-dig> "https://<domain>/path" | md5sum
curl -sk "https://<domain>/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).