diff --git a/src/app/(dashboard)/dashboard/providers/[id]/page.js b/src/app/(dashboard)/dashboard/providers/[id]/page.js index 4b1eb91..bd483b7 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/page.js +++ b/src/app/(dashboard)/dashboard/providers/[id]/page.js @@ -66,8 +66,8 @@ export default function ProviderDetailPage() { const fetchConnections = useCallback(async () => { try { const [connectionsRes, nodesRes] = await Promise.all([ - fetch("/api/providers"), - fetch("/api/provider-nodes"), + fetch("/api/providers", { cache: "no-store" }), + fetch("/api/provider-nodes", { cache: "no-store" }), ]); const connectionsData = await connectionsRes.json(); const nodesData = await nodesRes.json(); @@ -76,7 +76,21 @@ export default function ProviderDetailPage() { setConnections(filtered); } if (nodesRes.ok) { - const node = (nodesData.nodes || []).find((entry) => entry.id === providerId) || null; + let node = (nodesData.nodes || []).find((entry) => entry.id === providerId) || null; + + // Newly created compatible nodes can be briefly unavailable on one worker. + // Retry a few times before showing "Provider not found". + if (!node && isCompatible) { + for (let attempt = 0; attempt < 3; attempt += 1) { + await new Promise((resolve) => setTimeout(resolve, 150)); + const retryRes = await fetch("/api/provider-nodes", { cache: "no-store" }); + if (!retryRes.ok) continue; + const retryData = await retryRes.json(); + node = (retryData.nodes || []).find((entry) => entry.id === providerId) || null; + if (node) break; + } + } + setProviderNode(node); } } catch (error) { @@ -1025,6 +1039,7 @@ function AddApiKeyModal({ isOpen, provider, providerName, isCompatible, isAnthro }); const [validating, setValidating] = useState(false); const [validationResult, setValidationResult] = useState(null); + const [saving, setSaving] = useState(false); const handleValidate = async () => { setValidating(true); @@ -1043,13 +1058,38 @@ function AddApiKeyModal({ isOpen, provider, providerName, isCompatible, isAnthro } }; - const handleSubmit = () => { - onSave({ - name: formData.name, - apiKey: formData.apiKey, - priority: formData.priority, - testStatus: validationResult === "success" ? "active" : "unknown", - }); + const handleSubmit = async () => { + if (!provider || !formData.apiKey) return; + + setSaving(true); + try { + let isValid = false; + try { + setValidating(true); + setValidationResult(null); + const res = await fetch("/api/providers/validate", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ provider, apiKey: formData.apiKey }), + }); + const data = await res.json(); + isValid = !!data.valid; + setValidationResult(isValid ? "success" : "failed"); + } catch { + setValidationResult("failed"); + } finally { + setValidating(false); + } + + await onSave({ + name: formData.name, + apiKey: formData.apiKey, + priority: formData.priority, + testStatus: isValid ? "active" : "unknown", + }); + } finally { + setSaving(false); + } }; if (!provider) return null; @@ -1072,7 +1112,7 @@ function AddApiKeyModal({ isOpen, provider, providerName, isCompatible, isAnthro className="flex-1" />
-
@@ -1097,8 +1137,8 @@ function AddApiKeyModal({ isOpen, provider, providerName, isCompatible, isAnthro onChange={(e) => setFormData({ ...formData, priority: Number.parseInt(e.target.value) || 1 })} />
-
@@ -1256,7 +1321,7 @@ function EditConnectionModal({ isOpen, connection, onSave, onClose }) { )}
- +
diff --git a/src/lib/localDb.js b/src/lib/localDb.js index 690b2c1..481b5df 100644 --- a/src/lib/localDb.js +++ b/src/lib/localDb.js @@ -54,6 +54,60 @@ const defaultData = { pricing: {} // NEW: pricing configuration }; +function cloneDefaultData() { + return { + providerConnections: [], + providerNodes: [], + modelAliases: {}, + combos: [], + apiKeys: [], + settings: { + cloudEnabled: false, + stickyRoundRobinLimit: 3, + requireLogin: true, + }, + pricing: {}, + }; +} + +function ensureDbShape(data) { + const defaults = cloneDefaultData(); + const next = data && typeof data === "object" ? data : {}; + let changed = false; + + for (const [key, defaultValue] of Object.entries(defaults)) { + if (next[key] === undefined || next[key] === null) { + next[key] = defaultValue; + changed = true; + continue; + } + + if ( + key === "settings" && + (typeof next.settings !== "object" || Array.isArray(next.settings)) + ) { + next.settings = { ...defaultValue }; + changed = true; + continue; + } + + if ( + key === "settings" && + typeof next.settings === "object" && + !Array.isArray(next.settings) + ) { + for (const [settingKey, settingDefault] of Object.entries(defaultValue)) { + if (next.settings[settingKey] === undefined) { + next.settings[settingKey] = settingDefault; + changed = true; + } + } + } + } + + return { data: next, changed }; +} + // Singleton instance let dbInstance = null; @@ -64,35 +118,43 @@ export async function getDb() { if (isCloud) { // Return in-memory DB for Workers if (!dbInstance) { - dbInstance = new Low({ read: async () => {}, write: async () => {} }, defaultData); - dbInstance.data = defaultData; + const data = cloneDefaultData(); + dbInstance = new Low({ read: async () => {}, write: async () => {} }, data); + dbInstance.data = data; } return dbInstance; } if (!dbInstance) { const adapter = new JSONFile(DB_FILE); - dbInstance = new Low(adapter, defaultData); + dbInstance = new Low(adapter, cloneDefaultData()); + } - // Try to read DB with error recovery for corrupt JSON - try { - await dbInstance.read(); - } catch (error) { - if (error instanceof SyntaxError) { - console.warn('[DB] Corrupt JSON detected, resetting to defaults...'); - dbInstance.data = defaultData; - await dbInstance.write(); - } else { - throw error; - } + // Always read latest disk state to avoid stale singleton data across route workers. + try { + await dbInstance.read(); + } catch (error) { + if (error instanceof SyntaxError) { + console.warn('[DB] Corrupt JSON detected, resetting to defaults...'); + dbInstance.data = cloneDefaultData(); + await dbInstance.write(); + } else { + throw error; } + } - // Initialize with default data if empty - if (!dbInstance.data) { - dbInstance.data = defaultData; + // Initialize/migrate missing keys for older DB schema versions. + if (!dbInstance.data) { + dbInstance.data = cloneDefaultData(); + await dbInstance.write(); + } else { + const { data, changed } = ensureDbShape(dbInstance.data); + dbInstance.data = data; + if (changed) { await dbInstance.write(); } } + return dbInstance; }