feat: show the last account check time
Carry session-local check timestamps through bulk, single, OTP, and save validation paths, terminate missing responses cleanly, and render localized checked times on account cards.
This commit is contained in:
+32
-14
@@ -4355,22 +4355,29 @@ async function executeHealthCheck(hosters, _mode, generations) {
|
|||||||
renderHealthCheckResults([]);
|
renderHealthCheckResults([]);
|
||||||
const result = await window.api.runHealthCheck({ hosters });
|
const result = await window.api.runHealthCheck({ hosters });
|
||||||
const rows = result && Array.isArray(result.results) ? result.results : [];
|
const rows = result && Array.isArray(result.results) ? result.results : [];
|
||||||
|
const checkedAt = result?.checkedAt || new Date().toISOString();
|
||||||
const currentRows = rows.filter((row) => {
|
const currentRows = rows.filter((row) => {
|
||||||
if (!row) return false;
|
if (!row) return false;
|
||||||
const key = row.accountId || row.hoster;
|
const key = row.accountId || row.hoster;
|
||||||
const generation = generations?.get(key);
|
const generation = generations?.get(key);
|
||||||
return generation === undefined || _isCurrentAccountStatusGeneration(key, generation);
|
return generation === undefined || _isCurrentAccountStatusGeneration(key, generation);
|
||||||
});
|
});
|
||||||
|
const completedKeys = new Set();
|
||||||
currentRows.forEach((row) => {
|
currentRows.forEach((row) => {
|
||||||
const key = row.accountId || row.hoster;
|
const key = row.accountId || row.hoster;
|
||||||
if (key) {
|
if (key) {
|
||||||
accountStatuses[key] = {
|
completedKeys.add(key);
|
||||||
status: row.status || 'unchecked',
|
accountStatuses[key] = {
|
||||||
message: row.message || '',
|
status: row.status || 'unchecked',
|
||||||
checkedAt: result.checkedAt || new Date().toISOString()
|
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);
|
renderHealthCheckResults(currentRows);
|
||||||
renderAccounts();
|
renderAccounts();
|
||||||
renderHosterModal();
|
renderHosterModal();
|
||||||
@@ -4400,12 +4407,16 @@ async function runHealthCheck(mode = 'manual', requestedHosters = null) {
|
|||||||
for (const h of hosters) {
|
for (const h of hosters) {
|
||||||
const key = typeof h === 'string' ? h : (h.accountId || h.hoster);
|
const key = typeof h === 'string' ? h : (h.accountId || h.hoster);
|
||||||
generations.set(key, _nextAccountStatusGeneration(key));
|
generations.set(key, _nextAccountStatusGeneration(key));
|
||||||
accountStatuses[key] = { status: 'checking', message: '', checkedAt: null };
|
accountStatuses[key] = { ...(accountStatuses[key] || {}), status: 'checking', message: '' };
|
||||||
}
|
}
|
||||||
renderAccounts();
|
renderAccounts();
|
||||||
try {
|
try {
|
||||||
return await executeHealthCheck(hosters, mode, generations);
|
return await executeHealthCheck(hosters, mode, generations);
|
||||||
} catch (err) {
|
} 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 }]);
|
renderHealthCheckResults([{ hoster: 'System', status: 'error', message: err.message }]);
|
||||||
return [];
|
return [];
|
||||||
} finally {
|
} finally {
|
||||||
@@ -5470,7 +5481,10 @@ function _buildAccountCardHtml(name, account, idx) {
|
|||||||
// disambiguator for accounts that otherwise look identical (e.g. two byse
|
// 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).
|
// API-key accounts where you can't tell what's what from the masked key).
|
||||||
const subtitleText = (userLabel ? `Label: ${userLabel} • ` : '') + credLabel;
|
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 toggleLabel = isDisabled ? 'Aktivieren' : 'Deaktivieren';
|
||||||
const priorityLabel = idx === 0 ? 'Primär' : `Fallback #${idx}`;
|
const priorityLabel = idx === 0 ? 'Primär' : `Fallback #${idx}`;
|
||||||
|
|
||||||
@@ -6023,16 +6037,19 @@ async function checkSingleAccount(accountId) {
|
|||||||
if (!found) return;
|
if (!found) return;
|
||||||
const generation = _nextAccountStatusGeneration(accountId);
|
const generation = _nextAccountStatusGeneration(accountId);
|
||||||
healthCheckRunning = true;
|
healthCheckRunning = true;
|
||||||
accountStatuses[accountId] = { status: 'checking', message: '' };
|
accountStatuses[accountId] = { ...(accountStatuses[accountId] || {}), status: 'checking', message: '' };
|
||||||
updateAccountCard(accountId);
|
updateAccountCard(accountId);
|
||||||
let nextStatus = null;
|
let nextStatus = null;
|
||||||
try {
|
try {
|
||||||
const result = await window.api.runHealthCheck({ hosters: [{ hoster: found.name, accountId }] });
|
const result = await window.api.runHealthCheck({ hosters: [{ hoster: found.name, accountId }] });
|
||||||
const rows = result && Array.isArray(result.results) ? result.results : [];
|
const rows = result && Array.isArray(result.results) ? result.results : [];
|
||||||
const row = rows.find(r => r.accountId === accountId);
|
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) {
|
} 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 {
|
} finally {
|
||||||
healthCheckRunning = false;
|
healthCheckRunning = false;
|
||||||
}
|
}
|
||||||
@@ -6060,7 +6077,7 @@ async function submitAccountOtp(accountId) {
|
|||||||
const submitButton = card?.querySelector('[data-account-otp-submit]');
|
const submitButton = card?.querySelector('[data-account-otp-submit]');
|
||||||
const generation = _nextAccountStatusGeneration(accountId);
|
const generation = _nextAccountStatusGeneration(accountId);
|
||||||
healthCheckRunning = true;
|
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 (otpInput) otpInput.disabled = true;
|
||||||
if (submitButton) {
|
if (submitButton) {
|
||||||
submitButton.disabled = true;
|
submitButton.disabled = true;
|
||||||
@@ -6072,11 +6089,12 @@ async function submitAccountOtp(accountId) {
|
|||||||
const row = result && Array.isArray(result.results)
|
const row = result && Array.isArray(result.results)
|
||||||
? result.results.find(item => item.accountId === accountId)
|
? result.results.find(item => item.accountId === accountId)
|
||||||
: null;
|
: null;
|
||||||
|
const checkedAt = result?.checkedAt || new Date().toISOString();
|
||||||
nextStatus = row
|
nextStatus = row
|
||||||
? { status: row.status || 'error', message: row.message || '' }
|
? { status: row.status || 'error', message: row.message || '', checkedAt: row.checkedAt || checkedAt }
|
||||||
: { status: 'error', message: 'Keine Antwort vom Hoster erhalten' };
|
: { status: 'error', message: 'Keine Antwort vom Hoster erhalten', checkedAt };
|
||||||
} catch (err) {
|
} 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 {
|
} finally {
|
||||||
healthCheckRunning = false;
|
healthCheckRunning = false;
|
||||||
}
|
}
|
||||||
@@ -6446,7 +6464,7 @@ function _applyCommittedAccount(persisted, validation) {
|
|||||||
const { accountId, candidateHosters, isEdit } = persisted;
|
const { accountId, candidateHosters, isEdit } = persisted;
|
||||||
config.hosters = candidateHosters;
|
config.hosters = candidateHosters;
|
||||||
_invalidateAccountStatusGeneration(accountId);
|
_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();
|
ensureAccountStatusEntries();
|
||||||
syncSelectedUploadHosters();
|
syncSelectedUploadHosters();
|
||||||
if (isEdit) {
|
if (isEdit) {
|
||||||
|
|||||||
@@ -128,6 +128,7 @@
|
|||||||
['Warteschlange', 'Queue'],
|
['Warteschlange', 'Queue'],
|
||||||
['Fertig', 'Completed'],
|
['Fertig', 'Completed'],
|
||||||
['Fehler', 'Failed'],
|
['Fehler', 'Failed'],
|
||||||
|
['geprüft', 'checked'],
|
||||||
['Verfügbarkeit', 'Availability'],
|
['Verfügbarkeit', 'Availability'],
|
||||||
['Bereite Accounts', 'Ready accounts'],
|
['Bereite Accounts', 'Ready accounts'],
|
||||||
['Primär', 'Primary'],
|
['Primär', 'Primary'],
|
||||||
|
|||||||
@@ -16,6 +16,11 @@ test('translates the remaining upload size label', () => {
|
|||||||
assert.equal(translateText('Remaining size', 'de'), 'Verbleibende Größe');
|
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', () => {
|
test('English is the fallback language and German remains selectable', () => {
|
||||||
assert.equal(normalizeLanguage(), 'en');
|
assert.equal(normalizeLanguage(), 'en');
|
||||||
assert.equal(normalizeLanguage('fr'), 'en');
|
assert.equal(normalizeLanguage('fr'), 'en');
|
||||||
|
|||||||
+3
-3
@@ -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');
|
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.removeHandler('run-health-check');
|
||||||
ipcMain.handle('run-health-check', (_event, payload) => ({ results: (payload.hosters || []).map(item => ({ accountId: item.accountId, status: 'ok', message: 'Ready' })) }));
|
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, generations: accountStatusGenerations.size }))\`);
|
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 release their generation tokens', completedAccountCheckState.status === 'ok' && completedAccountCheckState.generations === 0);
|
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');
|
restoreInitialIpcHandler('run-health-check');
|
||||||
|
|
||||||
let resolveStaleAccountCheck = null;
|
let resolveStaleAccountCheck = null;
|
||||||
|
|||||||
Reference in New Issue
Block a user