Compare commits

...

2 Commits

Author SHA1 Message Date
librelad
4b8f2c698c Merge claude/1 2026-05-23 13:44:41 +01:00
librelad
c5ecc520aa feat(backup): system-driven location fields with an Advanced reveal
The Locations editor now renders field metadata from configs.json
(window.configData) instead of relying on the hardcoded BACKUP_LOC_FIELD_DEFS,
which drops to a fallback. Fields flagged advanced (URI override, SSH port,
append-only) move out of the main grid into a full-width "Advanced"
disclosure that's collapsed by default, so the common case stays simple.

Also load the unified config once on the backup page into window.configData
(metadata) + a flat window.systemConfigs (values). Previously systemConfigs
was only populated after a save — and with the full nested JSON, while the
code reads it as a flat map — so default-engine lookups and save-time change
detection silently misbehaved on first load. Both are now correct.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-23 13:44:41 +01:00
2 changed files with 119 additions and 14 deletions

View File

@ -723,6 +723,47 @@
border-top: 1px dashed rgba(var(--text-rgb), 0.08); border-top: 1px dashed rgba(var(--text-rgb), 0.08);
} }
/* Full-width "Advanced" disclosure in the location editor's Connection
section. Reveals the advanced fields (URI override, SSH port, append-only). */
.backup-loc-advanced-toggle {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
margin-top: 14px;
padding: 10px 12px;
background: rgba(var(--text-rgb), 0.03);
border: 1px solid rgba(var(--text-rgb), 0.08);
border-radius: 8px;
color: var(--text-primary);
font: inherit;
font-size: 0.9rem;
font-weight: 600;
cursor: pointer;
transition: background 0.15s ease;
}
.backup-loc-advanced-toggle:hover {
background: rgba(var(--text-rgb), 0.06);
}
.backup-loc-advanced-chevron {
transition: transform 0.15s ease;
flex-shrink: 0;
}
.backup-loc-advanced-toggle.open .backup-loc-advanced-chevron {
transform: rotate(90deg);
}
.backup-loc-advanced {
margin-top: 12px;
}
.backup-loc-advanced[hidden] {
display: none;
}
.backup-form-footer { .backup-form-footer {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;

View File

@ -198,6 +198,18 @@ class BackupPage {
return; return;
} }
const advToggle = e.target.closest('[data-action="toggle-loc-advanced"]');
if (advToggle) {
const sec = document.getElementById(advToggle.dataset.target);
if (sec) {
const show = sec.hasAttribute('hidden');
sec.toggleAttribute('hidden', !show);
advToggle.setAttribute('aria-expanded', show ? 'true' : 'false');
advToggle.classList.toggle('open', show);
}
return;
}
if (e.target.closest('[data-close-modal]') || e.target.matches('.backup-modal')) { if (e.target.closest('[data-close-modal]') || e.target.matches('.backup-modal')) {
this.closeAllModals(); this.closeAllModals();
return; return;
@ -263,7 +275,8 @@ class BackupPage {
const ts = Date.now(); const ts = Date.now();
const [dashboard, locations] = await Promise.all([ const [dashboard, locations] = await Promise.all([
this.fetchJson(`/data/backup/generated/dashboard.json?t=${ts}`), this.fetchJson(`/data/backup/generated/dashboard.json?t=${ts}`),
this.fetchJson(`/data/backup/generated/locations.json?t=${ts}`) this.fetchJson(`/data/backup/generated/locations.json?t=${ts}`),
this.loadSystemConfigs()
]); ]);
this.dashboard = dashboard; this.dashboard = dashboard;
this.locations = locations; this.locations = locations;
@ -285,6 +298,19 @@ class BackupPage {
catch { return null; } catch { return null; }
} }
/* Load the unified config file once for the Locations editor: configData
carries field metadata (titles/descriptions/options/advanced) the editor
renders from; systemConfigs is the flat key->value map used for default
lookups (e.g. CFG_BACKUP_ENGINE) and save-time change detection. */
async loadSystemConfigs() {
const data = await this.fetchJson(`/data/config/generated/configs.json?t=${Date.now()}`);
if (!data) return;
window.configData = data;
const flat = {};
for (const [k, v] of Object.entries(data.config || {})) flat[k] = v?.value ?? '';
window.systemConfigs = flat;
}
async loadEngines() { async loadEngines() {
const ts = Date.now(); const ts = Date.now();
const index = await this.fetchJson(`/data/backup/generated/engines/index.json?t=${ts}`); const index = await this.fetchJson(`/data/backup/generated/engines/index.json?t=${ts}`);
@ -1400,19 +1426,61 @@ class BackupPage {
KEEP_YEARLY: loc.keep_yearly KEEP_YEARLY: loc.keep_yearly
}; };
// Single .config-fields grid, exactly like /config's renderer — the grid // Field metadata now comes from configs.json (window.configData): the
// (repeat(3, 1fr)) handles the row layout itself, and hidden fields // generator emits CFG_BACKUP_LOC_N_* titles/descriptions plus an
// (PATH_MODE/SSH/etc.) drop out cleanly without leaving column gaps. // "advanced" flag. BACKUP_LOC_FIELD_DEFS stays only as a fallback for a
let html = '<div class="config-fields">'; // sparse location.config that doesn't describe a field yet.
for (const suffix of suffixes) { const advancedFallback = new Set(['URI', 'SSH_PORT', 'APPEND_ONLY']);
const def = BACKUP_LOC_FIELD_DEFS[suffix]; const fieldMeta = (suffix) => {
if (!def) continue;
const key = `CFG_BACKUP_LOC_${idx}_${suffix}`; const key = `CFG_BACKUP_LOC_${idx}_${suffix}`;
const cfg = window.configData?.config?.[key] || {};
const def = BACKUP_LOC_FIELD_DEFS[suffix] || {};
// Fallback set keeps these advanced even on legacy locations whose
// location.config predates the **ADVANCED** marker; configData can
// additionally flag others.
const advanced = advancedFallback.has(suffix) || cfg.advanced === true;
return {
key,
exists: !!(cfg.title || cfg.description || BACKUP_LOC_FIELD_DEFS[suffix]),
title: cfg.title || def.title || suffix,
description: cfg.description ?? def.description ?? '',
advanced
};
};
const renderField = (suffix) => {
const m = fieldMeta(suffix);
const value = (locValueLookup[suffix] ?? '').toString(); const value = (locValueLookup[suffix] ?? '').toString();
const fieldId = `config-${key}`; return ConfigShared.generateField(`config-${m.key}`, m.key, value, m.title, m.description, {}, {});
html += ConfigShared.generateField(fieldId, key, value, def.title, def.description, {}, {}); };
const basic = [];
const advanced = [];
for (const suffix of suffixes) {
const m = fieldMeta(suffix);
if (!m.exists) continue;
(m.advanced ? advanced : basic).push(suffix);
} }
// Basics stay in the .config-fields grid (same as /config). Advanced
// fields (URI override, SSH port, append-only) tuck behind a full-width
// disclosure so the common case opens simple.
let html = '<div class="config-fields">';
html += basic.map(renderField).join('');
html += '</div>'; html += '</div>';
if (advanced.length) {
const secId = `backup-loc-${idx}-advanced`;
html += `
<button type="button" class="backup-loc-advanced-toggle" data-action="toggle-loc-advanced" data-target="${secId}" aria-expanded="false" aria-controls="${secId}">
<svg class="backup-loc-advanced-chevron" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"></polyline></svg>
<span>Advanced</span>
</button>
<div class="config-fields backup-loc-advanced" id="${secId}" hidden>
${advanced.map(renderField).join('')}
</div>
`;
}
return html; return html;
} }
@ -1615,10 +1683,6 @@ class BackupPage {
} }
async reloadAfterSave() { async reloadAfterSave() {
try {
const r = await fetch(`/data/config/generated/configs.json?t=${Date.now()}`);
if (r.ok) window.systemConfigs = await r.json();
} catch {}
await this.refreshAll(); await this.refreshAll();
this.render(); this.render();
} }