401 Commits

Author SHA1 Message Date
librelad
ef100acb3c refactor(hosting): website -> containers/weblibreportal; getlibreportal = downloads-only
Move the loose root-level site/ into a proper containers/weblibreportal app
(mirrors getlibreportal): the Eleventy source + nginx serving ./data via publish.sh
(npm run build -> docroot). Fix gen-data.mjs repoRoot (now ../../.. from
containers/weblibreportal/scripts) so it still finds containers/ for the catalogue.

Decouple the two hosts:
- weblibreportal  -> the website (libreportal.org)
- getlibreportal  -> downloads only (install.sh + signed release channels); its
  publish.sh no longer builds the site, and its config text updated to match.

Both are dev-only project hosting and will move to a separate repo later; for now
they live under containers/ as normal apps. ignores updated for their built
docroots; dropped the dead 'site export-ignore'.

Verified: gen-data builds the catalogue from the new location (33 apps), and
weblibreportal/publish.sh produces a docroot with index.html.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-25 20:44:38 +01:00
librelad
ca01fd503f Merge claude/1 2026-05-25 20:02:47 +01:00
librelad
63b53c7751 feat(hosting): getlibreportal as a first-class LibrePortal app (phase E)
Redo the download/website host as a normal app under containers/ (dogfooded — the
project hosts its own downloads on LibrePortal), instead of the bespoke repo-root
thing. Modeled on speedtest: standard getlibreportal.{config,sh,svg} +
docker-compose.yml (tagged template) so it plugs into the app scan + install
dispatch (installGetlibreportal) like every other app. nginx serves ./data (the
app data dir) — no special /web.

- getlibreportal.config: features category, public (login=false — it's a download
  host), no backup (regenerable), healthcheck on.
- docker-compose.yml: nginx:alpine, ./data:ro docroot + ./nginx.conf, traefik tags.
- nginx.conf: install.sh + latest.json no-cache; tarball/.sha256/.minisig immutable.
- publish.sh: assembles the docroot (built site + install.sh + dist/<channel>) into
  a target data dir; run on a full repo checkout (site/ + dist/ are host-side).
- exec bits set on the run-directly scripts (make_release.sh, install.sh, publish.sh).
- .gitattributes: dropped the stray 'getlibreportal export-ignore' (the no-slash
  pattern would also have excluded containers/getlibreportal — the app must ship);
  data/ gitignored.

