Faithful prototype-augment split of backup-page.js (2353->753 line base) into fetch-client, dashboard, snapshots, locations, location-fields, ssh-key, retention-presets, configuration, engine-details, location-modal, snapshot-actions, migrate (+ the earlier cron-schedule). Methods relocated verbatim (mechanical sed/awk extraction, no logic change); all augment BackupPage.prototype and load after the base via the ordered kernel loader. Verified: all 99 original methods present exactly once across base+clusters, no duplicates, all 14 files node --check clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: librelad <librelad@digitalangels.vip>
121 lines
5.5 KiB
JavaScript
121 lines
5.5 KiB
JavaScript
// Auto-extracted from backup-page.js (verbatim) — augments BackupPage.prototype. Loaded after the base.
|
|
Object.assign(BackupPage.prototype, {
|
|
renderConfiguration() {
|
|
const body = document.getElementById('backup-configuration-body');
|
|
if (!body) return;
|
|
|
|
// Dismissed state is persisted server-side via Dismissible
|
|
// (data/ui-state.json), so it follows the user across browsers/devices.
|
|
// Banner + its divider are omitted entirely once dismissed.
|
|
const warningDismissed = !!window.Dismissible?.isDismissed('backup-config-warning');
|
|
const warningHTML = warningDismissed ? '' : `
|
|
<div class="backup-warning-banner">
|
|
<svg class="backup-warning-banner-icon" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
<path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path>
|
|
<line x1="12" y1="9" x2="12" y2="13"></line>
|
|
<line x1="12" y1="17" x2="12.01" y2="17"></line>
|
|
</svg>
|
|
<div class="backup-warning-banner-text">
|
|
<strong>Keep your LibrePortal config backed up offline.</strong>
|
|
<span>Repository passwords live inside the config directory. Without that backup, the others cannot be decrypted by anyone — including you.</span>
|
|
</div>
|
|
<button type="button" class="backup-warning-banner-close" data-action="dismiss-config-warning" title="Dismiss this warning" aria-label="Dismiss this warning">
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
<line x1="18" y1="6" x2="6" y2="18"></line>
|
|
<line x1="6" y1="6" x2="18" y2="18"></line>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
<div class="config-divider"></div>
|
|
`;
|
|
|
|
body.innerHTML = `
|
|
${warningHTML}
|
|
<div id="config-section" class="backup-embedded-config"></div>
|
|
`;
|
|
|
|
this.invokeConfigManager();
|
|
},
|
|
async invokeConfigManager(attempt = 0) {
|
|
if (window.configManager && typeof window.configManager.renderConfig === 'function') {
|
|
try {
|
|
await window.configManager.renderConfig('backup');
|
|
this.enhanceConfigurationWithPresets();
|
|
} catch (err) {
|
|
console.error('Backup configuration render failed:', err);
|
|
}
|
|
return;
|
|
}
|
|
if (attempt >= 20) {
|
|
const sec = document.getElementById('config-section');
|
|
if (sec) sec.innerHTML = `<div class="backup-empty-state">Configuration system not loaded. Try refreshing the page.</div>`;
|
|
return;
|
|
}
|
|
setTimeout(() => this.invokeConfigManager(attempt + 1), 150);
|
|
},
|
|
async exportRepositoryPasswords(triggerBtn) {
|
|
const restoreBtn = () => {
|
|
if (triggerBtn) {
|
|
triggerBtn.disabled = false;
|
|
triggerBtn.dataset.busy = '';
|
|
}
|
|
};
|
|
if (triggerBtn) {
|
|
triggerBtn.disabled = true;
|
|
triggerBtn.dataset.busy = '1';
|
|
}
|
|
|
|
try {
|
|
const task = await this.taskManager?.createTask(
|
|
'libreportal webui generate backup',
|
|
'webui',
|
|
null
|
|
);
|
|
if (task?.id) {
|
|
await this.waitForTask(task.id, 20000);
|
|
}
|
|
const res = await fetch(`/data/backup/generated/passwords.txt?t=${Date.now()}`, {
|
|
credentials: 'same-origin'
|
|
});
|
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
const text = await res.text();
|
|
if (!text || !text.includes('CFG_BACKUP_LOC_')) {
|
|
throw new Error('Password file is empty — no locations configured?');
|
|
}
|
|
const blob = new Blob([text], { type: 'text/plain;charset=utf-8' });
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
const host = (window.systemConfigs?.CFG_INSTALL_NAME || 'libreportal').replace(/[^a-z0-9_-]/gi, '_');
|
|
const stamp = new Date().toISOString().slice(0, 19).replace(/[:T]/g, '-');
|
|
a.href = url;
|
|
a.download = `libreportal-backup-passwords-${host}-${stamp}.txt`;
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
document.body.removeChild(a);
|
|
URL.revokeObjectURL(url);
|
|
this.notify('Password export downloaded — store it offline.', 'success');
|
|
} catch (err) {
|
|
this.notify(`Export failed: ${err.message || err}`, 'error');
|
|
} finally {
|
|
restoreBtn();
|
|
}
|
|
},
|
|
waitForTask(taskId, timeoutMs = 15000) {
|
|
return new Promise((resolve) => {
|
|
let done = false;
|
|
const finish = () => {
|
|
if (done) return;
|
|
done = true;
|
|
window.removeEventListener('taskCompleted', onComplete);
|
|
clearTimeout(timer);
|
|
resolve();
|
|
};
|
|
const onComplete = (e) => {
|
|
if (e?.detail?.taskId === taskId) finish();
|
|
};
|
|
window.addEventListener('taskCompleted', onComplete);
|
|
const timer = setTimeout(finish, timeoutMs);
|
|
});
|
|
},
|
|
});
|