The previous fix (84e0caa) embedded the token in HTML but still did a
302 redirect on first visit. This meant:
1. Phone opens URL with ?token=xxx
2. Server redirects to / (token stripped from URL)
3. redirected request has no token, buildHTML gets undefined
4. Token never embedded in the page
Also the old code passed the server's secret 'token' variable instead of
the URL parameter 'providedToken' — a security issue.
Fix: Remove the redirect entirely. Serve the HTML page directly on first
visit with Set-Cookie header (no Location redirect). Pass the actual
providedToken from the URL to buildHTML so it gets embedded correctly.
The JS includes the token in the WebSocket URL, so auth works even if
the cookie isn't available.
The WebSocket connection at /ws was not including the token parameter,
relying solely on the session cookie from the initial redirect. When
scanning the QR code on a phone, the token (longest part of URL) was
being truncated, so no cookie was ever set and all requests got 403.
Fix: embed the token directly into the page as a JS variable, and
append it to the WebSocket connection URL as a fallback. Now both
the HTTP page and the WebSocket upgrade work even if the cookie
isn't available.
- Add transport mode config (surge/tailscale) to remote-control.json
- Add detectTailscaleIp() with CLI and local API fallbacks
- Add startServerTailscale() binding to 0.0.0.0 (token-protected)
- Add Transport toggle in /remote-control menu
- Update README with Tailscale setup + Android connection guide
- Update ARCHITECTURE.md with dual transport documentation
Use client.terminate() instead of client.close() to avoid waiting for
unresponsive clients to acknowledge the WebSocket close handshake.
Add a 2-second safety timeout that closes the HTTP listener, destroys
lingering sockets, and resolves the promise so session_shutdown does not
block pi from exiting.
When the agent is streaming, the send button becomes a red stop button
that sends a { type: "stop" } WebSocket message. The server handles this
by calling ctx.abort() to cancel the current agent operation.
Use the 'qrcode' npm package instead of shelling out to the 'qrencode'
binary. Load via createRequire for ESM/CJS interop. Use margin: 2 to
avoid the utf8 renderer's invalid array length bug with odd margins.
/remote-control now opens a select menu with Turn on/off,
Configure URL, and Status instead of relying on subcommands.
Adds ability to stop the server. Shows current URL in the
Configure URL menu item and in the input dialog title.