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>
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>
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>