336 Commits

Author SHA1 Message Date
librelad
f4c24340b7 fix(webui): lighten faint empty-state messages on dark panels
Empty-state 'no data yet' messages rendered at rgba(text, 0.55) (and one at
text-muted), which is hard to read on the dark recessed panels. Switch them to
the theme's --text-secondary muted token so they stay de-emphasized but legible.

Covers .updater-empty, .updater-detail-empty, .sys-detail-empty, .eo-modal-empty.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-06-25 21:07:07 +01:00
librelad
246e687e45 fix(webui/tasks): equalize task-list panel top/bottom padding
The last task tile's 10px bottom margin stacked on the list's 16px bottom
padding, leaving a larger gap below the last row than above the first.
Zero the last tile's bottom margin so the dark panel reads symmetric.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-06-25 20:54:42 +01:00
librelad
c48f9f4ffc fix(webui/storage): drop redundant 'Reclaiming space…' toast
Clicking Reclaim space fired two notifications: routeAction → executeTask
already shows the rich 'Reclaim Space task started!' toast (icon + bold
LibrePortal + task link), then _reclaim added a second, plain, iconless
'Reclaiming space…' info toast on top of it. The image-removal path doesn't
double-notify like this. Drop the redundant one — the start toast plus the
completion toast give clean feedback on their own.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-06-25 13:26:51 +01:00
librelad
23712bd0c4 fix(webui/system): stop loading/empty overlays covering populated metric graph
.sys-detail-loading and .sys-detail-empty are absolutely-positioned overlays
that JS shows/hides via the `hidden` attribute. Their .sys-detail-* rule sets
`display: flex`, an author declaration that overrides the UA
`[hidden] { display: none }` — so `el.hidden = true` never actually hid them.
Both the 'Loading history…' and 'No samples in this range yet' overlays stayed
painted on top of a fully-populated chart (overlapping into garbled text).

Add `.sys-detail-loading[hidden], .sys-detail-empty[hidden] { display: none }`
(higher specificity than the bare class) so the hidden attribute wins.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-06-25 13:24:00 +01:00
librelad
9850b9d8e7 fix(admin/storage): make the Storage page tablet/mobile friendly
The donut legend collided into the stat cards and the image-row details
were squished on tablet widths — the 220px sidebar leaves the content
cramped while the old breakpoints assumed full-viewport width.

- Headline collapses to a single column at <=1024px (was 800px), the two
  stat cards reflow side-by-side, and on phones the donut stacks above its
  legend with one stat per row. Legend labels now ellipsis instead of
  overflowing into the stats.
- Image rows group the name+pill and the size/shared/age metadata so the
  metadata drops onto its own line under the name at <=1024px instead of
  competing for width; on phones the Delete button collapses to an icon.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-06-25 13:17:14 +01:00
librelad
e33701ee52 feat(admin/storage): filter images by in-use/unused, in-use first
The Images list on /admin/system/storage now has an All / In use / Unused
segmented filter (with live per-group counts), and the default All view
sorts in-use images to the top — the ones you can't reclaim lead, the
reclaimable ones follow. Select all / Clear All act on the visible rows,
so they honour the active filter.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-06-25 12:56:08 +01:00
librelad
c7484572df fix(webui/tasks): give app-less task notifications the LibrePortal identity
App-less system tasks (verify, regen, …) resolved to an empty displayName
and null icon in _taskNotificationDescriptor, so their completion toast
rendered an empty <strong></strong><br> — a blank bold line that showed as
a random gap above the message — and had no icon, unlike every other
notification. Treat any task with no app slug as a system task so it gets
the 'LibrePortal' subject and libreportal.svg icon.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-06-25 12:54:39 +01:00
librelad
be427e5376 fix(webui/admin): balance top/bottom padding in sys-tasklist
The .sys-tasklist panel spaces rows with a flex gap, but each .task-item
also carries margin-bottom: 10px from the shared tasks styles. That margin
only stacks on the last row, so the list had ~8px above the first row and
~18px below the last row, looking lopsided. Zero the row margin inside the
list so spacing is symmetric.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-06-25 12:50:36 +01:00
librelad
c1df1aef40 fix(webui/services): box the Services loading state like other tabs
The Services tab's loading placeholder (.services-loading) was a bare
centered row with no container chrome, unlike the Config and Tasks tab
loading cards. Give it the same boxed look (semi-opaque black fill,
hairline border, rounded corners, margin and min-height) so it reads as
a deliberate loading panel.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-06-22 14:58:54 +01:00
librelad
85e5920afe fix(webui/apps): app-detail tab clicks no longer snap back to config
showAppDetail() derived the target tab from the legacy ?tab= query
(searchParams.get('tab')), but the app is path-based now
(/app/<name>/<tab>), so that read was always null and defaulted to
'config'. Since loadTabContent() calls showAppDetail() on every switch,
clicking any non-config tab (services/backups/updater/tasks) immediately
rewrote the URL back to /app/<name> and rendered config.

