From c4b33580fee69a9abb43c30d8024c3dabcc1dca1 Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 29 Aug 2026 23:00:00 +0200 Subject: [PATCH 1/4] Wake Render backend before embedded login Add prepareEmbeddedLogin function to handle login preparation and backend readiness. --- frontend/components/XamanLoginPanel.tsx | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/frontend/components/XamanLoginPanel.tsx b/frontend/components/XamanLoginPanel.tsx index 76b2dba..59689fd 100644 --- a/frontend/components/XamanLoginPanel.tsx +++ b/frontend/components/XamanLoginPanel.tsx @@ -266,6 +266,7 @@ function retryAfterMilliseconds(response: Response): number { } type LoginStartRetryReason = "rate-limited" | "temporarily-unavailable"; +type EmbeddedLoginPreparationPhase = LoginStartRetryReason | "waking-up"; export async function startLoginWithRetry( signal: AbortSignal, @@ -319,6 +320,19 @@ export async function startLoginWithRetry( throw new BackendRequestTimeoutError(); } +export async function prepareEmbeddedLogin( + signal: AbortSignal, + onProgress: (phase: EmbeddedLoginPreparationPhase) => void, + retryWindowMs = EMBEDDED_LOGIN_START_RETRY_WINDOW_MS +): Promise { + // A GET readiness probe reliably wakes a spun-down Render Free backend. + // Sending login/start as the first request can be rejected by Render's edge + // with 429 responses before the Python service has started. + onProgress("waking-up"); + await waitForBackendReady(BACKEND_BASE_URL, signal); + return startLoginWithRetry(signal, onProgress, retryWindowMs); +} + export function XamanLoginPanel() { const [isLoading, setIsLoading] = useState(false); const [isLoggingOut, setIsLoggingOut] = useState(false); @@ -601,11 +615,13 @@ export function XamanLoginPanel() { ); try { - const data = await startLoginWithRetry( + const data = await prepareEmbeddedLogin( controller.signal, (reason) => { const message = - reason === "rate-limited" + reason === "waking-up" + ? "Xaman is ready. Starting the secure CalorieApp service. This can take about a minute. Keep this page open." + : reason === "rate-limited" ? "Xaman is ready. CalorieApp is temporarily busy and will retry automatically. Keep this page open." : "Xaman is ready. CalorieApp is still starting and will retry automatically. Keep this page open."; setLoginStatus(message); From f319bf8024d69a83c591036d673352f274fdcc0d Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 29 Aug 2026 23:00:49 +0200 Subject: [PATCH 2/4] Test Render wake-up before embedded login --- tools/tests/xaman_login_start_retry.test.mjs | 87 ++++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/tools/tests/xaman_login_start_retry.test.mjs b/tools/tests/xaman_login_start_retry.test.mjs index 7833e89..509247c 100644 --- a/tools/tests/xaman_login_start_retry.test.mjs +++ b/tools/tests/xaman_login_start_retry.test.mjs @@ -118,3 +118,90 @@ test("login start retries transport errors and transient responses", async () => ]); assert.equal(result.state, "state-abcdefghijklmnopqrstuvwxyz-0123456789"); }); + +test("embedded login wakes the backend before creating login state", async () => { + const typescript = requireFromFrontend("typescript"); + const source = await readFile(COMPONENT_PATH, "utf8"); + const compiled = typescript.transpileModule(source, { + compilerOptions: { + jsx: typescript.JsxEmit.ReactJSX, + module: typescript.ModuleKind.CommonJS, + target: typescript.ScriptTarget.ES2022, + }, + }).outputText; + + const events = []; + const loginResponse = { + ok: true, + status: 200, + headers: { get: () => null }, + json: async () => ({ + state: "state-abcdefghijklmnopqrstuvwxyz-0123456789", + expires_at: "2099-01-01T00:00:00Z", + wordpress_signin_url: "https://calorietoken.net/?xl-signin=1", + browser_handoff_token: "token-abcdefghijklmnopqrstuvwxyz-0123456789", + }), + }; + + class TestBackendRequestTimeoutError extends Error {} + const module = { exports: {} }; + const context = vm.createContext({ + AbortController, + Error, + Math, + Number, + Promise, + URL, + clearImmediate, + console, + module, + exports: module.exports, + process: { env: {} }, + require(specifier) { + if (specifier === "react") { + return {}; + } + if (specifier === "react/jsx-runtime") { + return { Fragment: Symbol("Fragment"), jsx() {}, jsxs() {} }; + } + if (specifier === "@/components/authEvents") { + return { announceAuthState() {} }; + } + if (specifier === "@/lib/backendRequest") { + return { + backendRequest: async () => { + events.push("login-start"); + return loginResponse; + }, + backendUnavailableMessage: (_error, fallback) => fallback, + BackendRequestTimeoutError: TestBackendRequestTimeoutError, + waitForBackendReady: async () => { + events.push("backend-ready"); + }, + }; + } + throw new Error(`Unexpected require: ${specifier}`); + }, + setImmediate, + window: { + clearTimeout(timer) { + clearImmediate(timer); + }, + setTimeout(callback) { + return setImmediate(callback); + }, + }, + }); + + vm.runInContext(compiled, context); + const phases = []; + const result = await module.exports.prepareEmbeddedLogin( + new AbortController().signal, + (phase) => phases.push(phase), + 10_000 + ); + + assert.deepEqual(events, ["backend-ready", "login-start"]); + assert.deepEqual(phases, ["waking-up"]); + assert.equal(result.state, "state-abcdefghijklmnopqrstuvwxyz-0123456789"); +}); From 9c0099460735658b0d5c3acc0b56413ded78b7b0 Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 29 Aug 2026 23:06:28 +0200 Subject: [PATCH 3/4] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- frontend/components/XamanLoginPanel.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/frontend/components/XamanLoginPanel.tsx b/frontend/components/XamanLoginPanel.tsx index 59689fd..a0e5ab7 100644 --- a/frontend/components/XamanLoginPanel.tsx +++ b/frontend/components/XamanLoginPanel.tsx @@ -328,6 +328,9 @@ export async function prepareEmbeddedLogin( // A GET readiness probe reliably wakes a spun-down Render Free backend. // Sending login/start as the first request can be rejected by Render's edge // with 429 responses before the Python service has started. + if (signal.aborted) { + throw signal.reason ?? new Error("Login cancelled"); + } onProgress("waking-up"); await waitForBackendReady(BACKEND_BASE_URL, signal); return startLoginWithRetry(signal, onProgress, retryWindowMs); From b20eccce8d277da95692199a8629ea64bbb7ede1 Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 29 Aug 2026 23:08:13 +0200 Subject: [PATCH 4/4] Add test for embedded login cancellation behavior --- tools/tests/xaman_login_start_retry.test.mjs | 77 ++++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/tools/tests/xaman_login_start_retry.test.mjs b/tools/tests/xaman_login_start_retry.test.mjs index 509247c..0073182 100644 --- a/tools/tests/xaman_login_start_retry.test.mjs +++ b/tools/tests/xaman_login_start_retry.test.mjs @@ -205,3 +205,80 @@ test("embedded login wakes the backend before creating login state", async () => assert.deepEqual(phases, ["waking-up"]); assert.equal(result.state, "state-abcdefghijklmnopqrstuvwxyz-0123456789"); }); + +test("embedded login does not report progress after cancellation", async () => { + const typescript = requireFromFrontend("typescript"); + const source = await readFile(COMPONENT_PATH, "utf8"); + const compiled = typescript.transpileModule(source, { + compilerOptions: { + jsx: typescript.JsxEmit.ReactJSX, + module: typescript.ModuleKind.CommonJS, + target: typescript.ScriptTarget.ES2022, + }, + }).outputText; + + const events = []; + class TestBackendRequestTimeoutError extends Error {} + const module = { exports: {} }; + const context = vm.createContext({ + AbortController, + Error, + Math, + Number, + Promise, + URL, + clearImmediate, + console, + module, + exports: module.exports, + process: { env: {} }, + require(specifier) { + if (specifier === "react") { + return {}; + } + if (specifier === "react/jsx-runtime") { + return { Fragment: Symbol("Fragment"), jsx() {}, jsxs() {} }; + } + if (specifier === "@/components/authEvents") { + return { announceAuthState() {} }; + } + if (specifier === "@/lib/backendRequest") { + return { + backendRequest: async () => { + events.push("login-start"); + }, + backendUnavailableMessage: (_error, fallback) => fallback, + BackendRequestTimeoutError: TestBackendRequestTimeoutError, + waitForBackendReady: async () => { + events.push("backend-ready"); + }, + }; + } + throw new Error(`Unexpected require: ${specifier}`); + }, + setImmediate, + window: { + clearTimeout(timer) { + clearImmediate(timer); + }, + setTimeout(callback) { + return setImmediate(callback); + }, + }, + }); + + vm.runInContext(compiled, context); + const controller = new AbortController(); + const abortReason = new Error("cancelled before login"); + controller.abort(abortReason); + + await assert.rejects( + module.exports.prepareEmbeddedLogin( + controller.signal, + (phase) => events.push(phase), + 10_000 + ), + abortReason + ); + assert.deepEqual(events, []); +});