Nothing in the repo explains how to launch this app in a headless container, and three separate things stop it from starting — none of them discoverable without hitting each in turn. The container's default node is v22.22.2, one patch below the floor `engines` declares. A satisfying build sits at /opt/node (v22.23.2), but PATH finds /opt/node22 first, and the names invert what you would guess. `ng serve` reports only the version and exits. The pinned Playwright wants a Chromium revision this image does not carry, under a directory layout the older build does not use, so `npm run e2e` cannot launch a browser at all until the expected path is shimmed. Downloads are blocked, so `playwright install` is not the answer. And the scene is software-rasterized here: entering a system takes a camera flight that has to be waited on rather than slept through, a single canvas click lands before picking is wired, and a screenshot taken the moment a DOM assertion passes catches a half-drawn frame. driver.mjs handles all of it — picks a node satisfying `engines`, owns the dev server, drives the camera to each scale, and either screenshots or dumps the HUD's labels as JSON for checking a change without eyeballing a picture. SKILL.md documents only commands that were run here, and records the traps in Gotchas, including that no `pkill -f` is safe for stopping the server: it matches your own command line and kills the shell. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WaySiNst4HhDXBHnMy8p5G
8.1 KiB
name, description
| name | description |
|---|---|
| run-star-map | Build, launch, run, drive, and screenshot the star-map Angular app in this container. Use when asked to run or start the app, take a screenshot of the map or HUD, check a UI change in a real browser, or run the unit/e2e test suites. |
Running star-map
An Angular 3D star map (three.js, WebGPU renderer falling back to WebGL2). There is no GPU here, so the scene is software-rasterized and slow — but it does run, and it screenshots.
Drive it with the committed driver, which owns the dev server, the browser, and the camera flights:
.claude/skills/run-star-map/driver.mjs
All paths below are relative to the repo root.
Prerequisites
Two container quirks have to be handled first. Both are one-liners, and both are silent footguns if skipped — see Gotchas for why.
1. Node. The default node on PATH is below the floor the Angular CLI enforces. A
satisfying build is at /opt/node. The driver picks it automatically; anything you run by hand
needs the prefix:
node -v # v22.22.2 ← too old, ng refuses to start
/opt/node/bin/node -v # v22.23.2 ← use this
export PATH=/opt/node/bin:$PATH
2. Playwright browser. The pinned @playwright/test (1.62.1) wants Chromium revision 1234;
this container ships 1194, under a different internal layout. Do not run
npx playwright install (the environment blocks browser downloads). Shim the expected path
instead — needed only for npm run e2e, not for the driver:
mkdir -p /opt/pw-browsers/chromium_headless_shell-1234/chrome-headless-shell-linux64
ln -sfn /opt/pw-browsers/chromium_headless_shell-1194/chrome-linux/headless_shell \
/opt/pw-browsers/chromium_headless_shell-1234/chrome-headless-shell-linux64/chrome-headless-shell
touch /opt/pw-browsers/chromium_headless_shell-1234/INSTALLATION_COMPLETE
Install
PATH=/opt/node/bin:$PATH npm ci # ~25s
Run (agent path) — the driver
Takes screenshots of the app at each scale. Starts the dev server on port 4300 if one isn't already up, and stops it on exit.
node .claude/skills/run-star-map/driver.mjs tour
starting dev server on http://localhost:4300 (node v22.23.2)
dev server ready
field:
/tmp/star-map-shots/1-star-field.png (level=Solar Neighbourhood)
galaxy:
/tmp/star-map-shots/2-galactic.png (level=Milky Way)
system:
/tmp/star-map-shots/3-system-sol.png (level=System)
inner:
zoomed to 5.53 AU
/tmp/star-map-shots/4-system-inner.png (level=System)
tour takes ~3m20s — most of it camera flights and software rasterization. For one view:
node .claude/skills/run-star-map/driver.mjs shot galaxy # ~1m
node .claude/skills/run-star-map/driver.mjs shot field system
| View | What it shows |
|---|---|
field |
Solar-neighbourhood star field — the default landing view |
galaxy |
Galactic scale: arms, bar, Sol's position |
system |
Sol at ~120 AU, outer planets labelled |
inner |
Sol zoomed to <6 AU, inner four planets labelled |
Look at the PNG you produced. A black frame means the scene never initialized — that is a failure, not a dark theme.
Flags: --out=DIR (default /tmp/star-map-shots), --stars=N (default 12000), --port=N
(default 4300), --keep-server, --headed.
Checking HUD state without screenshots
Faster than eyeballing a picture, and the right tool for label/HUD changes:
node .claude/skills/run-star-map/driver.mjs probe inner
{
"title": "Sol",
"level": "System",
"labelCount": 4,
"labels": [
{ "name": "Mars", "kind": "Planet" },
{ "name": "Earth", "kind": "Planet" },
{ "name": "Mercury", "kind": "Planet" },
{ "name": "Venus", "kind": "Planet" }
]
}
probe accepts the same four view names. Labels are read from the CSS2D layer
(.map-label > .map-label-name + .map-label-kind).
Run (human path)
PATH=/opt/node/bin:$PATH npm start -- --port=4300
Serves on http://localhost:4300. Useless headless on its own — there is no display to look at. Use it only when you want a long-lived server for the driver to attach to (the driver reuses an already-running server and leaves it alone on exit).
Test
PATH=/opt/node/bin:$PATH npm test # 496 tests, 28 files, ~5s
PATH=/opt/node/bin:$PATH npm run build # ~9s
PATH=/opt/node/bin:$PATH npm run etl:typecheck
PATH=/opt/node/bin:$PATH npm run e2e:typecheck
PATH=/opt/node/bin:$PATH npm run e2e -- --workers=1 # 6 tests, ~2.3m
--workers=1 on the e2e suite is not optional here — see Gotchas.
Gotchas
-
The wrong Node is first on
PATH./opt/node22is v22.22.2;/opt/nodeis v22.23.2.PATHfinds the older one, andpackage.json#enginesrequires^22.22.3.ng servethen dies with "Node.js version v22.22.2 detected" and nothing else. The names invert what you'd guess —/opt/nodeis newer than/opt/node22. -
npm run e2efails at full parallelism, passing 4 of 6. The two camera-flight specs time out when four browsers share a software rasterizer. The same specs pass alone and pass with--workers=1. It is CPU contention, not a broken test — don't "fix" the specs. -
Never stop the dev server with
pkill -f. It matches against every process's full command line — including the shell running your ownpkill, which then kills itself (exit 144). Narrowing the pattern does not save you, and neither does the[n]gbracket trick: the match is against the whole command line, so a literalng serveanywhere else in it — inside anecho, a comment, a laterpgrep— re-arms the self-match. Kill by port instead, which does no pattern matching at all:fuser -k 4300/tcpOr just let the driver clean up after itself, which it does unless you pass
--keep-server. -
The star count must be cut for anything interactive. The full catalogue is 68 388 stars and runs at roughly 1 fps here.
?stars=Noverrides it; the driver defaults to 12 000. -
One canvas click is not enough to enter a system. Bootstrap (data fetch + raycaster wiring) finishes asynchronously after load, so an early click hits nothing. The driver polls click-until-
hud-current-level-reads-System. Sol is at the origin, dead centre of the default camera, so the centre click is what reliably works. -
Screenshots need a settle delay. A DOM assertion passing does not mean the frame is drawn; the driver waits 2.5s before every capture. Without it you get half-rendered scenes.
-
At 120 AU the inner planets have no labels. They are inside the centre reticle, collapsed to a few pixels, and correctly suppressed. This is not a regression — zoom below ~6 AU (the
innerview) to see Mercury/Venus/Earth/Mars labelled. -
Port 4300, not Angular's usual 4200 — the project's own convention, set in
playwright.config.tsso it never collides with an unrelatedng serve. -
Don't write driver scripts in
.tsoutside the repo.tsxcompiles a bare.tsas CJS ("Top-level await is currently not supported with the cjs output format"), and a script outside the repo can't resolve@playwright/testat all. The driver is.mjsinside the repo for both reasons.
Troubleshooting
| Symptom | Fix |
|---|---|
Node.js version v22.22.2 detected. The Angular CLI requires... |
export PATH=/opt/node/bin:$PATH |
browserType.launch: Executable doesn't exist at .../chromium_headless_shell-1234/... |
Apply the browser shim in Prerequisites. Do not run npx playwright install. |
| e2e: 2 specs time out on camera flights | Add -- --workers=1 |
| Your shell exits with code 144 while cleaning up | A pkill -f matched your own command line. Use fuser -k 4300/tcp. |
| Screenshot is entirely black | Scene never initialized — check the driver's [pageerror] lines. |
Top-level await is currently not supported with the "cjs" output format |
Name the script .mjs (or .mts) and keep it inside the repo. |
| Driver hangs at "starting dev server" | Another server holds port 4300: fuser -k 4300/tcp, or pass --port=4301. |