feat(frontend): explain CivArchive availability errors

This commit is contained in:
Dariusz L
2026-08-02 15:28:52 +02:00
parent b9cc47cd00
commit ce52d957f9
4 changed files with 69 additions and 5 deletions
+27
View File
@@ -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'),
@@ -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 });
@@ -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 [
{
@@ -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 `<span class="mr-missing-source-dot mr-missing-source-${statusClass}" data-tooltip="${this.escapeHtml(title)}" aria-label="${this.escapeHtml(title)}">${iconHtml}</span>`;