8 Commits

Author SHA1 Message Date
librelad
0ba8e980ea ui(apps): shrink apps-section to visible-card count so few apps don't leave card-shaped gaps
The glass box was a CSS Grid with auto-fill columns of minmax(300px,
1fr), so it always painted across the full content area. With only 2
apps on a wide row the third/fourth column slots remained inside the
border as empty space — visually a card-shaped hole.

Drive the box's max-width off a --app-count CSS variable, capped at
(100% - 44px) so it can't escape the layout's symmetric 22px gutter.
margin: 22px auto keeps the horizontal padding symmetric in both the
capped (auto-centers the smaller box) and full-width (auto collapses
to 22+22) cases. --app-min (300/280 at the ≤1024 breakpoint) feeds
both the grid template and the cap formula so the responsive column
width stays a single source of truth.

apps-manager.js sets --app-count to the count of visible .app-card
elements after every render and after the sidebar search filter, so
filtering down to 2 hits also collapses the box. Floor of 1 keeps the
empty state usable.

Mobile (≤768) overrides max-width to none — single column already
fills, and the 10px gutter shouldn't be auto-centered.

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-27 20:54:39 +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
152d9c5d28 fix(webui): make all icon and data asset URLs absolute under path routing
Same class of bug as the topbar partial: icon and data-file references were
relative (icons/apps/x.svg, data/apps/...), so on deep path routes (/app/<name>,
/admin/config/x) the browser resolved them against the route dir and the SPA
catch-all served index.html with HTTP 200 instead of 404 — broken images and
silently-wrong JSON.

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

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

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

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-24 22:29:03 +01:00
librelad
a103aa6864 refactor(webui): path-based URLs for apps, app, tasks, backup
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>
2026-05-23 19:03:54 +01:00
librelad
3ba3f77f0b feat(backup): expose per-app strategy override on all apps, context-aware
Every backup-scope app now carries CFG_<APP>_BACKUP_STRATEGY=auto, so the
Backup Strategy dropdown appears in each app's Advanced tab — not just the
DB apps.

To keep it honest, the 'live' option is hidden where it isn't safe:
- apps.json generator emits backup_live_capable per app (from compose backup
  labels: a dumpable DB, or a live-safe marker).
- apps-manager filters the live option out of the strategy select when the
  current app isn't live-capable, so apps like gitea/focalboard (a DB we don't
  yet dump) never offer it.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-23 15:41:55 +01:00
librelad
c227c01969 fix(webui): hide empty config tabs; rename backup 'Advanced' to 'Engine'
Empty tabs: generateSimpleTabsAndContent gated tabs on a hasFields heuristic
that drifted from what generateConfigFields actually emits, so a category like
Network could show a tab whose body only read "No configuration options
available". Render each category's fields first and emit the tab only when the
output is non-empty, keeping tabs and content in lockstep.

Rename: the backup_advanced subcategory now displays as "Engine" via a
display-name override in formatSubcategoryName. File and CFG_BACKUP_* keys are
unchanged, so saved values are unaffected.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-22 23:46:12 +01:00
librelad
875a60f90f LibrePortal v0.1.0 — initial release
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>
2026-05-21 20:37:54 +01:00