Verified: app discovered by the site catalog (32 apps), installGetlibreportal matches
the dispatch name, and the full release->publish flow yields a docroot with the
website + install.sh + the signed/checksummed stable channel. The actual app-install
run + DNS/TLS for get.libreportal.org are operational steps (need a real host).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-25 20:02:47 +01:00
librelad
b9daae36e6 Merge claude/1 2026-05-25 19:40:30 +01:00
librelad
5700f78c6b feat(release): minisign signature signing + verification
The sha256 only proves a download is intact; a compromised host could swap the
tarball + its checksum. Add minisign signatures, which prove authenticity (the host
can't forge them without the offline secret key). Ships INACTIVE behind a REPLACE_ME
placeholder, so installs work until a real key is generated; then it's REQUIRED.

- make_release.sh: signs the tarball when LP_MINISIGN_SECKEY is set -> <tarball>.minisig.
- libreportal.pub: the public key (placeholder), ships in the tarball and is installed
  to the ROOT-OWNED footprint (/usr/local/lib/libreportal/libreportal.pub) by init.sh
  -> the manager can't swap it to accept forged updates. footprint_version -> 2.
- install.sh: LP_MINISIGN_PUBKEY constant; once non-placeholder, downloads + verifies
  the .minisig (minisign -P) and REFUSES on invalid/missing (auto-installs minisign if
  needed). --no-verify-signature is a dev-only escape hatch.
- fetch.sh (update path): verifies against the footprint .pub (minisign -p), refuses on
  invalid/missing.
- docs/DEVELOPMENT.md: keygen (minisign -G), paste pubkey into libreportal.pub +
  install.sh, keep the secret key offline, sign builds via LP_MINISIGN_SECKEY, bump
  footprint_version on key rotation.

Verified end-to-end with a real throwaway key: good signature accepted; tampered,
wrong-key, and missing-signature all refused; placeholder skips (sha256 still enforced).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-25 19:40:30 +01:00
librelad
d0162ccda1 Merge claude/1 2026-05-25 19:07:16 +01:00
librelad
3014965b66 feat(update): FOOTPRINT_VERSION drift detection — flag when a root re-install is needed
A manager-run 'update apply' refreshes code/apps/WebUI but CANNOT rewrite the
root-owned footprint (helpers/wrapper/uninstall/unit/sudoers) — that immutability
is the de-sudo boundary. Previously a release that changed those would silently
leave them stale. Make it explicit:

- init.sh: footprint_version=1 constant, baked at install into
  /usr/local/lib/libreportal/.footprint_version (root:root 0644) by initRootHelpers.
  Bump it whenever a root component changes.
- make_release.sh: publishes footprint_version in latest.json.
- fetch.sh: lpInstalledFootprintVersion (marker) + lpReleaseLatestFootprint (manifest).
- check_update.sh: 'update apply' REFUSES when the release's footprint_version
  exceeds the installed one, directing to a root re-install (which fetches +
  re-bakes everything atomically). No half-applied updates.
- webui_system_update.sh: badge sets footprint_update_needed + clears can_update so
  the WebUI won't offer a one-click apply for a footprint-bumping release.
- docs/DEVELOPMENT.md: the bump rule + the footprint exception explained.

Verified: manifest carries footprint_version; drift decision correct both ways
(no marker/older -> needs re-install; equal -> no drift).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-25 19:07:16 +01:00
librelad
d95560c48c Merge claude/1 2026-05-25 18:59:38 +01:00
librelad
f2c2b0485a refactor(uninstall): drop repo uninstall.sh; init.sh generates the command
Remove the redundant repo-root uninstall.sh (it duplicated libreportal-uninstall).
init.sh now GENERATES the libreportal-uninstall launcher into the fixed footprint
(/usr/local/lib/libreportal/uninstall.sh + the /usr/local/bin symlink) — same
pattern as the CLI wrapper, so the on-box command survives without a separate repo
file. The launcher just runs the engine's uninstall ($script_dir/init.sh baked in,
/root/init.sh fallback).

This resolves the install/uninstall asymmetry: a bootstrap (install.sh) exists only
because install faces a bare box with no code yet; uninstall always runs the engine
that's already installed, so it needs no bootstrap — just a generated door into
init.sh. Repo root install/uninstall surface is now init.sh + install.sh.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-25 18:59:38 +01:00
librelad
94d91ff551 Merge claude/1 2026-05-25 18:50:26 +01:00
librelad
8472fdf0ae feat(uninstall): location-agnostic 'libreportal-uninstall' command (+ de-hardcode docs)
The docs were telling users to run /libreportal-system/install/uninstall.sh — a
hardcoded data path, wrong for any custom --system-dir, contradicting the whole
relocatable design.

Fix it the way the CLI already works: install uninstall.sh to the FIXED footprint
(/usr/local/lib/libreportal/uninstall.sh) and symlink it onto $PATH as
'libreportal-uninstall' (initLibrePortalCommand). It self-resolves the real data
roots from the systemd unit, so the command is the same everywhere regardless of
where data lives. Teardown removes the new symlink; FOOTPRINT.md lists it.

Docs now say 'sudo libreportal-uninstall' — no data path. (Dev-from-clone still
uses ./uninstall.sh / ./init.sh uninstall.)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-25 18:50:26 +01:00
librelad
55dd62af56 Merge claude/1 2026-05-25 18:46:05 +01:00
librelad
cb298d41e6 docs(user): point uninstall at ./uninstall.sh (handles custom roots)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-25 18:46:05 +01:00
librelad
45626d0641 Merge claude/1 2026-05-25 18:45:16 +01:00
librelad
805d557fd7 fix(uninstall): resolve the real install roots + add ./uninstall.sh
Bug: runFullUninstall used the derived $docker_dir/$containers_dir/$backup_dir,
but a bare 'init.sh uninstall' on a CUSTOM-location install has no LP_*_DIR in
scope and no /docker marker — so it defaulted to /libreportal-* and would MISS the
real data (e.g. /mnt/ssd), leaving it behind.

