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>
Add libreportal.backup.db labels for the SQLite apps with confirmed db paths:
vaultwarden, linkding, trilium, headscale, authelia. These are dumped live via
sqlite3 .backup and rehydrated before start on restore.
gitea and focalboard are intentionally left out until their sqlite paths are
confirmed on a live install — a wrong path would just fall back to stop, but
there's no point shipping a descriptor that always falls back.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
Type is the choice that determines which other fields appear, so it should be
the first thing you pick; Name is just a label. Reorder the Connection-tab
fields (and the Add-location dialog) to Type → Name.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
- Add libreportal.backup.db labels to the MariaDB/Postgres apps (nextcloud,
owncloud, bookstack, mastodon, invidious) so they back up live + consistent.
- If a declared dump cannot be taken (DB down, wrong path), the backup falls
back to stop-snapshot-start for that run instead of snapshotting torn data —
a misconfiguration degrades to 'safe with downtime', never to 'unsafe'.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
Adds a logical-dump path so apps with a database can be backed up with zero
downtime and full consistency, instead of stopping the container.
- backup_db.sh: dump each declared DB live (mysqldump --single-transaction /
pg_dump / sqlite3 .backup), exclude the raw data dir from the snapshot, and
replay the dump on restore (pre-start rehydrate for sqlite, post-start load
for server engines).
- Databases are declared via a 'libreportal.backup.db' compose label so the
metadata travels with the app in the snapshot.
- New 'auto' strategy (now the default): live where a DB is dumpable or the app
is marked live-safe, stop-snapshot-start otherwise. Explicit stop/pause/live
remain as overrides.
- restic/borg/kopia adapters honour an exclude list on the live path.
- Manifest records the resolved per-app strategy and dumped databases.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
- Remove the per-tab 'How LibrePortal connects…' description lines; the tab
labels already say what each panel is, and the paragraphs read as misplaced
titles.
- Give the tab panels even, comfortable padding (tabs-content padding zeroed so
the panel owns it) instead of the cramped 2px sides.
- Round the tab strip's top corners (.tabs-list) so the strip + content read as
one card — .tabs-content already rounds the bottom, leaving the top square.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
Automatic path mode hardcoded /docker/backups/<id>, baked into the Path Mode
dropdown label. Add a CFG_BACKUP_DEFAULT_PATH option in the Backup Engine
config ("Default Backup Location", default /docker/backups) and have
backupLocationResolvedPath build the auto path from it (<base>/<id>, trailing
slash tolerated). Defaults to the old path, so existing auto locations are
unchanged.
Path Mode's option is now just "Automatic" (no inline path); its tooltip
points at the Default Backup Location config option instead.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
Reuse the shared .tabs-wrapper/.tab-button/.tab-panel components (same as an
app's Config/Tasks tabs) for the location editor instead of bespoke tab CSS:
emoji + label buttons, equal-width strip, accent active state. Panels toggle
via the .active class like the rest of the UI; only the panel padding is
trimmed so it nests inside the backup row.
Also drop the now-dead 'No advanced options' empty state — every type has at
least Engine + append-only in the Advanced tab.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
The backup engine is an implementation detail — LibrePortal picks a sensible
default and handles it — so it doesn't belong next to Name/Type on the
Connection tab. Add ENGINE to LOC_ADVANCED_SUFFIXES and mark it **ADVANCED**
in the location.config template + seed so it's metadata-driven.
Since the engine select now lives in the Advanced tab while SSH-auth and
path-mode stay on Connection, refreshInlineTypeFields re-applies the dynamic
behaviors (engine filtering, SSH/path visibility) against the shared
.task-details scope rather than a single panel.
Also fixed the live per-location engine label (restic -> Restic) which now
surfaces in the dropdown via the generator-emitted options.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
The expanded location row was one long form. Split it into tabs so it opens
showing only the Connection fields. Retention moves from a stacked section
into its own tab, and the advanced overrides (URI/SSH port/append-only) get
their own tab instead of the inline disclosure from the previous pass.
Field grouping is metadata-driven: locFieldGroups partitions a type's fields
into Connection vs Advanced via the configs.json "advanced" flag (with
LOC_ADVANCED_SUFFIXES as the legacy fallback). Type changes rebuild both the
Connection and Advanced panels since advanced fields are type-dependent too.
Save still reads every field across all panels (hidden tabs stay in the DOM).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
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>
The config generator only scanned flat per-category files, so the dynamic
CFG_BACKUP_LOC_N_* keys carried no titles/descriptions/options — the Locations
editor had to hardcode that metadata in backup-page.js. Add a pass that
descends into configs/backup/locations/<n>/location.config and emits each key
(value/title/description/options) into the config map, plus an "advanced"
flag parsed from a **ADVANCED** token in the field comment (stripped from the
user-facing description).
These keys use subcategory "backup_locations", which isn't in any category's
subcategory_order, so the generic /config page ignores them — only the custom
Locations editor consumes them. URI, SSH port, and append-only are marked
advanced. Verified: configs.json stays valid JSON and /config subcategories
are unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
The config-grid toggle box used the input's 12px vertical padding, but its
24px pill made it render 48px tall vs the inputs' 44px, so it sat too tall
to read as inline. Trim vertical padding to 10px so the box is 44px.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
- Display the restic engine as "Restic" to match BorgBackup/Kopia. The
lowercase name lived in scripts/backup/engines/restic.json (drives the
location-row engine pill, per-location engine select, and engine modal),
the hardcoded per-location dropdown options, the engine-list fallback, and
the config-option metadata. All set to "Restic".
- In each location's Engine dropdown, float the system-default engine
(CFG_BACKUP_ENGINE) to the top and tag it "(default)", mirroring the
retention-preset pattern.
Repo config metadata is the install template (add-only reconciliation), so
the live /docker/configs/backup/backup_engine label was updated in place too
for the global Configuration-tab dropdown on this install.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
On installs migrated from EasyDocker the spool file
/var/spool/cron/crontabs/<user> can be left owned by a defunct UID. The
sticky bit on the spool directory then blocks the current install user from
replacing it, so every `crontab -` write failed with
"rename: Operation not permitted" while the scripts still printed success.
crontabClear now removes the crontab as root (`crontab -u <user> -r`), which
bypasses the sticky bit and clears the stale file; the setup steps recreate
it owned by the install user, so the next crontab refresh self-heals.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
The WebUI data snapshots (locations.json, dashboard.json, snapshots_*.json,
etc.) are regenerated on every wizard/config change. Each file emitted two
extra success lines via createTouch — "Touching <file>" and "Updating
<file> with <user> ownership" — which spammed the output around the genuinely
useful "... JSON regenerated" line.
Add an optional "silent" flag to createTouch (third arg; default keeps the
existing loud behaviour for interactive install flows) and pass it from every
WebUI data generator/task. Touch + chown still run; only the logging is
suppressed for these background regenerations.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
WebUI-driven commands (`setup finalize`, `backup`, restore) ran with an
empty $docker_install_user because cliInitialize only called
checkInstallTypeRequirement for the `app` category. The backup engine then
ran `sudo -E -u "" restic init`, which sudo rejects with a usage dump —
surfacing as "Failed to initialize Local disk" in the setup wizard.
Factor the user resolution out of checkInstallTypeRequirement into a
side-effect-free resolveDockerInstallUser (rooted -> sudo_user_name,
rootless -> CFG_DOCKER_INSTALL_USER, with fallbacks so it is never empty)
and call it at the cliInitialize chokepoint so all command categories get a
valid install user, not just app.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
Divider: .domains-divider was a bold 2px accent bar under every section header,
which read as a stray line. Drop it to a subtle 1px low-opacity neutral rule so
it separates without shouting.
Toggle: the boxed config toggle (.checkbox-label) used a different radius (10px),
fill (0.04) and border (0.10) than the .form-control inputs beside it (8px /
0.05 / 0.20), so it looked off and out of line. Match it to the input field box
exactly so toggles and inputs read as the same surface. The app-config
borderless toggle override is unaffected.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
Give the per-location Save changes / Delete location row some breathing room
from the bottom of the expanded card.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
Buttons: the per-location Save changes / Delete location buttons had no icons,
unlike the apps-config action buttons. Add a save (floppy) icon and a trash
icon so they match the reference; colour comes from the nebula button groups
they already belong to.
Theme refactor: move the theme-specific [data-theme="nebula"] button/topbar/CTA
rules out of the shared css/themes.css and into themes/nebula/theme.css, where
the README says theme overrides belong. css/themes.css keeps only the generic,
non-theme-scoped defaults (solid status/accent buttons, danger-zone,
warning-banner) shared by dark-blue/light. No behaviour change: the nebula file
loads after css/themes.css so the moved rules still win.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
Two follow-ups to the button restyle:
- On the nebula theme, primary/danger CTAs are translucent (rgba accent/danger
fill + white text + border), not the solid generic .btn-primary. The earlier
change only added the backup classes to the generic groups, so on nebula the
Add location / Save changes / Delete buttons fell back to a solid fill with
dark text. Add .backup-primary-btn and .backup-danger-btn to the
[data-theme="nebula"] groups too, so they match the config-page buttons.
- The per-location action row used justify-content: space-between, throwing the
two buttons to opposite edges. Switch to flex-start with a gap (like
.config-actions) and put Save changes (primary) before Delete location.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
The backup pages' primary buttons (Add location, Save changes) and Delete
location button used a local gradient + glow in backup.css, so they didn't
match the flat solid-accent buttons on the config page. Add .backup-primary-btn
and .backup-danger-btn to the shared nebula button groups in themes.css
(.btn-primary / .btn-uninstall) so they get the same solid accent/danger fill
and hover with !important across themes, and drop the local gradient/shadow/lift
from backup.css. Top-right and bottom-of-page backup buttons now match config.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
Add a reusable Dismissible helper that persists 'hide this permanently' state server-side in data/ui-state.json via the existing authenticated /read-file + /write-file endpoints. It's a direct file write — no task is created (nothing in the task manager) and no system scan runs — so it sidesteps the heavyweight config_update path entirely and works across browsers/devices. The backup config-backup warning now dismisses through Dismissible instead of localStorage; any future notice can opt in with Dismissible.isDismissed(id)/dismiss(id).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
Drop the Export button from the config-backup warning banner — it's now just the alert + dismiss (x). On the Configuration tab the top-right primary action becomes an 'Export' dropdown (first item: Repository Passwords, reusing the existing export-passwords action) so more export types can be added later. Other tabs keep Backup all apps / Add location. Menu opens from the trigger and closes on outside click, item click, or tab switch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
Add a large amber alert-triangle icon to the 'keep your config backed up offline' banner and a close (x) button in its top-right. Dismissal is stored in localStorage (libreportal:backup-config-warning-dismissed) — a per-browser UI nudge, not server config — and hides both the banner and its divider until cleared.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>