diff --git a/containers/libreportal/frontend/css/backup.css b/containers/libreportal/frontend/css/backup.css index f3aab44..ede4a82 100755 --- a/containers/libreportal/frontend/css/backup.css +++ b/containers/libreportal/frontend/css/backup.css @@ -723,6 +723,47 @@ 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 { display: flex; justify-content: flex-end; diff --git a/containers/libreportal/frontend/js/components/backup/backup-page.js b/containers/libreportal/frontend/js/components/backup/backup-page.js index ec586c5..816f003 100644 --- a/containers/libreportal/frontend/js/components/backup/backup-page.js +++ b/containers/libreportal/frontend/js/components/backup/backup-page.js @@ -198,6 +198,18 @@ class BackupPage { 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')) { this.closeAllModals(); return; @@ -263,7 +275,8 @@ class BackupPage { const ts = Date.now(); const [dashboard, locations] = await Promise.all([ 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.locations = locations; @@ -285,6 +298,19 @@ class BackupPage { 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() { const ts = Date.now(); const index = await this.fetchJson(`/data/backup/generated/engines/index.json?t=${ts}`); @@ -1400,19 +1426,61 @@ class BackupPage { KEEP_YEARLY: loc.keep_yearly }; - // Single .config-fields grid, exactly like /config's renderer — the grid - // (repeat(3, 1fr)) handles the row layout itself, and hidden fields - // (PATH_MODE/SSH/etc.) drop out cleanly without leaving column gaps. - let html = '
'; - for (const suffix of suffixes) { - const def = BACKUP_LOC_FIELD_DEFS[suffix]; - if (!def) continue; + // Field metadata now comes from configs.json (window.configData): the + // generator emits CFG_BACKUP_LOC_N_* titles/descriptions plus an + // "advanced" flag. BACKUP_LOC_FIELD_DEFS stays only as a fallback for a + // sparse location.config that doesn't describe a field yet. + const advancedFallback = new Set(['URI', 'SSH_PORT', 'APPEND_ONLY']); + const fieldMeta = (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 fieldId = `config-${key}`; - html += ConfigShared.generateField(fieldId, key, value, def.title, def.description, {}, {}); + return ConfigShared.generateField(`config-${m.key}`, m.key, value, m.title, m.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 = '
'; + html += basic.map(renderField).join(''); html += '
'; + + if (advanced.length) { + const secId = `backup-loc-${idx}-advanced`; + html += ` + + + `; + } return html; } @@ -1615,10 +1683,6 @@ class BackupPage { } 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(); this.render(); }