811 Commits

Author SHA1 Message Date
librelad
cb055b4b1f fix(uninstall): wipe container sub-UID app data via root helper
dockerDeleteData (uninstall) and the wipe-before-restore step in
restoreAppStart both did `runFileOp rm -rf $containers_dir$app_name`,
which runs as $CFG_DOCKER_INSTALL_USER (dockerinstall, uid 1002 on
rootless). That user owns app-template files but CANNOT remove
container sub-UID dirs created by the daemon's userns mapping —
postgres data at uid 232070, nextcloud html at uid 33, etc. The rm
therefore silently failed with

  rm: cannot remove '/libreportal-containers/invidious/postgresdata':
    Permission denied

while still reporting "<app> successfully uninstalled" — leaving the
sub-UID directory tree on disk to confuse the next install and leak
storage.

Fix: route the wipe through a new `app-data-remove` action in the
root-owned libreportal-ownership helper. Root can rm sub-UID files
unconditionally. The helper validates the app name (alphanumeric +
. _ -, no traversal), refuses the WebUI's own slot (libreportal), and
is idempotent when the dir is already gone.

Two callers updated:
- scripts/docker/app/uninstall/delete_data.sh
- scripts/restore/restore_app_start.sh

The helper itself ships root-owned at /usr/local/lib/libreportal/, so a
fresh install or release upgrade is needed to pick up the new action.
Bumped init.sh footprint_version 2 → 3 so the runtime updater
prompts a root re-install on the next release.

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-27 15:32:44 +01:00
librelad
496c9ed1b3 Merge claude/1 2026-05-27 15:21:42 +01:00
librelad
1ccc4bba49 feat(tasks): redesign Clear All modal + add "cancel running too" toggle
The Clear All confirmation was the last destructive task action still
running through window.showConfirmation (the legacy dialog system) —
visually inconsistent with the rest of the tasks page (single-row delete,
Uninstall, etc., which all use openEoModal). Switches it to the same
eo-modal shape used by _showDeleteTaskModal so the destructive-confirm
family looks unified.

While here, adds a "Cancel running tasks too" toggle inside the modal,
off by default. Backed by the existing .eo-toggle.eo-toggle-card style
(modal.css). Drives a new opts.cancelRunning in performClearAll:

  Off  → skip any running/queued/pending tasks; only terminal rows
         are deleted. The success toast reports the split.
  On   → cancel each active task first (POST /cancel), wait for the
         terminal status via SSE, then delete (with the 409→force
         fallback the single-row deleteTask already uses).

Body composition mirrors the per-task delete modal:
  - danger empty-state ("This cannot be undone")
  - badge row with Total / Running / Terminal counts
  - the toggle (only shown when runningCount > 0 — no need otherwise)

The action button's label live-updates as the toggle changes:
  toggle off + running rows  → "Delete N (skip M running)"
  toggle on  / no runners    → "Delete N Tasks"

So the user sees exactly what they're about to do before clicking.
Cancel / backdrop / X all resolve to no-op (same contract as
_showDeleteTaskModal). Modal returns {confirmed, cancelRunning} so the
caller knows which path to take.

Sets up the multi-select work next: the modal already accepts an
arbitrary tasks array; the upcoming "Delete selected" is a one-line
call into the same _showClearAllModal with a filtered list.

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-27 15:21:42 +01:00
librelad
eb8192a84f Merge claude/2 2026-05-27 15:14:09 +01:00
librelad
9817d6945a fix(config): silence app-data Permission-denied chatter from config reconcile
checkApplicationsConfigFilesMissingVariables does a `find $containers_dir
-maxdepth 2 -type f -name '*.config'` to enumerate every app's live
config. runFileOp drops privileges to $CFG_DOCKER_INSTALL_USER
(dockerinstall), which is intentionally the *manager* for the rootless
data plane — but doesn't own the per-app container sub-UID dirs (e.g.
invidious/postgresdata uid 232070, nextcloud/html uid 33).

At maxdepth 2 find doesn't actually need to descend into those dirs to
satisfy the name filter, but it tries to anyway and emits chatter like

  find: '.../invidious/postgresdata': Permission denied

every time the function runs (config-reconcile path on install / app
start / restart). Cosmetic only — the actual .config files are at the
right depth and ARE found — but it shows up in the live CLI output
during installs.

2>/dev/null on the find. The function's purpose is purely to enumerate
LibrePortal-managed .config files; sub-UID data dirs are by design
unreachable to the manager and there's no signal in that error.

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-27 15:14:08 +01:00
librelad
338cd801fd Merge claude/2 2026-05-27 15:05:02 +01:00
librelad
a1315024c5 ui(tasks): delete notification matches the started/completed format
"Task deleted successfully" was a plain single-line toast while every
other task notification (started, completed, failed, cancelled) renders
as <App>-in-bold on the first line + "<Action> task <verb>" on the
second, with the app icon on the left and the per-action emoji as
custom-icon. Inconsistent.

Now reads e.g.

  [🗑️]  Ipinfo
        Install task deleted.

with the ipinfo logo as the row icon, matching the install/completion
toast format.

