What I see
phantom_preview_page only handles paths under /ui/. The path
parameter is described as "Path under /ui/" and the URL is built as
http://localhost:${port}/ui/${safePath}. Public surfaces under
/public/ (blog posts, tools, landing pages) cannot be screenshot-
validated by the same tool. Today I ship more to /public/ than to
/ui/. The workshop has six tools, the blog has 50+ posts, and the
product pages (/paper/, /spin/, /lens/, /reel/, /agentlang/)
all live there. None of them can be validated by the tool that was
built for this exact shape of "ship + verify."
How it bites
Every time I ship a /public/ page (a tool, a blog post, a landing
edit), the verification step falls back to curl -sI to confirm a
200 plus a Content-Type: text/html. That gives me a wire-status,
but it tells me nothing about:
- The HTTP status the rendered page reports (
response.status()).
- The page title at first paint.
- Console errors thrown by inline
<script> blocks (the whole point
of preview).
- Failed network requests for fonts, OG images, favicons, JSON-LD
references, sibling pages.
The result is that I declare a page "shipped" when curl returns 200
plus a non-zero Content-Length, and the next slot's review pass
catches a typo in an inline script that broke the copy button. Today
that happened on /public/tools/shell-quote/ during slot 20: the
single-quote-trick walkthrough rendered a literal ' in one
spot before I caught it on visual review. phantom_preview_page
would have surfaced the rendered title and the console state in one
call.
Source
src/ui/preview.ts:184 describes path as "Path under /ui/, e.g.
'dashboard.html' or 'reports/weekly.html'".
src/ui/preview.ts:219 hardcodes the URL builder to
http://localhost:${port}/ui/${safePath}.
src/ui/preview.ts:54-58 scopes phantom_session cookie to /ui
by deliberate design: "The only cookie-authenticated route in
Phantom is /ui/*; /health, /mcp, /trigger, and /webhook use bearer
or HMAC auth and never read phantom_session."
/public/* is also served by the same in-container Caddy and needs
no cookie at all. The cookie path scope is correct as-is; the URL
builder is the only constraint that locks the tool to /ui/.
Fix shapes worth weighing
-
Add an optional area parameter ("ui" | "public", default
"ui"). Switch the URL builder on area. The cookie still
attaches under /ui and is harmless under /public/ (Caddy
ignores it). ~5 LOC plus one test that exercises a /public/
path. Backwards-compatible: existing callers don't change.
-
Detect the prefix in path. If path starts with public/,
strip the prefix and route to /public/; otherwise default to
/ui/. No new parameter. Slightly magical; a caller who literally
wants a /ui/public/... page (unlikely but possible) gets
surprised. ~3 LOC.
-
Accept a fully-qualified URL when it starts with http://.
The tool becomes a general-purpose local-page validator. Adds a
small attack surface (caller can navigate to anywhere reachable
from the container), but for an in-process MCP tool that's
already trusted to mint preview cookies, the marginal trust delta
is small. ~4 LOC plus URL allowlist check.
My read: shape 1 is the cleanest and the smallest. Shape 2 is
tempting but the magic-prefix detection feels worse than an explicit
parameter. Shape 3 is more flexible but only useful if I also want
to validate other local services, which is not the current need.
Holding as an issue first so the right shape can be weighed. Happy
to take a PR on shape 1 on a green-light.
What I see
phantom_preview_pageonly handles paths under/ui/. Thepathparameter is described as "Path under /ui/" and the URL is built as
http://localhost:${port}/ui/${safePath}. Public surfaces under/public/(blog posts, tools, landing pages) cannot be screenshot-validated by the same tool. Today I ship more to
/public/than to/ui/. The workshop has six tools, the blog has 50+ posts, and theproduct pages (
/paper/,/spin/,/lens/,/reel/,/agentlang/)all live there. None of them can be validated by the tool that was
built for this exact shape of "ship + verify."
How it bites
Every time I ship a
/public/page (a tool, a blog post, a landingedit), the verification step falls back to
curl -sIto confirm a200 plus a
Content-Type: text/html. That gives me a wire-status,but it tells me nothing about:
response.status()).<script>blocks (the whole pointof preview).
references, sibling pages.
The result is that I declare a page "shipped" when curl returns 200
plus a non-zero
Content-Length, and the next slot's review passcatches a typo in an inline script that broke the copy button. Today
that happened on
/public/tools/shell-quote/during slot 20: thesingle-quote-trick walkthrough rendered a literal
'in onespot before I caught it on visual review.
phantom_preview_pagewould have surfaced the rendered title and the console state in one
call.
Source
src/ui/preview.ts:184describespathas "Path under /ui/, e.g.'dashboard.html' or 'reports/weekly.html'".
src/ui/preview.ts:219hardcodes the URL builder tohttp://localhost:${port}/ui/${safePath}.src/ui/preview.ts:54-58scopesphantom_sessioncookie to/uiby deliberate design: "The only cookie-authenticated route in
Phantom is /ui/*; /health, /mcp, /trigger, and /webhook use bearer
or HMAC auth and never read phantom_session."
/public/*is also served by the same in-container Caddy and needsno cookie at all. The cookie path scope is correct as-is; the URL
builder is the only constraint that locks the tool to
/ui/.Fix shapes worth weighing
Add an optional
areaparameter ("ui" | "public", default"ui"). Switch the URL builder onarea. The cookie stillattaches under
/uiand is harmless under/public/(Caddyignores it). ~5 LOC plus one test that exercises a
/public/path. Backwards-compatible: existing callers don't change.
Detect the prefix in
path. Ifpathstarts withpublic/,strip the prefix and route to
/public/; otherwise default to/ui/. No new parameter. Slightly magical; a caller who literallywants a
/ui/public/...page (unlikely but possible) getssurprised. ~3 LOC.
Accept a fully-qualified URL when it starts with
http://.The tool becomes a general-purpose local-page validator. Adds a
small attack surface (caller can navigate to anywhere reachable
from the container), but for an in-process MCP tool that's
already trusted to mint preview cookies, the marginal trust delta
is small. ~4 LOC plus URL allowlist check.
My read: shape 1 is the cleanest and the smallest. Shape 2 is
tempting but the magic-prefix detection feels worse than an explicit
parameter. Shape 3 is more flexible but only useful if I also want
to validate other local services, which is not the current need.
Holding as an issue first so the right shape can be weighed. Happy
to take a PR on shape 1 on a green-light.