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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
`libreportal config update` and `libreportal system update` tasks are
submitted with `task.app === 'system'` (see task-actions.js' configUpdate
+ systemUpdate). renderTaskIcons hit its first branch on any truthy
task.app and built an <img src="/icons/apps/system.svg"…> which 404s
(there is no per-app icon called "system"). The onerror handler then
hid the broken image, so those task rows showed only the 🛠️ type emoji
and no LibrePortal logo — visually inconsistent with sibling system-level
rows like "LibrePortal - Finalize Setup" (which happens to carry
`app: 'libreportal'`, matching a real icon, and renders correctly via
the same branch).
Treat `app: 'system'` as a category sentinel rather than a real slug:
skip the per-app icon path, fall through to the system-task branch that
loads /icons/libreportal.svg directly. That icon is already shipped + the
data shape stays intact ('system' is the meaningful category, not a lie
about the app identity).
Net: "LibrePortal - Apply Configuration" and "LibrePortal - System Update"
now show the LibrePortal logo alongside their type emoji, matching the
Setup / Update / Backup-All rows.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
formatCommandForUser was a 90-line if/else chain that grew one branch at
a time as new task shapes appeared. Three sites had escaped coverage and
fell through to the raw-command fallback:
libreportal config update '<changes>' → shown as the raw 50-char clip
libreportal peer add <name> <kind> … → same
libreportal regen webui --force → same
Restructure as a declarative `PATTERNS` array of `{match, title}` rows.
Each row is one regex + one title (string OR function for per-app rows
that extract the app slug). The matcher iterates once; first match wins.
Adding a new task shape is now a one-line append — no new code branch,
no copy-paste of the `if/match/return` boilerplate.
Behaviour-equivalent for every previously-formatted command (verified
by running 15 sample command strings through the new function against
the old expected titles); the three previously-broken ones now resolve:
libreportal config update CFG_DEV_MODE=true → "LibrePortal - Apply Configuration"
libreportal peer add Alice host … → "LibrePortal - Add Peer"
libreportal regen webui --force → "LibrePortal - Regenerate WebUI Data"
Plus a couple I noticed while in there:
libreportal backup system → "LibrePortal - Backup System Config"
libreportal peer remove / peer pair → friendly equivalents
The two non-table fall-throughs (the toolsCatalog-aware `app tool` lookup,
and the generic `libreportal app <action> <app>` map) stay inline since
they need richer logic than the table can carry — but everything else
lives in the one scannable list.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
The 'No backups from other hosts visible…' empty state was rendering as
centred text inside the outer card, which read as floating prose rather
than a defined block. Wrapped it in a bordered callout (matches the
visual weight of the per-app task cards): rounded border, surface-2
background, padding, plus a centred location-pin glyph above the
message and the existing 'Open Locations' button as the CTA.
Inline styles so it works against the existing theme vars without
needing a new CSS rule.
Signed-off-by: librelad <librelad@digitalangels.vip>
`webuiSystemUpdate` was calling `webuiSystemMetrics` and getting
"command not found": the lazy-load manifest was missing it (and
`webuiSystemApps`), even though both are defined in
webui_system_metrics.sh.
Cause: the manifest scanner's awk-based depth tracker is line-based.
The first function in that file, `_metricsReadCpu`, contains an
embedded awk script:
_metricsReadCpu() {
awk '/^cpu /{ <-- the open brace runs to end-of-line, depth += 1
...
}' /proc/stat <-- the close brace is mid-line, NOT counted
} <-- depth-- but we started one too high
The increment rule fired for the `/^cpu /{` line (open brace at EOL),
the matching close on the `}' /proc/stat` line was not counted because
the existing depth>0 branch only decremented when stripped was EXACTLY
`}`. Result: depth ended at 1 instead of 0 after _metricsReadCpu, and
every subsequent funcname() header was treated as "still inside a
function" and silently dropped. Same pattern across the ~6 files the
lazy-loader memory called out as "false-positive eager".
Fix: in the depth>0 branch, count open AND close braces per line
symmetrically and apply the net delta. Drops the redundant "exactly
`}`" special-case rule, clamps depth at 0 as a sanity backstop. False
positives from braces inside string literals could under/over-count
locally, but the clamp + the next legitimate `funcname()` header at
depth-0 re-anchors the tracker.
Result on a full regen:
- 858 → 876 functions indexed (+18 previously-stranded)
- webuiSystemMetrics + webuiSystemApps now correctly autoloaded
- eager-file count: 9 → 11 (two files that genuinely have both
function defs and top-level side effects are now correctly seen
both as eager AND get their functions indexed — net win on
every axis)
Verified live: the WebUI updater that was failing with "command not
found" now prints "Updated system information..." cleanly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
Symptom: after any commit / deploy on this box, the WebUI would log
users out ~60 seconds after they logged back in. Looked like a
short session timeout; was actually the auth file being deleted.
Cause: my recent update.sh change added --delete to the frontend
rsync so source-tree file removals propagate to the live install.
Excludes only protected data/. .auth.json sits at the top of
frontend/ (never in the source repo — it's the persisted credentials
+ JWT secret), so --delete nuked it on every deploy. The next
container start regenerated it with a fresh secret; all existing
cookies (signed with the old secret) became invalid. The dashboard's
60-second auto-refresh hits /data/system/*.json which is auth-gated,
gets 401, and the global 401 interceptor in auth-manager.js shows
the re-login overlay. Hence 'logged out after 60 seconds'.
Fix: extend the rsync exclude list with:
--exclude '.*' (any top-level dotfile — covers .auth.json
and future runtime state of the same shape)
--exclude '*.lock' (lockfiles like setup.lock if any ever land
outside data/)
--exclude '*.bak' (backup files from manual edits)
data/ exclude kept. JWT lifetime stays at 30 days as designed.
Also: feat(webui): icon on the 'Open Locations' button in the
backup → Migrate tab's empty state. Matches the location-pin icon
used by the sidebar's Locations entry so the visual carries over
when the user clicks through.
Signed-off-by: librelad <librelad@digitalangels.vip>
The Migrate tab carried two walls of explanation text — a 3-line hint
under the h2 ("Pulls a snapshot taken on another host…") and an even
longer empty-state paragraph ("Either no other LibrePortal has backed
up to a location this host can see, or this is the only host using its
locations…"). Both spelled out diagnosis the user can infer from the
empty list itself, and the tone didn't match the rest of the backup
page (cards elsewhere have a short title + a 4-6 word hint, with any
long explanation as a hover title attribute).
Three changes:
1. h2 down to "Cross-host migrate" with a small ℹ️ carrying the full
explanation as a title= tooltip — matches the existing tooltip
pattern in the Locations form (BACKUP_RETENTION_PRESET_META).
The short subtitle "Restore an app from another LibrePortal" stays
as backup-card-hint, mirroring "Per-app status / Latest backup per
app on this host" elsewhere on the page.
2. The empty state is now the standard `<div class="backup-empty-state">`
container (same shape Locations + Snapshots use), one trimmed line
("No backups from other hosts visible in any enabled location.
Add a shared backup location on both hosts to enable cross-host
migrate.") instead of two paragraphs.
3. Added an "Open Locations" CTA button inside the empty state — the
#1 next-step for a user staring at this empty list is to add a
shared location, which lives one tab over. New data-action
"go-to-locations" wired through the existing event-delegation
handler in backup-page.js calling switchTab('locations').
The renderMigrate JS still toggles #backup-migrate-empty.hidden — the
wrapper id is unchanged, only its inner markup tightened. No
behavioural change beyond the CTA + tab switch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
Existing installs were locked out of template-side description /
options / marker changes because reconciliation kept the user's whole
`CFG_KEY=value # comment` line verbatim. So new metadata like the
**DEV** marker I'm adding for the developer-mode feature wouldn't take
effect on already-deployed boxes — only fresh installs.
Updated reconcileConfigFile to split each line into value-part and
comment-part. User value is still sacred; the comment (title,
description, [options], **ADVANCED**/**DEV** markers) now comes from
the template. Field renames, label tweaks, marker additions/removals
shipped in a release reach existing installs on the next CLI
invocation (which runs the reconciler).
Specifically unblocks: the developer-mode WebUI feature (CFG_DEV_MODE
field gets added by the existing add-only path; CFG_INSTALL_MODE and
CFG_RELEASE_CHANNEL now pick up their new **DEV** markers and the
'Release - Stable' / 'Bleeding Edge' labels).
Signed-off-by: librelad <librelad@digitalangels.vip>
What this delivers (Stage 1+2 of the dev-mode feature):
1. New `**DEV**` marker for config fields. Mirrors the existing
`**ADVANCED**` pattern: stays in the description string, frontend
strips it for display, presence flips a 'hide unless dev mode is on'
behaviour. Implemented in ConfigUtils.cleanDescription /
isDevField / isDevModeOn and in ConfigShared._filterDevKeys, which
the two generateFieldsForCategory* helpers now call before rendering.
2. New CFG_DEV_MODE field in configs/general/general_install. Visible
under Advanced; defaults to false. The canonical place to toggle
dev mode (the WebUI easter egg writes to it, the auto-detector
writes to it, and users can flip it directly here too).
3. Marked CFG_INSTALL_MODE and CFG_RELEASE_CHANNEL with `**DEV**`.
Normal users no longer see either field — they install Release-
Stable and that's the whole story. Devs see both with the
user-facing labels you asked for:
CFG_INSTALL_MODE Release - Stable | Git clone | Local folder
CFG_RELEASE_CHANNEL Release - Stable | Release - Bleeding Edge
(CFG_INSTALL_MODE label for the release option also renamed to match.)
4. 10-click LibrePortal-logo easter egg in topbar.js:
- Counter on any .libreportal-logo click; idle-reset after 3 s
- Toast countdown from click 6 ('4 clicks away from being a developer…')
- At 10: toggles CFG_DEV_MODE via the standard config_update task
(same path the Config form uses); shows '🛠️ Developer mode
unlocked. Reload to see the extra options.'
- Re-using the same logo when dev mode is on toggles it back off
('… away from disabling developer mode') — symmetric, no separate UI
5. Auto-detect: on every WebUI load, if CFG_INSTALL_MODE is git or
local AND CFG_DEV_MODE is off, auto-flip to on with a one-time
toast 'Developer mode auto-enabled — you're on a git install.
Click the LibrePortal logo 10× to disable.' Stops dev-install
users getting locked out of the very options they need to manage
their install. Idempotent — runs once per page load; no-op if
already on or on release.
Disable surfaces: (a) CFG_DEV_MODE in Advanced on the Config form is
the canonical toggle; (b) 10 more logo clicks. A 3rd surface (a System
page banner) is deferred — those two cover the practical cases.
Signed-off-by: librelad <librelad@digitalangels.vip>
`libreportal app generate <name>` (and the menu's "g. Generate App" entry)
was broken three independent ways and incompatible with the per-app
architecture the project actually uses now:
1. Copies from $install_containers_dir/template/ which doesn't exist —
the only template/ in the tree was in scripts/unused/OLD_CONTAINERS/
and was never installed into the live tree. cp -r would just fail.
2. Every sed call used BSD/macOS syntax `sed -i '' -e …`. On Linux
(every distro this targets) the empty '' becomes a positional file
argument, so the substitutions never ran. 8 calls, all broken.
3. Even if it had run, the produced skeleton would have been a
pre-modular-tools / pre-per-port-subdomain app shape: no tools/,
no scripts/ subdir, HOST_NAME=test in the .config. Every active
containers/<app>/ today carries the modular layout the rest of the
framework expects.
Plus the recent cleanups (the prompt loop fix in 9ffc8e4, the per-port
subdomain refactor in 2e4f420) had been peeling pieces off it without
the root question — does the function still belong? — getting asked.
Delete the whole surface:
- scripts/app/app_generate.sh (157 lines, the function body)
- scripts/unused/OLD_CONTAINERS/template/ (the never-installed source
files appGenerate would have copied — stale enough to still carry
HOST_NAME=test, CFG_<X>_HOST_NAME, and 248 lines of compose template)
- menu entry "g. Generate App" + its dispatch in menu_main.sh
- "generate" case branch in cli_app_commands.sh
- `libreportal app generate` line in cli_app_header.sh
- The corresponding entries auto-drop from files_app.sh +
function_manifest.sh via regen.
New apps are added the way the catalog already grew — by hand-crafting
containers/<app>/{<app>.sh, <app>.config, docker-compose.yml,
tools/<app>.tools.json, scripts/<app>_*.sh}. Copying an existing app's
folder + renaming is the closest thing to a "generator" and it's a one-
command operation.
Net: -556 lines, no behaviour lost (the function never worked).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>