diff --git a/open-sse/services/accountFallback.ts b/open-sse/services/accountFallback.ts index bf481dc5f5..e1335e7f75 100644 --- a/open-sse/services/accountFallback.ts +++ b/open-sse/services/accountFallback.ts @@ -24,14 +24,25 @@ export function getProviderProfile(provider) { // In-memory map: "provider:connectionId:model" → { reason, until, lockedAt } const modelLockouts = new Map(); -// Auto-cleanup expired lockouts every 15 seconds -const _cleanupTimer = setInterval(() => { - const now = Date.now(); - for (const [key, entry] of modelLockouts) { - if (now > entry.until) modelLockouts.delete(key); +// Auto-cleanup expired lockouts every 15 seconds (lazy init for Cloudflare Workers compatibility) +let _cleanupTimer: ReturnType | null = null; + +function ensureCleanupTimer() { + if (_cleanupTimer) return; + try { + _cleanupTimer = setInterval(() => { + const now = Date.now(); + for (const [key, entry] of modelLockouts) { + if (now > entry.until) modelLockouts.delete(key); + } + }, 15_000); + if (typeof _cleanupTimer === "object" && "unref" in _cleanupTimer) { + (_cleanupTimer as any).unref(); // Don't prevent process exit (Node.js only) + } + } catch { + // Cloudflare Workers may not support setInterval outside handlers — skip cleanup timer } -}, 15_000); -_cleanupTimer.unref(); // Don't prevent process exit +} /** * Lock a specific model on a specific account @@ -43,6 +54,7 @@ _cleanupTimer.unref(); // Don't prevent process exit */ export function lockModel(provider, connectionId, model, reason, cooldownMs) { if (!model) return; // No model → skip model-level locking + ensureCleanupTimer(); const key = `${provider}:${connectionId}:${model}`; modelLockouts.set(key, { reason, diff --git a/src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.tsx b/src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.tsx index 7ad90a2b02..8825817f04 100644 --- a/src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.tsx +++ b/src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.tsx @@ -7,7 +7,7 @@ import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard"; import { AI_PROVIDERS, getProviderByAlias } from "@/shared/constants/providers"; import { useTranslations } from "next-intl"; -const CLOUD_URL = process.env.NEXT_PUBLIC_CLOUD_URL; +const CLOUD_URL = process.env.NEXT_PUBLIC_CLOUD_URL || null; const CLOUD_ACTION_TIMEOUT_MS = 15000; export default function APIPageClient({ machineId }) { @@ -29,6 +29,7 @@ export default function APIPageClient({ machineId }) { const [syncStep, setSyncStep] = useState(""); // "syncing" | "verifying" | "disabling" | "done" | "" const [modalSuccess, setModalSuccess] = useState(false); // show success state in modal before closing const [selectedProvider, setSelectedProvider] = useState(null); // for provider models popup + const [cloudBaseUrl, setCloudBaseUrl] = useState(CLOUD_URL); // dynamic cloud URL from API response const { copied, copy } = useCopyToClipboard(); @@ -204,6 +205,10 @@ export default function APIPageClient({ machineId }) { if (data.createdKey) { await fetchData(); } + // Update cloud URL from API response (fixes undefined/v1 when env var not set) + if (data.cloudUrl) { + setCloudBaseUrl(data.cloudUrl); + } // Reload settings to ensure fresh state await loadCloudSettings(); } else { @@ -274,7 +279,7 @@ export default function APIPageClient({ machineId }) { }; const [baseUrl, setBaseUrl] = useState("/v1"); - const cloudEndpointNew = `${CLOUD_URL}/v1`; + const cloudEndpointNew = cloudBaseUrl ? `${cloudBaseUrl}/v1` : null; // Hydration fix: Only access window on client side useEffect(() => { @@ -293,7 +298,7 @@ export default function APIPageClient({ machineId }) { } // Use new format endpoint (machineId embedded in key) - const currentEndpoint = cloudEnabled ? cloudEndpointNew : baseUrl; + const currentEndpoint = cloudEnabled && cloudEndpointNew ? cloudEndpointNew : baseUrl; return (
diff --git a/src/app/api/sync/cloud/route.ts b/src/app/api/sync/cloud/route.ts index d40306b418..8e6df37f41 100644 --- a/src/app/api/sync/cloud/route.ts +++ b/src/app/api/sync/cloud/route.ts @@ -114,11 +114,15 @@ async function syncAndVerify(machineId: string, createdKey: any, existingKeys: a ); } + // Build the cloud URL for the frontend to use + const cloudUrl = CLOUD_URL ? `${CLOUD_URL}/${machineId}` : null; + // Step 2: Verify connection by pinging the cloud (with retry) const apiKey = createdKey || existingKeys[0]?.key; if (!apiKey) { return NextResponse.json({ ...syncResult, + cloudUrl, verified: false, verifyError: "No API key available", }); @@ -146,6 +150,7 @@ async function syncAndVerify(machineId: string, createdKey: any, existingKeys: a if (pingResponse.ok) { return NextResponse.json({ ...syncResult, + cloudUrl, verified: true, }); } @@ -163,6 +168,7 @@ async function syncAndVerify(machineId: string, createdKey: any, existingKeys: a // Sync succeeded but verify failed — still return success with warning return NextResponse.json({ ...syncResult, + cloudUrl, verified: false, verifyError: lastVerifyError || "Verification failed after retries", });