feat: improve CivArchive status feedback
This commit is contained in:
@@ -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' };
|
||||
|
||||
@@ -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.'
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user