Compare commits

...

2 Commits

Author SHA1 Message Date
librelad
557e9319d6 Merge claude/1 2026-05-23 15:22:53 +01:00
librelad
d6e7df8ada refactor(backup): move location field schema to a generated JSON
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 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-23 15:22:53 +01:00
4 changed files with 73 additions and 8 deletions

View File

@ -51,9 +51,11 @@ const BACKUP_LOC_FIELD_DEFS = {
KEEP_YEARLY: { title: 'Keep yearly', description: 'One snapshot per year for this many years.' } 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 // Fallback for the per-type field schema. The live source is the generator-
// it renders before the friendly Name. ENGINE stays here but is filtered into // emitted data/backup/generated/schema.json (loaded into this.locSchema and
// the Advanced tab by locFieldGroups. // 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 = { const BACKUP_LOC_FIELDS_BY_TYPE = {
local: ['TYPE', 'NAME', 'ENGINE', 'PATH_MODE', 'PATH', 'APPEND_ONLY'], 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'], 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() { async refreshAll() {
const ts = Date.now(); 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/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.loadSystemConfigs(),
this.fetchJson(`/data/backup/generated/schema.json?t=${ts}`)
]); ]);
this.dashboard = dashboard; this.dashboard = dashboard;
this.locations = locations; this.locations = locations;
this.locSchema = schema;
this.snapshotsByLoc = {}; this.snapshotsByLoc = {};
if (!this.engines.length) await this.loadEngines(); if (!this.engines.length) await this.loadEngines();
@ -1400,7 +1404,7 @@ class BackupPage {
const idx = parseInt(modal.dataset.locIdx, 10); const idx = parseInt(modal.dataset.locIdx, 10);
const loc = locOverride || (this.locations?.locations || []).find(l => l.idx === idx) || {}; 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); container.innerHTML = this.renderLocFields(idx, suffixes, loc);
this.tagFieldsForSave(container); 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. */ /* Split a type's fields into the Connection tab vs the Advanced tab. */
locFieldGroups(idx, type) { 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 connection = [];
const advanced = []; const advanced = [];
for (const suffix of suffixes) { for (const suffix of suffixes) {

View File

@ -23,6 +23,7 @@ cliHandleWebuiCommands()
webuiGenerateBackupDashboard webuiGenerateBackupDashboard
webuiGenerateBackupSnapshots "${options:-all}" webuiGenerateBackupSnapshots "${options:-all}"
webuiGenerateBackupAppStatus webuiGenerateBackupAppStatus
webuiGenerateBackupSchema
webuiGenerateBackupPasswords webuiGenerateBackupPasswords
elif [ "$config_type" = "system" ]; then elif [ "$config_type" = "system" ]; then
webuiSystemUpdate webuiSystemUpdate

View File

@ -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"
# "<type>|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"
}

View File

@ -82,7 +82,7 @@ webuiLibrePortalUpdate() {
fi fi
# Generate Backup locations / snapshots / engines / dashboards # 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..." checkSuccess "Refreshed backup dashboard data..."
# Sync app icons # Sync app icons