Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions frontend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ BACKEND_URL=http://localhost:8000
# Supported as a deployment-compatible fallback for existing environments.
NEXT_PUBLIC_BACKEND_URL=http://localhost:8000

# Optional public backend origin used only for the unauthenticated health
# probe that wakes a sleeping Render service. Login and private data requests
# continue through BACKEND_URL and the same-origin frontend proxy.
NEXT_PUBLIC_BACKEND_WAKE_URL=http://localhost:8000

# Same-browser WordPress launcher used only when the app is opened outside the
# [calorieapp_embed] page. Keep this on the canonical CalorieToken site.
NEXT_PUBLIC_WORDPRESS_APP_URL=https://calorietoken.net/calorieapp/
Expand Down
3 changes: 2 additions & 1 deletion frontend/components/FoodSearchPlaceholder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
} from "@/components/authEvents";
import type { AuthStateChangedDetail } from "@/components/authEvents";
import {
BACKEND_WAKE_BASE_URL,
backendRequest,
backendUnavailableMessage,
waitForBackendReady,
Expand Down Expand Up @@ -372,7 +373,7 @@ export function FoodSearchPlaceholder() {
);

try {
await waitForBackendReady(BACKEND_BASE_URL, controller.signal);
await waitForBackendReady(BACKEND_WAKE_BASE_URL, controller.signal);
setSearchStatus("Searching foods...");

const response = await backendRequest(
Expand Down
11 changes: 6 additions & 5 deletions frontend/components/XamanLoginPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { announceAuthState } from "@/components/authEvents";
import {
BACKEND_WAKE_BASE_URL,
backendRequest,
backendUnavailableMessage,
BackendRequestTimeoutError,
Expand Down Expand Up @@ -332,7 +333,7 @@ export async function prepareEmbeddedLogin(
throw signal.reason ?? new Error("Login cancelled");
}
onProgress("waking-up");
await waitForBackendReady(BACKEND_BASE_URL, signal);
await waitForBackendReady(BACKEND_WAKE_BASE_URL, signal);
return startLoginWithRetry(signal, onProgress, retryWindowMs);
}

Expand Down Expand Up @@ -699,10 +700,10 @@ export function XamanLoginPanel() {
}
}, 8_000);

// Keep the mobile browser on the CalorieApp origin while Render wakes.
// A direct cross-origin health request can be blocked by browser privacy
// controls before the WordPress/Xaman navigation has even started.
await waitForBackendReady(BACKEND_BASE_URL, controller.signal);
// Wake Render directly from the browser. Requests relayed from the
// frontend Render service can be rate-limited without starting the
// sleeping backend; the health probe is public and sends no credentials.
await waitForBackendReady(BACKEND_WAKE_BASE_URL, controller.signal);
window.clearTimeout(startupNoticeTimer);
startupNoticeTimer = null;
setLoginStatus("Service ready. Opening Xaman...");
Expand Down
7 changes: 7 additions & 0 deletions frontend/lib/backendRequest.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
export const DEFAULT_BACKEND_TIMEOUT_MS = 20_000;
export const DEFAULT_BACKEND_WARMUP_TIMEOUT_MS = 180_000;

// Render may not wake one free service from another free service's proxy
// request. In production this public URL lets the browser wake the backend
// directly with the unauthenticated health probe; all application requests
// continue to use the same-origin proxy.
export const BACKEND_WAKE_BASE_URL =
process.env.NEXT_PUBLIC_BACKEND_WAKE_URL?.trim() || "/api/backend";

Comment on lines +4 to +10
const BACKEND_WARMUP_ATTEMPT_TIMEOUT_MS = 70_000;
const BACKEND_WARMUP_INITIAL_RETRY_DELAY_MS = 5_000;
const BACKEND_WARMUP_MAX_RETRY_DELAY_MS = 30_000;
Expand Down
12 changes: 9 additions & 3 deletions tools/tests/xaman_login_start_retry.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ test("login start retries transport errors and transient responses", async () =>
}
if (specifier === "@/lib/backendRequest") {
return {
BACKEND_WAKE_BASE_URL: "https://backend.example",
backendRequest,
backendUnavailableMessage: (_error, fallback) => fallback,
BackendRequestTimeoutError: TestBackendRequestTimeoutError,
Expand Down Expand Up @@ -169,14 +170,15 @@ test("embedded login wakes the backend before creating login state", async () =>
}
if (specifier === "@/lib/backendRequest") {
return {
BACKEND_WAKE_BASE_URL: "https://backend.example",
backendRequest: async () => {
events.push("login-start");
return loginResponse;
},
backendUnavailableMessage: (_error, fallback) => fallback,
BackendRequestTimeoutError: TestBackendRequestTimeoutError,
waitForBackendReady: async () => {
events.push("backend-ready");
waitForBackendReady: async (backendUrl) => {
events.push(`backend-ready:${backendUrl}`);
},
};
}
Expand All @@ -201,7 +203,10 @@ test("embedded login wakes the backend before creating login state", async () =>
10_000
);

assert.deepEqual(events, ["backend-ready", "login-start"]);
assert.deepEqual(events, [
"backend-ready:https://backend.example",
"login-start",
]);
assert.deepEqual(phases, ["waking-up"]);
assert.equal(result.state, "state-abcdefghijklmnopqrstuvwxyz-0123456789");
});
Expand Down Expand Up @@ -244,6 +249,7 @@ test("embedded login does not report progress after cancellation", async () => {
}
if (specifier === "@/lib/backendRequest") {
return {
BACKEND_WAKE_BASE_URL: "https://backend.example",
backendRequest: async () => {
events.push("login-start");
},
Expand Down