This commit is contained in:
+102
-28
@@ -65,6 +65,7 @@ let config = { hosters: {}, hosterSettings: {}, globalSettings: {} };
|
||||
let hosterSettings = {};
|
||||
let uploading = false;
|
||||
let healthCheckRunning = false;
|
||||
let healthCheckRequestSequence = 0;
|
||||
let automationRuntimeStatus = Object.freeze({});
|
||||
let automationRuntimeStatusAvailable = false;
|
||||
let automationPauseResumeBusy = false;
|
||||
@@ -87,6 +88,7 @@ let managedOnlineBackupAuthoritativeLoadGeneration = 0;
|
||||
let managedOnlineBackupActiveMutations = 0;
|
||||
let onlineBackupStatusContextGeneration = 0;
|
||||
let managedOnlineBackupRefreshIssue = null;
|
||||
let managedOnlineBackupExpiryTimer = null;
|
||||
const managedOnlineBackupOperationQueues = new Map();
|
||||
|
||||
let _rLongTasks = 0, _rLongTaskMax = 0, _rFrameLast = 0, _rFrameWorst = 0, _rFrameCount = 0, _rFrameJank = 0, _rPerfLastLog = 0, _rPerfWindowStart = 0;
|
||||
@@ -1345,6 +1347,10 @@ async function init() {
|
||||
setUiLanguage(config.globalSettings?.language);
|
||||
hosterSettings = config.hosterSettings || {};
|
||||
autoHealthCheckEnabled = loadAutoCheckPreference();
|
||||
if (config.globalSettings?.autoHealthCheckEnabled !== autoHealthCheckEnabled) {
|
||||
config.globalSettings = { ...(config.globalSettings || {}), autoHealthCheckEnabled };
|
||||
saveGlobalSettingsTracked(config.globalSettings).catch(() => {});
|
||||
}
|
||||
ensureAccountStatusEntries();
|
||||
syncSelectedUploadHosters();
|
||||
restoreQueueStateFromConfig();
|
||||
@@ -3599,7 +3605,13 @@ function applyImportedConfig(importedConfig, message) {
|
||||
accountStatuses = {};
|
||||
ensureAccountStatusEntries();
|
||||
syncSelectedUploadHosters();
|
||||
autoHealthCheckEnabled = config.globalSettings?.autoHealthCheckEnabled !== false;
|
||||
try { localStorage.setItem(AUTO_CHECK_PREF_KEY, autoHealthCheckEnabled ? '1' : '0'); } catch {}
|
||||
alwaysOnTopState = !!(config.globalSettings && config.globalSettings.alwaysOnTop);
|
||||
const importedLanguage = setUiLanguage(config.globalSettings?.language);
|
||||
const importedUrl = new URL(window.location.href);
|
||||
importedUrl.searchParams.set('language', importedLanguage);
|
||||
window.history.replaceState(null, '', importedUrl.href);
|
||||
renderSettings();
|
||||
renderAccounts();
|
||||
renderHosterSummary();
|
||||
@@ -3678,12 +3690,20 @@ function normalizeManagedOnlineBackups(entries) {
|
||||
const candidates = [];
|
||||
for (const entry of Array.isArray(entries) ? entries : []) {
|
||||
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) continue;
|
||||
if (Object.keys(entry).sort().join(',') !== 'createdAt,displayKey,id') continue;
|
||||
const shape = Object.keys(entry).sort().join(',');
|
||||
if (shape !== 'createdAt,displayKey,id' && shape !== 'createdAt,displayKey,expiresAt,id') continue;
|
||||
if (!isCanonicalManagedOnlineBackupId(entry.id)) continue;
|
||||
if (typeof entry.displayKey !== 'string' || !/^MHU2-[A-Za-z0-9_-]{4}…[A-Za-z0-9_-]{4}$/.test(entry.displayKey)) continue;
|
||||
const createdAt = new Date(entry.createdAt);
|
||||
if (Number.isNaN(createdAt.getTime()) || createdAt.toISOString() !== entry.createdAt) continue;
|
||||
candidates.push({ id: entry.id, displayKey: entry.displayKey, createdAt: entry.createdAt });
|
||||
let expiresAt = null;
|
||||
if (shape.includes('expiresAt') && entry.expiresAt !== null) {
|
||||
const expiration = new Date(entry.expiresAt);
|
||||
if (typeof entry.expiresAt !== 'string' || Number.isNaN(expiration.getTime()) || expiration.toISOString() !== entry.expiresAt || entry.expiresAt <= entry.createdAt) continue;
|
||||
expiresAt = entry.expiresAt;
|
||||
}
|
||||
if (expiresAt !== null && new Date(expiresAt).getTime() <= Date.now()) continue;
|
||||
candidates.push({ id: entry.id, displayKey: entry.displayKey, createdAt: entry.createdAt, expiresAt });
|
||||
}
|
||||
const counts = new Map();
|
||||
for (const entry of candidates) counts.set(entry.id, (counts.get(entry.id) || 0) + 1);
|
||||
@@ -3739,6 +3759,8 @@ function removeManagedOnlineBackup(id, focusTarget = null) {
|
||||
function renderManagedOnlineBackups(focusTarget = undefined) {
|
||||
const list = document.getElementById('managedOnlineBackupList');
|
||||
if (!list) return;
|
||||
clearTimeout(managedOnlineBackupExpiryTimer);
|
||||
managedOnlineBackupExpiryTimer = null;
|
||||
const target = focusTarget === undefined ? managedOnlineBackupFocusTarget() : focusTarget;
|
||||
const content = document.createDocumentFragment();
|
||||
if (!managedOnlineBackupsAuthoritative) {
|
||||
@@ -3761,7 +3783,13 @@ function renderManagedOnlineBackups(focusTarget = undefined) {
|
||||
key.textContent = entry.displayKey;
|
||||
const created = document.createElement('span');
|
||||
created.className = 'online-backup-managed-created';
|
||||
created.textContent = formatDateTime(entry.createdAt).text;
|
||||
const createdLabel = document.createElement('span');
|
||||
createdLabel.textContent = `${localizeUiText('Erstellt')}: ${formatDateTime(entry.createdAt).text}`;
|
||||
const expirationLabel = document.createElement('span');
|
||||
expirationLabel.textContent = entry.expiresAt
|
||||
? `${localizeUiText('Gültig bis')}: ${formatDateTime(entry.expiresAt).text}`
|
||||
: localizeUiText('Unbegrenzt gültig');
|
||||
created.append(createdLabel, expirationLabel);
|
||||
const actions = document.createElement('div');
|
||||
actions.className = 'online-backup-managed-actions';
|
||||
const copyButton = document.createElement('button');
|
||||
@@ -3790,6 +3818,17 @@ function renderManagedOnlineBackups(focusTarget = undefined) {
|
||||
}
|
||||
list.replaceChildren(content);
|
||||
restoreManagedOnlineBackupFocus(target);
|
||||
const nextExpiry = managedOnlineBackups
|
||||
.map(entry => entry.expiresAt ? new Date(entry.expiresAt).getTime() : 0)
|
||||
.filter(timestamp => timestamp > Date.now())
|
||||
.sort((left, right) => left - right)[0];
|
||||
if (nextExpiry) {
|
||||
const delay = Math.min(2_147_000_000, Math.max(0, nextExpiry - Date.now() + 50));
|
||||
managedOnlineBackupExpiryTimer = setTimeout(() => {
|
||||
managedOnlineBackupExpiryTimer = null;
|
||||
loadManagedOnlineBackups();
|
||||
}, delay);
|
||||
}
|
||||
}
|
||||
|
||||
function renderManagedOnlineBackupRefreshIssue() {
|
||||
@@ -3948,10 +3987,13 @@ async function doOnlineBackupCreate() {
|
||||
}
|
||||
const authority = beginManagedOnlineBackupMutation();
|
||||
const createButton = document.getElementById('createOnlineBackupBtn');
|
||||
const retentionSelect = document.getElementById('onlineBackupRetentionSelect');
|
||||
const retention = retentionSelect?.value || '7d';
|
||||
if (createButton) createButton.disabled = true;
|
||||
if (retentionSelect) retentionSelect.disabled = true;
|
||||
setOnlineBackupStatus('Verschlüssele und speichere Einstellungen…', 'busy', authority.statusContext);
|
||||
try {
|
||||
const result = await window.api.createManagedOnlineBackup();
|
||||
const result = await window.api.createManagedOnlineBackup(retention);
|
||||
if (!result?.ok) {
|
||||
setOnlineBackupStatus(result?.error || 'Online-Sicherung konnte nicht erstellt werden', 'error', authority.statusContext);
|
||||
return;
|
||||
@@ -3964,6 +4006,7 @@ async function doOnlineBackupCreate() {
|
||||
} finally {
|
||||
endManagedOnlineBackupMutation();
|
||||
if (createButton?.isConnected) createButton.disabled = false;
|
||||
if (retentionSelect?.isConnected) retentionSelect.disabled = false;
|
||||
doOnlineBackupCreate.busy = false;
|
||||
}
|
||||
}
|
||||
@@ -6013,33 +6056,46 @@ function showAppChoice({ message, title, confirmText, alternateText, cancelText
|
||||
|
||||
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) => {
|
||||
const requestId = `hc-${Date.now()}-${++healthCheckRequestSequence}`;
|
||||
const rowsByKey = new Map();
|
||||
const applyResult = (row, checkedAt) => {
|
||||
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) {
|
||||
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 };
|
||||
if (!key || (generation !== undefined && !_isCurrentAccountStatusGeneration(key, generation))) return false;
|
||||
rowsByKey.set(key, row);
|
||||
accountStatuses[key] = {
|
||||
status: row.status || 'unchecked',
|
||||
message: row.message || '',
|
||||
checkedAt: row.checkedAt || checkedAt
|
||||
};
|
||||
if (row.accountId) updateAccountCard(row.accountId);
|
||||
else renderAccounts();
|
||||
renderHosterModal();
|
||||
renderHealthCheckResults([...rowsByKey.values()]);
|
||||
return true;
|
||||
};
|
||||
const stopListening = typeof window.api.onHealthCheckResult === 'function'
|
||||
? window.api.onHealthCheckResult((payload) => {
|
||||
if (payload?.requestId === requestId) applyResult(payload.result, payload.checkedAt || new Date().toISOString());
|
||||
})
|
||||
: null;
|
||||
let result;
|
||||
try {
|
||||
result = await window.api.runHealthCheck({ hosters, requestId });
|
||||
} finally {
|
||||
if (typeof stopListening === 'function') stopListening();
|
||||
}
|
||||
const rows = result && Array.isArray(result.results) ? result.results : [];
|
||||
const checkedAt = result?.checkedAt || new Date().toISOString();
|
||||
rows.forEach(row => applyResult(row, checkedAt));
|
||||
for (const [key, generation] of generations || []) {
|
||||
if (rowsByKey.has(key) || !_isCurrentAccountStatusGeneration(key, generation)) continue;
|
||||
accountStatuses[key] = { status: 'error', message: 'Keine Antwort vom Hoster erhalten', checkedAt };
|
||||
updateAccountCard(key);
|
||||
}
|
||||
const currentRows = [...rowsByKey.values()];
|
||||
renderHealthCheckResults(currentRows);
|
||||
renderAccounts();
|
||||
renderHosterModal();
|
||||
return currentRows;
|
||||
}
|
||||
@@ -6553,6 +6609,18 @@ function renderSettings() {
|
||||
</section>
|
||||
<div class="online-backup-status" id="onlineBackupStatus" role="status" aria-live="polite"></div>
|
||||
<footer class="online-backup-footer" data-settings-search-entry data-settings-search-section="Online-Backup" data-settings-search-label="Neuen Schlüssel erzeugen">
|
||||
<div class="online-backup-retention-field">
|
||||
<label for="onlineBackupRetentionSelect">Gültigkeitsdauer</label>
|
||||
<span class="online-backup-retention-select">
|
||||
<select id="onlineBackupRetentionSelect">
|
||||
<option value="1d">24 Stunden</option>
|
||||
<option value="3d">3 Tage</option>
|
||||
<option value="7d" selected>7 Tage (Standard)</option>
|
||||
<option value="31d">31 Tage</option>
|
||||
<option value="forever">Unbegrenzt</option>
|
||||
</select>
|
||||
</span>
|
||||
</div>
|
||||
<button class="btn btn-primary" id="createOnlineBackupBtn">Neuen Schlüssel erzeugen</button>
|
||||
</footer>
|
||||
</section>
|
||||
@@ -9283,6 +9351,8 @@ function setupListeners() {
|
||||
autoToggle.addEventListener('change', (e) => {
|
||||
autoHealthCheckEnabled = !!e.target.checked;
|
||||
try { localStorage.setItem(AUTO_CHECK_PREF_KEY, autoHealthCheckEnabled ? '1' : '0'); } catch {}
|
||||
config.globalSettings = { ...(config.globalSettings || {}), autoHealthCheckEnabled };
|
||||
saveGlobalSettingsTracked(config.globalSettings).catch(() => {});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -9967,8 +10037,12 @@ function formatDateTime(value) {
|
||||
}
|
||||
|
||||
function loadAutoCheckPreference() {
|
||||
try { const r = localStorage.getItem(AUTO_CHECK_PREF_KEY); return r === null || r === '1'; }
|
||||
catch { return true; }
|
||||
try {
|
||||
const stored = localStorage.getItem(AUTO_CHECK_PREF_KEY);
|
||||
if (stored === '0' || stored === '1') return stored === '1';
|
||||
}
|
||||
catch {}
|
||||
return config.globalSettings?.autoHealthCheckEnabled !== false;
|
||||
}
|
||||
|
||||
// --- Queue table column resizing (JDownloader-style) ---
|
||||
|
||||
@@ -378,6 +378,14 @@
|
||||
['Verschlüsseltes Online-Backup', 'Encrypted online backup'],
|
||||
['Die Verschlüsselung findet ausschließlich auf diesem Gerät statt. Der Server speichert nur verschlüsselte Daten.', 'Encryption takes place only on this device. The server stores encrypted data only.'],
|
||||
['Neuen Schlüssel erzeugen', 'Generate new key'],
|
||||
['Gültigkeitsdauer', 'Validity period'],
|
||||
['24 Stunden', '24 hours'],
|
||||
['3 Tage', '3 days'],
|
||||
['7 Tage (Standard)', '7 days (default)'],
|
||||
['31 Tage', '31 days'],
|
||||
['Erstellt', 'Created'],
|
||||
['Gültig bis', 'Valid until'],
|
||||
['Unbegrenzt gültig', 'Valid indefinitely'],
|
||||
['Auf diesem Gerät erstellt', 'Created on this device'],
|
||||
['Noch keine Schlüssel auf diesem Gerät erstellt.', 'No keys have been created on this device yet.'],
|
||||
['Schlüssel kopieren', 'Copy key'],
|
||||
@@ -541,6 +549,8 @@
|
||||
['Nicht alle Einstellungen konnten gespeichert werden', 'Not all settings could be saved'],
|
||||
['Online-Schlüssel kopiert', 'Online key copied'],
|
||||
['Online-Sicherung konnte nicht erstellt werden', 'Online backup could not be created'],
|
||||
['Gültigkeitsdauer der Online-Sicherung ist ungültig', 'Online backup validity period is invalid'],
|
||||
['Erstellungszeit der Online-Sicherung ist ungültig', 'Online backup creation time is invalid'],
|
||||
['Online-Sicherungen konnten nicht geladen werden', 'Online backups could not be loaded'],
|
||||
['Online-Sicherung konnte nicht kopiert werden', 'Online backup could not be copied'],
|
||||
['Online-Sicherung konnte nicht importiert werden', 'Online backup could not be imported'],
|
||||
|
||||
+66
-1
@@ -2173,7 +2173,7 @@ select.hs-input { max-width: none; width: auto; min-width: 140px; }
|
||||
|
||||
.online-backup-managed-row {
|
||||
display: grid;
|
||||
grid-template-columns: 168px 188px minmax(0, 1fr);
|
||||
grid-template-columns: 168px 238px minmax(0, 1fr);
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
@@ -2192,6 +2192,8 @@ select.hs-input { max-width: none; width: auto; min-width: 140px; }
|
||||
}
|
||||
|
||||
.online-backup-managed-created {
|
||||
display: grid;
|
||||
gap: 3px;
|
||||
min-width: 0;
|
||||
color: var(--text-dim);
|
||||
font-size: 13px;
|
||||
@@ -2240,10 +2242,64 @@ select.hs-input { max-width: none; width: auto; min-width: 140px; }
|
||||
|
||||
.online-backup-footer {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 12px;
|
||||
justify-content: flex-end;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.online-backup-retention-field {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
min-width: 190px;
|
||||
}
|
||||
|
||||
.online-backup-retention-field label {
|
||||
color: var(--text);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.online-backup-retention-select {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.online-backup-retention-select::after {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 14px;
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-right: 2px solid var(--text-dim);
|
||||
border-bottom: 2px solid var(--text-dim);
|
||||
content: '';
|
||||
pointer-events: none;
|
||||
transform: translateY(-65%) rotate(45deg);
|
||||
}
|
||||
|
||||
.online-backup-retention-select select {
|
||||
width: 100%;
|
||||
min-height: 34px;
|
||||
padding: 7px 38px 7px 11px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
appearance: none;
|
||||
color: var(--text);
|
||||
background: var(--bg-card);
|
||||
font: inherit;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.online-backup-retention-select select:focus {
|
||||
border-color: var(--accent);
|
||||
outline: 2px solid color-mix(in srgb, var(--accent) 32%, transparent);
|
||||
outline-offset: 1px;
|
||||
}
|
||||
|
||||
.online-backup-retention-select:has(select:disabled)::after {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.online-backup-status {
|
||||
min-height: 18px;
|
||||
color: var(--text-dim);
|
||||
@@ -2273,6 +2329,15 @@ select.hs-input { max-width: none; width: auto; min-width: 140px; }
|
||||
.online-backup-key-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.online-backup-footer {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.online-backup-retention-field {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-sprite {
|
||||
|
||||
Reference in New Issue
Block a user