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>
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>
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>
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>
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>
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>
The config reconcile pass printed one 'Reconciled config: <name> (backup:
.<name>.bak)' line per changed file. Drop the per-file message entirely:
the intro notice and the two per-section '...completed.' confirmations are
enough, and the backup mention added noise. The hidden .<file>.bak sibling
is still written for safety — it's just no longer announced.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
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>
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>
Reconcile backups now land as .<file>.bak instead of <file>.bak, so they
no longer clutter the configs folder. The .bak suffix is kept, so every
existing walker/sourcing exclusion still applies.
Also exclude dotfiles and *.bak from findConfigFileForOption: it walked
the configs tree with no backup exclusion, so depending on directory
order a 'config update' could resolve a key to the backup file and write
the user's change there — silently lost.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
Move the WebUI-updater settings out of general_terminal into their own
advanced webui-category file (webui_logs precedent): new
configs/webui/webui_updater holds CFG_UPDATER_SCAN_INTERVAL and the
migrated CFG_HOTFIX_AUTO, listed in webui/.category.
The move only reaches existing installs if the config convergence
machinery works, and three pieces of it silently didn't:
- checkConfigFilesMissingFiles walked a stale hardcoded category list
('general features network' — features doesn't exist; webui/backup/
security never healed). Derive the categories from the template tree
instead, and heal .category metadata too: copy it when absent and
merge missing SUBCATEGORY_ORDER entries when present, so healed files
actually appear in the WebUI Config editor. core_categories removed.
- Option reconciliation never touched ANY nested config file: configs_dir
carries a trailing slash, so rel stripping missed ('configs//'), the
template lookup failed, and reconcileConfigFile early-returned for
every file. Strip the slash before matching.
- reconcileConfigFile's AUTO_DELETE=false branch read a never-populated
live_line array, losing the dropped keys it promised to keep. Populate
it alongside live_value.
Also exclude *.bak from config sourcing (reconciliation writes <file>.bak
next to live configs — now that it runs, sourcing backups would resurrect
deleted keys), and add 'libreportal config check' as a non-interactive
front door to the converge pass (was only reachable via install flows and
the interactive menu).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>