From bb3ccdd47f6e6aaf9d5359fce81d648e560b7012 Mon Sep 17 00:00:00 2001 From: Dariusz L Date: Sun, 2 Aug 2026 22:06:38 +0200 Subject: [PATCH] feat: improve CivArchive status feedback --- tests/test_missing_model_state_methods.mjs | 23 ++++++++++ tests/test_search_source_methods.mjs | 32 +++++++++++++- web/css/resolver-main.css | 17 ++++++++ .../actions/resolve_download_methods.js | 42 +++++++++++++++---- web/resolver/search/search_source_methods.js | 33 +++++++++++---- web/resolver/views/missing_browser_methods.js | 7 +++- .../views/missing_model_state_methods.js | 3 ++ 7 files changed, 139 insertions(+), 18 deletions(-) diff --git a/tests/test_missing_model_state_methods.mjs b/tests/test_missing_model_state_methods.mjs index f17edea..0cd9a48 100644 --- a/tests/test_missing_model_state_methods.mjs +++ b/tests/test_missing_model_state_methods.mjs @@ -49,6 +49,29 @@ test('source statuses distinguish exact, partial, found, and idle results', () = assert.equal(dialog.hasMissingSourceSearchAttempt(missing, 'huggingface'), false); }); +test('provider failures expose unavailable and rate-limited source statuses', () => { + const dialog = createDialog(); + const missing = { key: 'missing-key' }; + dialog.searchResultCache.set('missing-key', { + sourceProgress: { + civarchive: { + status: 'error', + providerState: 'unavailable', + providerMessage: 'CivArchive may be overloaded or temporarily unavailable.' + }, + civitai: { + status: 'error', + providerState: 'rate_limited', + providerMessage: 'Rate limit reached.' + } + }, + results: {} + }); + + assert.equal(dialog.getMissingSourceStatus(missing, 'civarchive'), 'unavailable'); + assert.equal(dialog.getMissingSourceStatus(missing, 'civitai'), 'rate_limited'); +}); + test('known local download sources are hidden until local search was attempted', () => { const dialog = createDialog(); const source = { source: 'model_list', url: 'https://example.test/model' }; diff --git a/tests/test_search_source_methods.mjs b/tests/test_search_source_methods.mjs index 45376b7..2f6c814 100644 --- a/tests/test_search_source_methods.mjs +++ b/tests/test_search_source_methods.mjs @@ -64,7 +64,7 @@ test('CivArchive availability failures receive a user-facing retry message', () assert.equal( temporaryFailure, - 'CivArchive may be overloaded or temporarily unavailable. Please try again later.' + 'CivArchive may be overloaded or temporarily unavailable. Please try again.' ); assert.equal( getSearchSourceErrorMessage.call(dialog, 'civarchive', 'CivArchive search failed: HTTP 502'), @@ -80,7 +80,35 @@ test('CivArchive availability failures receive a user-facing retry message', () ); assert.equal( getSearchSourceErrorTooltip.call(dialog, 'civarchive', 'CivArchive search failed: HTTP 522'), - 'CivArchive may be overloaded or temporarily unavailable. Please try again later. Details: CivArchive search failed: HTTP 522' + 'CivArchive may be overloaded or temporarily unavailable. Please try again. Details: CivArchive search failed: HTTP 522' + ); +}); + +test('structured CivArchive status uses safe messages and HTTP details', () => { + const dialog = { getSearchSourceLabel, getSearchSourceErrorMessage, getSearchSourceErrorTooltip }; + const status = { + state: 'unavailable', + code: 'provider_unavailable', + retryable: true, + http_status: 522, + message: 'CivArchive may be overloaded or temporarily unavailable. Please try again.' + }; + + assert.equal( + getSearchSourceErrorMessage.call(dialog, 'civarchive', 'CivArchive search failed: HTTP 522', status), + status.message + ); + assert.equal( + getSearchSourceErrorTooltip.call(dialog, 'civarchive', '', status), + `${status.message} Details: HTTP 522` + ); + assert.equal( + getSearchSourceErrorMessage.call(dialog, 'civarchive', '', { + state: 'rate_limited', + code: 'rate_limited', + retryable: true + }), + 'CivArchive rate limit was reached. Please try again later.' ); }); diff --git a/web/css/resolver-main.css b/web/css/resolver-main.css index 2dcb697..80ed4cb 100644 --- a/web/css/resolver-main.css +++ b/web/css/resolver-main.css @@ -1137,6 +1137,23 @@ --mr-source-glow-alpha: 0.22; } +.mr-missing-source-unavailable, +.mr-missing-source-rate_limited { + --mr-source-rgb: 255, 190, 92; + --mr-source-fill-alpha: 0.16; + --mr-source-border-hi-alpha: 0.82; + --mr-source-border-alpha: 0.62; + --mr-source-glow-alpha: 0.22; +} + +.mr-missing-source-not_found { + --mr-source-rgb: 150, 155, 162; + --mr-source-fill-alpha: 0.08; + --mr-source-border-hi-alpha: 0.42; + --mr-source-border-alpha: 0.28; + --mr-source-glow-alpha: 0.04; +} + .mr-missing-detail-pane { flex: 0 0 var(--mr-missing-detail-track, 42%); overflow-y: auto; diff --git a/web/resolver/actions/resolve_download_methods.js b/web/resolver/actions/resolve_download_methods.js index 32addcf..4e45eaa 100644 --- a/web/resolver/actions/resolve_download_methods.js +++ b/web/resolver/actions/resolve_download_methods.js @@ -2422,6 +2422,7 @@ export const resolveDownloadMethods = { const attemptedSources = new Set(); const sourceErrorMessages = new Set(); + const notifiedSourceErrors = new Set(); let anyFound = false; let hadError = false; @@ -2480,22 +2481,40 @@ export const resolveDownloadMethods = { const sourceErrors = data.source_errors && typeof data.source_errors === 'object' ? data.source_errors : {}; + const sourceStatuses = data.source_status && typeof data.source_status === 'object' + ? data.source_status + : {}; const sourceError = sourceErrors[source] || responseSources.map(responseSource => sourceErrors[responseSource]).find(Boolean) || null; + const sourceStatus = sourceStatuses[source] + || responseSources.map(responseSource => sourceStatuses[responseSource]).find(Boolean) + || null; + const hasSourceError = Boolean(sourceError || sourceStatus?.state); const sourceErrorMessage = sourceError - ? (this.getSearchSourceErrorMessage?.(source, sourceError) || sourceError) - : null; + ? (this.getSearchSourceErrorMessage?.(source, sourceError, sourceStatus) || sourceError) + : (sourceStatus + ? (this.getSearchSourceErrorMessage?.(source, '', sourceStatus) || sourceStatus.message) + : null); const sourceErrorTooltip = sourceError - ? (this.getSearchSourceErrorTooltip?.(source, sourceError) || sourceErrorMessage) - : null; + ? (this.getSearchSourceErrorTooltip?.(source, sourceError, sourceStatus) || sourceErrorMessage) + : (sourceStatus + ? (this.getSearchSourceErrorTooltip?.(source, '', sourceStatus) || sourceErrorMessage) + : null); if (sourceErrorMessage) sourceErrorMessages.add(sourceErrorMessage); + if (sourceErrorMessage && !notifiedSourceErrors.has(source)) { + this.showNotification?.( + sourceErrorMessage, + sourceStatus?.retryable ? 'warning' : 'error' + ); + notifiedSourceErrors.add(source); + } const found = this.hasSearchResults(data); const foundViaAnyModel = this.isAnyModelSearchResult?.(data[source]); const foundMessage = foundViaAnyModel ? 'Found (Any Model)' : 'Found'; anyFound = anyFound || found; - hadError = hadError || Boolean(sourceErrorMessage); + hadError = hadError || hasSourceError; state.results = this.mergeSearchResults(state.results, data, { searchedAt: new Date().toISOString(), forceRefresh: Boolean(forceSearch) @@ -2506,10 +2525,15 @@ export const resolveDownloadMethods = { this.clearSearchProgressTimer(searchRunId, source); this.clearBackendSearchProgressTimer?.(searchRunId, source); this.setSourceProgress(state, source, { - status: sourceError ? 'error' : (found ? 'found' : 'none'), + status: hasSourceError ? 'error' : (found ? 'found' : 'none'), percent: 100, message: sourceErrorMessage || (found ? foundMessage : 'No match'), - error: sourceErrorTooltip + error: sourceErrorTooltip, + providerState: sourceStatus?.state || null, + providerCode: sourceStatus?.code || null, + providerMessage: sourceStatus?.message || null, + providerHttpStatus: sourceStatus?.http_status || null, + retryable: Boolean(sourceStatus?.retryable) }, missing, { workflowKey }); if (data.civitai) { @@ -2559,6 +2583,10 @@ export const resolveDownloadMethods = { const sourceErrorTooltip = this.getSearchSourceErrorTooltip?.(source, error.message) || sourceErrorMessage; sourceErrorMessages.add(sourceErrorMessage); + if (!notifiedSourceErrors.has(source)) { + this.showNotification?.(sourceErrorMessage, 'error'); + notifiedSourceErrors.add(source); + } state.lastAttemptSources = Array.from(attemptedSources); state.lastAttemptFound = anyFound; this.clearSearchProgressTimer(searchRunId, source); diff --git a/web/resolver/search/search_source_methods.js b/web/resolver/search/search_source_methods.js index 9c897d8..8a2d1d2 100644 --- a/web/resolver/search/search_source_methods.js +++ b/web/resolver/search/search_source_methods.js @@ -73,30 +73,49 @@ export const searchSourceMethods = { return labels[source] || source; }, - getSearchSourceErrorMessage(source, error) { + getSearchSourceErrorMessage(source, error, status = null) { + if (status && typeof status.message === 'string' && status.message.trim()) { + return status.message.trim(); + } + const message = String(error || '').trim(); + const normalizedSource = String(source || '').trim().toLowerCase().replace(/-/g, '_'); + const statusCode = String(status?.code || '').trim().toLowerCase(); + if (normalizedSource === 'civarchive') { + const messagesByCode = { + timeout: 'CivArchive did not respond in time. It may be temporarily overloaded. Please try again.', + network_error: 'CivArchive could not be reached. It may be temporarily unavailable. Please try again.', + provider_unavailable: 'CivArchive may be overloaded or temporarily unavailable. Please try again.', + rate_limited: 'CivArchive rate limit was reached. Please try again later.', + not_found: 'CivArchive did not find a matching page.', + provider_rejected: 'CivArchive rejected the search request.' + }; + if (messagesByCode[statusCode]) return messagesByCode[statusCode]; + } + if (!message) { return `${this.getSearchSourceLabel(source)} search failed.`; } - const normalizedSource = String(source || '').trim().toLowerCase().replace(/-/g, '_'); const indicatesTemporaryUnavailable = ( /network error|timeout|timed out|connection (?:error|reset|refused)|temporarily unavailable/i.test(message) || /\bHTTP\s+(?:429|5\d{2})\b/i.test(message) ); if (normalizedSource === 'civarchive' && indicatesTemporaryUnavailable) { - return 'CivArchive may be overloaded or temporarily unavailable. Please try again later.'; + return 'CivArchive may be overloaded or temporarily unavailable. Please try again.'; } return message; }, - getSearchSourceErrorTooltip(source, error) { + getSearchSourceErrorTooltip(source, error, status = null) { const technicalMessage = String(error || '').trim(); - const displayMessage = this.getSearchSourceErrorMessage(source, technicalMessage); - if (!technicalMessage || technicalMessage === displayMessage) return displayMessage; - return `${displayMessage} Details: ${technicalMessage}`; + const displayMessage = this.getSearchSourceErrorMessage(source, technicalMessage, status); + const statusDetails = status?.http_status ? `HTTP ${status.http_status}` : ''; + const details = statusDetails || technicalMessage; + if (!details || details === displayMessage) return displayMessage; + return `${displayMessage} Details: ${details}`; }, getSearchSourceDefinitions() { diff --git a/web/resolver/views/missing_browser_methods.js b/web/resolver/views/missing_browser_methods.js index 34384a0..5959b97 100644 --- a/web/resolver/views/missing_browser_methods.js +++ b/web/resolver/views/missing_browser_methods.js @@ -285,11 +285,14 @@ export const missingBrowserMethods = { partial: 'Partial match', found: 'Found', none: 'No match', + unavailable: 'Temporarily unavailable', + rate_limited: 'Rate limited', + not_found: 'Provider page not found', error: 'Error', idle: 'Not searched' }; - const statusMessage = sourceProgress?.status === 'error' && sourceProgress.message - ? sourceProgress.message + const statusMessage = sourceProgress?.status === 'error' && (sourceProgress.providerMessage || sourceProgress.message) + ? (sourceProgress.providerMessage || sourceProgress.message) : (statusLabels[status] || status); const title = `${label}: ${statusMessage}`; const iconName = this.getSearchSourceIconName(item.source); diff --git a/web/resolver/views/missing_model_state_methods.js b/web/resolver/views/missing_model_state_methods.js index 1bd2ba7..8d1acf5 100644 --- a/web/resolver/views/missing_model_state_methods.js +++ b/web/resolver/views/missing_model_state_methods.js @@ -25,6 +25,9 @@ export const missingModelStateMethods = { const progress = state?.sourceProgress?.[source]; const resultStatus = this.getMissingSourceResultStatus(missing, source, state); if (progress?.status === 'found') return resultStatus || 'found'; + if (progress?.status === 'error' && progress.providerState) { + return progress.providerState; + } if (progress?.status) return progress.status; if (resultStatus) return resultStatus;