Skip to content

Commit c8a16ee

Browse files
fix(web): keep VPS IP out of normal wizard URLs
The Create VPS step told users their VPS IP stayed on-device, but the normal localStorage path also wrote the IP into the URL query string. That made ordinary wizard navigation carry the IP in browser-visible URLs even though URL state is only needed for storage-blocked browsers. Change setVPSIP so successful localStorage persistence removes any stale ip query parameter, while storage failures still use the query string fallback. Update the page copy to describe that fallback accurately. Pin the behavior with unit tests for both persistence paths, and update the wizard E2E expectations so normal flows require the IP to stay out of the URL while the no-localStorage flow still carries it. Also fixed the VPS persistence E2E setup to unlock steps 1-4 before visiting create-vps, matching the route guard contract instead of racing a redirect. Verified with: git diff --check; bun test lib/progressPersistence.test.ts; CI=1 PW_WORKERS=1 PW_PORT=3117 bun run test -- e2e/wizard-flow.spec.ts --grep 'Create VPS|LocalStorage Persistence|No localStorage|complete full wizard flow' --project=chromium --reporter=line; CI=1 PW_WORKERS=1 PW_PORT=3119 bun run test -- e2e/wizard-flow.spec.ts --grep 'State Persistence|Complete Wizard Flow Integration' --project=chromium --reporter=line; ubs apps/web/lib/userPreferences.ts apps/web/app/wizard/create-vps/page.tsx apps/web/e2e/wizard-flow.spec.ts apps/web/lib/progressPersistence.test.ts; bun run type-check; bun run lint; bun run build.
1 parent b2a37d8 commit c8a16ee

4 files changed

Lines changed: 78 additions & 19 deletions

File tree

apps/web/app/wizard/create-vps/page.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -353,8 +353,8 @@ export default function CreateVPSPage() {
353353
Your data stays on your device
354354
</p>
355355
<p className="text-[12px] leading-relaxed text-muted-foreground sm:text-[13px]">
356-
This IP address is stored <strong className="text-foreground/80">only in your browser&apos;s local storage</strong>. It&apos;s
357-
never sent to our servers or any third party. The{" "}
356+
This IP address is stored <strong className="text-foreground/80">in your browser&apos;s local storage</strong>.
357+
If browser storage is blocked, the wizard keeps it in the page URL so the next step still works. The{" "}
358358
<a
359359
href="https://github.com/Dicklesworthstone/agentic_coding_flywheel_setup"
360360
target="_blank"

apps/web/e2e/wizard-flow.spec.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,7 @@ test.describe("Wizard Flow", () => {
271271
await expect(page).toHaveURL(urlPathWithOptionalQuery("/wizard/ssh-connect"));
272272
const step6Url = new URL(page.url());
273273
expect(step6Url.searchParams.get("os")).toBe("mac");
274-
expect(step6Url.searchParams.get("ip")).toBe("192.168.1.100");
274+
expect(step6Url.searchParams.get("ip")).toBeNull();
275275
});
276276
});
277277