Read the current main tab off the path via appPartsFromPath, honouring
it only when already on this app; cross-app/cold nav still starts at
config. The legacy ?tab= shape is already normalised to the path by the
SPA's handleAppDetail before this runs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-06-22 14:57:06 +01:00
librelad
70f16ef1e3 fix(webui/tasks): auto-expand opens one row, not all of them
autoExpandTask (the monitorTask path) opened its row directly without
collapsing the others and never set highlightedTaskId — unlike every
other opener (toggleTaskDetails, selectTask), which enforce a single
open row. So a burst of monitored task creations, e.g. a multi-app
first install, stacked every panel open at once.

Wait for the row to render, then delegate to selectTask, which collapses
any other open panel, sets highlightedTaskId, attaches the right log
view (live stream vs snapshot) and scrolls into view. Setting
highlightedTaskId also makes monitorTask's own guard trip after the
first task, so the running-task auto-follow takes over from there.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-06-22 14:41:36 +01:00
librelad
655dbc2bb9 fix(install): restore webui_logins container-group after credential write
The rootless WebUI container reads its bind-mount sources (configs/webui/*)
through the container-owner GROUP since a2376e2 switched those files from
world-readable to 0640 group=container-owner. But the WebUI credential
randomizer rewrites webui_logins via `sed -i` as the non-root manager, which
recreates the file with the manager's own group — dropping the container-owner
group. The installer then started the container immediately, so node hit
EACCES on /app/webui_logins at require-time (parseConfigFile) and exited 1;
nothing listened on the WebUI port. `libreportal webui login reset` had the
same latent bug (rewrite → restart). Under the old world-readable model a
post-sed file stayed o+r so the container could still read it, which is why
this only surfaced on fresh rootless installs after a2376e2.

Fix: make reconcileWebuiDirOwnership the single "ready the WebUI for its
container" pass — it now also restores the configs/webui bind access (new
`webui-bind` ownership action) on top of the container-dir chown. Reorder the
installer so the credential randomizer runs BEFORE the before-start permission
pass, making that pass the last ownership touch before the container starts;
and call reconcileWebuiDirOwnership before the restart in login reset.

Live box recovered via `libreportal-ownership reconcile`; WebUI 200.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-06-21 23:00:47 +01:00
librelad
0d5ae61e32 fix(app-config): restore config sub-tab on cold-load deep-link/refresh
/app/<name>/config/<sub> URLs (e.g. .../config/ports) are generated by the app
itself and shown in the address bar, but a refresh or deep-link always reset to
the first config category. Cause: showAppDetail() rebuilt the URL via
appPath(appName, targetTab) with NO sub argument and pushState'd it BEFORE
renderAppDetail() read the sub back off the path — so the /<sub> segment was
already gone and preferredCategory stayed null.

Preserve the sub when the URL already points at this app's config (matched by
appPartsFromPath().app === appName), so cold-load/refresh lands on the encoded
sub-tab. Cross-app switches still start at the first category. The sibling
showAppDetailWithConfig() (the grid 'manage' button) is intentionally left to
land on the first category.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-06-18 16:42:46 +01:00
librelad
75162af648 fix(webui): Services-tab Advanced toggle reveals rich detail again
The per-service rich detail panel (.service-rich: limits, healthcheck,
networks, mounts) and the live mem chip both did `const fmt =
window.SystemFmt` and bailed when it was absent. SystemFmt is defined by
the lazy admin System page module, so on the app Services tab it's usually
undefined — `_renderRichDetail` returned '' and the panel was never in the
DOM, so flipping Advanced revealed nothing.

Give the Services component its own SVC_FMT formatter (mirrors SystemFmt)
and use `window.SystemFmt || SVC_FMT` everywhere, dropping the hard bail.
The rich panel now renders whether or not /admin/system was ever visited.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-06-18 16:15:15 +01:00
librelad
626041a39e style(ports): use the shared lp-ui-advanced-toggle for 'Show advanced fields'
The port manager's 'Show advanced fields' control was a raw <input type=
checkbox> + <span>, out of step with the modern pill switch the Services tab
uses for its Advanced toggle. Swap the markup to the shared .lp-ui-advanced-
toggle (track + thumb) structure — defined in services.css, which is loaded
globally — keeping .port-manager-show-advanced on the input for the JS hook.
Drop the now-dead .port-manager-advanced-toggle CSS.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-06-18 16:04:06 +01:00
librelad
0fbbde1000 style(app-config): wrap each config field in a translucent card
App-config fields rendered as a loose vertical stack with no grouping. Give
each .app-config .form-field the .admin-card glass recipe (var(--card-bg) fill,
faint rgba(text,0.10) hairline border, 12px radius, 16px padding) so each
option reads as a discrete container. Spacing now comes from the .panel-fields
flex gap, so the per-field margin-bottom is dropped to avoid doubling.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-06-18 15:45:57 +01:00
librelad
bbf2f9a5f4 style(overview): match sidebar Overview entry to app-category rows
The pinned 'Overview' sidebar entry used a rounded, inset pill (margin +
border-radius:8px) so its hover/active highlight floated in the middle of the
sidebar, unlike the full-width app-category rows below it. Drop the margin and
radius, adopt the .category padding (15px 20px), border-bottom separator and
var(--surface-hover) hover, so the highlight spans the full sidebar width with
square corners and matches the categories. Keeps font-weight 600 + the
page-updater active tint as its only distinguishing marks.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-06-18 15:32:46 +01:00
librelad
01961e5bb9 fix(webui): tasks list panel hugs its content instead of overhanging
The recessed task-list box had flex:1, so its background filled the full
height and ran well past the last task. Move the scroll onto .tasks-terminal
and let .tasks-list size to its content, so the box ends at the last task
(and still scrolls when the list overflows). Scrollbar styling follows to
the new scroll container.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-06-17 18:49:06 +01:00
librelad
c02202d620 fix(webui): stop Backups-tab card bg from running past the footer buttons
On /overview Backups the card surface lived on .main, which wraps both the
body and the flipped footer — so the background overhung past the action
buttons. Move the card surface onto .backup-page-body (rounded bottom,
joined to the tab strip) and let the footer sit transparent below it,
matching the app Config tab's .tabs-content + .config-actions split.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-06-17 18:45:56 +01:00
librelad
0bcde854e6 refactor(webui): move fleet Overview under /apps/overview; retire standalone /backup
The fleet Overview area (Overview/Updates/Improvements/Backups/Migrate) now
lives at /apps/overview* instead of /overview*, reflecting that it belongs to
App Center. The Backups tab is therefore /apps/overview/backups, and the old
standalone Backup Center page is removed entirely:

- apps feature owns /apps/overview* (covered by the existing /apps* route); its
  mount() dispatches /apps/overview -> fleet Overview before the grid check.
- _legacyRedirect() rewrites old short URLs so bookmarks/links keep working and
  the address bar shows the canonical path:
    /overview[/tab] -> /apps/overview[/tab]
    /backup[/sub]   -> /apps/overview/backups[/sub]
    /updater, /peers redirects retargeted to /apps/overview*
- Removed the standalone backup feature: components/backup/{index.js,feature.json},
  its manifest entry, the /backup route registrations and the dead handleBackup().
  The BackupPage classes stay — the Overview Backups tab embeds them.
- Repointed every backups/overview link: the admin dashboard's 'Open backup
  center', the app-card 'Open backup center' button + snapshot-overflow link,
  the sidebar Overview entry, the improvements deep-link, and the Migrate
  'go to locations' deep-link.

Also drop the redundant inline Check button from the Security empty state
(same rationale as Improvements: the host auto-scan repopulates it and the
header carries a manual Check).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-06-17 18:40:40 +01:00
librelad
82325bce43 style(webui): wrap /admin/system body in the recessed admin panel
Apply the same recessed-panel treatment as the Dashboard/Tasks to the
System page: gauges, Trends, Storage, Host and Per-app now sit inside one
dark rounded box under the header divider. Generalise the Dashboard's
.admin-card-grid-wrap into a shared .admin-panel class so both admin pages
use one source of truth.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-06-17 18:36:25 +01:00
librelad
0641a9b790 style(webui): wrap admin dashboard cards + task list in recessed panel
Match the fleet Overview's .ov-tab-body treatment: the /admin/dashboard
card grid (under the header divider) and the /tasks list now sit inside a
recessed dark rounded box instead of floating directly on the page
gradient.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-06-17 18:23:49 +01:00
librelad
830d361351 fix(overview): drop redundant Check button from empty Improvements state
The Improvements tab's empty state ('No hotfix data yet …') rendered an
inline 'Check now' button. It was redundant: the embedding Overview header
already carries a manual Check, and the host-side auto-scan repopulates the
signed improvements index on its own within a couple of minutes (the empty
message already says so). Remove the button so the empty state is just the
self-explanatory, automation-backed message.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-06-17 17:57:26 +01:00
librelad
168924757e fix(tasks): reap orphaned running tasks immediately at processor startup
Holding the singleton flock at startup proves no other processor is alive
to heartbeat or complete anything, so every task still marked running is
a corpse from a killed predecessor. Recover them all before the first
dispatch (recoverOrphans now takes an 'all' mode) instead of waiting out
the 60s heartbeat-staleness window — which used to leave a dead task
showing 'running' alongside the genuinely-running next task for a minute
whenever the service was restarted mid-task (e.g. by the deploy chain
during initial setup). The idle-loop pass keeps the stale-only gate.

refactor(dashboard): slim the storage card back to chart + percentage

The disk card was only ever meant to be the donut and the % figure; drop
the Apps/Docker/Other/Free legend rows and signal the deeper view with a
corner expand glyph instead (the System page's chart-expand icon) — the
card already opens /admin/system/storage on click.

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-06-12 23:39:28 +01:00
librelad
a28eed0729 fix(services): route per-service restart through the task system + CLI
The Services tab restart button POSTed to a backend endpoint that (a)
checked the app's compose path from INSIDE the webui container, where
the host's containers root isn't mounted — so every restart failed with
'Compose file not found' — and (b) queued a raw 'docker compose restart'
that the host task processor would run as the manager user, which can't
talk to the rootless daemon anyway. Errors surfaced via a bare alert().

Per-service restart now follows the exact shape of the whole-app verbs:

- CLI: 'libreportal app restart <app> [service]' — the optional service
  arg makes dockerRestartApp restart just that compose service, via
  dockerCommandRun (right user in rootless mode) from the app dir on the
  host, where the compose file actually lives. Service names validated
  against compose-legal characters before touching a shell line.
- WebUI: the button dispatches a 'service_restart' task action through
  the task router (mutations-via-tasks), runs in the background with the
  standard task toast + link — no page switch — and failures use the
  notification system instead of alert(). Because the task runs host-
  side, restarting the WebUI's own libreportal-service now works too.
- Backend: the mutating restart endpoint and its now-unused helpers are
  removed; service-routes.js is read-only surface (status + log tails).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-06-12 23:26:40 +01:00
librelad
7db2a707b2 refactor(overview): turn the fleet Overview tab into an action board
Replace the stat-tile grid with a needs-attention board: a health hero on
top (one big status circle — green all-clear / amber something-to-do /
neutral pre-scan — matching the admin dashboard's dot language) over one
status row per area (updates, security, improvements, backups). Rows that
want a decision are tinted with their area hue and carry their action
buttons inline (Review / Update all / Open Backups); healthy areas
collapse to a quiet neutral one-liner. An Everything / Needs action chip
pair filters the board down to just the actionable rows.

Board rows deep-link with intent: Security lands on the Updates tab
pre-filtered to the affected apps via a new data-filter hop on the goto
action. backupSummary() now splits never-backed-up from week-stale apps
so the backup row can say which it is.

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-06-12 22:33:23 +01:00
librelad
fa47e16cab feat(updater): automatic background scan for versions, CVEs & improvements
Replace the click-to-scan-only flow with a self-throttled auto-scan that
rides the existing task-processor idle poll (the same shape as the
network-drift check — no new daemon, unit, or endpoint):

- 'libreportal updater check auto' gates on the age of the generated
  updates.json vs CFG_UPDATER_SCAN_INTERVAL (minutes, default 30,
  0 disables); a fresh file makes the 60s tick a single stat() + return.
  Manual checks and post-update rescans reset the clock for free, and a
  missing file means the first scan runs ~a minute after install.
- Eligible signed hotfixes keep flowing through artifactApplyAuto, which
  only enqueues ordinary tasks — mutations stay on the task path.
- Open updater surfaces (standalone /updater and the fleet Overview's
  headless UpdaterPage) follow along with a 60s static-JSON re-read that
  repaints only when a generated_at stamp changed; timer released via
  dispose() on unmount, ticks skipped while hidden.
- Empty states now say the first scan happens automatically; Check now
  stays as the immediate manual override.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-06-12 22:07:42 +01:00
librelad
c05e4af6f0 refactor(webui): move update status from dashboard banner to a topbar pill
The dashboard carried a persistent, full-width "LibrePortal vX.Y.Z" /
up-to-date strip — passive info occupying prime real estate above the
user's actual content, and out of place next to genuinely actionable
cards. Version/update state is chrome, so it now lives in one persistent
pill in the global topbar (every page), with detail + actions behind the
existing modal.

The pill is calm by default and escalates only when warranted:
  * up to date     -> subtle "✓ Up to date"
  * local/dev      -> neutral version chip ("v0.2.0")
  * checking        -> spinning "Checking…"
  * update waiting  -> accented, pulsing-dot "⟳ Update"
Clicking opens the details modal (unchanged) for the full readout and the
Update now / Check for updates actions.

The dashboard update banner is removed. The network-notifier's banner
stays — it's attention-only (shown solely on a real, actionable network
conflict), which is exactly when a dashboard banner earns its place; its
topbar badge now anchors after the pill, and the dashboard data-loader
re-asserts that banner on mount.

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-06-12 19:47:41 +01:00
librelad
3afe40bbbc refactor(overview): de-chrome the embedded Backups + Peers sub-tabs
The Backups tab's embedded BackupPage repeated the active section as a
big page header (icon + 'Dashboard' + subtitle) right under the nested
strip that already names it. Embedded-scoped CSS now hides the title
block and flips the header below the body (flex order), so its actions
(Refresh + per-section primary) become a bottom-left footer row — the
same place app-detail tabs keep theirs. The export dropdown flips to
open upward from the footer. The standalone /backup page is untouched.

The Migrate ▸ Peers sub-tab drops its page header (breadcrumb + title
+ blurb) the same way: the peer list/empty state now sit in the shared
recessed .ov-tab-body container with the four actions in a bottom-left
.peers-actions footer.

Signed-off-by: librelad <librelad@digitalangels.vip>

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 19:41:35 +01:00
librelad
a06b6cd1d8 feat(overview): match fleet tab content to the app-detail tab layout
Every fleet Overview tab now follows the per-app detail tab idiom the
rest of the app uses: title + description on the left, action buttons
on the right, a divider underneath, and the body inside the recessed
dark container (.tasks-container recipe).

- renderHeader() gains an action slot; Check/Check now/Update all move
  out of in-body toolbars into the header (Updates keeps its filter
  chips in the body; the Apps-tracked stat card drops its duplicate
  Check button; UpdaterPage.renderImprovements can skip its toolbar).
- String tabs wrap their body in .ov-tab-body — margin/padding 16px,
  rgba(bg,.2) panel — mirroring backup/tasks/updater containers.
- The Backups tab's embedded nested strip (Dashboard/Backups/Locations/
  Configuration) now sits on the same surface as every other tab strip:
  added to the nebula sidebar-bg anchor rule (it was stuck on the
  lighter --hover-bg) and its buttons use .main-tab-button type.

Signed-off-by: librelad <librelad@digitalangels.vip>

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 18:55:10 +01:00
librelad
9582671072 feat(tasks): path-based single-task permalink (/tasks/<cat>/<id>)
The global tasks page still deep-linked a single task with a ?task=<id>
query while the rest of the SPA moved to path-based permalinks
(/app/<name>, /admin/<…>). Bring it in line: the task is now a path
segment, /tasks/<category>/<id>.

Task ids are guaranteed `task_<digits>_<base36>` (isValidTaskId), so the
redundant `task_` prefix is dropped in the URL and restored on read via a
new window.taskPath / window.taskPartsFromPath helper pair (mirrors
appPath/appPartsFromPath). The parser still accepts the legacy ?task=
query and the full-prefixed id, so old links, bookmarks and notifications
keep resolving.

Updated every builder (tasks-manager updateURL + notification url,
task-id link, task-actions, admin config-form, setup-wizard handoff with
its &from=setup flag) and the notification navigation handler / button
text to recognise the path form.

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-06-12 18:31:18 +01:00
librelad
9dace1ed95 feat(tasks): auto-select the running task on the tasks page
Landing on /tasks (directly, via a deep link, or from the setup-wizard
handoff) now opens the row the visit is actually about:

- init() re-reads the URL on every SPA (re)mount, so ?task= deep links
  work after the first visit instead of using constructor-stale state.
- applyInitialSelection() opens the deep-linked task, or — for setup
  handoffs whose first task the queue has already moved past, and for
  plain visits with no deep link — the currently running task (else the
  next queued one).
- The selection then follows the queue: when a new task starts running
  the open panel moves with it, until the user manually toggles a row
  or switches category (their choice then wins for the visit).
- selectTask() is the shared programmatic open: exclusive expand, live
  log stream for active tasks, smooth scroll into view.

Signed-off-by: librelad <librelad@digitalangels.vip>

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 18:27:06 +01:00
librelad
2188a99787 fix(apps): make the instance family bar a full-width row
The app-detail header is a flex row, so the switcher rendered inline to the
right of the service buttons. Wrap the detail header and make the bar span
100% so it sits under the title/service buttons, above the tab strip, as
intended.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-06-05 00:21:02 +01:00
librelad
ab1d335d35 refactor(apps): manage instances on the type's page, not the grid
Replaces the per-instance grid cards + per-card "New instance" action with a
single card per app type and an in-page family switcher — the UX you asked
for (swap between instances via LibrePortal navigation; manage on the page).

- Grid: hide any app declaring INSTANCE_OF (loadApps filter), so there's one
  card per type. A subtle "N instances" chip replaces the old card button.
- App detail: a "family switcher" bar under the title for multi-instance
  types and their instances — a pill per member (base + each instance) that
  path-navigates to that slug's detail (current tab kept), plus "+ Add"
  (existing create modal). When viewing an instance, a "Remove instance"
  action sits in the bar.
- Remove flow: instance_remove task verb (-> existing `libreportal instance
  remove`) with an on-brand confirm modal; lands back on the base type page.

Frontend-only; the instance create/remove backend and CLI are unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-06-05 00:17:30 +01:00
librelad
376610cd11 feat(apps): scoped multi-instance support (run two of an app)
Lets a *multi-instance-capable* app run as several fully isolated instances
on one box (e.g. two Bookstack/WordPress sites, or a "family" + "work"
Nextcloud) — distinct data, DB, subdomain, backups and update cadence.

Design: an instance is just another app. It gets its own slug (<type>_<id>),
its own CFG_<SLUG>_* namespace, deployed dir, DB row, IP/port allocation and
host, so the entire existing pipeline (scan, install, services, routing,
updater, backups) treats it like any app with zero changes. All
instance-specific rewriting is confined to a clone of the type's template;
the shipped template and the core engine are untouched.

Gating: opt-in per app via CFG_<TYPE>_MULTI_INSTANCE=true. Only Bookstack
carries it for now (the validated reference). The other 31 apps are
unaffected — the feature is invisible unless the flag is present.

- scripts/instance/instance_create.sh — clone + re-namespace config, rewrite
  compose identity (container_name / Traefik routers / backup labels) and
  per-app tools, set a hostname-safe subdomain (PORT field 10), then hand off
  to dockerInstallApp. Plus instanceList / instanceRemove.
- libreportal instance create|remove|list — new CLI category; mutations route
  through the task system (no new mutating API endpoint).
- WebUI: "instance of <type>" badge + a "New instance" card action on capable
  apps, and a create modal (name + domain# + subdomain, live host preview)
  that dispatches the standard task. Capability/instance-of read straight off
  the already-exposed app config.

Known follow-ups (documented): flip the flag on more apps after a compose
identity check (Nextcloud next); per-app tools are best-effort isolated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-06-04 23:34:52 +01:00
librelad
8006ddba75 fix(webui): give the app-detail Updates tab the standard tab chrome
The per-app Updates tab rendered a bare version/badge bar + detail with no
title, no dividers and no recessed container — unlike the Config / Backups /
Tasks tabs.

- Add a .updater-title block (⬆️ Updates + description, Check/Update actions,
  bottom-border divider) mirroring .backup-title.
- Wrap the body in a .updater-detail-container recessed dark panel (same recipe
  as .backup-snapshots-container / .tasks-container).
- Separate the Version/Security/Recovery/History sections with divider lines
  (scoped to the container; fleet row-details keep their gap-only spacing).
- renderAppDetail() gains an opt-in Version section so the version/badge reads
  as a section in the panel; fleet rows omit it (the row head shows it already).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 00:56:01 +01:00
librelad
25dc51d63e fix(webui): make Overview sub-tab areas match the app-detail layout
Migrate and Backups detached their sub-tab strip from the content (16px gap)
and Backups had no top-level header and a transparent (card-less) body — so
both read as different from the per-app Config sub-tabs.

- Migrate: nest .tabs-content inside the same .tabs-wrapper as .tabs-list (the
  canonical app-detail structure) and drop the gap, so the strip joins the
  content card with no space and clean corners.
- Backups: inject the shared "Backups" .config-title header above the embedded
  BackupPage; drop the sub-tab strip's bottom gap; give the content (.main) the
  connected .tabs-content surface (card bg + rounded bottom) and round the
  strip's outer top corners — so it reads as one tabs-within-tabs unit.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 00:23:10 +01:00
librelad
4d54d6a9b0 feat(webui): unify Overview tab headers as in-content, app-detail style
Drop the floating fleet header; every Overview tab now renders its heading
inside the pane, under the tab strip, in the per-app detail .config-title
format (emoji + title + description) — matching the Migrate tab.

Introduces a small modular system so the area can't drift:
- OV_TAB_META is the single source of truth for each tab's icon/title/blurb.
- renderHeader(id) is the only thing that turns it into markup; renderTab()
  prepends it for the string tabs and mountMigrate() injects it once for the
  static Migrate pane. Body renderers now only ever produce the body.

Retires the now-dead floating-header plumbing: updateHeader(), the
ov-backups-active/ov-migrate-active hide toggles + CSS, and the .overview-header
rules. The tabbed interface owns the top padding the header used to provide.

Backups is the documented exception — it embeds the full BackupPage, which
supplies its own header.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 23:29:16 +01:00
librelad
f6f29bf68b feat(webui): match Migrate tab to app-detail tab design
Give the fleet Overview › Migrate tab the same in-content header + sub-tab
styling the per-app detail tabs use:

- Add a .config-title header (icon + title + description) inside the Migrate
  pane, above the Restore/Peers sub-tabs, and hide the generic floating fleet
  header for Migrate (mirrors how Backups already supplies its own heading).
- Make the .tab-button icon↔label gap explicit (flex + 6px gap) so sub-tabs
  render identically whether or not the markup has whitespace between the
  emoji and name spans; align the Backups sub-tab strip gap to match.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 23:04:44 +01:00
librelad
9986a8a814 fix(webui): stop rapid clicks selecting text on setup level cards
The Beginner/Advanced experience cards are tap targets, but had no
user-select guard. Rapid clicking — notably the Advanced card's 10-tap
dev-mode unlock — selected the card title/description text as a side
effect. Add user-select: none to .setup-level-card.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-06-02 16:21:39 +01:00
librelad
f1e1330cd8 feat(webui): network-drift notifier (topbar badge + dashboard banner)
Surfaces network_status.json in the WebUI, attention-only: a rose badge
+ dashboard banner appear ONLY when conflicts_found, with a details panel
listing the stranded apps and a 'Heal now' button that runs the heal
through the task pipeline (libreportal system network heal). Re-reads on
task completion via taskRefresh.

Cloned from update-notifier; the badge anchors just after the update
badge so the two coexist in a stable order. New --page-network hue.
Wired into system-loader scripts[], topbar onTopbarReady, and index.html.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 16:07:54 +01:00
librelad
164f782a95 fix(webui): address Migrate-refactor review findings
- Restore empty-state 'Open Locations' now deep-links to the backup center's
  Locations sub-tab: the embedded center honors /overview/backups/<sub> and
  switchTab()s to it after mount (was landing on Dashboard).
- PeersPage.notify + MigratePage.notify use the real window.notificationSystem
  (were calling a never-defined window.showNotification → console-only).
- Remove the now-dead Admin config-manager peers branch (Peers left Admin).
- Trim the dead migrate.json/peers fetch + hostnameToPeerName from BackupPage
  (no consumer after the migrate removal).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 11:12:11 +01:00
librelad
69bb5532b7 refactor(webui): move Peers out of Admin; harmonize Backups sub-tab strip
- Remove the Admin sidebar Peers entry; /peers and /admin/tools/peers now
  redirect to /overview/migrate/peers (its new home next to cross-host Restore).
- Re-skin the embedded Backups center's sub-tab strip from pills to the per-app
  Config .tabs-list/.tab-button segmented look (full-width bar, accent underline)
  so every nested sub-tab row is consistent.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 10:46:30 +01:00
librelad
25e25230fd fix(webui): drop deleted backup-migrate.js from the embedded center asset list
The embedded backup center's lazy bundle still listed backup-migrate.js, which
was just removed — its 404 failed the whole load chain. Drop it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 10:39:36 +01:00
librelad
c449641b9c refactor(webui): remove Migrate from the backup center (moved to Overview)
Migrate now lives at Overview › Migrate › Restore (standalone MigratePage). Strip
it out of BackupPage: drop the migrate sidebar item/panel/modal from the
fragment, the 'migrate' tab from the allowed set / titleFor / subtitleFor /
iconFor, the renderMigrate() call, and the migrate-host/app/confirm click
handlers; delete the now-orphaned backup-migrate.js. The backup center is now
Dashboard/Backups/Locations/Configuration.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 10:37:46 +01:00
librelad
ebdae15838 feat(webui): deep-link the Migrate sub-tabs (/overview/migrate/{restore,peers})
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 10:28:58 +01:00
librelad
4a964c42a2 feat(webui): add Migrate fleet tab (Restore + Peers sub-tabs)
New 5th Overview tab 'Migrate' with a nested segmented sub-tab row reusing the
per-app Config-tab .tabs-list/.tab-button design:
- Restore: a standalone MigratePage (cross-host migrate moved out of BackupPage
  into its own controller + fragment + modal; own data fetch + task dispatch).
- Peers: reuses the existing PeersPage (container-parameterized) + its template.
Both lazy-loaded on first open and disposed on apps-feature unmount. Additive —
migrate is still in the backup center and Peers still in Admin until the next
commits remove the duplicates.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 10:24:54 +01:00
librelad
efdbed8e0c fix(webui): embedded backup-center review fixes
- BackupPage task-refresh run-guard is instance-relative now (window.backupPage
  OR window.overviewBackupPage), so the embedded center's own auto-refresh works
  instead of relying on OverviewManager's overlapping coverage.
- _ensureBackupAssets no longer memoizes a rejected promise — a transient
  script-load failure no longer bricks the backup center until reload; the next
  open retries.
- spaClean.loadScript removes the failed <script> element on error so the
  getElementById dedupe can't make a retry resolve without re-fetching.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 01:55:09 +01:00
librelad
f1f0cf7516 fix(webui): scope embedded backup-center mount check to the pane
The 'already mounted?' guard checked #backup-section, but the per-app Backups
tab also defines that id — detect the prior mount via the pane's own
.backup-layout instead so the check is correct, not coincidental.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 01:40:29 +01:00
librelad
c508a20605 feat(webui): embed the full backup center in the Overview Backups tab
Instead of a glance + 'Open backup center' button, the Backups tab now mounts
the real BackupPage (dashboard/snapshots/locations/migrate/configuration) inline,
with its sidebar restyled as a nested tab strip and its own header taking over.

- BackupPage gains an embedded mode (opts.embedded): no /backup URL coupling, so
  sub-tabs switch in-page under /overview/backups. Backward compatible.
- OverviewManager lazy-loads the backup bundle + fragment on first open, news a
  BackupPage({embedded:true}), and disposes it on apps-feature unmount. Colliding
  ids (#sidebar/#mobile-overlay) are stripped on inject.
- Revert the Admin backup-config surface — the embedded center (incl.
  Configuration) is now the single home for backup settings.
- The updater needs no equivalent: its sections were already unpacked into the
  Overview/Updates/Improvements tabs + the per-row expander.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 01:36:25 +01:00