From ce52d957f9880e76739d71778cd845ba26dc11aa Mon Sep 17 00:00:00 2001 From: Dariusz L Date: Sun, 2 Aug 2026 15:28:52 +0200 Subject: [PATCH] feat(frontend): explain CivArchive availability errors --- tests/test_search_source_methods.mjs | 27 +++++++++++++++++++ .../actions/resolve_download_methods.js | 20 +++++++++++--- web/resolver/search/search_source_methods.js | 19 +++++++++++++ web/resolver/views/missing_browser_methods.js | 8 +++++- 4 files changed, 69 insertions(+), 5 deletions(-) diff --git a/tests/test_search_source_methods.mjs b/tests/test_search_source_methods.mjs index a0ccc0e..5c67c87 100644 --- a/tests/test_search_source_methods.mjs +++ b/tests/test_search_source_methods.mjs @@ -4,6 +4,7 @@ import { searchSourceMethods } from '../web/resolver/search/search_source_method const { getSearchSourceLabel, + getSearchSourceErrorMessage, getSearchSourceDefinitions, getSearchResultKeysForSources, getHashLookupSourcesForSearchSources, @@ -52,6 +53,32 @@ test('search source labels and definitions remain stable', () => { ); }); +test('CivArchive availability failures receive a user-facing retry message', () => { + const dialog = { getSearchSourceLabel }; + const temporaryFailure = getSearchSourceErrorMessage.call( + dialog, + 'civarchive', + 'CivArchive search failed: CivArchive search request failed (network error or timeout)' + ); + + assert.equal( + temporaryFailure, + 'CivArchive may be overloaded or temporarily unavailable. Please try again later.' + ); + assert.equal( + getSearchSourceErrorMessage.call(dialog, 'civarchive', 'CivArchive search failed: HTTP 502'), + temporaryFailure + ); + assert.equal( + getSearchSourceErrorMessage.call(dialog, 'civarchive', 'CivArchive search failed: HTTP 400'), + 'CivArchive search failed: HTTP 400' + ); + assert.equal( + getSearchSourceErrorMessage.call(dialog, 'civitai', 'CivAI request timed out'), + 'CivAI request timed out' + ); +}); + test('search source selection maps source ids to result and hash keys', () => { assert.deepEqual( getSearchResultKeysForSources('local'), diff --git a/web/resolver/actions/resolve_download_methods.js b/web/resolver/actions/resolve_download_methods.js index 60c76a5..a2540e4 100644 --- a/web/resolver/actions/resolve_download_methods.js +++ b/web/resolver/actions/resolve_download_methods.js @@ -2421,6 +2421,7 @@ export const resolveDownloadMethods = { } const attemptedSources = new Set(); + const sourceErrorMessages = new Set(); let anyFound = false; let hadError = false; @@ -2482,12 +2483,16 @@ export const resolveDownloadMethods = { const sourceError = sourceErrors[source] || responseSources.map(responseSource => sourceErrors[responseSource]).find(Boolean) || null; + const sourceErrorMessage = sourceError + ? (this.getSearchSourceErrorMessage?.(source, sourceError) || sourceError) + : null; + if (sourceErrorMessage) sourceErrorMessages.add(sourceErrorMessage); const found = this.hasSearchResults(data); const foundViaAnyModel = this.isAnyModelSearchResult?.(data[source]); const foundMessage = foundViaAnyModel ? 'Found (Any Model)' : 'Found'; anyFound = anyFound || found; - hadError = hadError || Boolean(sourceError); + hadError = hadError || Boolean(sourceErrorMessage); state.results = this.mergeSearchResults(state.results, data, { searchedAt: new Date().toISOString(), forceRefresh: Boolean(forceSearch) @@ -2500,7 +2505,7 @@ export const resolveDownloadMethods = { this.setSourceProgress(state, source, { status: sourceError ? 'error' : (found ? 'found' : 'none'), percent: 100, - message: sourceError ? 'Error' : (found ? foundMessage : 'No match'), + message: sourceErrorMessage || (found ? foundMessage : 'No match'), error: sourceError || null }, missing, { workflowKey }); @@ -2545,6 +2550,10 @@ export const resolveDownloadMethods = { hadError = true; attemptedSources.add(source); + const sourceErrorMessage = this.getSearchSourceErrorMessage?.(source, error.message) + || error.message + || 'Error'; + sourceErrorMessages.add(sourceErrorMessage); state.lastAttemptSources = Array.from(attemptedSources); state.lastAttemptFound = anyFound; this.clearSearchProgressTimer(searchRunId, source); @@ -2552,7 +2561,8 @@ export const resolveDownloadMethods = { this.setSourceProgress(state, source, { status: 'error', percent: 100, - message: error.message || 'Error' + message: sourceErrorMessage, + error: error.message || null }, missing, { workflowKey }); this.persistSearchStateForWorkflow(workflowKey, missing, state); this.refreshSearchUiForMissing(missing, state, { workflowKey }); @@ -2590,7 +2600,9 @@ export const resolveDownloadMethods = { state.lastAttemptSources = attemptedSources.size ? Array.from(attemptedSources) : sourceIds; state.lastAttemptFound = anyFound; state.lastAttemptError = hadError && !anyFound - ? 'Search finished with errors. Check source statuses above.' + ? (sourceErrorMessages.size === 1 + ? Array.from(sourceErrorMessages)[0] + : 'Search finished with errors. Check source statuses above.') : null; this.persistSearchStateForWorkflow(workflowKey, missing, state); this.refreshSearchUiForMissing(missing, state, { workflowKey }); diff --git a/web/resolver/search/search_source_methods.js b/web/resolver/search/search_source_methods.js index b76a2e2..69c78e7 100644 --- a/web/resolver/search/search_source_methods.js +++ b/web/resolver/search/search_source_methods.js @@ -73,6 +73,25 @@ export const searchSourceMethods = { return labels[source] || source; }, + getSearchSourceErrorMessage(source, error) { + const message = String(error || '').trim(); + 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 message; + }, + getSearchSourceDefinitions() { return [ { diff --git a/web/resolver/views/missing_browser_methods.js b/web/resolver/views/missing_browser_methods.js index 1736bec..34384a0 100644 --- a/web/resolver/views/missing_browser_methods.js +++ b/web/resolver/views/missing_browser_methods.js @@ -273,6 +273,9 @@ export const missingBrowserMethods = { return sourceItems.map(item => { const status = this.getMissingSourceStatus(missing, item.source); + const sourceProgress = this.searchResultCache + ?.get(this.getMissingSearchKey(missing)) + ?.sourceProgress?.[item.source]; const statusClass = String(status || 'idle').replace(/[^a-z0-9_-]/gi, ''); const label = this.getSearchSourceLabel(item.source); const statusLabels = { @@ -285,7 +288,10 @@ export const missingBrowserMethods = { error: 'Error', idle: 'Not searched' }; - const title = `${label}: ${statusLabels[status] || status}`; + const statusMessage = sourceProgress?.status === 'error' && sourceProgress.message + ? sourceProgress.message + : (statusLabels[status] || status); + const title = `${label}: ${statusMessage}`; const iconName = this.getSearchSourceIconName(item.source); const iconHtml = getSvgIcon(iconName, 'currentColor', 'mr-missing-source-icon'); return `${iconHtml}`;