24 Commits

Author SHA1 Message Date
librelad
bfda700794 fix(apps): stretch cards to fill the row width so the box stays full-width on any zoom level
Fixed-width tracks + cap formula kept the box pinned to "N cards at
328px" outer regardless of viewport size, so zooming out left a
massive empty band between the box's right edge and the layout edge.
The box was no longer "dynamic" in any real sense — it scaled with
the card count, not with the available content.

Switching grid-template-columns to repeat(auto-fit, minmax(--app-min,
1fr)) lets cards stretch to fill the row, and auto-fit collapses
trailing empty tracks so a 2-card row in a 3-track-wide viewport
doesn't leave a 328px hole at the end. Zoom in/out now just widens
or narrows the cards; the box always reaches the layout edge.

This drops the cross-category card-width uniformity that the earlier
fixed-width pass introduced — a 2-card category now lays out as two
wide cards while a 3-card category gets three narrower ones. That's
mutually exclusive with "box always full width" without leaving
holes, and the user has shifted priority to full-width-always.

JS cleanup: dropped updateAppsCount + its window-resize listener +
its callsites in renderApps/filterAppsByQuery — no more --app-count
or column-count measurement needed when the grid handles everything
natively.

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-28 01:31:56 +01:00
librelad
adf79db9e2 ux(tools): play icon on the Run button
Each tool row's Run button gains a small play-triangle SVG to the left
of the label, matching the iconography pattern the Services tab uses
for its Restart and Open buttons. Same green colour (currentColor), so
the icon inherits the success/destructive variants without extra CSS.

Button becomes a flex container with a 6px gap so icon + label stay
nicely centred regardless of label width.

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-28 01:30:46 +01:00
librelad
64c0652ddf revert(apps): drop balanced-column rebalance — keeping rows dense beats avoiding the orphan
The orphan-1 rebalance (4-on-3 → 2x2) cost too much: it dropped a
4-card category from 3 cards per row to 2 across the board, and would
do the same for any N where N % maxCols == 1. User feedback: 3
densely-packed cards with a small orphan-row gap reads better than
2 wider cards in a 2x2 layout — denser rows feel more compact and
let the eye scan more apps at once.

Back to the post-d4b7731 state: fixed-width tracks (auto-fill,
--app-min) so card widths line up across categories, plus the
sentinel that sets --app-count to 99 when visible cards meet the
natural full-width column count so the box reaches the layout edge.
The 4-on-3 case is now 3+1 again — the lone card on row 2 has empty
cells to its right, accepted as the lesser of two evils.

If the orphan ever becomes a real visual issue, the next move would
be JS-rendered last row (own sub-grid sized to its item count) rather
than reducing the column count globally.

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-28 01:12:16 +01:00
librelad
35c06a90a5 fix(apps): balance column count so 4-on-3-col wraps to 2x2 instead of leaving an orphan card
Screenshot showed a 4-card category laying out as 3+1 (three cards on
row 1, Wireguard Easy alone on row 2 with two card-shaped empty cells
on its right). Fixed-width tracks + auto-fill kept the cards aligned
across categories but couldn't avoid the orphan — pure CSS grid has
no way to collapse partial-row trailing cells when the column above
them is filled.

apps-manager.js now picks --app-cols deliberately: the natural
column count for the viewport, reduced by one when the last row
would otherwise be exactly one orphan card. 4 cards on a 3-col
viewport becomes 2x2; 5 cards stays at 3+2; 6 stays at 3+3+0; 7
drops to 2-col so the last row gets a partner (still has one orphan
at the very end since 7 is prime, but never below 2 cols — a single
column stack reads worse than an orphan).

CSS swap: grid-template-columns now consumes the new --app-cols
custom property and uses minmax(--app-min, 1fr) so cards stretch
within their tracks (the orphan-prevention dance means widths can
vary across categories now — tradeoff for never having internal
gaps). 1-card view still shrinks the box via the existing formula
so a lone card isn't stretched across the full row.

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-28 01:06:50 +01:00
librelad
d4b7731bdc fix(apps): drop cap when visible cards meet the natural row width so the box reaches the layout edge
The fixed-width tracks change kept card widths uniform across
categories but reintroduced the "large gap on the right" outside the
glass box — with cards locked at 300px and the cap formula tracking
exactly N cards, the box stopped wherever the visible cards ended,
leaving up to 150px+ of empty parent space to its right on a wide
viewport.

