feat: improve CivArchive status feedback

This commit is contained in:
Dariusz L
2026-08-02 22:06:38 +02:00
parent 0531f4e9b2
commit bb3ccdd47f
7 changed files with 139 additions and 18 deletions
@@ -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' };
+30 -2
View File
@@ -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.'
);
});
+17
View File
@@ -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);
+26 -7
View File
@@ -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;