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>
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>
`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>
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>
Convert the remaining sections off the legacy ?= query form to clean paths,
matching the Admin area:
/apps/<category> (was /apps?=<category>)
/app/<name>?tab=&task= (was /app?=<name>&tab=&task=)
/tasks/<category>?task= (was /tasks?=<category>&task=)
/backup/<tab> (was /backup?=<tab>)
Builders updated everywhere (sidebar, dashboard, notifications, tasks, apps,
app tabs, task-actions, setup watcher); parsers now read the resource from the
path with the legacy ?= kept as a fallback so old links/bookmarks still work
(server already serves index.html at any depth). Route table gains /apps* and
orders it before /app* (since '/apps' startsWith '/app'); active-nav and
config/apps data-loading recognise the new paths.
Tab/task remain ordinary query params (modifiers, not the primary resource).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
Surface when LibrePortal is behind upstream and let users update from the
WebUI, reusing the proven git-update path instead of reinventing it.
Detection (host): webuiSystemUpdateCheck writes
frontend/data/system/update_status.json from a throttled git fetch +
behind-count + VERSION compare, off the existing per-minute
`webui generate system` cron. A new /VERSION file is the canonical version.
Display (frontend): update-notifier.js/.css render a global topbar badge
(every page) and a dashboard banner (prominent when behind, subtle "up to
date" with a manual check otherwise), plus a details panel.
Actions go through the task pipeline:
- `libreportal update apply` -> webuiRunUpdate (non-interactive: guards,
forced check, gitPerformUpdate, then dockerInstallApp libreportal)
- `libreportal update check` -> forced recheck
gitFolderResetAndBackup's body is extracted into gitPerformUpdate (no exit)
so the WebUI path can reuse it; the interactive CLI flow is unchanged.
Detection JSON verified against the repo (up-to-date and behind cases).
webuiRunUpdate's re-clone + redeploy still needs validation on a live host.
The latest-version source is git for now and is the single swap point for
get.libreportal.org later — the JSON contract and frontend stay unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
A free, open, self-hosted app platform (GNU AGPLv3): one-click app deploys,
Traefik reverse proxy with automatic SSL, rootless Docker support, gluetun
VPN routing, and a web dashboard to manage it all.
Free & open forever to self-host; optional paid hosted services fund it.
See PROMISE.md.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>