From 91015b649923122f816197165517492845638579 Mon Sep 17 00:00:00 2001 From: diegosouzapw Date: Sun, 15 Mar 2026 18:48:40 -0300 Subject: [PATCH] fix(media,auth,oauth): hide unconfigured local providers, improve round-robin, fix OAuth popup (#390 #340 #344) --- .../dashboard/media/MediaPageClient.tsx | 41 ++++++++++++++- src/shared/components/OAuthModal.tsx | 51 ++++++++++++++++++- src/sse/services/auth.ts | 10 ++++ 3 files changed, 100 insertions(+), 2 deletions(-) diff --git a/src/app/(dashboard)/dashboard/media/MediaPageClient.tsx b/src/app/(dashboard)/dashboard/media/MediaPageClient.tsx index 7a88e6a8b7..d3aa8e0a11 100644 --- a/src/app/(dashboard)/dashboard/media/MediaPageClient.tsx +++ b/src/app/(dashboard)/dashboard/media/MediaPageClient.tsx @@ -419,7 +419,46 @@ export default function MediaPageClient() { // Transcription-specific const [audioFile, setAudioFile] = useState(null); - const currentProviders = PROVIDER_MODELS[activeTab] ?? []; + // Fix #390: Track which local providers (sdwebui, comfyui) are actually configured + // so we can hide them when they haven't been set up in the providers page + const LOCAL_PROVIDERS = ["sdwebui", "comfyui"]; + const [configuredLocalProviders, setConfiguredLocalProviders] = useState>( + new Set(LOCAL_PROVIDERS) // Optimistic: show all until we know otherwise + ); + + useEffect(() => { + // Fetch configured provider connections to determine which local providers are set up + fetch("/api/providers") + .then((r) => r.json()) + .then((data) => { + const connections: { provider?: string; testStatus?: string }[] = Array.isArray(data) + ? data + : (data?.connections ?? data?.providers ?? []); + const configured = new Set(); + for (const conn of connections) { + const pId = conn?.provider; + if (pId && LOCAL_PROVIDERS.includes(pId)) { + configured.add(pId); + } + } + // Only update if at least one local provider was found, otherwise keep optimistic + if (configured.size > 0) { + setConfiguredLocalProviders(configured); + } else { + // No local providers configured — hide sdwebui/comfyui + setConfiguredLocalProviders(new Set()); + } + }) + .catch(() => { + // On error, keep showing all (fail-open) + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + // Filter out unconfigured local providers from the provider list + const currentProviders = (PROVIDER_MODELS[activeTab] ?? []).filter( + (p) => !LOCAL_PROVIDERS.includes(p.id) || configuredLocalProviders.has(p.id) + ); const currentModels = currentProviders.find((p) => p.id === selectedProvider)?.models ?? []; const switchTab = (tab: Modality) => { diff --git a/src/shared/components/OAuthModal.tsx b/src/shared/components/OAuthModal.tsx index cfd81087aa..534ad01893 100644 --- a/src/shared/components/OAuthModal.tsx +++ b/src/shared/components/OAuthModal.tsx @@ -433,6 +433,51 @@ export default function OAuthModal({ }; }, [authData, exchangeTokens]); + // Fix #344: Detect when OAuth popup is closed without completing authorization + // Some providers (like iFlow) redirect to their own chat UI instead of sending a callback, + // leaving the modal stuck at "Waiting for Authorization" forever. + useEffect(() => { + if (step !== "waiting" || isDeviceCode || !popupRef.current) return; + + let closed = false; + const popupClosedInterval = setInterval(() => { + if (callbackProcessedRef.current) { + clearInterval(popupClosedInterval); + return; + } + try { + if (popupRef.current?.closed) { + closed = true; + clearInterval(popupClosedInterval); + // Popup was closed without completing OAuth — switch to manual input mode + // so user can paste the callback URL from their browser address bar + if (step === "waiting") { + setStep("input"); + } + } + } catch { + // Cross-origin access may throw — ignore + } + }, 1000); + + // Safety timeout: 5 minutes + const safetyTimeout = setTimeout( + () => { + if (!callbackProcessedRef.current && step === "waiting") { + clearInterval(popupClosedInterval); + setStep("input"); + } + }, + 5 * 60 * 1000 + ); + + return () => { + clearInterval(popupClosedInterval); + clearTimeout(safetyTimeout); + }; + + }, [step, isDeviceCode]); + // Handle manual URL input const handleManualSubmit = async () => { try { @@ -471,9 +516,13 @@ export default function OAuthModal({

Waiting for Authorization

-

+

Complete the authorization in the popup window.

+

+ If the popup closes without redirecting back (e.g. iFlow), this dialog will + automatically switch to manual URL input mode. +

diff --git a/src/sse/services/auth.ts b/src/sse/services/auth.ts index 1e3214af1f..ac8bd67785 100644 --- a/src/sse/services/auth.ts +++ b/src/sse/services/auth.ts @@ -382,7 +382,13 @@ export async function getProviderCredentials( }); } else { // Pick the least recently used (excluding current if possible) + // Also penalize accounts with high backoffLevel (previously rate-limited) + // so they don't get immediately re-selected after cooldown (#340) const sortedByOldest = [...orderedConnections].sort((a: any, b: any) => { + // Penalize previously rate-limited accounts (backoffLevel > 0) + const aBackoff = a.backoffLevel || 0; + const bBackoff = b.backoffLevel || 0; + if (aBackoff !== bBackoff) return aBackoff - bBackoff; // lower backoff first if (!a.lastUsedAt && !b.lastUsedAt) return (a.priority || 999) - (b.priority || 999); if (!a.lastUsedAt) return -1; if (!b.lastUsedAt) return 1; @@ -404,7 +410,11 @@ export async function getProviderCredentials( } else { // Fallback scenario: excluded an account due to failure // Always pick the least recently used to ensure proper cycling + // Also penalize accounts with high backoffLevel (#340) const sortedByOldest = [...orderedConnections].sort((a: any, b: any) => { + const aBackoff = a.backoffLevel || 0; + const bBackoff = b.backoffLevel || 0; + if (aBackoff !== bBackoff) return aBackoff - bBackoff; if (!a.lastUsedAt && !b.lastUsedAt) return (a.priority || 999) - (b.priority || 999); if (!a.lastUsedAt) return -1; if (!b.lastUsedAt) return 1;