Fix: libreportalReadBakedRoots reads the authoritative baked record from the
systemd unit (Environment=LP_SYSTEM_DIR/CONTAINERS_DIR/BACKUPS_DIR + User=<manager>)
and runFullUninstall re-derives from it before removing anything. Legacy units
(no LP_*_DIR) fall through to the derive defaults + /docker compat shim.

Add top-level uninstall.sh: a root-only convenience that finds the installed
init.sh (via the unit's system root, then common locations) and runs it —
'sudo ./uninstall.sh [--skip-docker-images]'. Verified the unit parsing extracts
custom roots/manager and the discovery picks the right init.sh (without running the
destructive teardown).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-25 18:45:16 +01:00
librelad
800c70fefd Merge claude/1 2026-05-25 18:40:03 +01:00
librelad
40aa6d9d1a docs: move dev/reference docs into docs/ + refresh FOOTPRINT for the 3-root layout
Tidy the repo root (README + LICENSE stay there per convention; everything else
moves):
- CONTRIBUTING.md, PROMISE.md, FOOTPRINT.md -> docs/ (alongside USER.md/DEVELOPMENT.md)
- update the references: README links, the website site.json raw URLs, init.sh's
  'see FOOTPRINT.md' comments -> docs/FOOTPRINT.md; drop the now-redundant
  CONTRIBUTING.md export-ignore (docs/ is already export-ignored).

Refresh FOOTPRINT.md: it claimed 'everything lives under /docker', which is no
longer true. Now describes the three relocatable roots (system/containers/backups)
and makes explicit that the roots + manager name are baked into the helpers/unit/
wrapper at install (the privilege boundary) while this out-of-root footprint stays
fixed by design. Uninstall sketch + sudoers/unit rows updated for the configurable
manager. CONTRIBUTING/PROMISE were already current — left as-is.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-25 18:40:03 +01:00
librelad
846936617e Merge claude/1 2026-05-25 18:27:59 +01:00
librelad
a48a241fbe docs: add docs/USER.md + docs/DEVELOPMENT.md (+ README pointer)
Two guides covering what wasn't written down:
- USER.md: install (the install.sh one-liner), placing the three roots on separate
  disks/external drives, channels, updating, backups (REQUIRE_MOUNT), uninstall.
- DEVELOPMENT.md: the install-mode/roots/users model + key files; running a dev copy
  (local/git); cutting stable/edge releases (bump VERSION -> make_release.sh ->
  dist/<channel>/{tarball,.sha256,latest.json} -> publish); testing a release
  locally via LP_RELEASE_BASE_URL + python3 -m http.server (incl. checksum-refusal);
  how release updates work; conventions.

README Quick start updated to the release flow + a docs pointer. docs/ is
export-ignored so it doesn't bloat release tarballs.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-25 18:27:59 +01:00
librelad
89049864a6 Merge claude/1 2026-05-25 18:16:35 +01:00
librelad
ddea6b8a4d feat(update): route reset/reinstall recovery paths through the release fetch (phase D)
The git-era recovery commands now do the right thing in release mode instead of
attempting a clone:
- gitReset (libreportal reset / update reset) and runReinstall (CLI/system reset,
  missing-files recovery): a release branch re-fetches the verified tarball via
  lpFetchRelease, then refreshes /root/init.sh + ownership.
- the CLI wrapper's clone_and_install (libreportal reset): sources fetch.sh and
  re-fetches the release; falls back to directing the user to the install.sh
  bootstrap if the helper isn't present.

git/local behaviour unchanged. Wrapper still bakes cleanly (no placeholders left).

Phases A–D (release build, bootstrap installer, fetch abstraction, release-aware
install + update + recovery) are complete and locally verified. Remaining: phase E
(host install.sh + channels + tarballs on get.libreportal.org) and a real fresh
install on a throwaway box.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-25 18:16:35 +01:00
librelad
540b0518c5 Merge claude/2 2026-05-25 18:13:55 +01:00
librelad
3064328aa8 fix(webui): populate admin sidebar on cold visit
The admin landing (overview) and the tools pages (ssh-access, system) call
populateSidebar() without first loading window.configData. On a cold admin
visit — e.g. navigating straight from the dashboard — configData is undefined,
so populateSidebar() bails early and the sidebar renders empty. Visiting
Backups happened to set window.configData, which is why returning to admin
afterward showed the sidebar.

Load (cached) config data up front in renderConfig before any branch renders so
the sidebar always has its categories. The config-category path's later
loadConfig is now a cache hit.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-25 18:13:55 +01:00
librelad
6be0db57da Merge claude/1 2026-05-25 18:13:09 +01:00
librelad
acfe7d6bfa feat(update): release-aware update detection + apply (phase D)
Make the WebUI updater work off release versions, not git commits, in release mode
(git/local paths untouched):

- webui_system_update.sh: a release branch resolves latest_version from the channel
  manifest (lpReleaseLatestVersion), computes update_available via lpVersionGt vs
  the local VERSION, reuses the same throttle + the same update_status.json schema
  (source="release"); reuses last-known latest when throttled so the badge
  doesn't flicker.
- check_update.sh webuiRunUpdate: a release branch version-compares and, if newer,
  lpFetchRelease (download + checksum-verify) the new tarball + dockerInstallApp
  redeploy + regen. No config-backup dance — lpFetchRelease replaces only the
  install tree; configs/logs are in the separate system tree.

Verified against a local server: latest-version read + the no-update / update-
available decision (0.2.0==0.2.0 no; 0.3.0>0.2.0 yes). Remaining: route the
reset/reinstall recovery paths through the release fetch.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-25 18:13:09 +01:00
librelad
584d330636 Merge claude/1 2026-05-25 18:10:42 +01:00
librelad
c78391753b fix(release): exclude scripts/release/ from sourced arrays
Adding scripts/release/ made generate_arrays emit files_release.sh, which would
source the build tool make_release.sh at runtime (it runs git archive at load —
wrong + harmful). Skip release/ in the folder scan (like system/ and unused/),
remove the orphaned files_release.sh, and regenerate the meta-list. fetch.sh stays
sourced; make_release is in no array.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-25 18:10:42 +01:00
librelad
d9b76db157 Merge claude/1 2026-05-25 18:08:39 +01:00
librelad
90663a077a feat(install): release fetch mode + lpFetchSource abstraction (phase C)
scripts/source/fetch.sh (sourced at runtime via files_source.sh):
- lpFetchRelease [ver]: resolve channel manifest -> download tarball -> VERIFY
  sha256 (refuse on mismatch/absence) -> replace the install tree ( is
  code-only now; configs/logs live in the separate system tree, so no backup
  dance). Host/channel from LP_RELEASE_BASE_URL/CFG_RELEASE_BASE_URL + channel.
- lpFetchSource: dispatch release|git|local.
- lpVersionGt: numeric dotted semver compare (used by the updater + badge).

init.sh initGIT is now release-aware: the bootstrap (install.sh) stages+verifies
the code and sets LP_ALREADY_FETCHED=1 (skip re-fetch); a direct release run sources
fetch.sh; a bare /root reinstall is directed to install.sh. install.sh exports
LP_ALREADY_FETCHED + LP_RELEASE_BASE_URL on hand-off. validateUnattended already
accepts release (git-url is gated on git mode).

Config: CFG_INSTALL_MODE default -> release, + CFG_RELEASE_BASE_URL / CFG_RELEASE_CHANNEL
(add-only reconcile preserves existing installs' git/local mode).

Verified: lpVersionGt across cases; lpFetchRelease downloads+verifies+extracts a
clean tree against a local server. The updater + reset/reinstall release paths are
phase D.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-25 18:08:39 +01:00
librelad
c03b54d9a5 Merge claude/1 2026-05-25 18:04:14 +01:00
librelad
9207ec384b feat(install): thin bootstrap installer install.sh (phase B)
curl -fsSL https://get.libreportal.org/install.sh | sudo bash [-s -- FLAGS]

Self-contained bootstrap (downloaded and run before any LibrePortal code exists):
resolves the channel manifest (<base>/<channel>/latest.json), downloads the
release tarball, VERIFIES the sha256 (manifest or .sha256 sidecar) and refuses on
mismatch, extracts to <system>/install, then hands off to init.sh unattended with
the relocatable flags (--system-dir/--containers-dir/--backups-dir/--manager-user/
--allow-home). curl-or-wget, sha256sum-or-shasum; honours LP_RELEASE_BASE_URL for
testing against a local/file server.

Dev modes preserved: --local=PATH and --git-url= (with --git-user/--git-token).
Generates a random manager/WebUI password if none given. --dry-run stages + verifies
without installing.

Verified against a local http server: dry-run resolves→downloads→verifies→extracts
and reports the correct init.sh handoff; a corrupted tarball is refused. The
end-to-end exec needs init.sh release-mode awareness (phase C) + a throwaway box.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-25 18:04:13 +01:00
librelad
7f6e397cc9 Merge claude/1 2026-05-25 18:00:11 +01:00
librelad
2ce0d22954 build(release): add make_release.sh + export-ignore for clean tarballs (phase A)
scripts/release/make_release.sh builds a versioned, checksum-verified release
artifact from the committed tree via 'git archive' — the basis for installing
LibrePortal without git/auth. Output lands in dist/<channel>/ laid out as the
hosting will serve it (libreportal-<ver>.tar.gz + .sha256 + latest.json), so it
can be served locally for testing via LP_RELEASE_BASE_URL.

.gitattributes marks dev-only trees export-ignore (scripts/unused, scripts/release,
site, .claude, CONTRIBUTING.md, the git meta files) so they never ship; validated
the archive includes init.sh/start.sh/scripts/configs/containers/VERSION and
excludes all of the above. dist/ gitignored.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-25 18:00:11 +01:00
librelad
295ca2a9b4 Merge claude/1 2026-05-25 17:47:05 +01:00
librelad
b47e20133d feat(install): make the control-plane manager user configurable
sudo_user_name (the real manager — owns the install, runs the runtime, baked as
__MANAGER__ into the root helpers) was hardcoded to 'libreportal'. Make it
configurable, consistent with the relocatable roots:

- --manager-user=NAME flag + LP_MANAGER_USER env (default libreportal); resolved
  early in init.sh and in scripts/source/paths.sh (so the standalone processors
  get it too), validated as a real Linux username in libreportalValidatePaths.
- Baked everywhere it must be stable: the helpers + CLI wrapper (CHECK_USER now
  __MANAGER__, exports LP_MANAGER_USER) via the install-time sed; the systemd unit
  exports LP_MANAGER_USER=<manager>. User creation (initUsers), the sudoers
  drop-in, and ~35 call sites already used $sudo_user_name, so they follow.
- Fix the stray manager-name literals: install_crowdsec.sh chown, the
  check_install_type fallback. (Brand/identity strings like the backup
  engine:libreportal tag are left — they're not the username.)

Verified: resolves default/env/flag; wrapper bakes a custom name (admin) with no
placeholders left; validation rejects invalid usernames. The footprint paths
(/etc, /usr/local) stay fixed by design.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-25 17:47:05 +01:00
librelad
f56bbda2a8 Merge claude/1 2026-05-25 17:42:20 +01:00
librelad
27b4a877f5 refactor(cleanup): retire the dead SFTP 'Docker Manager User' to unused/
CFG_DOCKER_MANAGER_USER / installDockerManagerUser was a chrooted SFTP file-access
user — unrelated to the LibrePortal control-plane manager (sudo_user_name), and
the source of the 'two managers' confusion. It was permanently-off dead code: the
gate CFG_DOCKER_MANAGER_ENABLED and the CFG_DOCKER_MANAGER_USER/_PASS keys are
defined in no config template, so it never ran. Its SSH-key-management sibling
(unused/ssh_manager.sh) was already retired; admin host SSH access is handled by
the current /ssh page + scripts/ssh/host_access.sh.

Move install_user_manager.sh / uninstall_user_manager.sh / check_manager.sh to
scripts/unused/manager/ (recoverable, matches the graveyard convention — not
deleted, in case the SFTP-user idea is rebuilt cleanly later), drop the two call
sites (start_preinstall.sh, check_requirements.sh), regenerate the arrays.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-25 17:42:20 +01:00
librelad
f5afb91968 Merge claude/1 2026-05-25 17:18:46 +01:00
librelad
16571134b5 refactor(paths): scrub residual /docker references in display text + comments
Audit follow-up — after a full-repo sweep, the only remaining functional /docker
refs are intentional (the legacy compat shim + the env-overridden legacy-safe
backend default). Fix the last user-visible/stale ones:
- config-options.js: backup PATH_MODE 'auto' label no longer hardcodes
  /docker/backups (the path is relocatable) — describes the behaviour instead.
- config.js / setup-detector.js / webui_install_image.sh: refresh comments that
  named /docker to the relocatable system/containers roots.

No behaviour change. Active container app scripts already use $containers_dir;
the remaining /docker hits across the tree are docker-compose.yml filenames,
/var/lib/docker, the docker binary, relative array paths, docs/site, and the
unused/ graveyard.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-25 17:18:46 +01:00
librelad
67921eeaf2 Merge claude/1 2026-05-25 15:35:00 +01:00
librelad
61cebb5ab8 feat(backup): external/removable drive safety guards (phase 3b)
backupLocationLocalGuard (engine-agnostic, in location_paths.sh), wired into the
dispatcher before init, readiness, and every backup write (engineInitLocation /
engineEnsureLocationReady / engineBackupApp):

- Filesystem warning: the ownership model chowns the repo to the backup user, which
  needs POSIX permissions — warn (non-fatal) on FAT/exFAT/NTFS via findmnt FSTYPE.
- Mount-presence refusal: a location with CFG_BACKUP_LOC_<idx>_REQUIRE_MOUNT=true
  (an external/removable disk) is refused when its path isn't on a real mount
  (findmnt TARGET is '/' or unknown) — so an unplugged drive never silently fills
  the system disk. Opt-in; default false leaves on-disk locations unaffected.

New REQUIRE_MOUNT field documented in the location.config template (location_add.sh)
so it surfaces on the Locations page. Verified: REQUIRE_MOUNT+unmounted refuses;
default allows; non-local no-ops.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-25 15:35:00 +01:00
librelad
5bc1abb5e1 Merge claude/1 2026-05-25 15:29:34 +01:00
librelad
38e531ed6e feat(install): custom, root-baked install locations (phase 3)
Make the three roots selectable at install and bake them into the CLI wrapper
(the last /docker-hardcoded consumer).

- init.sh: --system-dir= / --containers-dir= / --backups-dir= flags (=form keeps
  the single-token shift logic), plus --allow-home; LP_*_DIR env also honored.
  Re-derives paths after flag parsing.
- libreportalValidatePaths (run only in the install flow): each root must be a
  non-root absolute path outside protected system trees; the three must not nest
  (except the legacy /docker compat layout); a containers/backups root inside a
  human home is refused unless --allow-home (rootless o+x traversal = privacy
  trade-off). The root helpers re-check at runtime (defence in depth).
- CLI wrapper: a baked bootstrap (the same __ROOT__ placeholder mechanism as the
  helpers) exports LP_*_DIR and derives docker_dir/configs_dir/script_dir; every
  /docker literal in the heredoc now resolves from those at runtime. init.sh seds
  the placeholders into the root-owned wrapper after writing it.

The scoped sudoers needs no change (it references only the fixed helper paths +
system binaries, never a data root). Custom locations verified end-to-end:
generate+bake the wrapper with /mnt/* roots → syntax OK, no placeholders left,
paths resolve. Live box untouched (wrapper/helpers only change on reinstall).

Phase 3b (external-drive guards) + phase 4 (verify) follow.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-25 15:29:34 +01:00
librelad
492e62b6d0 Merge claude/1 2026-05-25 15:21:28 +01:00
librelad
edcdf00aca feat(layout): three-root split + ownership model (phase 2)
Split the single tree into three owner-isolated roots and fix the backup
permission failure (restic, running as the container user, could not write the
manager-owned /docker/backups).

Ownership helper (libreportal-ownership), rewritten for three baked roots:
  SYSTEM_DIR (manager)  CONTAINERS_DIR + BACKUPS_DIR (container user)
- reconcile now drives each tree to its single owner; backups + the WebUI dir go
  to the container user (the actual fix). The container user reaches only the
  WebUI bind-mount sources (configs/webui/*) via a scoped _webui_bind_access —
  traverse the system root + configs, read configs/webui only, nothing else.
- defence-in-depth: refuse dangerous/relative roots even if mis-baked; new
  backups-top action.

Baking: init.sh initRootHelpers now seds __SYSTEM_DIR__/__CONTAINERS_DIR__/
__BACKUPS_DIR__ (alongside __MANAGER__) into every helper at install — the trust
boundary stays root-controlled. svc/socket/appcfg helpers updated to derive from
the baked SYSTEM_DIR; the svc unit now exports LP_*_DIR so the processor resolves
roots authoritatively. A baking-safe '*"__"*' sentinel check survives the sed.

Install/uninstall: initFolders creates the three roots; initContainerLayer hands
containers + backups to the container user; uninstall removes all three
(idempotent on legacy single-tree installs). Remaining functional /docker
literals in init.sh (config reads, setupConfigsFromRepo, uninstall) parameterised.

Compose: the WebUI's two relative ../../configs mounts (the only cross-tree
relative mounts in the tree) are now absolute, filled at generation via a new
CONFIGS_DIR_TAG; CONTAINERS_DIR_TAG likewise for the LP_CONTAINERS_DIR env.

Live box unaffected: installed helpers + the live compose only change on reinstall/
rebuild (both of which fill the tags); the CLI-wrapper heredoc paths are baked in
phase 3.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-25 15:21:28 +01:00
librelad
fc2c6a6197 Merge claude/1 2026-05-25 15:09:39 +01:00
librelad
e4872ab511 refactor(paths): single source of truth for a relocatable, split layout (phase 1)
Introduce scripts/source/paths.sh as the canonical path resolver for three
independently-relocatable roots:
  LP_SYSTEM_DIR      manager-owned control plane (configs/logs/install/db/ssl/ssh/migrate)
  LP_CONTAINERS_DIR  container-user-owned live app data
  LP_BACKUPS_DIR     container-user-owned backup repos (own mount-able)

Roots come from the environment when set (install bakes them; CLI/app inherit
from init.sh), else default to /libreportal-*. A transitional compat default
keeps EXISTING installs (legacy single /docker tree, by config marker) on /docker
until a deliberate reinstall, so deploying this never strands a running box.

- init.sh derives the same vars inline (self-contained for the bare /root/init.sh
  reinstall case); paths.sh mirrors it for the standalone task/check processors,
  which now self-locate their scripts dir and source it.
- Replace functional /docker literals with the derived vars across runtime,
  install, backup, crontab, crowdsec/restic, headscale, and reinstall paths;
  clean the inert '== /docker/containers/*' guard fallbacks to the variable form.
- backend: CONTAINERS_DIR now from LP_CONTAINERS_DIR (compose env, filled at
  generation via a new CONTAINERS_DIR_TAG), legacy-safe default for un-recreated
  containers.
- backup default path falls back to the backups root; exclude paths.sh from the
  sourced-file arrays (bootstrap file, sourced explicitly).

The CLI-wrapper heredoc + root helpers still reference /docker; those get baked
in phase 3. No layout/ownership change yet (phase 2).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-25 15:09:39 +01:00
librelad
3190a4584a Merge claude/1 2026-05-25 13:53:57 +01:00
librelad
742c3b2f10 style(install): show only helper name in install success line
The root-owned helpers all live in the same fixed dir, so printing the
full /usr/local/lib/libreportal/... path on each success line was long and
repetitive. Use the bare helper name, matching the error branch below.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-25 13:53:57 +01:00
librelad
5aa51ff3b0 Merge claude/1 2026-05-25 13:53:23 +01:00