feat(frontend): explain CivArchive availability errors
This commit is contained in:
@@ -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>`;
|
||||
|
||||
Reference in New Issue
Block a user