Also factored the three duplicate "task → identity (display name + app
icon + friendly action title + emoji)" blocks (taskCompleted listener,
delete-modal title, delete notification) into one helper —
_taskNotificationDescriptor(task) — so the four surfaces (started,
completed/failed/cancelled, delete modal, delete notification) always
agree on what to call a task. Net -20 lines.

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-27 15:05:02 +01:00
librelad
a1a896e59c Merge claude/1 2026-05-27 14:58:43 +01:00
librelad
3294ca4e41 fix(boot): source run_privileged.sh before checkConfigFilesMissingFiles
load_sources.sh calls checkConfigFilesMissingFiles() after init.sh +
variables.sh but BEFORE initilize_files.sh sources the function
manifest. checkConfigFilesMissingFiles uses runInstallOp (in
docker/command/run_privileged.sh) to copy missing config templates —
under LP_LAZY=1 that's an autoload stub that only exists once the
manifest is sourced. So when any template is genuinely missing, the
copy call hits "runInstallOp: command not found" and the file silently
never gets copied.

Symptom on a fresh CLI invocation (foreground or processor subprocess
inheriting LIBREPORTAL_TASK_EXEC=1) where a new config category was
added:
  config_check_missing.sh: line 33: runInstallOp: command not found
  ✓ Success 1 config files were missing and have been added to the
    configs folder.    ← false success: the count incremented but the
                         copy itself didn't happen

Fix: source run_privileged.sh directly in load_sources.sh just before
the missing-files check. The file is pure function definitions (runAsManager
/ runFileOp / runFileWrite / runInstallOp / runInstallWrite), no side
effects, ~150 lines — safe to source unconditionally and idempotent
with the eager/lazy load that happens later. Adds <1ms to every CLI
invocation; saves silent failures on the rare path that calls it.

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-27 14:58:43 +01:00
librelad
68bade609b Merge claude/1 2026-05-27 14:51:13 +01:00
librelad
f252d7680b fix(cli): camelCase task fields match WebUI shape (createdAt, not created_at)
WebUI-created tasks emit camelCase initial fields (createdAt, startedAt,
completedAt, heartbeatAt, exitCode, errorMessage) per
tasks-manager.js / task-manager.js conventions, with createdAt in
ISO-UTC-with-ms (`2026-05-27T13:01:26.345Z`). The processor then layers
snake_case status fields (started_at, heartbeat_at, …) on top as the
task runs.

