385 Commits

Author SHA1 Message Date
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
librelad
3a97c228da fix(checks): silence docker daemon-down error in webui image check
When the task processor service is down the rootless daemon socket is
absent, so the pre-install `docker images` probe printed a raw daemon
connection error to the terminal. The surrounding notices already convey
the meaningful state (service not running → image not setup), so the raw
error was noise.

Capture the probe output and redirect its stderr to libreportal.log
instead of the terminal, keeping the detail for diagnostics.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-25 13:53:23 +01:00
librelad
a87f1b49eb Merge claude/1 2026-05-24 23:20:42 +01:00
librelad
152d9c5d28 fix(webui): make all icon and data asset URLs absolute under path routing
Same class of bug as the topbar partial: icon and data-file references were
relative (icons/apps/x.svg, data/apps/...), so on deep path routes (/app/<name>,
/admin/config/x) the browser resolved them against the route dir and the SPA
catch-all served index.html with HTTP 200 instead of 404 — broken images and
silently-wrong JSON.

Make every reference absolute (anchored on the quote/backtick so already-absolute
/icons paths are untouched):
- JS: all icons/ and data/ literals + templates across components/utils/system
- html/topbar.html: logo <img>
- generators: webui_config.sh and webui_create_app_categories.sh now emit
  /icons/... into apps.json / apps-categories.json (regenerated on install)
- updated the two icon-path comments to match

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-24 23:20:42 +01:00
librelad
45d2f5f5c7 Merge claude/2 2026-05-24 23:17:47 +01:00
librelad
3515d06e0a fix(install): clean stale cron spools (recycled-uid "rename: not permitted")
userdel does NOT remove /var/spool/cron/crontabs/<user>, so across an
uninstall->reinstall the manager's uid can be recycled (e.g. 1001 -> 1003)
while the old spool file stays owned by the dead uid. The spool dir is
sticky (1730), so the new manager can't rename its temp over the
old-uid-owned file → "crontab: crontabs/libreportal: rename: Operation
not permitted", and the crontab silently never updates (the "added"
success message doesn't check the rename). Same class as the stale
easydocker spool left by the pre-rename migration.

Two fixes:
- runFullUninstall removes each torn-down user's cron spool (+ the legacy
  easydocker one) so teardown stops leaving orphans.
- initUsers defensively drops a manager cron spool owned by a different
  uid (recycled) before the manager-run crontab setup runs — fixes an
  already-dirty box and any uid drift, in both modes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-24 23:17:47 +01:00
librelad
6c95b7b1a2 Merge claude/1 2026-05-24 23:15:46 +01:00
librelad
447f57ac63 fix(webui): use absolute URLs for topbar partial + nav under path routing
Path-based routes (e.g. /app/<name>) made the relative fetch('html/topbar.html')
resolve to /app/html/topbar.html. The SPA catch-all returns index.html with HTTP
200 instead of 404, so response.ok passed and index.html got injected as the
topbar, leaving #nav-app-center absent -> 'Nav element not found' in setActiveNav.

Make the topbar fetch and the loadConfig fetch absolute, and switch the remaining
relative topbar nav hrefs (index/dashboard/tasks .html) to absolute paths so the
SPA click interceptor routes them instead of doing a real browser navigation.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-24 23:15:46 +01:00
librelad
8f6d0120a4 Merge claude/2 2026-05-24 23:01:04 +01:00
librelad
ebab6accb5 fix(install): make /docker traversable in the root-phase container layer
The previous commit handed /docker/containers to the container user but
left /docker itself at initFolders' 750 (manager-only) during the install
— so the container user couldn't traverse INTO /docker to reach its now-
owned containers/, and the boot scan still hit "find:
'/docker/containers/': Permission denied" (the dir's documented rootless
mode is 751, but the reconcile that sets it runs later). initContainerLayer
now adds the o+x traversal bit to /docker (→ 751) alongside the
containers/ handover, so the boot scan can both enter /docker and read
containers/.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-24 23:01:04 +01:00
librelad
6ca52f9f98 Merge claude/2 2026-05-24 22:53:11 +01:00
librelad
cdb2fc633d fix(install): establish container layer in root phase (real fix for scan noise)
Reverts the 2>/dev/null band-aids and fixes the root cause. The
manager-run install boot scans app configs under /docker/containers AS
the container user (runFileOp). But init.sh's initFolders creates that
dir manager-owned, and the handover to the container user happened later
(start_preinstall), AFTER the boot scans — so the scans ran as the
container user against a dir it didn't own yet: "find:
'/docker/containers/': Permission denied" (cosmetic; the dir is empty
that early, but it's the wrong ownership at the wrong time).

Add initContainerLayer() to init.sh's root phase (after initGIT +
initUpdateConfigs, before the manager-run handoff): rootless-only, it
creates the docker-install user if missing and chowns /docker/containers
to it (751). The later rootless setup is now idempotent — it finds the
user existing and just (re)asserts its password + daemon config (moved
updateDockerInstallPassword out of the create-only branch). Rooted is
unaffected (containers stay manager-owned, which the manager reads).

Result: by the time the boot scans run, /docker/containers is owned by
the user doing the scanning — no permission error, nothing suppressed.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-24 22:53:11 +01:00
librelad
cf87cbeb0f Merge claude/2 2026-05-24 22:39:44 +01:00
librelad
32c33e27be fix(install): silence find-permission noise on early best-effort scans
Early in an install the docker-type config isn't loaded yet, so runFileOp
falls back to the manager, which can't list the container-owned (751)
/docker/containers/ dir. Two best-effort scans then leaked
"find: '/docker/containers/': Permission denied" to the install output
(x3 per run): scan_files.sh's app_configs scan and the application config
reconcile. No app configs exist that early on a fresh install, so the
empty result is correct — just suppress the find stderr (the -print0
output still flows). Cosmetic only; doesn't change what's enumerated once
the config is loaded.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-24 22:39:43 +01:00
librelad
9b249b99e3 Merge claude/2 2026-05-24 22:29:03 +01:00
librelad
42f2509193 fix(webui): finish ?=…→/… URL migration in two missed nav spots
Both used the pre-migration query/.html URL form through navigation that
no longer exists, so they landed on a not-found / wrong page:

- setup-wizard handoffToTasks: navigated to `tasks.html?task=<id>` via the
  never-defined window.router, falling back to a *relative*
  window.location.href. From any non-root path that resolves under the
  current path (e.g. /admin/config/tasks.html → matches the /admin*
  route), so the first-install "x of x installing" hand-off hit a
  not-found task page. Now navigates to the path-based
  `/tasks/all?task=<id>&from=setup` via window.navigateToRoute (absolute
  full-load fallback).
- apps-manager getNavigationButton / handleNavigation: the "Install
  <Service>" buttons on config requirement fields used
  `app.html?app=<name>` with a relative window.location.href; from the
  /admin/config/* pages they render on, that resolved to
  /admin/config/app.html (wrong route). Now `/app/<name>` via
  navigateToRoute.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-24 22:29:03 +01:00
librelad
5532d6eee3 Merge claude/2 2026-05-24 22:07:57 +01:00