Files
platform/src/servers/api/dev-proxy
pastilhasandClaude Opus 5 d771736a0d the preview panel follows the app's navigation
The panel knew one URL: the one you typed. Navigate three pages into an app and the
header still showed the front door, "open in a new tab" still went there, and a
reload — which fires by itself whenever a dev server restarts — threw away where you
were and started again at `/`.

Now the app reports where it is, and a reload composes it back.

── How the panel finds out ──

The frame is same-origin for a proxied preview, so `contentWindow.location` is
readable. It is PUSHED rather than read, because a dev server is an SPA: a route
change is `history.pushState`, which fires no event and never reloads the document.
Polling would have to be constant and would still lag. The script the proxy already
injects now patches `pushState`/`replaceState`, listens for `popstate`/`hashchange`,
and posts the path — with the proxy prefix stripped, since that is plumbing and the
panel should not have to know about it to show a URL.

Only proxied previews can do this. A cross-origin frame reports nothing, and grafting
a remembered path onto someone else's host would be inventing a URL, so `targetUrl`
composes only when there is a port.

── Two fields, and the loop that forced them ──

`url` is what you pointed the panel at; `current` is where it has got to. The obvious
design is one field — a browser has one address bar — and it feeds itself: the stored
value IS the iframe's `src`, so recording a route change hands the frame a new `src`
for the page it is already showing, which browsers treat as a navigation. Every click
would cost a reload.

Belt and braces, because one of the two would be enough only until someone changed
the other: the fields are separate, AND `PreviewFrame` captures its `src` at mount
and ignores later changes. Remount-by-key is still how a reload happens.

`ev.source` identifies which frame is talking. `message` is a window-level event and
two same-origin previews hear each other; comparing the posting window to this
iframe's `contentWindow` is exact, where matching on URL would break the moment two
panels pointed at the same port.

Writes are debounced 800ms. `useDashboardState.setValue` PATCHes the server on every
call and an SPA emits a route change per click; the value is only read at mount, so
being a second behind costs nothing.

Also: the header shows `localhost:3001/settings` rather than
`api/dev-proxy/3001/settings`, and editing starts from that — one is a thing you can
retype, the other is not.

── Tests ──

The reporter is RUN, not string-matched: `new Function` against a fake window, which
also means a syntax error in the injected script fails the suite rather than silently
breaking every proxied page. Covers prefix stripping, the app root reporting `/`
rather than empty, query and hash surviving, the report on load, pushState, and
popstate. Plus the composition helpers, including that an ordinary URL is never
composed with a path.

── One stale comment corrected while in there ──

The file claimed `allow-same-origin` "is present, and has to be". There is no
`sandbox` attribute at all, so nothing was present — and a sandbox with
`allow-same-origin` plus `allow-scripts` is worth roughly nothing anyway, since the
frame can remove its own sandbox attribute. Now says that.

Not verified against a live dev server through the real proxy — that needs the server
restarted with this code. tsgo clean, 934 pass / 7 fail unchanged, frontend builds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 18:39:38 +00:00
..