fix: cloud proxy endpoint shows undefined/v1 when env var not set (#171)

- syncAndVerify now returns cloudUrl in API response for frontend to use
- EndpointPageClient uses dynamic cloudBaseUrl state instead of relying on env var
- Falls back gracefully when NEXT_PUBLIC_CLOUD_URL is not set (Docker deployments)
- Fixed setInterval in accountFallback.ts global scope for Cloudflare Workers compat
This commit is contained in:
diegosouzapw
2026-03-02 10:18:21 -03:00
parent 8fbae5e467
commit 527c542d6d
3 changed files with 33 additions and 10 deletions

View File

@@ -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<typeof setInterval> | 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,

View File

@@ -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 (
<div className="flex flex-col gap-8">

View File

@@ -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",
});