feat: copy sanitized upload failure details

Expose a contextual copy action only for failed uploads, target the exact right-clicked job during multi-selection, and defensively redact URLs and credential-shaped values from clipboard diagnostics.
This commit is contained in:
Sucukdeluxe
2026-08-21 02:24:40 +02:00
parent d58854c6c9
commit 8373f3ff7b
6 changed files with 87 additions and 2 deletions
+6
View File
@@ -21,6 +21,12 @@ test('translates the account check timestamp label', () => {
assert.equal(translateText('checked', 'de'), 'geprüft');
});
test('translates the failure detail clipboard action', () => {
assert.equal(translateText('Fehlerdetails kopieren', 'en'), 'Copy failure details');
assert.equal(translateText('Failure details copied', 'de'), 'Fehlerdetails kopiert');
assert.equal(translateText('Antwortauszug', 'en'), 'Response excerpt');
});
test('English is the fallback language and German remains selectable', () => {
assert.equal(normalizeLanguage(), 'en');
assert.equal(normalizeLanguage('fr'), 'en');
+1
View File
@@ -179,6 +179,7 @@ test('header occupies its final geometry before asynchronous initialization', ()
assert.match(html, /class="update-progress-footer"[\s\S]*id="updateProgressDetails"[\s\S]*id="updateProgressSize"[\s\S]*id="updateProgressSpeed"[\s\S]*id="updateProgressEta"[\s\S]*id="updateProgressText"/u);
assert.match(html, /id="queueFilterResetBtn"[^>]*disabled[^>]*>Filter zurücksetzen</u);
assert.match(html, /class="queue-filter-summary"[\s\S]*id="queueActiveFilterCount"[^>]*>0</u);
assert.match(html, /data-action="copy-failure-details"[^>]*style="display:none"[^>]*>Fehlerdetails kopieren</u);
assert.doesNotMatch(html, /<\/div>\s*<div class="queue-filter-bar"/u);
assert.match(css, /\.queue-filter-bar\s*\{[^}]*display:\s*flex;[^}]*margin-left:\s*auto;[^}]*border:\s*1px solid var\(--border\);[^}]*border-radius:\s*7px;/su);
assert.match(css, /#updateProgressDetails\s*\{[^}]*display:\s*grid;[^}]*grid-template-columns:\s*19ch 2ch 11ch 2ch 10ch;[^}]*column-gap:\s*0;[^}]*font-variant-numeric:\s*tabular-nums;/su);
+28
View File
@@ -2092,6 +2092,34 @@ setTimeout(async () => {
if (!(recentCapScrollState.before > 0 && Math.abs(recentCapScrollState.delta - 28) <= 1)) console.log('Recent cap scroll state: ' + JSON.stringify(recentCapScrollState));
check('A capped recent-upload list preserves the visible rows when a new item arrives', recentCapScrollState.before > 0 && Math.abs(recentCapScrollState.delta - 28) <= 1);
const failureClipboardContract = await wc.executeJavaScript(\`(() => {
setUiLanguage('en');
queueJobs = [
{ id: 'failure-a', file: 'C:/ui/failure-a.mkv', fileName: 'failure-a.mkv', hoster: 'byse.sx', accountId: 'account-a', status: 'error', error: 'Wrong failure', attempt: 1, maxAttempts: 3, bytesUploaded: 0, bytesTotal: 100, progress: 0 },
{ id: 'failure-b', file: 'C:/ui/failure-b.mkv', fileName: 'failure-b.mkv', hoster: 'doodstream.com', accountId: 'account-b', status: 'error', error: 'Request failed at https://secret.invalid/path?token=secret', attempt: 2, maxAttempts: 4, failureDetails: { httpStatus: 503, contentType: 'text/html', responseSnippet: 'apiKey=very-secret temporary gateway response' }, bytesUploaded: 0, bytesTotal: 100, progress: 0 }
];
selectedJobIds.clear();
selectedJobIds.add('failure-a');
selectedJobIds.add('failure-b');
rebuildJobIndex();
setUploadSidebarFilter('all');
updateUploadView();
renderQueueTable();
const row = document.querySelector('[data-job-id="failure-b"]');
handleRowContextMenu({ preventDefault() {}, clientX: 50, clientY: 50 }, row);
const item = document.querySelector('[data-action="copy-failure-details"]');
const text = formatFailureDetailsForClipboard(_jobIndexById.get(contextMenuTargetJobId));
hideContextMenu();
setUiLanguage('de');
queueJobs = [];
selectedJobIds.clear();
rebuildJobIndex();
updateUploadView();
renderQueueTable();
return { target: contextMenuTargetJobId, visible: getComputedStyle(item).display !== 'none', label: item.textContent.trim(), text };
})()\`);
check('Failure detail copying targets the right-clicked job and redacts sensitive values', failureClipboardContract.target === 'failure-b' && failureClipboardContract.visible === true && failureClipboardContract.label === 'Copy failure details' && failureClipboardContract.text.includes('File: failure-b.mkv') && failureClipboardContract.text.includes('HTTP status: 503') && failureClipboardContract.text.includes('Content type: text/html') && failureClipboardContract.text.includes('[URL]') && failureClipboardContract.text.includes('apiKey=[redacted]') && !failureClipboardContract.text.includes('secret.invalid') && !failureClipboardContract.text.includes('very-secret'));
const keyboardInteractionContract = await wc.executeJavaScript(\`(() => {
HOSTERS.forEach(name => { config.hosters[name] = []; });
config.hosters['byse.sx'] = [{ id: 'ui-keyboard-account', enabled: true, authType: 'api', apiKey: 'keyboard-key' }];