diff --git a/renderer/app.js b/renderer/app.js index 5449469..0de478f 100644 --- a/renderer/app.js +++ b/renderer/app.js @@ -4355,22 +4355,29 @@ async function executeHealthCheck(hosters, _mode, generations) { renderHealthCheckResults([]); const result = await window.api.runHealthCheck({ hosters }); const rows = result && Array.isArray(result.results) ? result.results : []; + const checkedAt = result?.checkedAt || new Date().toISOString(); const currentRows = rows.filter((row) => { if (!row) return false; const key = row.accountId || row.hoster; const generation = generations?.get(key); return generation === undefined || _isCurrentAccountStatusGeneration(key, generation); }); + const completedKeys = new Set(); currentRows.forEach((row) => { const key = row.accountId || row.hoster; if (key) { - accountStatuses[key] = { - status: row.status || 'unchecked', - message: row.message || '', - checkedAt: result.checkedAt || new Date().toISOString() + completedKeys.add(key); + accountStatuses[key] = { + status: row.status || 'unchecked', + message: row.message || '', + checkedAt: row.checkedAt || checkedAt }; } }); + for (const [key, generation] of generations || []) { + if (completedKeys.has(key) || !_isCurrentAccountStatusGeneration(key, generation)) continue; + accountStatuses[key] = { status: 'error', message: 'Keine Antwort vom Hoster erhalten', checkedAt }; + } renderHealthCheckResults(currentRows); renderAccounts(); renderHosterModal(); @@ -4400,12 +4407,16 @@ async function runHealthCheck(mode = 'manual', requestedHosters = null) { for (const h of hosters) { const key = typeof h === 'string' ? h : (h.accountId || h.hoster); generations.set(key, _nextAccountStatusGeneration(key)); - accountStatuses[key] = { status: 'checking', message: '', checkedAt: null }; + accountStatuses[key] = { ...(accountStatuses[key] || {}), status: 'checking', message: '' }; } renderAccounts(); try { return await executeHealthCheck(hosters, mode, generations); } catch (err) { + const checkedAt = new Date().toISOString(); + for (const [key, generation] of generations) { + if (_isCurrentAccountStatusGeneration(key, generation)) accountStatuses[key] = { status: 'error', message: err.message || 'Prüfung fehlgeschlagen', checkedAt }; + } renderHealthCheckResults([{ hoster: 'System', status: 'error', message: err.message }]); return []; } finally { @@ -5470,7 +5481,10 @@ function _buildAccountCardHtml(name, account, idx) { // disambiguator for accounts that otherwise look identical (e.g. two byse // API-key accounts where you can't tell what's what from the masked key). const subtitleText = (userLabel ? `Label: ${userLabel} • ` : '') + credLabel; - const checkedText = st.checkedAt ? ` • geprüft ${new Date(st.checkedAt).toLocaleTimeString(getUiLocale(), { hour: '2-digit', minute: '2-digit' })}` : ''; + const checkedDate = st.checkedAt ? new Date(st.checkedAt) : null; + const checkedText = checkedDate && !Number.isNaN(checkedDate.getTime()) + ? ` • ${localizeUiText('geprüft')} ${checkedDate.toLocaleTimeString(getUiLocale(), { hour: '2-digit', minute: '2-digit' })}` + : ''; const toggleLabel = isDisabled ? 'Aktivieren' : 'Deaktivieren'; const priorityLabel = idx === 0 ? 'Primär' : `Fallback #${idx}`; @@ -6023,16 +6037,19 @@ async function checkSingleAccount(accountId) { if (!found) return; const generation = _nextAccountStatusGeneration(accountId); healthCheckRunning = true; - accountStatuses[accountId] = { status: 'checking', message: '' }; + accountStatuses[accountId] = { ...(accountStatuses[accountId] || {}), status: 'checking', message: '' }; updateAccountCard(accountId); let nextStatus = null; try { const result = await window.api.runHealthCheck({ hosters: [{ hoster: found.name, accountId }] }); const rows = result && Array.isArray(result.results) ? result.results : []; const row = rows.find(r => r.accountId === accountId); - if (row) nextStatus = { status: row.status || 'error', message: row.message || '' }; + const checkedAt = result?.checkedAt || new Date().toISOString(); + nextStatus = row + ? { status: row.status || 'error', message: row.message || '', checkedAt: row.checkedAt || checkedAt } + : { status: 'error', message: 'Keine Antwort vom Hoster erhalten', checkedAt }; } catch (err) { - nextStatus = { status: 'error', message: err.message || 'Prüfung fehlgeschlagen' }; + nextStatus = { status: 'error', message: err.message || 'Prüfung fehlgeschlagen', checkedAt: new Date().toISOString() }; } finally { healthCheckRunning = false; } @@ -6060,7 +6077,7 @@ async function submitAccountOtp(accountId) { const submitButton = card?.querySelector('[data-account-otp-submit]'); const generation = _nextAccountStatusGeneration(accountId); healthCheckRunning = true; - accountStatuses[accountId] = { status: 'checking', message: 'OTP wird geprüft…' }; + accountStatuses[accountId] = { ...(accountStatuses[accountId] || {}), status: 'checking', message: 'OTP wird geprüft…' }; if (otpInput) otpInput.disabled = true; if (submitButton) { submitButton.disabled = true; @@ -6072,11 +6089,12 @@ async function submitAccountOtp(accountId) { const row = result && Array.isArray(result.results) ? result.results.find(item => item.accountId === accountId) : null; + const checkedAt = result?.checkedAt || new Date().toISOString(); nextStatus = row - ? { status: row.status || 'error', message: row.message || '' } - : { status: 'error', message: 'Keine Antwort vom Hoster erhalten' }; + ? { status: row.status || 'error', message: row.message || '', checkedAt: row.checkedAt || checkedAt } + : { status: 'error', message: 'Keine Antwort vom Hoster erhalten', checkedAt }; } catch (err) { - nextStatus = { status: 'error', message: err.message || 'OTP-Prüfung fehlgeschlagen' }; + nextStatus = { status: 'error', message: err.message || 'OTP-Prüfung fehlgeschlagen', checkedAt: new Date().toISOString() }; } finally { healthCheckRunning = false; } @@ -6446,7 +6464,7 @@ function _applyCommittedAccount(persisted, validation) { const { accountId, candidateHosters, isEdit } = persisted; config.hosters = candidateHosters; _invalidateAccountStatusGeneration(accountId); - accountStatuses[accountId] = { status: validation.status, message: validation.message || '' }; + accountStatuses[accountId] = { status: validation.status, message: validation.message || '', checkedAt: validation.checkedAt || new Date().toISOString() }; ensureAccountStatusEntries(); syncSelectedUploadHosters(); if (isEdit) { diff --git a/renderer/i18n.js b/renderer/i18n.js index d1d4473..b8b6815 100644 --- a/renderer/i18n.js +++ b/renderer/i18n.js @@ -128,6 +128,7 @@ ['Warteschlange', 'Queue'], ['Fertig', 'Completed'], ['Fehler', 'Failed'], + ['geprüft', 'checked'], ['Verfügbarkeit', 'Availability'], ['Bereite Accounts', 'Ready accounts'], ['Primär', 'Primary'], diff --git a/tests/i18n.test.js b/tests/i18n.test.js index 78e7a65..2d4f432 100644 --- a/tests/i18n.test.js +++ b/tests/i18n.test.js @@ -16,6 +16,11 @@ test('translates the remaining upload size label', () => { assert.equal(translateText('Remaining size', 'de'), 'Verbleibende Größe'); }); +test('translates the account check timestamp label', () => { + assert.equal(translateText('geprüft', 'en'), 'checked'); + assert.equal(translateText('checked', 'de'), 'geprüft'); +}); + test('English is the fallback language and German remains selectable', () => { assert.equal(normalizeLanguage(), 'en'); assert.equal(normalizeLanguage('fr'), 'en'); diff --git a/tests/ui-smoke.js b/tests/ui-smoke.js index 248af38..955c020 100644 --- a/tests/ui-smoke.js +++ b/tests/ui-smoke.js @@ -1152,9 +1152,9 @@ setTimeout(async () => { check('Account sidebar exposes exactly one pressed filter', accountFilterState.error.pressed.join('|') === 'error' && accountFilterState.error.active.join('|') === 'error' && accountFilterState.all.pressed.join('|') === 'all' && accountFilterState.all.active.join('|') === 'all'); ipcMain.removeHandler('run-health-check'); - ipcMain.handle('run-health-check', (_event, payload) => ({ results: (payload.hosters || []).map(item => ({ accountId: item.accountId, status: 'ok', message: 'Ready' })) })); - const completedAccountCheckState = await wc.executeJavaScript(\`checkSingleAccount('ui-filter-ready').then(() => ({ status: accountStatuses['ui-filter-ready']?.status, generations: accountStatusGenerations.size }))\`); - check('Completed account checks release their generation tokens', completedAccountCheckState.status === 'ok' && completedAccountCheckState.generations === 0); + ipcMain.handle('run-health-check', (_event, payload) => ({ checkedAt: '2026-08-20T12:34:00.000Z', results: (payload.hosters || []).map(item => ({ accountId: item.accountId, status: 'ok', message: 'Ready' })) })); + const completedAccountCheckState = await wc.executeJavaScript(\`checkSingleAccount('ui-filter-ready').then(() => ({ status: accountStatuses['ui-filter-ready']?.status, checkedAt: accountStatuses['ui-filter-ready']?.checkedAt, subtitle: document.querySelector('[data-account-id="ui-filter-ready"] .account-card-subtitle')?.textContent, generations: accountStatusGenerations.size }))\`); + check('Completed account checks expose their timestamp and release generation tokens', completedAccountCheckState.status === 'ok' && completedAccountCheckState.checkedAt === '2026-08-20T12:34:00.000Z' && completedAccountCheckState.subtitle.includes('geprüft') && completedAccountCheckState.generations === 0); restoreInitialIpcHandler('run-health-check'); let resolveStaleAccountCheck = null;