feat(dashboard): auto-detect tailscale target port; clearer scan block
* Drop the Target port field from Settings — the dashboard already knows what port it's serving on, so use window.location.port (falling back to 80/443 by protocol). A user-overridable port adds no value and a stale stored value silently misroutes the funnel. * Restructure the running-tunnel scan block to show three rows: the raw tailnet URL, the remote access code (rt= token, separate), and the QR image. Previously the auth URL was rendered as one long string that read as "the bare hostname" when truncated. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3996,29 +3996,45 @@ export function SettingsModal({
|
||||
{remoteStatus?.url && <code className="remote-status-url">{remoteStatus.url}</code>}
|
||||
{remoteStatus?.lastError && <span className="field-error">{remoteStatus.lastError}</span>}
|
||||
</div>
|
||||
{tunnelState === "running" && (remoteStatus?.url || tunnelShareLink) && (
|
||||
<div className="remote-share-block">
|
||||
{remoteStatus?.url && (
|
||||
<div className="remote-share-row">
|
||||
<small>Tunnel URL:</small>
|
||||
<code className="settings-url-output">{remoteStatus.url}</code>
|
||||
</div>
|
||||
)}
|
||||
{tunnelShareLink?.url && (
|
||||
<div className="remote-share-row">
|
||||
<small>Scan to access:</small>
|
||||
<code className="settings-url-output">{tunnelShareLink.url}</code>
|
||||
{tunnelShareLink.qrSvg && (
|
||||
{tunnelState === "running" && (remoteStatus?.url || tunnelShareLink) && (() => {
|
||||
let accessCode: string | null = null;
|
||||
let tailnetUrl: string | null = remoteStatus?.url ?? null;
|
||||
if (tunnelShareLink?.url) {
|
||||
try {
|
||||
const parsed = new URL(tunnelShareLink.url);
|
||||
accessCode = parsed.searchParams.get("rt");
|
||||
if (!tailnetUrl) tailnetUrl = `${parsed.origin}/`;
|
||||
} catch {
|
||||
// fall through
|
||||
}
|
||||
}
|
||||
return (
|
||||
<div className="remote-share-block">
|
||||
{tailnetUrl && (
|
||||
<div className="remote-share-row">
|
||||
<small>Tailnet URL:</small>
|
||||
<code className="settings-url-output">{tailnetUrl}</code>
|
||||
</div>
|
||||
)}
|
||||
{accessCode && (
|
||||
<div className="remote-share-row">
|
||||
<small>Remote access code:</small>
|
||||
<code className="settings-url-output">{accessCode}</code>
|
||||
</div>
|
||||
)}
|
||||
{tunnelShareLink?.qrSvg && (
|
||||
<div className="remote-share-row">
|
||||
<small>Scan to connect:</small>
|
||||
<img
|
||||
src={`data:image/svg+xml;utf8,${encodeURIComponent(tunnelShareLink.qrSvg)}`}
|
||||
alt="Remote access QR code"
|
||||
className="settings-qr-preview-image"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
|
||||
<div className="form-group">
|
||||
<div className="remote-provider-selector" role="radiogroup" aria-label="Remote provider">
|
||||
@@ -4038,9 +4054,7 @@ export function SettingsModal({
|
||||
<div className="form-group remote-provider-settings">
|
||||
{activeProvider === "tailscale" ? (
|
||||
<>
|
||||
<small>Tailscale Funnel exposes the configured port on your tailnet's public {`https://<machine>.<tailnet>.ts.net/`} URL — no hostname configuration is needed.</small>
|
||||
<label htmlFor="remoteTailscaleTargetPort">Target port</label>
|
||||
<input id="remoteTailscaleTargetPort" type="number" min={1} max={65535} value={Number(remoteForm.remoteTailscaleTargetPort ?? 4040)} onChange={(e) => setForm((f) => ({ ...f, remoteTailscaleTargetPort: Number(e.target.value || 4040) } as SettingsFormState))} />
|
||||
<small>Tailscale Funnel will expose this dashboard on your tailnet's public {`https://<machine>.<tailnet>.ts.net/`} URL — no hostname or port configuration needed.</small>
|
||||
<label htmlFor="remoteTailscaleAcceptRoutes" className="checkbox-label">
|
||||
<input id="remoteTailscaleAcceptRoutes" type="checkbox" checked={Boolean(remoteForm.remoteTailscaleAcceptRoutes)} onChange={(e) => setForm((f) => ({ ...f, remoteTailscaleAcceptRoutes: e.target.checked } as SettingsFormState))} />
|
||||
Accept routes
|
||||
@@ -4100,7 +4114,14 @@ export function SettingsModal({
|
||||
remoteActiveProvider: activeProvider,
|
||||
remoteTailscaleEnabled: activeProvider === "tailscale",
|
||||
remoteTailscaleHostname: String(formState.remoteTailscaleHostname ?? ""),
|
||||
remoteTailscaleTargetPort: Number(formState.remoteTailscaleTargetPort ?? 4040),
|
||||
remoteTailscaleTargetPort: (() => {
|
||||
const livePort = typeof window !== "undefined" ? Number(window.location.port) : NaN;
|
||||
if (Number.isFinite(livePort) && livePort > 0) return livePort;
|
||||
const proto = typeof window !== "undefined" ? window.location.protocol : "";
|
||||
if (proto === "https:") return 443;
|
||||
if (proto === "http:") return 80;
|
||||
return Number(formState.remoteTailscaleTargetPort ?? 4040);
|
||||
})(),
|
||||
remoteTailscaleAcceptRoutes: Boolean(formState.remoteTailscaleAcceptRoutes),
|
||||
remoteCloudflareEnabled: activeProvider === "cloudflare",
|
||||
remoteCloudflareQuickTunnel: Boolean(formState.remoteCloudflareQuickTunnel ?? true),
|
||||
|
||||
@@ -1615,14 +1615,12 @@ describe("SettingsModal", () => {
|
||||
|
||||
await userEvent.click(screen.getByLabelText("Tailscale"));
|
||||
expect(screen.queryByLabelText("Hostname label")).not.toBeInTheDocument();
|
||||
expect(screen.getByLabelText("Target port")).toBeInTheDocument();
|
||||
expect(screen.queryByLabelText("Target port")).not.toBeInTheDocument();
|
||||
expect(screen.getByLabelText("Accept routes")).toBeInTheDocument();
|
||||
expect(screen.queryByLabelText("Tunnel name")).not.toBeInTheDocument();
|
||||
|
||||
fireEvent.change(screen.getByLabelText("Target port"), { target: { value: "4242" } });
|
||||
|
||||
await userEvent.click(screen.getByLabelText("Cloudflare"));
|
||||
expect(screen.queryByLabelText("Target port")).not.toBeInTheDocument();
|
||||
expect(screen.queryByLabelText("Accept routes")).not.toBeInTheDocument();
|
||||
|
||||
if (!screen.queryByLabelText("Tunnel name")) {
|
||||
const advancedDetails = screen.getByText(/Advanced \(Named Tunnel\)/i, { selector: "summary" }).closest("details") as HTMLDetailsElement;
|
||||
@@ -1855,12 +1853,12 @@ describe("SettingsModal", () => {
|
||||
await openRemoteSection();
|
||||
|
||||
await userEvent.click(screen.getByLabelText("Tailscale"));
|
||||
expect(screen.getByLabelText("Target port")).toBeInTheDocument();
|
||||
expect(screen.getByLabelText("Accept routes")).toBeInTheDocument();
|
||||
expect(screen.queryByText(/Advanced \(Named Tunnel\)/i)).not.toBeInTheDocument();
|
||||
|
||||
await userEvent.click(screen.getByLabelText("Cloudflare"));
|
||||
expect(screen.getByText(/Advanced \(Named Tunnel\)/i)).toBeInTheDocument();
|
||||
expect(screen.queryByLabelText("Target port")).not.toBeInTheDocument();
|
||||
expect(screen.queryByLabelText("Accept routes")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("sets quick tunnel false when opening Cloudflare advanced details", async () => {
|
||||
|
||||
Reference in New Issue
Block a user