9 Commits

Author SHA1 Message Date
librelad
8b14f26125 refactor(desudo): route scattered runtime sudo through privilege helpers
Convert the remaining ad-hoc 'sudo' calls across the data plane to the
run_privileged helpers so every file op lands as the correct owner with
no blanket root:

- DB/configs (manager-owned): db_list_all_apps, delete_db_file,
  install_sqlite, cli_webui_commands -> runInstallOp
- containers (dockerinstall-owned): scan_container_socket, delete_data,
  webui_task_files, webui_app_log, webui_config_patch,
  application_missing_variables, uninstall_app -> runFileOp/runFileWrite
- genuine root: passwd, tailscale, ufw-docker, sysctl grep, systemd
  unit read, authorized_keys read, nobody chown -> runSystem
- interactive editors and 'id -u': drop sudo entirely (run as caller)
- owncloud/adguard container-UID config edits -> runSystem (funnel;
  docker-exec rework deferred)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-24 18:00:19 +01:00
librelad
bade6eaacb feat(webui): collect host + per-app metrics with history ring buffer
Add webui_system_metrics.sh, run each minute from webuiSystemUpdate:
- whole-server snapshot (metrics.json): CPU% + load, memory + swap,
  per-mount disk + inodes, network rx/tx rate, docker summary
- capped ring buffer (metrics_history.json, 24h default) for trend charts
- per-app docker stats grouped by compose project (metrics_apps.json)
  plus a short per-app history (metrics_apps_history.json) for sparklines

CPU% and network rate use stateful deltas stashed beside the JSON; all
host metrics read from /proc and docker via runFileOp, so it works rootless.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-24 16:46:46 +01:00
librelad
ed9697cdc0 fix(rootless): apps/categories/config/system generators write as container owner
The remaining WebUI generators built JSON into a temp file inside the
output dir then placed it with mv/sudo mv + a createTouch that can't re-own,
so in rootless they produced root/libreportal-owned data and 'touch:
Permission denied' spam. Two problems: the temp lived in the (now
dockerinstall-owned) output dir, which the cron updater — running as
libreportal — can't write; and the final file landed wrong-owned.

Move each temp to mktemp (/tmp, writable by whoever runs the updater) and
place the result via runFileWrite (writes as the container owner:
dockerinstall in rootless, manager in rooted), dropping the redundant
createTouch; convert the dir mkdirs to runFileOp. Covers apps
(services/config/tools/app_status/gluetun/config_patch), categories
(app/config-categories/field-mappings), config (configs.json) and system
(info/memory/disk/update). The logs file is handled by the now mode-aware
createFolders + createTouch.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-24 14:07:46 +01:00
librelad
75dfb3849b fix(rootless): backup/ssh WebUI generators write as the container owner
The backup + ssh generators created their frontend/data dirs via plain/sudo
mkdir and wrote files via sudo tee/mv (root-owned), then called createTouch
(dockerinstall) which can't re-own a root file — so every write hit
'touch: Permission denied' in rootless and left root-owned data the
dockerinstall container/generators can't rewrite. Convert dir creation to
runFileOp mkdir and file writes to runFileWrite (both run as the container
owner: dockerinstall in rootless, manager in rooted), dropping the
temp/mv/createTouch dance. Also make the createFolders chokepoint mode-aware
(containers/ paths created via runFileOp) so it mirrors createTouch.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-24 14:04:06 +01:00
librelad
ecd3db5632 fix(rootless): route requirement/disk docker checks through runFileOp
Three docker calls ran the binary directly (two plain, one sudo), so in
rootless they hit /var/run/docker.sock (the rooted socket, absent) and
printed 'Cannot connect to the Docker daemon' — the WebUI-image requirement
check, the system-disk WebUI generator (docker system df), and the
app-install fallback (docker ps). Route all three through runFileOp, which in
rootless runs as the docker install user with DOCKER_HOST set and is
argv-safe for --format, and in rooted runs as the manager via the docker
group.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-24 13:39:06 +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
315c528306 refactor(webui): silence per-file touch/chown noise in data generators
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>
2026-05-23 12:40:32 +01:00
librelad
d5fe1bc56b feat(webui): out-of-date detection + one-click update
Surface when LibrePortal is behind upstream and let users update from the
WebUI, reusing the proven git-update path instead of reinventing it.

Detection (host): webuiSystemUpdateCheck writes
frontend/data/system/update_status.json from a throttled git fetch +
behind-count + VERSION compare, off the existing per-minute
`webui generate system` cron. A new /VERSION file is the canonical version.

Display (frontend): update-notifier.js/.css render a global topbar badge
(every page) and a dashboard banner (prominent when behind, subtle "up to
date" with a manual check otherwise), plus a details panel.

Actions go through the task pipeline:
- `libreportal update apply` -> webuiRunUpdate (non-interactive: guards,
  forced check, gitPerformUpdate, then dockerInstallApp libreportal)
- `libreportal update check` -> forced recheck

gitFolderResetAndBackup's body is extracted into gitPerformUpdate (no exit)
so the WebUI path can reuse it; the interactive CLI flow is unchanged.

Detection JSON verified against the repo (up-to-date and behind cases).
webuiRunUpdate's re-clone + redeploy still needs validation on a live host.

The latest-version source is git for now and is the single swap point for
get.libreportal.org later — the JSON contract and frontend stay unchanged.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-21 23:33:43 +01:00
librelad
875a60f90f LibrePortal v0.1.0 — initial release
A free, open, self-hosted app platform (GNU AGPLv3): one-click app deploys,
Traefik reverse proxy with automatic SSL, rootless Docker support, gluetun
VPN routing, and a web dashboard to manage it all.

Free & open forever to self-host; optional paid hosted services fund it.
See PROMISE.md.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-21 20:37:54 +01:00