7 Commits

Author SHA1 Message Date
librelad
8b5e02c760 refactor(storage): resolve every app directory through appDir
The main sweep — ~260 call sites across ~100 files move from string
concatenation on a single root to appDir/storageAppDirs/storageAppConfigs.
On a single-root install the resolved paths are identical, so this is a
no-op until a location is registered.

Enumerators were the interesting half. `for d in "$containers_dir"/*/`
appears in the menus, the registry/artifact scanners and the DNS setup —
and a shell glob cannot list a rootless 751 tree at all, which is the
same bug config_find_file.sh already documents in a comment. Routing them
through storageAppDirs (which enumerates as the owning user) fixes that
alongside the multi-root work.

Three places needed judgement rather than substitution:

db_app_scan.sh deletes database rows and port allocations for apps whose
folder is missing, and reaps "empty" app dirs. With a storage location
unmounted, every app on it looks exactly like that. Each of those
branches now gates on appStorageAvailable first — an app on an unplugged
drive is skipped with a notice, never deleted.

instance_create.sh rewrites cloned hooks so an instance touches its own
directory instead of the base app's. Its sed matched ${containers_dir}<type>,
which this sweep just replaced with $(appDir <type>) — so it would have
silently stopped redirecting, and an instance would have written to the
original's files (the adguard auth adapter case its own comment warns
about). Now matches both appDir forms, verified against bare, quoted,
unrelated-app, legacy and prose cases.

peer_shell/peer_pull streamed and extracted relative to the primary root.
Both now use the app's own root, and peer_shell keeps a single-root
fallback since it runs as a restricted SSH shell with no LibrePortal env.

Also fixes a pre-existing bug found on the way: webui_app_config.sh
tested "$containers_dir/frontend/data/last_update", one level short of the
real tree under the libreportal app dir, so the WebUI refresh trigger
after a config update has never once fired.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 04:09:51 +01:00
librelad
20f8ca2eb5 feat(network): detect + heal apps stranded off the docker subnet
Closes the gap behind the vpn-recreate bug: when the shared network is
recreated with a different /24, every app's stored static IP is left
outside it and adoptDockerSubnet only realigns CFG, not the apps.

- networkScanConflicts (network_conflicts.sh): read-only scan diffing each
  active network_resources IP against docker's real subnet (via ipInSubnet).
  Per-service routing-aware — skips gateway-routed services whose ipv4 is
  commented out in the deployed compose, so gluetun apps don't false-positive.
  Distinguishes 'daemon down' (benign) from 'network missing' (real).

- webuiSystemNetworkCheck (webui_system_network.sh): self-throttled generator
  that writes frontend/data/system/network_status.json (modelled on
  verify_status.json). Wired into webuiSystemUpdate AND run unconditionally
  every ~60s from the task-processor poll (regen webui is mtime-gated and
  would never fire on drift, which touches no source file).

- networkHealConflicts (network_heal.sh) + 'libreportal system network
  check|heal [app]': the heal adopts docker's subnet in-process, then re-IPs
  stranded apps with reset_network=ip (ports preserved), gluetun first.
  Mutating path runs only through the task system (dual-mode, like update
  apply); read-only check runs inline.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 16:03:53 +01:00
librelad
14e6d4aba1 fix(network): converge when the docker network already exists
installDockerNetwork errored with 'network with name <x> already exists'
on re-runs: the requirement check sets DOCKER_NETWORK_SETUP_NEEDED=true
whenever 'docker network inspect' returns non-zero, but that also happens
when the rootless daemon socket isn't reachable yet — indistinguishable
from the network being genuinely absent. A prior install also leaves the
network behind, so the flag fires on every re-install.

Re-check existence right before creating and converge: if the network is
already there, leave it in place and adopt its real subnet into CFG rather
than erroring. This also stops the spurious subnet randomization (and the
resulting CFG drift) that ran before the doomed create.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 15:02:36 +01:00
librelad
053a620e22 fix(reliability): split local result=$(cmd) so $? survives for checkSuccess
'local result=$(cmd)' resets $? to 0 (the local builtin's own exit), so the
following checkSuccess always saw success regardless of cmd's real exit — the
mechanism that masked the de-sudo write failures. Split declaration from
assignment ('local result; result=$(cmd)') across all 235 active-code sites
(84 files) so the command's exit reaches checkSuccess. No behaviour change
beyond $? now being accurate (no set -e in runtime code; multi-line
assignments transform safely).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-31 03:09:25 +01:00
librelad
e56e6918a7 refactor(network): drop dead 'migrate apps to new subnet' machinery
The migrate/ helpers were either uncallable or no-ops:

- migrateAppsToNewNetwork + updateComposeFileNetwork: never called from
  anywhere. The intended sed-on-compose subnet rewrite would also have
  fought the tag system / network_resources DB.
- checkAppNetworkCompatibility: called from updateDockerNetworkConfig as
  a gate, but never explicitly returns, so it's effectively always-true
  and both branches do the same work. Pure noise.
- getInstalledApps: only used by the above.
- updateDockerNetworkConfig: collapses to a 2-line 'CFG := docker's
  reported subnet' adoption — inlined into check_docker_network.sh as
  adoptDockerSubnet(), which is what it actually does.

The legitimate 'subnet changed, refresh apps' path is already covered by
the idempotent per-app reinstall (dockerInstallApp ... reset_network=true
→ clears DB allocations → installer re-runs → ipUpdateComposeTags picks
fresh IPs from the current CFG_NETWORK_SUBNET). Migration (infrastructure
regen) vs restore (data) stays clean: reinstall regenerates compose+IPs,
restore lays data on top. No new pathway needed.

Files dropped:
  scripts/docker/network/migrate/migrate_apps_to_new_network.sh
  scripts/docker/network/migrate/migrate_check_app_network_compatibility.sh
  scripts/docker/network/migrate/migrate_get_installed_apps.sh
  scripts/docker/network/migrate/migrate_update_compose_file_network.sh
  scripts/docker/network/migrate/migrate_update_docker_network_config.sh

Plus the now-empty migrate/ subdir; files_docker.sh regenerated to drop
the references.

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-26 16:11:13 +01:00
librelad
a3afb2aeae feat(model-a): run app as manager; route bare docker calls through runFileOp
Model A prototype (run start.sh AS the manager, escalate only via helpers):
- check_root.sh: accept the manager user, not root-only (init.sh keeps its own
  install-time root check).
- init.sh: guard the top-level root-check + installer entrypoint with
  BASH_SOURCE!=$0 so it runs ONLY when init.sh is executed directly; when
  start.sh sources it as the manager the entrypoint (and its root check) no
  longer fires.

Also: convert bare daemon-touching 'docker' calls (no helper -> hit the
nonexistent /var/run socket in rootless) to runFileOp docker across
app_status, app_health_*, network_prune, ip_is_available, check_docker_network,
backup_db (db dumps) and crontab_check_processor. cd&&compose rooted-branches
and 'docker compose --version' checks left as-is (rooted-only / no daemon).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-24 16:53:37 +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