feat(backup): tidy location editor — section dividers, style tooltip, row enable toggle

Make the expanded location editor read like /config: Connection and Retention now use the section header + .domains-divider layout, and Connection gets a description. Move the retention 'Backup style' guidance into a tooltip and drop the always-visible hint line below it. Move the Enabled toggle out of the Connection fields into the collapsed location row header so a location can be enabled/disabled without expanding it; setLocationEnabled persists the change via the same config_update routing as saveSection.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
This commit is contained in:
librelad 2026-05-22 14:09:32 +01:00
parent f3ac9f8684
commit 3b13a67ca7
2 changed files with 62 additions and 20 deletions

View File

@ -794,6 +794,12 @@
transform: translateX(16px);
}
/* Enable/disable toggle in a location row header sits between the row
info and the expand chevron, controlling the location without expanding it. */
.backup-loc-enable-toggle {
margin-right: 6px;
}
/* Repo card extras */
.backup-repo-stats {
display: flex;

View File

@ -52,14 +52,14 @@ const BACKUP_LOC_FIELD_DEFS = {
};
const BACKUP_LOC_FIELDS_BY_TYPE = {
local: ['NAME', 'ENABLED', 'ENGINE', 'TYPE', 'PATH_MODE', 'PATH', 'APPEND_ONLY'],
sftp: ['NAME', 'ENABLED', 'ENGINE', 'TYPE', 'SSH_USER', 'SSH_HOST', 'SSH_PORT', 'SSH_PATH', 'SSH_AUTH', 'SSH_PASS', 'URI', 'APPEND_ONLY'],
rest: ['NAME', 'ENABLED', 'ENGINE', 'TYPE', 'URI', 'APPEND_ONLY'],
s3: ['NAME', 'ENABLED', 'ENGINE', 'TYPE', 'URI', 'S3_ACCESS_KEY', 'S3_SECRET_KEY', 'APPEND_ONLY'],
b2: ['NAME', 'ENABLED', 'ENGINE', 'TYPE', 'URI', 'B2_ACCOUNT_ID', 'B2_ACCOUNT_KEY', 'APPEND_ONLY'],
gs: ['NAME', 'ENABLED', 'ENGINE', 'TYPE', 'URI', 'APPEND_ONLY'],
azure: ['NAME', 'ENABLED', 'ENGINE', 'TYPE', 'URI', 'APPEND_ONLY'],
rclone: ['NAME', 'ENABLED', 'ENGINE', 'TYPE', 'URI', 'APPEND_ONLY']
local: ['NAME', 'ENGINE', 'TYPE', 'PATH_MODE', 'PATH', 'APPEND_ONLY'],
sftp: ['NAME', 'ENGINE', 'TYPE', 'SSH_USER', 'SSH_HOST', 'SSH_PORT', 'SSH_PATH', 'SSH_AUTH', 'SSH_PASS', 'URI', 'APPEND_ONLY'],
rest: ['NAME', 'ENGINE', 'TYPE', 'URI', 'APPEND_ONLY'],
s3: ['NAME', 'ENGINE', 'TYPE', 'URI', 'S3_ACCESS_KEY', 'S3_SECRET_KEY', 'APPEND_ONLY'],
b2: ['NAME', 'ENGINE', 'TYPE', 'URI', 'B2_ACCOUNT_ID', 'B2_ACCOUNT_KEY', 'APPEND_ONLY'],
gs: ['NAME', 'ENGINE', 'TYPE', 'URI', 'APPEND_ONLY'],
azure: ['NAME', 'ENGINE', 'TYPE', 'URI', 'APPEND_ONLY'],
rclone: ['NAME', 'ENGINE', 'TYPE', 'URI', 'APPEND_ONLY']
};
function backupRetentionDetectPreset(values, includeInherit = false) {
@ -164,6 +164,13 @@ class BackupPage {
return;
}
const locEnable = e.target.closest('[data-action="toggle-location-enabled"]');
if (locEnable) {
const cb = locEnable.querySelector('input[type="checkbox"]');
this.setLocationEnabled(parseInt(locEnable.dataset.loc, 10), cb ? cb.checked : true);
return;
}
const locHeader = e.target.closest('[data-action="toggle-location"]');
if (locHeader) {
this.toggleLocationExpand(parseInt(locHeader.dataset.loc, 10));
@ -546,6 +553,10 @@ class BackupPage {
<span class="backup-location-row-sep">·</span>
<span class="backup-location-row-stat">${size}</span>
</div>
<span class="backup-toggle backup-loc-enable-toggle" data-action="toggle-location-enabled" data-loc="${l.idx}" title="${l.enabled ? 'Enabled — click to disable this location' : 'Disabled — click to enable this location'}">
<input type="checkbox" ${l.enabled ? 'checked' : ''} aria-label="Enable this location">
<span class="backup-toggle-slider"></span>
</span>
<svg class="backup-location-chevron" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
@ -585,18 +596,33 @@ class BackupPage {
return `
<div class="config-category backup-location-config" data-section="location-${idx}">
<div class="domains-wrapper">
<div class="domains-header">
<div>
<h3>Connection</h3>
<p class="category-description">How LibrePortal connects to this storage location.</p>
</div>
</div>
<div class="domains-divider"></div>
<div class="backup-location-connection-fields" id="backup-location-${idx}-connection">
${this.renderLocFields(idx, connectionFields, l)}
</div>
</div>
</div>
<div class="config-category backup-location-config">
<div class="domains-wrapper">
<div class="domains-header">
<div>
<h3>Retention</h3>
<p class="category-description">When to delete old backups from this location.</p>
</div>
</div>
<div class="domains-divider"></div>
<div id="backup-location-${idx}-retention">
${this.formRetention(`CFG_BACKUP_LOC_${idx}_`, retentionValues, true)}
</div>
</div>
</div>
<div class="backup-location-actions">
<button class="backup-danger-btn" data-action="delete-location" data-loc="${idx}">Delete location</button>
<button class="backup-primary-btn" data-action="save-location" data-loc="${idx}">Save changes</button>
@ -964,7 +990,6 @@ class BackupPage {
"Custom…" is selected. */
formRetention(prefix, values, includeInherit = false) {
const preset = backupRetentionDetectPreset(values, includeInherit);
const meta = BACKUP_RETENTION_PRESET_META[preset];
const presetOptions = Object.entries(BACKUP_RETENTION_PRESET_META)
.filter(([k]) => k !== 'inherit-global' || includeInherit)
.map(([k, v]) => `<option value="${k}" ${k === preset ? 'selected' : ''}>${this.escape(v.label)}</option>`)
@ -977,10 +1002,9 @@ class BackupPage {
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>
<span class="backup-form-label">Backup style <span class="tooltip" title="Use whatever the Configuration tab specifies. Pick something else here only when this location needs a different policy."></span></span>
<select class="form-control" data-retention-preset>${presetOptions}</select>
</label>
<div class="backup-retention-hint backup-card-hint" data-retention-hint>${this.escape(meta?.hint || '')}</div>
${customRetentionHidden}
</div>
<div class="backup-retention-advanced" data-retention-advanced ${preset === 'custom' ? '' : 'hidden'}>
@ -1002,8 +1026,6 @@ class BackupPage {
const prefix = block.dataset.retentionPrefix;
const allowInherit = block.dataset.retentionAllowInherit === '1';
const preset = selectEl.value;
const hintEl = block.querySelector('[data-retention-hint]');
if (hintEl) hintEl.textContent = BACKUP_RETENTION_PRESET_META[preset]?.hint || '';
if (preset === 'custom') {
if (advanced) advanced.hidden = false;
@ -1437,6 +1459,20 @@ class BackupPage {
/* ----- Generic save handler ----- */
async setLocationEnabled(idx, enabled) {
const encoded = `CFG_BACKUP_LOC_${idx}_ENABLED=${enabled ? 'true' : 'false'}`;
try {
if (!window.tasksManager?.router) throw new Error('Task system not available');
await window.tasksManager.router.routeAction('config_update', {
changes: `'${encoded.replace(/'/g, "'\\''")}'`
});
this.notify(`${enabled ? 'Enabling' : 'Disabling'} this location…`, 'success');
setTimeout(() => this.reloadAfterSave(), 2500);
} catch (err) {
this.notify(`Save failed: ${err.message || err}`, 'error');
}
}
async saveSection(sectionId) {
let scope;
if (sectionId.startsWith('location-')) {