@@ -428,9 +428,9 @@ test.describe("State Persistence", () => {
428428

429429
test("should persist VPS IP across page reloads", async ({ page }) => {
430430
// Set up prerequisite state
431-
await page.goto("/");
432-
await page.evaluate(() => {
433-
localStorage.setItem("agent-flywheel-user-os", "mac");
431+
await setupWizardState(page, {
432+
os: "mac",
433+
completedSteps: [1, 2, 3, 4],
434434
});
435435

436436
await page.goto("/wizard/create-vps");
@@ -464,8 +464,8 @@ test.describe("State Persistence", () => {
464464
const ip = await page.evaluate(() => localStorage.getItem("agent-flywheel-vps-ip"));
465465
expect(ip).toBe("10.0.0.50");
466466

467-
// URL query string should also reflect the IP
468-
expect(new URL(page.url()).searchParams.get("ip")).toBe("10.0.0.50");
467+
// When localStorage works, the IP should stay out of the URL.
468+
expect(new URL(page.url()).searchParams.get("ip")).toBeNull();
469469
});
470470
});
471471

@@ -669,7 +669,7 @@ test.describe("Complete Wizard Flow Integration", () => {
669669
await expect(page).toHaveURL(urlPathWithOptionalQuery("/wizard/ssh-connect"));
670670
const sshConnectUrl = new URL(page.url());
671671
expect(sshConnectUrl.searchParams.get("os")).toBe("mac");
672-
expect(sshConnectUrl.searchParams.get("ip")).toBe("192.168.1.100");
672+
expect(sshConnectUrl.searchParams.get("ip")).toBeNull();
673673

674674
// Step 6: SSH Connect - THE CRITICAL TEST
675675
// This should NOT get stuck on a loading spinner

apps/web/lib/progressPersistence.test.ts

Lines changed: 66 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,33 +21,65 @@ import {
2121
CREATE_VPS_CHECKLIST_KEY,
2222
getACFSRef,
2323
getCreateVPSChecklist,
24+
getVPSIP,
2425
setACFSRef,
2526
setCreateVPSChecklist,
27+
setVPSIP,
2628
} from "./userPreferences";
2729

2830
type StorageController = {
2931
dispatchCalls: Event[];
32+
getCurrentUrl: () => string | null;
3033
getStoredValue: (key: string) => string | null;
3134
};
3235

3336
const originalWindow = globalThis.window;
3437
const originalLocalStorage = globalThis.localStorage;
38+
const VPS_IP_TEST_KEY = "agent-flywheel-vps-ip";
3539

3640
function installMockBrowser(options?: {
3741
failSetItemForKey?: string;
3842
initialValues?: Record<string, string>;
43+
url?: string;
3944
}): StorageController {
4045
const dispatchCalls: Event[] = [];
4146
const storage = new Map(Object.entries(options?.initialValues ?? {}));
47+
let currentUrl = options?.url ? new URL(options.url) : null;
48+
let historyState: unknown = null;
49+
50+
const windowValue = {
51+
dispatchEvent(event: Event) {
52+
dispatchCalls.push(event);
53+
return true;
54+
},
55+
};
56+
57+
if (currentUrl) {
58+
Object.defineProperty(windowValue, "location", {
59+
configurable: true,
60+
get() {
61+
return currentUrl;
62+
},
63+
});
64+
Object.defineProperty(windowValue, "history", {
65+
configurable: true,
66+
value: {
67+
get state() {
68+
return historyState;
69+
},
70+
replaceState(state: unknown, _unused: string, url?: string | URL | null) {
71+
historyState = state;
72+
if (url) {
73+
currentUrl = new URL(String(url), currentUrl?.href);
74+
}
75+
},
76+
},
77+
});
78+
}
4279

4380
Object.defineProperty(globalThis, "window", {
4481
configurable: true,
45-
value: {
46-
dispatchEvent(event: Event) {
47-
dispatchCalls.push(event);
48-
return true;
49-
},
50-
},
82+
value: windowValue,
5183
});
5284

5385
Object.defineProperty(globalThis, "localStorage", {
@@ -70,6 +102,9 @@ function installMockBrowser(options?: {
70102

71103
return {
72104
dispatchCalls,
105+
getCurrentUrl() {
106+
return currentUrl?.toString() ?? null;
107+
},
73108
getStoredValue(key: string) {
74109
return storage.get(key) ?? null;
75110
},
@@ -187,6 +222,31 @@ describe("progress persistence guards", () => {
187222
expect(failingBrowser.dispatchCalls).toHaveLength(0);
188223
});
189224

225+
test("VPS IP stays out of the URL when localStorage works", () => {
226+
const browser = installMockBrowser({
227+
url: "https://example.test/wizard/create-vps?os=mac&ip=192.0.2.10",
228+
});
229+
230+
expect(setVPSIP("10.0.0.50")).toBe(true);
231+
expect(browser.getStoredValue(VPS_IP_TEST_KEY)).toBe("10.0.0.50");
232+
expect(new URL(browser.getCurrentUrl() ?? "").searchParams.get("ip")).toBeNull();
233+
expect(getVPSIP()).toBe("10.0.0.50");
234+
expect(browser.dispatchCalls).toHaveLength(1);
235+
});
236+
237+
test("VPS IP uses the URL only when localStorage is blocked", () => {
238+
const browser = installMockBrowser({
239+
failSetItemForKey: VPS_IP_TEST_KEY,
240+
url: "https://example.test/wizard/create-vps?os=mac",
241+
});
242+
243+
expect(setVPSIP("10.0.0.50")).toBe(true);
244+
expect(browser.getStoredValue(VPS_IP_TEST_KEY)).toBeNull();
245+
expect(new URL(browser.getCurrentUrl() ?? "").searchParams.get("ip")).toBe("10.0.0.50");
246+
expect(getVPSIP()).toBe("10.0.0.50");
247+
expect(browser.dispatchCalls).toHaveLength(1);
248+
});
249+
190250
test("ACFS ref persistence rejects invalid refs without clearing the saved ref", () => {
191251
const browser = installMockBrowser({
192252
initialValues: {

apps/web/lib/userPreferences.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ export function detectOS(): OperatingSystem | null {
185185
}
186186

187187
/**
188-
* Get the user's VPS IP address from localStorage.
188+
* Get the user's VPS IP address from the URL fallback or localStorage.
189189
*/
190190
export function getVPSIP(): string | null {
191191
const fromQuery = getQueryParam(VPS_IP_QUERY_KEY);
@@ -203,16 +203,15 @@ export function getVPSIP(): string | null {
203203

204204
/**
205205
* Save the user's VPS IP address to localStorage.
206-
* Only saves if the IP is valid to prevent storing malformed data.
207-
* Returns true if saved successfully, false otherwise.
206+
* Only keeps the IP in the URL when localStorage is unavailable.
208207
*/
209208
export function setVPSIP(ip: string): boolean {
210209
const normalized = ip.trim();
211210
if (!isValidIP(normalized)) {
212211
return false;
213212
}
214213
const storedOk = safeSetItem(VPS_IP_KEY, normalized);
215-
const urlOk = setQueryParam(VPS_IP_QUERY_KEY, normalized);
214+
const urlOk = setQueryParam(VPS_IP_QUERY_KEY, storedOk ? null : normalized);
216215
if (storedOk || urlOk) {
217216
emitUserPreferencesUpdate();
218217
}

0 commit comments

Comments
 (0)