librelad 82989069e2 refactor(backup): decompose backup-page god-file into 13 responsibility files
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>
2026-05-30 14:02:45 +01:00

163 lines
8.0 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Auto-extracted from backup-page.js (verbatim) — augments BackupPage.prototype. Loaded after the base.
Object.assign(BackupPage.prototype, {
renderLocFields(idx, suffixes, loc) {
if (typeof ConfigShared === 'undefined' || !ConfigShared.generateField) {
return `<div class="backup-empty-state">Configuration system not loaded.</div>`;
}
const locValueLookup = {
NAME: loc.name, ENABLED: loc.enabled ? 'true' : 'false', TYPE: loc.type,
ENGINE: loc.engine || 'restic',
PATH_MODE: loc.path_mode || 'custom',
PATH: loc.path, URI: loc.uri, SSH_USER: loc.ssh_user, SSH_HOST: loc.ssh_host,
SSH_PORT: loc.ssh_port, SSH_PATH: loc.ssh_path,
SSH_AUTH: loc.ssh_auth || 'key', SSH_PASS: '',
S3_ACCESS_KEY: '', S3_SECRET_KEY: '',
B2_ACCOUNT_ID: '', B2_ACCOUNT_KEY: '',
APPEND_ONLY: loc.append_only ? 'true' : 'false',
CUSTOM_RETENTION: loc.custom_retention ? 'true' : 'false',
KEEP_LAST: loc.keep_last, KEEP_DAILY: loc.keep_daily,
KEEP_WEEKLY: loc.keep_weekly, KEEP_MONTHLY: loc.keep_monthly,
KEEP_YEARLY: loc.keep_yearly
};
// Field metadata comes from configs.json (window.configData) via
// locFieldMeta; the basic/advanced split is decided by the caller, which
// renders each group into its own tab (Connection vs Advanced).
let html = '<div class="config-fields">';
for (const suffix of suffixes) {
const m = this.locFieldMeta(idx, suffix);
if (!m.exists) continue;
const value = (locValueLookup[suffix] ?? '').toString();
html += ConfigShared.generateField(`config-${m.key}`, m.key, value, m.title, m.description, {}, {});
}
html += '</div>';
return html;
},
locFieldMeta(idx, suffix) {
const key = `CFG_BACKUP_LOC_${idx}_${suffix}`;
const cfg = window.configData?.config?.[key] || {};
const def = BACKUP_LOC_FIELD_DEFS[suffix] || {};
return {
key,
exists: !!(cfg.title || cfg.description || BACKUP_LOC_FIELD_DEFS[suffix]),
title: cfg.title || def.title || suffix,
description: cfg.description ?? def.description ?? '',
advanced: LOC_ADVANCED_SUFFIXES.has(suffix) || cfg.advanced === true
};
},
locFieldsForType(type) {
return this.locSchema?.types?.[type]
|| BACKUP_LOC_FIELDS_BY_TYPE[type]
|| BACKUP_LOC_FIELDS_BY_TYPE.local;
},
locFieldGroups(idx, type) {
const suffixes = this.locFieldsForType(type);
const connection = [];
const advanced = [];
for (const suffix of suffixes) {
const m = this.locFieldMeta(idx, suffix);
if (!m.exists) continue;
(m.advanced ? advanced : connection).push(suffix);
}
return { connection, advanced };
},
renderConnectionInner(idx, type, loc, connectionSuffixes) {
let html = this.renderLocFields(idx, connectionSuffixes, loc);
if (type === 'sftp') html += this.renderBackupSshKeyCard(loc);
return html;
},
formRetention(prefix, values, includeInherit = false) {
const preset = backupRetentionDetectPreset(values, includeInherit);
const meta = BACKUP_RETENTION_PRESET_META[preset];
const presetOptions = this.retentionPresetOptions(preset, includeInherit);
const customRetentionHidden = includeInherit
? `<input type="hidden" name="${prefix}CUSTOM_RETENTION" value="${preset === 'inherit-global' ? 'false' : 'true'}" data-backup-field>`
: '';
return `
<div class="backup-form-grid backup-retention-block" data-retention-prefix="${this.escape(prefix)}" data-retention-allow-inherit="${includeInherit ? '1' : '0'}">
<label class="backup-form-row">
<span class="backup-form-label">Backup style <span class="tooltip" data-retention-tooltip title="${this.escape(meta?.hint || '')}"></span></span>
<select class="form-control" data-retention-preset>${presetOptions}</select>
</label>
${customRetentionHidden}
</div>
<div class="backup-retention-advanced" data-retention-advanced ${preset === 'custom' ? '' : 'hidden'}>
<div class="backup-form-grid">
${this.formInput(`${prefix}KEEP_LAST`, 'Keep last', values.last, 'number', '', 'backups')}
${this.formInput(`${prefix}KEEP_DAILY`, 'Keep daily', values.daily, 'number', '', 'days')}
${this.formInput(`${prefix}KEEP_WEEKLY`, 'Keep weekly', values.weekly, 'number', '', 'weeks')}
${this.formInput(`${prefix}KEEP_MONTHLY`, 'Keep monthly', values.monthly, 'number', '', 'months')}
${this.formInput(`${prefix}KEEP_YEARLY`, 'Keep yearly', values.yearly, 'number', '', 'years')}
</div>
</div>
`;
},
formInput(name, label, value, type = 'text', placeholder = '', unit = '') {
const escVal = this.escape(value ?? '');
const escPh = this.escape(placeholder);
const escLabel = this.escape(label);
const inputHTML = `<input type="${type}" name="${name}" value="${escVal}" placeholder="${escPh}" class="form-control" data-backup-field>`;
const wrapped = unit ? `<div class="input-group">${inputHTML}<span class="input-group-text">${this.escape(unit)}</span></div>` : inputHTML;
return `
<label class="backup-form-row">
<span class="backup-form-label">${escLabel}</span>
${wrapped}
</label>
`;
},
formSelect(name, label, value, options) {
const escLabel = this.escape(label);
const opts = options.map(([v, lbl]) => `<option value="${this.escape(v)}" ${v === value ? 'selected' : ''}>${this.escape(lbl)}</option>`).join('');
return `
<label class="backup-form-row">
<span class="backup-form-label">${escLabel}</span>
<select name="${name}" class="form-control" data-backup-field>${opts}</select>
</label>
`;
},
formToggle(name, label, checked) {
const escLabel = this.escape(label);
return `
<label class="backup-form-row backup-form-row-toggle">
<span class="backup-form-label">${escLabel}</span>
<span class="backup-toggle">
<input type="checkbox" name="${name}" ${checked ? 'checked' : ''} data-backup-field data-backup-bool>
<span class="backup-toggle-slider"></span>
</span>
</label>
`;
},
formCrontab(name, label, value) {
if (typeof ConfigShared === 'undefined' || !ConfigShared.createCrontabField) {
return this.formInput(name, label, value, 'text', 'minute hour day month weekday');
}
const fieldId = `config-${name}`;
let cronHtml = ConfigShared.createCrontabField(fieldId, name, value, label, '');
cronHtml = cronHtml.replace(`name="${name}"`, `name="${name}" data-backup-field`);
return `
<label class="backup-form-row">
<span class="backup-form-label">${this.escape(label)}</span>
${cronHtml}
</label>
`;
},
formReadOnly(label, value) {
return `
<div class="backup-form-row">
<span class="backup-form-label">${this.escape(label)}</span>
<span class="backup-form-readonly">${this.escape(value)}</span>
</div>
`;
},
tagFieldsForSave(container) {
container.querySelectorAll('input[name], select[name], textarea[name]').forEach(el => {
if (!el.hasAttribute('data-backup-field')) {
el.setAttribute('data-backup-field', '');
if (el.type === 'checkbox') el.setAttribute('data-backup-bool', '');
}
});
},
});