Bringing back the natural-columns sentinel from the earlier pass.
updateAppsCount measures the parent's inner width (minus the
section's 90px of margin/padding/border), computes the column count
the auto-fill grid would pick at full width, and passes a huge
sentinel (99) as --app-count whenever visible cards >= that count.
The formula then overshoots the 100%-44px parent cap and the box
runs edge-to-edge. Cards themselves still come out at --app-min
because the grid template is repeat(auto-fill, var(--app-min)) — no
1fr stretching — so the cross-category uniformity from the previous
fix is preserved.

Net effect: 1-2 cards on a 3-col viewport still shrink (no card-shaped
hole), 3+ cards reach the right edge of the layout, every card lines
up across categories regardless of which branch fires.

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-28 00:58:04 +01:00
librelad
40b15de471 ui(tools+services): brighten tool-run buttons on Nebula and split service Logs into Details + opt-in log tail
Two bundled UI fixes.

1. Tools page Run / destructive buttons — the base recipe in tools.css
(rgba green/red 0.12 bg + 0.30 border + full-saturation text) reads
muddy against Nebula's cosmic gradient, same readability problem
.install-btn / .uninstall-btn had before the nebula overrides bumped
them to 0.35/0.65 with --text-primary text. .tool-run-btn and its
.destructive variant now ride those same overrides so Run pops as
green-tint and the dangerous variant pops as red-tint, both with
neutral text against the gradient.

2. Services tab row — the "Logs" button now reads "Details" because
that's what it actually toggles (meta + rich detail + log toggle).
The data-action moves from toggle-logs to toggle-details, and the
expanded panel no longer auto-opens a log stream. A small footer
"Show logs" / "Hide logs" toggle at the bottom of the open panel
explicitly opts in to tailing, kicking off the existing SSE stream
on click (auto-updates while shown). Closing the parent details
panel also resets the log block back to its hidden state so the
next reopen starts clean. app-tabbed-manager's task-running button
disable was taught about the new actions so they stay clickable
while a long task is running.

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-28 00:45:00 +01:00
librelad
de6acc1f92 fix(apps): fixed-width grid tracks so card widths line up across categories
The auto-fill minmax(300px, 1fr) template stretched cards to fill the
glass box, so a 2-card category landed at ~301px each (the box
shrunk-and-stretched to a hair over 2*300) while a 3-card category
(box now full-width) landed at ~323px each. Cards visibly didn't
align between categories — the user spotted the 22px difference.

Switching the grid template to fixed-width tracks
(repeat(auto-fill, var(--app-min))) means cards are always exactly
--app-min (300px / 280px at ≤1024) regardless of how many are
visible. Card positions and widths line up across every category.

The natural-columns sentinel from the previous pass is no longer
load-bearing — with fixed-width cards, "full width" at high N gives
no extra card-width benefit, only trailing space inside the box.
updateAppsCount drops the measurement step and just sets the visible
count, letting the formula shrink the box around the cards.

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-28 00:33:41 +01:00
librelad
39558d82b0 fix(apps): drop apps-section cap when visible cards already fill the row at full width
Previous cap shrank the box to "exactly N cards at min width", which
made 3 cards sit a few pixels short of the layout edge on a 3-column
viewport while 4 cards (which wraps internally) ran edge-to-edge —
visually inconsistent and the user flagged the gap.

updateAppsCount now measures the parent's available inner width
(minus the section's own 90px overhead: 22 margin + 22 padding + 1
border, doubled) and computes the natural column count the auto-fill
grid would pick at full width. If visible cards >= that count, the
function passes a sentinel (99) as --app-count so the formula
overshoots the 100%-44px parent cap and yields the layout-edge box.
Otherwise the cap still kicks in to hide card-shaped holes for 1-2
cards.

Also wired a window resize listener in the constructor so dragging
the window, snapping it, or opening devtools re-evaluates the
decision — the natural column count is viewport-dependent.

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-28 00:25:23 +01:00
librelad
06f6e5c71d Merge claude/2 2026-05-27 23:31:50 +01:00
librelad
9a92805bdb feat(ui): Beginner/Advanced experience level + linked dev mode + setup-wizard step
Adds the install-time Beginner/Advanced choice the user described, with
the linked dev-mode escape hatch and global body-class machinery that
any surface can hang advanced/dev-only DOM off.

