Merge pull request #391 from diegosouzapw/fix/multi-issues-390-340-378

fix(media,auth,oauth): hide unconfigured local providers, round-robin improvement, OAuth popup fix
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-03-15 19:08:45 -03:00
committed by GitHub
3 changed files with 100 additions and 2 deletions

View File

@@ -419,7 +419,46 @@ export default function MediaPageClient() {
// Transcription-specific
const [audioFile, setAudioFile] = useState<File | null>(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<Set<string>>(
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<string>();
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) => {

View File

@@ -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({
</span>
</div>
<h3 className="text-lg font-semibold mb-2">Waiting for Authorization</h3>
<p className="text-sm text-text-muted mb-4">
<p className="text-sm text-text-muted mb-2">
Complete the authorization in the popup window.
</p>
<p className="text-xs text-text-muted mb-4 opacity-70">
If the popup closes without redirecting back (e.g. iFlow), this dialog will
automatically switch to manual URL input mode.
</p>
<Button variant="ghost" onClick={() => setStep("input")}>
Popup blocked? Enter URL manually
</Button>

View File

@@ -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;