137 Commits

Author SHA1 Message Date
librelad
8315a58226 Merge claude/2 2026-05-23 18:27:28 +01:00
librelad
036f72d3c2 fix(backup): verify against snapshot restorability, not the live dir
With live dumps + container-side file captures the live app dir intentionally
differs from the snapshot (raw DB dirs and private trees are excluded, replaced
by dumps/captures), so the old source-vs-restored file-count check false-failed.
The scratch restore succeeding already proves restorability (restic hash-checks
every blob); keep a non-empty sanity check instead.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-23 18:27:28 +01:00
librelad
3d7fc0a3f6 Merge claude/2 2026-05-23 18:23:03 +01:00
librelad
3a1cd8464e fix(backup): make captured file staging readable by the backup user
The container capture preserved the app's ownership (e.g. www-data 0640), so
restic still hit permission denied on the staging copy. chown the staging tree
to the backup user after capture (modes unchanged, so the owner reads fine);
real ownership is reapplied from the descriptor on restore.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-23 18:23:03 +01:00
librelad
c460f7afcb Merge claude/1 2026-05-23 18:19:25 +01:00
librelad
23a15345fb refactor(admin): sidebar Config/Tools groups, per-group breadcrumbs, SSH matches config layout
- Sidebar now groups items: Overview at top, a 'Config' heading over the config
  categories, and the existing 'Tools' heading over SSH Access.
- Breadcrumb reflects the group: config pages read 'Config' (was 'Admin'), SSH
  reads 'Tools', Overview stays 'Admin'.
- SSH Access page restyled to the config page's section layout
  (.config-category/.domains-wrapper sections) instead of backup-style cards, so
  it matches the other Admin config pages.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-23 18:19:25 +01:00
librelad
b1c84a9b3c Merge claude/2 2026-05-23 18:15:53 +01:00
librelad
94c9e83c42 feat(backup): container-side capture of private app files
Reads files the backup user can't see from the host (container-owned, e.g.
Nextcloud's www-data data dir) by streaming them out THROUGH the container
(docker exec tar) — no host root, no host read perms, works rooted + rootless.
Extracts to staging as plain files so restic keeps full dedup + per-file
restore (not a piped tar blob); the live path is excluded from the snapshot.
Restore streams the staging copy back through a throwaway in-namespace
container that recreates the tree with the app's uid:gid.

Declared via a libreportal.backup.files compose label; Nextcloud (html, 33:33)
is the first to use it. Live capture failure falls back to stop-snapshot-start.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-23 18:15:53 +01:00
librelad
0d7cab8c97 Merge claude/1 2026-05-23 17:57:22 +01:00
librelad
b5107e30cc feat(admin): Admin Overview landing + unified Admin page headers
Add an Admin Overview as the Admin landing (default when you open Admin): an
ops/health board distinct from the user Dashboard. Four cards built from data
we already generate — Updates (update_status.json, with one-click update),
Backups (backup dashboard.json), SSH & Security (access.json), System
(disk/memory/system_info) — each with a Manage link into the right section.
Styled like the backup dashboard (tiles/status dots).