Three-tier mental model, two flags in the data model:

  Beginner            default. nothing extra shown.
  Advanced            .lp-advanced DOM revealed; advanced wizard steps shown
  Adv+Dev             .lp-dev DOM also revealed; dev-only fields visible

Linking rule (enforced inside LpUi):
  - enabling dev auto-enables advanced (dev w/o advanced is incoherent)
  - disabling advanced auto-disables dev

Wire shape:
  CFG_INSTALL_LEVEL                  beginner | advanced (general_basic)
  CFG_DEV_MODE                       existing, unchanged behaviour
  window.LpUi.{advanced,dev}         {get(), set(), apply()}
  localStorage keys                  lp.ui.advanced, lp.ui.dev, lp.ui.seeded
  body classes                       lp-ui--advanced, lp-ui--dev
  events                             lp-ui-advanced-changed, lp-ui-dev-changed
  global CSS gates                   body:not(.lp-ui--advanced) .lp-advanced { hide }
                                     body:not(.lp-ui--dev) .lp-dev { hide }

Setup wizard:
  - New step 1 "Choose your experience" with Beginner/Advanced cards.
    Beginner is preselected so race-through gets the safe default.
  - Picking a level updates totalSteps live (4 for beginner, 5 for
    advanced) so the progress bar reflects the choice.
  - Metrics step (Prometheus + Grafana) is gated to Advanced — beginner
    never sees it, never gets asked, never installs them by accident.
  - Submit payload now carries install_level; setup-routes.js validates
    it against the enum (beginner|advanced).
  - scripts/setup/setup_apply.sh writes it to CFG_INSTALL_LEVEL via
    updateConfigOption.
  - On submit, LpUi.advanced.set is called immediately so the next
    surface (running-tasks page) is already in the right mode — no
    refresh needed.

WebUI bootstrap:
  - js/utils/lp-ui.js loads first thing in index.html (before any other
    bootstrap) so body.lp-ui--advanced is applied pre-paint — no FOUC
    of advanced content on a fresh tab.
  - On first run, seeds lp.ui.advanced from CFG_INSTALL_LEVEL.
    Subsequent loads honour the user's per-browser override.
  - Mirrors CFG_DEV_MODE → lp.ui.dev on the seed pass.

Dev-mode unlock:
  - Existing 10-click LibrePortal-logo easter egg unchanged.
  - NEW: same 10-click unlock on the Advanced toggle (in services-manager).
    Reuses the countdown-toast pattern; on the 10th click delegates to
    the topbar's _setDevMode so there's one canonical setter and the
    config_update task path stays singular.
  - TopbarComponent now exposes its instance as window.topbar so the
    toggle's tap handler can reach _setDevMode.
  - topbar._setDevMode also calls LpUi.dev.set(enabled) so the body
    class flips immediately (no reload needed to see dev-only DOM).

Convention rolled out:
  - Services tab's .service-rich panel was already gated on
    body.lp-ui--advanced.
  - .lp-advanced / .lp-dev are now first-class hide classes any
    component can tag DOM with — see style.css globals.

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-27 23:31:50 +01:00
librelad
dd1264e335 ui(spa): stamp initial history entry + close live buses on pagehide so back-button works like a real SPA
Two reasons the back button was unreliable:

1. The very first history entry (the URL the user landed on) had
   state: null because handleInitialRoute() called navigate(path,
   false), and the pushState branch only ran when addToHistory=true.
   When the user later pushState'd forward and then hit back, the
   popstate handler's guard "e.state && e.state.route" was false on
   the initial entry, so it silently did nothing — back appeared
   broken. Now navigate() replaceState's the current entry whenever
   addToHistory=false, so the initial entry (and any back-compat
   URL rewrite) always carries its route. The popstate handler also
   now falls back to window.location when state.route is missing,
   so third-party history manipulation can't break us.

2. Open SSE streams (LiveSystem, taskEventBus, services-manager log
   tails) block the browser's back-forward cache. Without BFCache,
   back has to fully re-mount the page instead of restoring it
   instantly the way Amazon/GitHub feel. Now pagehide closes every
   live bus we own, and pageshow(persisted=true) reopens them when
   the page is restored from BFCache. Log tails aren't auto-resumed
   — Resume overlay handles that if the user comes back to a
   services tab.

