From d6e7df8ada9a4e7ae9752b56a5a3f592e2a18906 Mon Sep 17 00:00:00 2001 From: librelad Date: Sat, 23 May 2026 15:22:53 +0100 Subject: [PATCH] refactor(backup): move location field schema to a generated JSON MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The per-type field map lived hardcoded in backup-page.js. Add a webuiGenerateBackupSchema generator that emits the type -> ordered field list to data/backup/generated/schema.json (wired into the backup regen chain and the CLI 'webui generate backup'). The editor fetches it into this.locSchema and reads it via locFieldsForType; BACKUP_LOC_FIELDS_BY_TYPE stays only as a fallback if the fetch fails. Keeps the data-in-generators pattern consistent — the schema now has one backend source of truth. The dynamic show/hide behaviors (SSH auth, path mode, engine filtering) remain frontend logic by nature. Co-Authored-By: Claude Opus 4.7 Signed-off-by: librelad --- .../js/components/backup/backup-page.js | 27 +++++++--- .../cli/commands/webui/cli_webui_commands.sh | 1 + .../generators/backup/webui_backup_schema.sh | 51 +++++++++++++++++++ scripts/webui/webui_updater.sh | 2 +- 4 files changed, 73 insertions(+), 8 deletions(-) create mode 100644 scripts/webui/data/generators/backup/webui_backup_schema.sh diff --git a/containers/libreportal/frontend/js/components/backup/backup-page.js b/containers/libreportal/frontend/js/components/backup/backup-page.js index e139cd3..67648a2 100644 --- a/containers/libreportal/frontend/js/components/backup/backup-page.js +++ b/containers/libreportal/frontend/js/components/backup/backup-page.js @@ -51,9 +51,11 @@ const BACKUP_LOC_FIELD_DEFS = { KEEP_YEARLY: { title: 'Keep yearly', description: 'One snapshot per year for this many years.' } }; -// Type leads each list: it's the choice that shapes the rest of the form, so -// it renders before the friendly Name. ENGINE stays here but is filtered into -// the Advanced tab by locFieldGroups. +// Fallback for the per-type field schema. The live source is the generator- +// emitted data/backup/generated/schema.json (loaded into this.locSchema and +// read via locFieldsForType); this map is only used if that fetch fails. +// Type leads each list (it shapes the rest of the form); ENGINE stays in the +// list but locFieldGroups folds it into the Advanced tab. const BACKUP_LOC_FIELDS_BY_TYPE = { local: ['TYPE', 'NAME', 'ENGINE', 'PATH_MODE', 'PATH', 'APPEND_ONLY'], sftp: ['TYPE', 'NAME', 'ENGINE', 'SSH_USER', 'SSH_HOST', 'SSH_PORT', 'SSH_PATH', 'SSH_AUTH', 'SSH_PASS', 'URI', 'APPEND_ONLY'], @@ -286,13 +288,15 @@ class BackupPage { async refreshAll() { const ts = Date.now(); - const [dashboard, locations] = await Promise.all([ + const [dashboard, locations, , schema] = await Promise.all([ this.fetchJson(`/data/backup/generated/dashboard.json?t=${ts}`), this.fetchJson(`/data/backup/generated/locations.json?t=${ts}`), - this.loadSystemConfigs() + this.loadSystemConfigs(), + this.fetchJson(`/data/backup/generated/schema.json?t=${ts}`) ]); this.dashboard = dashboard; this.locations = locations; + this.locSchema = schema; this.snapshotsByLoc = {}; if (!this.engines.length) await this.loadEngines(); @@ -1400,7 +1404,7 @@ class BackupPage { const idx = parseInt(modal.dataset.locIdx, 10); const loc = locOverride || (this.locations?.locations || []).find(l => l.idx === idx) || {}; - const suffixes = BACKUP_LOC_FIELDS_BY_TYPE[type] || BACKUP_LOC_FIELDS_BY_TYPE.local; + const suffixes = this.locFieldsForType(type); container.innerHTML = this.renderLocFields(idx, suffixes, loc); this.tagFieldsForSave(container); } @@ -1494,9 +1498,18 @@ class BackupPage { }; } + /* Ordered field list for a location type. Primary source is the generator- + emitted schema.json (this.locSchema); BACKUP_LOC_FIELDS_BY_TYPE is the + fallback if that file didn't load. */ + locFieldsForType(type) { + return this.locSchema?.types?.[type] + || BACKUP_LOC_FIELDS_BY_TYPE[type] + || BACKUP_LOC_FIELDS_BY_TYPE.local; + } + /* Split a type's fields into the Connection tab vs the Advanced tab. */ locFieldGroups(idx, type) { - const suffixes = BACKUP_LOC_FIELDS_BY_TYPE[type] || BACKUP_LOC_FIELDS_BY_TYPE.local; + const suffixes = this.locFieldsForType(type); const connection = []; const advanced = []; for (const suffix of suffixes) { diff --git a/scripts/cli/commands/webui/cli_webui_commands.sh b/scripts/cli/commands/webui/cli_webui_commands.sh index 50136a7..4b4b014 100755 --- a/scripts/cli/commands/webui/cli_webui_commands.sh +++ b/scripts/cli/commands/webui/cli_webui_commands.sh @@ -23,6 +23,7 @@ cliHandleWebuiCommands() webuiGenerateBackupDashboard webuiGenerateBackupSnapshots "${options:-all}" webuiGenerateBackupAppStatus + webuiGenerateBackupSchema webuiGenerateBackupPasswords elif [ "$config_type" = "system" ]; then webuiSystemUpdate diff --git a/scripts/webui/data/generators/backup/webui_backup_schema.sh b/scripts/webui/data/generators/backup/webui_backup_schema.sh new file mode 100644 index 0000000..f75e99b --- /dev/null +++ b/scripts/webui/data/generators/backup/webui_backup_schema.sh @@ -0,0 +1,51 @@ +#!/bin/bash + +# Source of truth for the Locations editor form layout: which CFG_BACKUP_LOC_* +# fields apply to each backup location type, in render order. Emitted as +# data/backup/generated/schema.json so the frontend builds the form from data +# instead of a hardcoded map (it keeps a fallback only). ENGINE stays in the +# list — the frontend folds it into the Advanced tab via the per-field +# "advanced" flag from configs.json; this file is purely about applicability +# and order. + +webuiGenerateBackupSchema() +{ + local out_dir="$containers_dir/libreportal/frontend/data/backup/generated" + local out_file="$out_dir/schema.json" + sudo mkdir -p "$out_dir" + + # "|FIELD,FIELD,..." — order here is the form's render order. + local rows=( + "local|TYPE,NAME,ENGINE,PATH_MODE,PATH,APPEND_ONLY" + "sftp|TYPE,NAME,ENGINE,SSH_USER,SSH_HOST,SSH_PORT,SSH_PATH,SSH_AUTH,SSH_PASS,URI,APPEND_ONLY" + "rest|TYPE,NAME,ENGINE,URI,APPEND_ONLY" + "s3|TYPE,NAME,ENGINE,URI,S3_ACCESS_KEY,S3_SECRET_KEY,APPEND_ONLY" + "b2|TYPE,NAME,ENGINE,URI,B2_ACCOUNT_ID,B2_ACCOUNT_KEY,APPEND_ONLY" + "gs|TYPE,NAME,ENGINE,URI,APPEND_ONLY" + "azure|TYPE,NAME,ENGINE,URI,APPEND_ONLY" + "rclone|TYPE,NAME,ENGINE,URI,APPEND_ONLY" + ) + + local json="{\"generated_at\":\"$(date -Iseconds)\",\"types\":{" + local first=true row type fields f farr firstf + for row in "${rows[@]}"; do + type="${row%%|*}" + fields="${row#*|}" + $first || json+="," + first=false + json+="\"$type\":[" + firstf=true + IFS=',' read -ra farr <<< "$fields" + for f in "${farr[@]}"; do + $firstf || json+="," + firstf=false + json+="\"$f\"" + done + json+="]" + done + json+="}}" + + echo "$json" | sudo tee "$out_file" >/dev/null + createTouch "$out_file" "$docker_install_user" "silent" + isSuccessful "Backup location schema regenerated" +} diff --git a/scripts/webui/webui_updater.sh b/scripts/webui/webui_updater.sh index 38f49c5..ac10306 100755 --- a/scripts/webui/webui_updater.sh +++ b/scripts/webui/webui_updater.sh @@ -82,7 +82,7 @@ webuiLibrePortalUpdate() { fi # Generate Backup locations / snapshots / engines / dashboards - local result=$(webuiGenerateBackupLocations && webuiGenerateBackupDashboard && webuiGenerateBackupSnapshots all && webuiGenerateBackupAppStatus && webuiGenerateBackupEngines && webuiGenerateBackupPasswords) + local result=$(webuiGenerateBackupLocations && webuiGenerateBackupDashboard && webuiGenerateBackupSnapshots all && webuiGenerateBackupAppStatus && webuiGenerateBackupEngines && webuiGenerateBackupSchema && webuiGenerateBackupPasswords) checkSuccess "Refreshed backup dashboard data..." # Sync app icons