mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
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:
@@ -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,
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user