Public surface added: LiveSystem.pause()/resume() and
ServicesManager.pauseStreams(). TaskEventBus already had stop()/
start(). The legacy-URL rewrite in handleAppDetail also now
replaceState's with { route: canonical } instead of {} so the
stamp is consistent across all internal history updates.

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-27 23:28:25 +01:00
librelad
8001e678e0 ux(services): global Beginner/Advanced UI mode + log block first in panel
Adds a foundational global UI-mode flag — Beginner (default) vs Advanced —
gated by a single toggle on the Services tab title row. First foothold
of a project-wide pattern: any surface that grows extra-technical detail
(mounts, limits, internals, raw IDs, …) will gate it on the same flag,
so a newcomer doesn't see a wall of operator information while a power
user gets everything site-wide with one flip.

How it's wired:

  window.LpUi.advanced   — { get(), set(on), apply() }
  localStorage key       — lp.ui.advanced ('0' | '1')
  body class             — lp-ui--advanced
  event                  — window 'lp-ui-advanced-changed' { advanced }

Surfaces gate their advanced-only DOM via CSS:

    body:not(.lp-ui--advanced) .service-rich { display: none; }

So flipping the toggle is instant and DOM-free — no re-render needed.
The Services tab's rich container panel (limits, image, healthcheck,
networks, mounts) is the first thing behind the flag; live CPU%/memory
chips in each row stay visible always because they read just as easily
as a status colour and are useful to everyone.

Title row gets a small slider toggle styled in the project's accent —
unobtrusive, labelled "Advanced". Default OFF (Beginner).

The same _renderRow reorders the log block above the rich-detail block
inside .task-details, so when Advanced is on AND a row is expanded, the
live log appears right where the "Logs" click landed rather than below
a wall of metadata. Helps with the old simple-click feel even when the
extra detail is showing.

Plumbed deliberately to be project-wide so the upcoming first-install
"Beginner vs Advanced" wizard step can seed the flag (planned:
CFG_INSTALL_LEVEL in general config → emit body class server-side at
template render time → no FOUC on a fresh load).

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-27 23:09:07 +01:00
librelad
57a565aac2 refactor(system): per-app deep-dive moves to the app's Services tab
The Admin → System area was growing a parallel per-container surface
(/admin/config/system/app/<name>) alongside the existing per-app Services
tab on the app page. Two pages onto the same thing is the kind of
duplication that rots fast — they drift, users have to remember which
one to use, and the next person adding a feature has to decide twice.

This commit consolidates onto the existing Services tab (which already
has compose-service awareness, docker socket access, restart actions via
the task system, and live log streaming) and decommissions the parallel
admin sub-page:

  - Delete system-app-page.js and its lazyLoad entry. The dispatch in
    admin-system.js for the 'app' view now redirects to the app page's
    Services tab so old bookmarks still resolve cleanly.

  - System index per-app rows navigate to /app/<name>/services (not
    /admin/config/system/app/<name>) and the row hint copy is updated
    to match.

  - Services tab gains the rich container detail the old admin page
    rendered, fed by /api/system/containers + /containers/:id +
    /containers/:id/stats:

      * Inline live chips in each service header: CPU% and memory
        (with limit + percent if a limit is set). Memory chip flips
        amber at 80% and red at 95% of the configured limit.
      * New "service-rich" panel inside the existing expandable
        details section (above the log block, so the existing Logs
        toggle reveals both):
          - Image + image-id + uptime + restart count
          - Memory / CPU / PIDs limits + restart policy
          - Healthcheck pill + last 3 probes (collapsible per-probe)
          - Networks table (name, IP, gateway, MAC)
          - Mounts table with type badges (volume/bind/tmpfs)
      * Live stats refresh every 5 s; existing status refresh stays
        on 10 s. Both gated on the Services tab being active.

  - Backups for the app already live on the existing /app/<name>/backups
    tab (loadAppBackups → BackupAppCard.render), so the navigational
    promise of "one place per-app" is already met — System index just
    needed to route there.

  - CSS: services.css picks up .service-live-chip (with warn/danger
    colour cues) and the full .service-rich block (grid, tables, mount
    badges, healthcheck pills).

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-27 22:51:53 +01:00
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
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
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
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
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
b17ac3707e fix(webui): refresh stale per-port Subdomain tooltip
The Subdomain field's help text still said it inherits CFG_HOST_NAME and that
the label-generation refactor was pending — both untrue now that per-port
subdomain routing has shipped. Reword to: empty -> app-name default, @ ->
domain apex, multi-level supported.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-22 11:16:16 +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