Wire-up: 'Overview' is the top sidebar item and the default category
(handleConfig + sidebar), rendered by AdminOverview into #config-section via a
renderConfig('overview') special case. Every Admin page now shows the same
'Admin' breadcrumb header (Overview, SSH Access, and the config categories) for
a consistent Admin → Section feel. User Dashboard gets an 'Admin overview →'
link.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-23 17:57:21 +01:00
librelad
193206cbf5 Merge claude/1 2026-05-23 17:31:27 +01:00
librelad
4fd043a852 refactor(webui): fold SSH Access into an Admin area
Rename the Config top-nav to 'Admin' and move SSH Access into its sidebar
under a 'Tools' group, instead of a separate top-level nav item. SSH Access is
rendered by SshPage into the config main pane via a renderConfig('ssh-access')
special case; the sidebar item (config-sidebar.js) routes there. SshPage now
mounts into any container (defaults to #config-section). /ssh redirects to
/config?=ssh-access for old links; the standalone ssh-content.html is removed.

Declutters the top bar and gives system/admin features one home that scales
(updates, users, Connect settings can become sidebar entries later).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-23 17:31:26 +01:00
librelad
403b7055c8 Merge claude/2 2026-05-23 17:18:01 +01:00
librelad
d0ec43e3ca fix(focalboard): chown data dir to runtime uid so db persists
Focalboard runs as nobody (65534) but fixPermissionsBeforeStart hands the app
dir to the install user, so the server couldn't open its sqlite db on the newly
mounted data dir. Chown data/ to 65534 in appUpdateSpecifics and restart.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-23 17:18:00 +01:00
librelad
9e4ebdf360 Merge claude/2 2026-05-23 17:12:09 +01:00
librelad
d0a53d4c7a fix(focalboard): persist database + enable live backup
Focalboard writes its sqlite db and uploads under /opt/focalboard/data (its
working dir), but the compose mounted ./data:/data — an unused path — so the
database was never persisted and was lost on every container recreation. Mount
./data:/opt/focalboard/data so db + files survive, and declare the db for live
backup (data/focalboard.db).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-23 17:12:09 +01:00
librelad
f440dc548d Merge claude/2 2026-05-23 17:04:49 +01:00
librelad
fd3f652fb9 feat(backup): declare gitea sqlite database for live backup
Verified on a live install: gitea's app.ini sets [database] PATH=/data/gitea/
gitea.db with mount ./data/gitea:/data, so the host path is data/gitea/gitea/
gitea.db. (Created only after gitea's web setup completes; until then live
backup safely falls back to stop.)

focalboard intentionally still omitted — its db lives at /opt/focalboard/data/
focalboard.db inside the container, which the compose does not mount, so it
isn't persisted to the app dir and can't be backed up until that's fixed.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-23 17:04:49 +01:00
librelad
a517bd9889 Merge claude/2 2026-05-23 16:55:58 +01:00
librelad
1e6eb628ce fix(backup): survive DB engine first-init restart on restore
Live-restore of a server DB (MariaDB/Postgres) raced the engine's first-init:
it starts a throwaway temp server, runs setup, then restarts the real one. The
old ping-based readiness passed against the temp server and the load hit the
restart, failing once.

- _backupDbWaitReady now requires a real query to succeed on two consecutive
  checks, so the restart breaks the streak and we only proceed once the real
  server is stably up.
- The dump load is retried (idempotent — the dump drops+recreates each object)
  to ride past a final init bounce.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-23 16:55:58 +01:00
librelad
e238e3b52f Merge claude/1 2026-05-23 16:52:47 +01:00
librelad
e75f10618d feat(ssh): WebUI SSH Access page
New /ssh page (topbar nav + SPA route + SshPage controller + ssh-content.html
+ ssh.css). Reads data/ssh/access.json and lets the admin: paste a public key
to authorize a machine, remove keys, and toggle key-only login — all via
'libreportal ssh ...' tasks through the backend's lockout guards. Reuses the
backup key-card styles for a consistent look. This is the inbound counterpart
to the backup location key card (outbound): same paste-a-key model, opposite
direction.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-23 16:52:47 +01:00
librelad
44ad449e21 Merge claude/1 2026-05-23 16:40:59 +01:00
librelad
3a679d7343 feat(ssh): admin host SSH-access engine (backend + CLI + snapshot)
Fresh, on-demand inbound SSH-access management for the host (replaces the old
maze). scripts/ssh/host_access.sh manages the install user's authorized_keys —
add a pasted public key (validated), list, remove — and toggles sshd password
login behind a lockout guard (won't disable passwords with no key; won't drop
the last key while passwords are off; sshd -t before reload, with backup).

New 'ssh' CLI category (status/key-add/key-remove/password-auth/generate) and
a webuiGenerateSshAccess snapshot (data/ssh/access.json: user, password_auth,
authorized keys as type+fingerprint+comment — public only) wired into the
regen chain. Nothing runs automatically; only explicit admin actions change
anything. WebUI page next.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-23 16:40:59 +01:00
librelad
78194ec94e Merge claude/2 2026-05-23 16:39:56 +01:00
librelad
7b32dc2e29 fix(backup): clean snapshot-id capture + accept --latest on restore
Found while testing live backups end-to-end:

- Engine backup adapters logged to stdout, so the caller's $() snapshot-id
  capture was polluted with log text — verify-after-backup then failed with
  'no matching ID' on every run. Route their log lines to stderr so stdout is
  only the id (restic/borg/kopia).
- 'libreportal app restore <app> --latest' (as the help advertises) and the
  bare 'restore <app>' both failed: --latest was passed to restic verbatim and
  unset args arrive as the literal 'empty'. Normalise both to 'latest'.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-23 16:39:56 +01:00
librelad
0339398fc1 Merge claude/1 2026-05-23 16:31:10 +01:00
librelad
2873a92b80 refactor(ssh): remove the vestigial host-SSH key system
The old inbound-admin-SSH layer was effectively dead: gated on config flags
that don't exist (CFG_SSHKEY_*_ENABLED, CFG_REQUIREMENT_SSHREMOTE), its
authorized_keys installer was unwired, and its download path (sshdownload
container) was already retired. What remained reachable was either a no-op or
a lockout footgun (disable-passwords with no working key install).

Remove it whole: scripts/ssh/*, the four SSH requirement checks, the SSH tools
menu, the dead webui SSH populater, and the unused ssh DB inserts; drop their
calls from the start/requirements/menu flows. A fresh, WebUI-driven admin SSH
access feature replaces it next.

Also make generate_arrays.sh self-healing: prune files_*.sh whose source
folder no longer exists (cleared the now-stale files_ssh.sh + an orphan
files_api.sh) so removed areas don't linger in the sourced set.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-23 16:31:10 +01:00
librelad
4078468a97 Merge claude/1 2026-05-23 16:17:34 +01:00
librelad
d3faa2514f feat(backup): SSH key card in the sftp location editor
When a location uses SSH key auth, show a key card: paste an existing private
key, or 'Generate keypair', then the card displays the public key to copy into
the remote server's authorized_keys (with Copy/Delete). Wires to the
ssh-key-set/generate/delete CLI; key mutations refresh locations.json so the
card reflects state immediately. applySshAuthVisibility toggles the card vs the
password field by auth mode. Private key only ever flows in (base64); only the
public key is ever shown.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-23 16:17:34 +01:00
librelad
5db4d518cd Merge claude/1 2026-05-23 16:11:31 +01:00
librelad
19c76f0a3f feat(backup): CLI + data plumbing for per-location SSH keys
Expose the existing location_ssh.sh key store through the backup CLI:
'backup location ssh-key-set|ssh-key-generate|ssh-key-public|ssh-key-delete <idx>'
(the WebUI runs these as tasks). The locations generator now emits
ssh_key_exists + ssh_public_key (public key only — the private key never
leaves the per-location ssh.key file), so the editor can show the key state.
Also fix the stale SSH_AUTH label (~/.ssh/id_rsa -> managed per-location key).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-23 16:11:31 +01:00
librelad
520595bc67 Merge claude/2 2026-05-23 15:41:55 +01:00
librelad
3ba3f77f0b feat(backup): expose per-app strategy override on all apps, context-aware
Every backup-scope app now carries CFG_<APP>_BACKUP_STRATEGY=auto, so the
Backup Strategy dropdown appears in each app's Advanced tab — not just the
DB apps.

To keep it honest, the 'live' option is hidden where it isn't safe:
- apps.json generator emits backup_live_capable per app (from compose backup
  labels: a dumpable DB, or a live-safe marker).
- apps-manager filters the live option out of the strategy select when the
  current app isn't live-capable, so apps like gitea/focalboard (a DB we don't
  yet dump) never offer it.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-23 15:41:55 +01:00
librelad
a11a7a7a71 Merge claude/2 2026-05-23 15:34:17 +01:00
librelad
27ad517626 feat(backup): per-app strategy override (advanced, context-aware)
Adds CFG_<APP>_BACKUP_STRATEGY (default auto) so an app's backup strategy can
be overridden from its Advanced config tab, taking precedence over the global
default. Added to the 10 live-capable apps, so the dropdown's 'live' option only
appears where it actually works.

- backupResolveStrategy now checks the per-app override before the global value.
- backupAppLiveCapable / backupAppStrategyOptions expose capability + the valid
  option set; predicate helpers hardened with explicit returns so they behave
  identically with or without shell errexit.
- BACKUP_STRATEGY field mapping (select, advanced) renders the dropdown.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-23 15:34:17 +01:00
librelad
ab101691cc Merge claude/1 2026-05-23 15:31:57 +01:00
librelad
c14cc8f4f2 fix(webui): register webui_backup_schema.sh in the source arrays
New script files are sourced from the committed files_*.sh arrays (built by
generate_arrays.sh), not a live tree scan — and quick deploys don't rerun
generate_arrays. So the schema generator added last commit was never loaded
live: webuiGenerateBackupSchema was undefined, breaking the webui_updater
backup chain at that step (skipping the passwords regen after it) and leaving
schema.json un-generated.

Regenerate the arrays so the file is registered; deploy now sources it and
'webui generate all' rebuilds schema.json on its own.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-23 15:31:57 +01:00
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
librelad
d1ffb806bd Merge claude/2 2026-05-23 15:16:06 +01:00
librelad
d97a09b119 feat(backup): declare sqlite databases for live backup
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>
2026-05-23 15:16:05 +01:00
librelad
397713d4a8 Merge claude/1 2026-05-23 15:14:47 +01:00
librelad
1cc4b74b2e style(backup): show Type before Name in the location editor and add dialog
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>
2026-05-23 15:14:47 +01:00
librelad
052792a789 Merge claude/2 2026-05-23 15:12:55 +01:00
librelad
69f7289b4a feat(backup): declare server databases + fail safe to stop on dump failure
- 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>
2026-05-23 15:12:55 +01:00
librelad
cc2ea503ee Merge claude/2 2026-05-23 15:09:15 +01:00
librelad
d9f2feef05 feat(backup): consistent live database backups with auto strategy
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>
2026-05-23 15:09:14 +01:00
librelad
ab5b70129f Merge claude/1 2026-05-23 15:07:20 +01:00
librelad
459609a35b style(backup): polish location tabs — drop stray descriptions, pad panels, round corners
- 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>
2026-05-23 15:07:19 +01:00