The CLI's cliTaskRun was writing snake_case only — `created_at` with
local-tz offset. The task panel's renderer reads `task.createdAt`
directly (no alias), so CLI-queued tasks showed blank Created/Started
columns until the processor wrote its own snake_case overlay
(which doesn't include createdAt at all). Visible symptom: dates
"broken" on CLI-queued tasks.

Now the initial JSON cliTaskRun writes matches what the WebUI's
"Install" button writes:

  {
    id, command, status: queued,
    createdAt: "<ISO-UTC-with-ms>",
    startedAt: null, completedAt: null, heartbeatAt: null,
    exitCode: null, errorMessage: null,
    type, app
  }

Processor side is unchanged (still adds snake_case overlay on
status transitions — that's how WebUI tasks already work). No JSON
shape change for in-flight tasks.

ALSO (out-of-repo): /home/user/Documents/Scripts/update.sh now restarts
the systemd `libreportal.service` task processor after the docker
`libreportal-service` container restart. Same reason — both pre-load
code at startup, both need a restart to pick up changes. Without this,
deploys silently kept a stale processor running old code while the
disk reflected the new code; the install task-routing recursion I just
saw was a direct consequence.

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-27 14:51:13 +01:00
librelad
ce7b704084 Merge claude/2 2026-05-27 14:48:39 +01:00
librelad
8802006659 ui(tasks): delete-task modal shows friendly title + app icon
The Delete Task confirmation modal was rendering the raw command
("libreportal app install ipinfo") as its title with no app icon, while
the rest of the WebUI (task notifications, task rows) shows
"Install Ipinfo" and the ipinfo logo. Inconsistent and slightly
intimidating for a confirmation step.

Now mirrors the completion-notification flow:
- Title: `${formatActionTitle(task.type)} ${getAppDisplayName(task.app)}`
  → "Install Ipinfo", "Backup Nextcloud", etc.
- Icon: /icons/apps/<slug>.svg (or libreportal.svg for system tasks)
- Tool tasks borrow the same tool-catalog-lookup the completion toast
  uses so a tool deletion reads as "Manage Shortcuts" rather than the
  raw tool id.

Reuses the existing TasksManager.formatActionTitle() helper so any
future task type added to that map flows through here automatically.

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-27 14:48:39 +01:00
librelad
cd6f1354ca Merge claude/2 2026-05-27 14:39:58 +01:00
librelad
8a3bf505c3 refactor(config): disperse Features section into category Advanced groups
The Features section was a grab-bag of ~27 toggles, most of which are
either category-specific (firewall, SSL, Docker network, SSH hardening)
or install-time choices that brick the box if flipped on a live
install (the WebUI / config / CLI / Docker requirements). One page
made auditing easier but flattened the risk hierarchy.

Reorganised so each toggle lives where it conceptually belongs, and
the dangerous install-time set is double-gated:

  network_docker     (Advanced)  DOCKER_NETWORK, DOCKER_NETWORK_PRUNE,
                                  DOCKER_SWITCHER
  network_firewall   (Advanced)  UFW, UFWD, WHITELIST_PORT_UPDATER  [new]
  network_domains    (field-Adv) SSLCERTS
  security_ssh       (Advanced)  SSHKEY_DOWNLOADER, SSH_DISABLE_PASSWORDS,
                                  BCRYPT_SAVE, GLUETUN_FOR_ALL          [new]
  general_terminal   (Advanced)  CRONTAB, CONFIGS_CHECK,
                                  CONFIGS_AUTO_UPDATE, CONFIGS_AUTO_DELETE,
                                  MISSING_IPS, CONTINUE_PROMPT,
                                  SUGGEST_INSTALLS, SUGGEST_METRICS
  general_install    (Adv+DEV)   CONFIG, COMMAND, WEBUI, WEBUI_SERVICE,
                                  DATABASE, PASSWORDS, DOCKER_CE,
                                  DOCKER_COMPOSE

The install-time eight are marked **ADVANCED** **DEV** — invisible
unless Developer Mode is on AND "Show Advanced Options" is expanded.
Each field's description was updated to note "Disabling on an existing
install will brick the system" / "install-time choice only" so a user
who does get to the toggle understands the gun before pulling the
trigger.

Other cleanup that fell out:
- Removed `configs/features/` directory entirely.
- Added the two new subcategories to SUBCATEGORY_ORDER in
  network/.category and security/.category.
- Dropped the `category === 'features'` Danger Zone header special-case
  in config-manager.js and its .danger-zone-section--header-only CSS
  variant (sole user).
- Trimmed an obsolete "Edit the features config" notice in
  check_requirements.sh.

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-27 14:39:58 +01:00
librelad
eac81abdbc Merge claude/1 2026-05-27 14:38:14 +01:00
librelad
3f582120ba feat(cli): route all long-running app + update commands through tasks
Extends the install-routing spike (e5273a4) to every long-running CLI
command, so CLI and WebUI now share one execution path everywhere:

  app install      ← already done
  app uninstall
  app start / stop / restart / up / down / reload
  app backup
  app restore
  update apply
  backup app create   (matches `app backup` — same end target)

Each handler now has the same shape:
  if [[ "$LIBREPORTAL_TASK_EXEC" == "1" ]]; then
      <inline call>            # processor's recursive invocation
  else
      cliTaskRun "<cmd>" <type> <app>   # user invocation: enqueue + follow
  fi

Processor change — crontab_task_processor.sh:
  Adds `export LIBREPORTAL_TASK_EXEC=1` next to LIBREPORTAL_NONINTERACTIVE.
  Universal bypass: every task command the processor runs (CLI-queued OR
  pre-existing WebUI-queued like `libreportal app install adguard`)
  inherits the env var, so the inline branch fires and we never
  re-enqueue. This also lets us drop the env-var prefix the install spike
  was baking into the command string (e5273a4) — cleaner task files +
  one place to think about the bypass.

`backup app schedule` (the cron-driven path that already enqueues via
createTaskFile in backup_app_schedule.sh) is left alone — different
entry point, different runtime context, already correctly task-routed.

Why route the fast ones too (start/stop/restart/up/down):
  Consistency beats the ~1s task-roundtrip latency for a CLI button.
  Locking now serialises a CLI `app stop foo` against a WebUI restart of
  the same app; the audit trail covers every state change. Cheap to
  revert any individually if the latency turns out to bother someone.

Validated live earlier with `libreportal app install dashy` — task file
written, processor dispatched, follower streamed install live, exit 0
propagated. Same machinery now powers the other 9 handlers.

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-27 14:38:14 +01:00
librelad
0c4c607282 Merge claude/1 2026-05-27 14:29:30 +01:00
librelad
e5273a482d feat(cli): route app install through the task processor + live follower
Spike — closes the gap where the CLI install bypassed the very task system
the WebUI uses. Now both surfaces hit the same path:

  user types `libreportal app install dashy`
    → CLI enqueues a task file in $TASK_DIR (identical shape to the
      WebUI's createTaskFile)
    → pokes $TASK_DIR/.queue.fifo so the processor dispatches in <100ms
      instead of waiting up to IDLE_POLL_SECS
    → CLI tails the task log + polls .status, exits with the task's
      exit_code on terminal state
    → Ctrl-C detaches the follower without killing the task — the
      WebUI's tasks panel keeps showing it

Bypass: the recursive command in the task file is prefixed
`LIBREPORTAL_TASK_EXEC=1 libreportal app install <name>`. The install
branch in cli_app_commands.sh honours that env var by running inline,
which is what the processor's eval invocation hits. No processor
changes — the bypass travels with the task.

Wins:
  - one log file per install, shared by CLI + WebUI (audit trail + replay)
  - locking serialises CLI + WebUI installs (no more two-frontend race)
  - WebUI's "current task" indicator now reflects CLI work too
  - free `--detach` for fire-and-forget queueing

New: scripts/cli/task/cli_task_run.sh
  cliTaskRun <cmd> [type] [app] [--detach]
    Enqueues + follows; --detach prints the task id and exits 0.
  cliTaskFollow <task_id>
    `tail -F` the log + jq-poll the status; returns the task's exit_code.
    Designed to be reused for `libreportal task log <id>` reattach later.

Trade-off: ~200-500ms latency before the first byte (write task file,
processor wakes, opens log, follower starts tailing). Negligible for
install/update/backup — fast commands (list/status/config get) still
run inline. The current branch only changes `app install`; uninstall +
update + backup can be moved on the same pattern once this lands clean.

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-27 14:29:30 +01:00
librelad
25fa30fd5d Merge claude/2 2026-05-27 14:20:18 +01:00
librelad
f5fc659c96 ui(tasks+devmode): friendly task titles + restyle dev-mode strip
Two unrelated UI polish items.

1. Task notification titles. The "Config_update task completed!" toast
   was leaking the literal task-type id because the friendly-name map
   in the taskCompleted listener didn't list `config_update`,
   `update_config`, or `system_update`, and the fallback only
   capitalized the first letter. Same fallback was duplicated in
   task-actions.js's started-toast path.

   Extracted into `TasksManager.formatActionTitle(action)`:
   - Adds the missing entries (`config_update`/`update_config` → "Update
     Config", `system_update` → "Update System").
   - Smarter fallback: snake/kebab → Title Case, so an unmapped future
     type renders as e.g. "Foo Bar" instead of "Foo_bar".
   - Both the started (task-actions.js) and completed/failed/cancelled
     (tasks-manager.js) notification paths now route through it, so the
     started/done pair always reads the same.

2. Dev-mode strip styling. Earlier amber-on-amber recipe read as a
   warning state; the strip is just informational. Switched to a
   neutral glass surface (rgba(text,0.04) + rgba(text,0.12) border),
   text-primary copy, and the icon alone in var(--accent). Label is
   now centered with the close button absolute-positioned on the
   right (24px / 56px right padding so the centered text never sits
   under the X).

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-27 14:20:18 +01:00
librelad
b754794789 Merge claude/2 2026-05-27 14:13:56 +01:00
librelad
5655835398 ui(devmode): persistent banner under topbar + shorter auto-enable toast
Two small dev-mode UX changes.

1. Banner. When CFG_DEV_MODE is on, a 36px amber-tinted strip sits flush
   under the topbar with "You are currently running in Developer mode"
   and a dismiss X. Dismissal is remembered in localStorage and cleared
   whenever dev mode is toggled back on, so re-enabling the mode brings
   the banner back. Body picks up `.has-dev-banner` while visible to
   bump padding-top by the strip's height (also adjusts the mobile
   drawer's top/height).

2. Toast. The auto-enable message dropped the trailing
   "Click the LibrePortal logo 10× to disable." — too noisy on every
   git/local page load; the easter egg is still discoverable. New
   message is just "Developer mode auto-enabled — you're on a <mode>
   install."

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-27 14:13:56 +01:00
librelad
e793c7a0e4 Merge claude/1 2026-05-27 13:26:49 +01:00
librelad
4d7027258d feat(app): Wave B + C — collapse 28 per-app installers onto generic driver
Finishes the installApp refactor started in d941f59 (Wave A). Every app
whose <app>.sh was either pure boilerplate (Wave B) or boilerplate +
small custom logic (Wave C) now routes through the generic driver in
scripts/app/install/app_install.sh; bespoke logic moved to declarative
hooks in containers/<app>/scripts/<app>_install_hooks.sh.

Net: ~4,000 lines of duplicated 10-step sequence gone. From 31 per-app
.sh files (pre-Wave-A) down to 2 intentional keepers.

DELETED outright (pure boilerplate — driver replaces them identically):
  jellyfin, mastodon, focalboard, ipinfo, speedtest, dashy, invidious,
  nextcloud, ollama, vaultwarden, pihole

DELETED + hook-extracted (small bespoke step preserved in a hook):
  bookstack, moneyapp, owncloud, trilium, searxng, gitea, headscale,
  unbound, prometheus, grafana, gluetun, wireguard, jitsimeet, authelia,
  traefik, adguard, onlyoffice

KEPT (intentional special cases):
  crowdsec      — host-app pattern (no docker compose, runs as apt+
                   systemd via installCrowdsecHost; uninstall/stop/
                   restart hooks already live in this file and are
                   invoked by dockerUninstall/Stop/RestartApp directly).
  libreportal   — WebUI bootstrap. Pre-compose image build + post-install
                   webuiLibrePortalUpdate + bootstrap-time suppression of
                   menuShowFinalMessages don't fit the generic flow.

Driver change — scripts/app/install/app_install.sh:
  Moved monitoringToggleAppConfig "$app_name" "docker-compose.yml" from
  the post-start integrations block into the install body at post-compose
  (right after dockerComposeSetupFile, before docker-compose up). The
  toggle edits the compose file on disk — running it after start meant
  the container had already been brought up with the unmodified compose,
  so the metrics endpoint wouldn't reflect CFG_<APP>_MONITORING until
  the next restart. Matches the original ordering in every per-app .sh
  that used to call it inline.

Hook surface (declare-f-gated, silent no-op when absent):
  <slug>_install_pre              before any install work
  <slug>_install_post_setup       after dockerConfigSetupToContainer
  <slug>_install_post_compose     after dockerComposeSetupFile (+ the
                                  shared monitoring toggle on the compose)
  <slug>_install_post_start       after dockerComposeUpdateAndStartApp
  <slug>_install_message_data     echoes extra argv for menuShowFinalMessages
  <slug>_install_post             very last thing, after the final message
  + the existing _uninstall_pre/_post, _stop_post, _restart_post

Notable extractions:
  bookstack  — _install_post_start: probe :PORT_1/login until 200/302,
               then `bookstack:create-admin` inside the container with
               CFG_BOOKSTACK_ADMIN_{EMAIL,PASSWORD}; falls back to the
               seeded admin@admin.com on timeout.
  adguard    — _install_post_start drives the wizard's HTTP API
               (POST /control/install/configure) so the admin doesn't
               click through five pages, then pins the admin bind back
               to 0.0.0.0:3000 (matches the compose mapping) and health
               checks. _install_message_data echoes user/password to
               menuShowFinalMessages.
  authelia   — _install_pre requirements; _install_post_compose copies
               configuration.yml + users_database.yml, substitutes
               theme/domain/host, generates JWT/session/storage secrets,
               toggles monitoring on configuration.yml; _install_post_start
               argon2-hashes the admin password via the container, writes
               users_database.yml, restarts; _install_post echoes creds.
  traefik    — _install_pre prompts for the LE email if CFG_TRAEFIK_EMAIL
               is unset; _install_post_compose copies static + dynamic
               configs, wires CFG_TRAEFIK_DASHBOARD_ACCESS (local-only /
               domain-only / public), toggles monitoring on traefik.yml,
               then traefikUpdateWhitelist + traefikSetupLoginCredentials.
  wireguard  — _install_pre host-conflict guard (/etc/wireguard/params);
               _install_post_compose persists CFG_WIREGUARD_SUBNET,
               resolves WG_HOST (domain+traefik → host_setup, else IP),
               runs runAppCfg wireguard-ip-forward; _install_post_start
               restarts after wg-easy installs its iptables rules.
  jitsimeet  — _install_post_setup downloads the tagged release zip from
               GitHub; _install_post_compose mass-edits the .env and runs
               gen-passwords.sh; _install_post_start rewrites nginx
               default site to usedport1/2 + restart.
  prometheus — _install_post_compose seeds prometheus.yml under
               $containers_dir/prometheus/prometheus/; _install_post_start
               sets 0777 on storage dirs so the container TSDB can write
               regardless of host UID mapping.
  grafana    — _install_pre requirements; _install_post_start 0777 on
               grafana_storage.
  gluetun    — _install_post_start refreshes the provider snapshot,
               reattaches every routed app (the netns container ID is
               stale after gluetun gets recreated), then prompts to
               onboard any existing apps.
  + the smaller bookstack-shape extractions for owncloud (version scrape),
    trilium / searxng (wait-for-first-boot-config), gitea (Prometheus
    bearer token sync), headscale / unbound (config copy), moneyapp
    (Auth.js AUTH_URL), onlyoffice (compose-resolved user/pass into the
    final message).

Manifest + arrays regenerated. Verified end-to-end:
  - bash -n on every hook file + the driver: clean
  - Each hook file sources cleanly in a subshell, exposes only the
    intended functions, flagged lazy-loadable (not eager)
  - Smoke-stubbed install run for jellyfin (pure), nextcloud (pure),
    bookstack (hooked), crowdsec (kept): correct dispatch in all cases —
    deleted apps route to installApp, kept apps still hit their real
    function

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-27 13:26:49 +01:00
librelad
d1d64d12a9 Merge branch 'claude/2' 2026-05-27 13:24:31 +01:00
librelad
9d5d0103b6 fix(routing): _previewUrl uses port.subdomain, not the retired HOST_NAME
routing-manager.js read CFG_<APP>_HOST_NAME for its preview URL, but that
key was retired by the per-port subdomain refactor (2e4f420, 2026-05-22)
and no .config defines it anymore. The lookup always returned undefined,
so even with a configured domain the preview fell through to the
`<your-domain>` placeholder instead of showing the real host.

Now derives the preview from the port's own subdomain (parts[10] of
the 12-col PORT row), matching the canonical host_setup rule in
scripts/network/variables/variables_init_app.sh:
  @ / root      -> apex (`https://<domain>`)
  set           -> `https://<sub>.<domain>`
  empty         -> `https://<app>.<domain>`

Also adds `subdomain` to the port object emitted by _collectPorts so
this and any future per-row consumer can read it.

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-27 13:23:47 +01:00
librelad
e46c619d82 Merge claude/2 2026-05-27 01:43:08 +01:00
librelad
d941f59388 feat(app): generic installApp driver + dispatcher fallback (Wave A)
The 31 containers/<app>/<app>.sh files each defined install<App>() with
the SAME 10-step sequence — ~4,000 lines of duplicated boilerplate.
Replaces all that with one generic driver + hook surface.

scripts/app/install/app_install.sh:
  installApp <slug> [config_variables]
    — Dispatches on $<slug> (c/u/s/r/i) the same way the per-app .sh
      files did. Same convention; dockerInstallApp's existing
      `declare $app=i` callsite needs no change.
    — Runs the standard sequence: dockerConfigSetupToContainer →
      dockerComposeSetupFile → optional .env copy → fixPermissions →
      dockerComposeUpdateAndStartApp → standard post-install steps
      (appUpdateSpecifics, setupHeadscale, databaseInstallApp,
      webuiContainerSetup, monitoring registration) → final message.
    — Hooks (all declare-f-gated, silent no-op when absent):
        <slug>_install_pre / _post_setup / _post_compose / _post_start
        <slug>_install_message_data   (echoes extra args for menu)
        <slug>_install_post
        <slug>_uninstall_pre / _post
        <slug>_stop_post
        <slug>_restart_post
      Hooks live in containers/<app>/tools/<app>_tools.sh (auto-sourced
      per the modular-per-app-tools convention).

function_install_app.sh:
  When no install<App>() function exists, fall through to
  `installApp <app_name>` instead of erroring. So an app with no .sh
  at all becomes a zero-byte addition — drop in <app>.config +
  docker-compose.yml + <app>.svg, done.

containers/linkding/linkding.sh:
  Deleted (canary). Linkding's body was 100% standard sequence;
  fallback handles it identically. Smoke-tested with stubbed helpers
  — dispatcher fires, generic runs full flow, monitoring integration
  + final-message hook plumbing all intact.

Wave B (next): delete the .sh for every other 'pure-boilerplate' app
(~15 candidates per the survey). Wave C: extract custom logic from
the 7 fat apps into hooks before deleting their .sh.

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-27 01:43:08 +01:00
librelad
6f2d7ca0dc Merge claude/1 2026-05-27 01:40:06 +01:00
librelad
e57d42ddf6 refactor(webui): path-based URLs for app tabs + config sub-tabs
The app-detail page was the last corner of the SPA still using query
parameters for navigation state. Two related complaints surfaced it:

  - `/app/adguard?tab=tasks` should mirror admin (`/admin/tools/peers`,
    `/admin/config/network`) and be `/app/adguard/tasks`.
  - The config sub-tab (general / advanced / features / network / …)
    had no URL representation at all — `showTab` was a pure visual
    swap with no history push, so refreshing a deep config sub-tab
    sent the user back to the default first category.

New URL shape:

  /app/<name>                          → config tab, default sub-tab
  /app/<name>/<tab>                    → non-config main tab (tasks, backups, …)
  /app/<name>/config/<category>        → config tab + specific sub-tab
  …?task=<id>                          → optional deep-link to a single task

Mirrors `adminPath` / `adminCategoryFromPath`. Two new helpers in
spa.js carry the convention:

  window.appPath(name, tab, sub, taskId) → URL
  window.appPartsFromPath(pathname)      → { app, tab, sub }

Every URL constructor in the WebUI was replaced with `window.appPath`:

  spa.js                               — handleAppDetail back-compat redirect
  app-tabbed-manager.js                — getTabFromURL + new getConfigSubFromURL
                                          (path first, ?tab= fallback for legacy)
                                          updateURL + updateApp use appPath
                                          the inline task-deep-link constructor
  apps-manager.js                      — showAppDetail + showAppDetailWithConfig
                                          showTab now pushes /app/<n>/config/<sub>
                                          renderAppDetail picks the sub-tab out of
                                          the URL on first load
                                          4 fallback task-URL constructors
  tasks-manager.js                     — completion-notification URL
  task-actions.js                      — start-notification URL
  notifications.js                     — 2 task deep-link URLs

Back-compat: handleAppDetail detects legacy `?tab=` / `?config=` /
`?task=` queries and replaceState()s the URL to the canonical path
shape BEFORE anything else reads URL state — old bookmarks land on
the right page and end up with a clean URL.

Verified by running every appPath / appPartsFromPath case (including
the `logs` → `tasks` legacy alias) and confirming the round-trip is
identity. JS syntax checks clean on all six files. No remaining
hardcoded `/app/<x>?tab=` strings outside the back-compat comment.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-27 01:40:05 +01:00
librelad
92e585deba Merge claude/1 2026-05-27 01:26:27 +01:00
librelad
e72d3d278d style(backup): shorten the BACKUP_STRATEGY tooltip to one practical sentence
The advanced field was carrying a four-clause explanation walking through
"quiesce", "live dump", "stop fallback", and the live-option eligibility
rules — useful background for engineers, overwhelming as a hover tooltip
for the typical user picking from the dropdown.

New tooltip:
  "How the app is prepared before each backup. Automatic is recommended;
   pick Live only if you need zero downtime."

Two lines, plain language, points at the right default. The dropdown's
own labels (Automatic (recommended) / Stop→snapshot→start / Pause→snapshot
→unpause / Live — no downtime) carry the technical specifics; the
tooltip no longer has to.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-27 01:26:27 +01:00
librelad
c615820c35 Merge claude/2 2026-05-27 01:23:55 +01:00
librelad
1e9e65225b fix(gluetun): wrap rm -f on dockerinstall-owned tempfiles in runFileOp
gluetun_providers.sh writes its working files ($raw, $headers) next to
$output_file, which lives at
  containers_dir/libreportal/frontend/data/apps/generated/gluetun-providers.json
— dockerinstall-owned in rootless. The five rm -f calls on those paths
were unwrapped, so the manager running the script (e.g. from the
task processor) would get Permission denied — same class as the
updateConfigOption sed -i bug that was just fixed.

$tmp comes from mktemp (/tmp), so the rm -f for it stays unwrapped.

Audit context: this was the only remaining raw filesystem op against
container-tree paths in any containers/*/*.sh. The rest of the
container .sh files are clean — every sed -i / chmod / chown / cp / mv
is already routed through runFileOp or runFileWrite, and the
per-app install bodies delegate fs work to high-level helpers
(dockerConfigSetupToContainer, copyResource, dockerComposeSetupFile)
which themselves use the wrappers.

Hooks (<app>_migrate_pre/_post, restoreAppRunHook pre/post) are
present in the framework but unused by any app today — that's by
design (opt-in per-app). If a future app needs federation-key rotation
post-migrate, or a hostname rewrite that the generic URL-rewrite
layer doesn't cover, those slots are ready.

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-27 01:23:55 +01:00
librelad
cd30d90a81 Merge claude/1 2026-05-27 01:16:43 +01:00
librelad
01a125db55 style(notif): unify task notifications + drop the App:/System: prefix
Started, completed, and failed task toasts were rendered by three
different code paths producing three different layouts:

  • task-actions.js executeTask              "App: AdGuard\n…task started!"   (with type emoji)
  • task-actions.js executeTaskMonitoring    "App: AdGuard\n…task started!"   (without type emoji) — dead code
  • tasks-manager.js createAndExecuteTask    "Task created: install adguard"  (raw shape) — dead code
  • tasks-manager.js complete/fail notif     "App: AdGuard\n…task completed!" (with type emoji)

…plus the system-task path was reading the literal `'system'` slug into
the toast: "App: System / Config_update task started!" with a 404'd
/icons/apps/system.svg (the same bug renderTaskIcons had on the row
itself, fixed in 59ee92b).

Three changes:

1. Drop the "App: " / "System: " label prefix on every toast. The bold
   line is now just the subject name (the row's title still carries the
   semantic with its leading App-or-LibrePortal icon). Three tasks of
   the same app no longer read like a column heading repeated.

2. Treat `appName === 'system'` as the LibrePortal sentinel everywhere
   the toast renders — displayName resolves to "LibrePortal" and the
   app-icon slot loads /icons/libreportal.svg. Mirrors the row-icon
   fix in 59ee92b. The completion-path `isSystemTask` check now also
   accepts `appName === 'system'` in addition to `setup-*` types.

3. Delete the dead code that produced the inconsistent shapes:
   - executeTaskMonitoring in task-actions.js (no callers anywhere)
   - window.createAndExecuteTask in tasks-manager.js (no callers; only
     surviving reference was a stale comment in app-tabbed-manager.js,
     updated to point at executeTask instead)

Net: every task toast in the WebUI now follows the same three-slot
layout — [type emoji] [app/LibrePortal logo] <strong>Name</strong> +
"Action task started/completed/failed/cancelled!".

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-27 01:16:43 +01:00
librelad
3e63dfbdbb Merge claude/2 2026-05-27 01:16:02 +01:00
librelad
56d2f8105c fix(config): updateConfigOption uses the right de-sudo helper for the file's tree
Symptom (reported on adguard install):
  sed: couldn't open temporary file /libreportal-containers/adguard/sedZaGX2H:
       Permission denied
  ✗ Error Updated CFG_ADGUARD_BACKUP to true
  ! Notice Non-interactive mode: aborting on error.

Root cause: updateConfigOption ran a raw `sed -i` regardless of which tree
the config file sits in. Works fine for /libreportal-system/configs/*
(manager-owned) but breaks on /libreportal-containers/<app>/<app>.config
(dockerinstall-owned in rootless mode) — sed -i writes its temp file next
to the target, inheriting the directory's perms, and the manager can't
write inside dockerinstall dirs. EVERY app installer that mutates a
CFG_<APP>_* value (the autogenerated random password, BACKUP toggle,
PORT override, etc.) goes through this function, so this was a latent
ticking bomb across all containers.

Fix: pick the helper based on path —
  -  under $containers_dir  → runFileOp  (escalates to
    dockerinstall in rootless, runs as manager in rooted)
  - otherwise                            → runInstallOp (always manager)
Read paths (grep / source) stay unwrapped — both dirs are world-readable;
only the write needs the privilege swap.

Net: no more 'Permission denied' on app installs; the de-sudo pattern is
now respected end-to-end for CFG writes.

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-27 01:16:02 +01:00
librelad
3d7f5fbdeb Merge claude/2 2026-05-27 01:05:44 +01:00
librelad
14bc0c3386 ui(backup): tile-click → Back-up checklist modal; LibrePortal icon on System tile; 2-up grid
Reshape the dashboard's Backup status grid into a click-to-pick UI:

- Removed the inline Back-up / Restore buttons from the System config
  tile. Same shape as an app tile now; LibrePortal app icon instead of
  the server-stack glyph.
- Grid is 2 columns (was auto-fill min 220px). Tiles are wider, read
  better, and the System tile no longer needs to span a full row to fit
  inline buttons.
- Click any tile (System or app) → opens a new "Back up" modal:
    * System config first (key=__system__, LibrePortal icon)
    * Every installed app, alphabetical
    * Checkbox per row + 'Select all' / 'Clear' shortcuts
    * The tile clicked is pre-ticked
- Confirm queues backup tasks:
    * Everything ticked  → single `libreportal backup all` (which also
      runs `backup system`) — one task instead of N
    * Subset            → one task per ticked item (`backup system`
      and/or `backup app create <slug>`)

Restore for System config used to live on the dashboard's inline
'Restore' button. It's now reachable via the Backups tab — system
snapshots appear in the snapshot list with the standard per-row
Restore action — same path apps already use. No new UI required;
just one fewer dashboard button.

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-27 01:05:44 +01:00
librelad
c49007d6e0 Merge claude/1 2026-05-27 01:04:03 +01:00
librelad
ae790853bf chore(dashboard): drop the redundant "Admin overview" link
The user Dashboard carried a small chevron link "Admin overview →" just
above the installed-apps grid. The topbar already has a top-level "Admin"
nav-item (topbar.html:34) that goes to the same /admin route. The
dashboard link was a redundant second entry point with no extra value;
removing it tightens the dashboard layout without losing navigation.

Drops:
  - dashboard-content.html: the <a class="dashboard-admin-link"> block
  - admin.css: the .dashboard-admin-link rule + :hover (now orphaned)

The /admin route, the topbar Admin nav-item, and the AdminOverview JS
component all stay as-is — only the dashboard-side entry point goes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-27 01:04:03 +01:00
librelad
a265b86cc9 Merge claude/2 2026-05-27 00:50:53 +01:00
librelad
57d6fdaa7c ui(backup): tighten Backup-status tooltip — short and sweet
Was: 'What's saved. Save System config first — if anything breaks, you
     need it to get everything else back.' — read a bit kindergarten.

Now: 'Latest backup per app + System config. Back up System first —
     it's needed to restore the rest.' — same info, tighter, still
     reads at a glance.
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-27 00:50:53 +01:00
librelad
a9baf1d0fa Merge claude/2 2026-05-27 00:49:19 +01:00
librelad
a9af8d93c7 ui(backup): drop Backup-status hint text; move it to a plain-language tooltip
The two-line hint under 'Backup status' was redundant — the System
config tile speaks for itself once it's there. Replaced with an ℹ️
tooltip on the heading (same pattern as 'Cross-host migrate' on the
Migrate tab).

Tooltip text deliberately plain: 'What's saved. Save System config
first — if anything breaks, you need it to get everything else back.'
No 'bare-metal restore' jargon, no 'snapshot' — the kind of sentence
that lands for someone who's never heard of either.

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-27 00:49:19 +01:00
librelad
420964c6e8 Merge claude/2 2026-05-27 00:42:58 +01:00
librelad
102fc38da0 ui(backup): merge System config into the Backup status grid
The dashboard had two parallel sections — 'Per-app status' (every app's
latest backup) and a standalone 'System config' card below it. Folded
them into one grid: a single 'Backup status' card with the System config
tile rendered FIRST, then every app tile.

Why first: a bare-metal restore needs the system config (CFG_* +
backup-location credentials) — without it the backups exist but the
keys to reach them don't. Putting it at eye-level above the app tiles
makes the dependency visible.

System tile reuses the .backup-app-tile shape: server-stack icon,
'System config' as the name, status dot + 'Last backed up X ago' /
'No backup yet'. Plus two compact inline action buttons (Back up /
Restore) on the right that wire into the same data-action handlers
the old standalone card used — no behaviour change, just the visual
container.

grid-column: 1 / -1 on the system tile makes it span the row so the
two action buttons fit alongside the meta text without crushing the
app-tile grid template.

Section header: 'Per-app status' → 'Backup status' + hint 'System
config and every installed app's latest backup. System config always
first — a bare-metal restore needs it.' Dashboard subtitle updated
to match.

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-27 00:42:58 +01:00
librelad
6fb595e481 Merge claude/1 2026-05-27 00:37:32 +01:00