The "Refreshing backup data..." step on every WebUI update fired one
`restic stats` (restore-size mode — the slowest restic op) plus ~4
identical unfiltered `restic snapshots --json` pulls per enabled
location (dashboard, snapshots, app-status, migrate each pulled their
own), all over a fresh SSH connection for remote repos, on every pass
with no throttle — the slow, "frozen"-looking line users hit on poor
links.
Two fixes:
1. Dedupe. engineSnapshotsJson transparently memoises the first
unfiltered whole-repo pull per location to a shared cache dir
(LP_SNAP_CACHE_DIR, set by webui_updater around the chain), so the
four generators reuse one restic call instead of four. Filtered and
failed/empty pulls always fall through to a live call.
2. Throttle. Backups and location changes already regenerate this data
live when they happen, so the routine pass is only a drift catch-up.
Split the cheap local-only generators (engines/schema/passwords —
no remote I/O) out to always run, and gate the remote pull behind
CFG_BACKUP_DASHBOARD_REFRESH_INTERVAL (minutes, default 30, 0 =
every update). A completed backup touches a dirty marker that forces
the next pass to pull; WEBUI_UPDATER_FORCE still forces a full
refresh.
Net: N locations x 5 remote restic calls every update -> 1 call per
location, only when something actually changed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
The prior .ov-row override tied .updater-row on specificity, and updater.css
loads later, so `display:grid` won and the detail stayed boxed. Use the
two-class selector .updater-row.ov-row to win regardless of load order.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The fleet Updates expander row inherited .updater-row's 3-column grid, so an
open row's detail body (CVEs, recovery, history) landed in a narrow right-hand
`auto` column with dead space to its left. Make .ov-row a block: the head stacks
on top and the detail body uses the full width.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
An app with many CVEs (e.g. Trivy's 28) rendered every row full-height, pushing
the page down. Extract one renderCveList() shared by the standalone Security tab
and the per-app expander: sort worst-severity-first, and once past ~6 rows cap
the height (260px) with an internal scroll + themed scrollbar and a bottom fade
hint. Add a count pill to the expander's "Security" heading. Per-app sections
stay stacked and independently collapsible as before.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The client scan resolved images by pulling from Docker Hub, which fails on an
offline/privacy box ("index.docker.io ... network is unreachable") — yet every
installed app's image is already present locally. Point Trivy at the docker
socket that's already bind-mounted into the container (--image-src docker, plus
DOCKER_HOST=unix:///run/user/<uid>/docker.sock for rootless, derived from the
install user — rooted's default path is found automatically). Scans local
images with zero network. Verified: aquasec/trivy:latest -> 28 CVEs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Standalone `docker exec trivy-service trivy image ...` opens the vuln-DB cache
the long-lived server already holds, failing with "cache may be in use by
another process: timeout" — so every scan silently returned [] (apps: 0 even on
vulnerable images). Point the exec'd client at the server (--server
http://localhost:4954, the container's fixed --listen port); the server owns the
DB, the client just submits the image. Verified against alpine:3.10.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Trivy runs as a server whose vulnerability DB downloads on first boot; until it
lands no scan can produce results. Previously the updater generator wrote an
empty-but-valid cves.json the moment the file was missing, so installing Trivy
painted a green "no known vulnerabilities" all-clear that was actually a lie —
the DB hadn't even downloaded, and the Updates/Security view gave no signal.
Add an honest scanner state the WebUI branches on:
- containers/trivy/scripts/trivy_scan.sh — trivyScannerState (absent |
db_updating | ready) via `trivy version -f json`, trivyDbUpdatedAt, and
trivyScanImageCves (per-image scan normalized to {id,severity,package,
installed,fixed_in,url}, deduped). All degrade safely on error.
- webui_updater_scan.sh — stamp cves.json with scanner.state; only run real
per-image scans once the DB is ready. Always rewritten so state tracks live.
- updater-page.js — Security tab shows a loading box while the DB updates, an
install nudge when absent, and the genuine 🎉 only when ready+empty; Overview
CVE card sub + hint reflect the state.
- overview-manager.js — fleet Security row surfaces the "building CVE database"
pending state instead of silently omitting.
- function_manifest.sh — regenerated for the new trivy_scan.sh functions.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
New containers/trivy app (aquasec/trivy in server mode, private API port,
docker socket + cache volume) in the security,recommended categories, plus
placement in the setup wizard recommended step, the server-side install
tier after crowdsec, and the CLI first-install prompt. The updater's CVE
scan (design doc P4) will gate on this app being installed; §5 of the
design doc updated to record the app-based decision.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
Fills the gap updates-and-distribution.md always deferred: how versions
are actually detected, pinned, and reverted, and how cves.json gets real
data. Core decisions: floating tag stays the channel while the live
compose pins the digest (making detection possible and rollback honest),
and CVE scanning runs as a LibrePortal-orchestrated ephemeral Trivy
container (no host binary, no always-on scanner app).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
With no apps tracked the sub line read '0 apps tracked · 0/0 backed up ·
last scan …' — pure noise. Show only the scan time in that case.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
After 'Generated apps-tools.json...' the next step reaches remote backup
locations (restic over SSH) with its output captured in $(...), so on a
slow connection the updater looked stalled with no message. Print an
in-progress notice before it, mirroring the per-app refresh hook loop.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
.backup-retention-preset-block carried a permanent border-bottom (+ padding),
but the Keep-* fields it separates the dropdown from are hidden for every
preset except Custom — so on Self-hosting/etc. it was an orphaned line with
empty space beneath it. Moved the divider + spacing onto an .is-custom state
that applyVisibility() toggles alongside the fields.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A background task like the updater check surfaces its result in the page it was
launched from (the Updates/Overview tab repaints itself), yet it still fired the
full "View Task" pair — "Check task started!" then "Check task completed!" —
on top of the launcher's own small "Checking apps for updates…" line. Three
notifications for something you have no reason to open the Tasks page for.
Add a small, generic classifier (LP_BACKGROUND_TASKS) keyed by task action/type
(with a command regex that also catches the backend `updater check auto` run).
For a classified task:
- executeTask() skips the standard "task started!" toast and, when the run is
hand-launched, records its id as pending.
- the taskCompleted handler skips the standard "task completed!" toast; if the
id was pending (user launched it) it shows one small plain result line
("Apps checked for updates & vulnerabilities."), otherwise stays silent so
the periodic auto-scan makes no noise at all.
Non-background tasks are unaffected — the standard task-notification style still
applies. Extend byAction to quiet another self-surfacing action.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
In the config grid (e.g. Backups > Configuration > Engine) toggle pills sat
~6px below and 2px taller than the text/select controls beside them, so they
read as misaligned. Two causes, both scoped to .config-fields:
- .field-group carried a vestigial margin-bottom:6px (the grid's own 16px gap
already spaces rows). It inflated the grid track, so the align-self:end
toggle landed 6px below the input controls. Zeroed inside the grid.
- The .checkbox-label pill's 24px switch + 10px vertical padding rendered 46px
vs the inputs' 44px. Trimmed to 9px padding so the pill is 44px.
Toggles now share the exact vertical band (top + height) as the inputs in
their row. Applies to every config-grid toggle, not just the backup engine.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
On /admin/config/general the Basic section's fields sit in the darker inset
".config-fields" box, but the Mail Enabled toggle and Installation Mode select
did not — their control sits in its own .mail-master-toggle / .git-master-toggle
wrapper, a sibling of .config-fields, which the boxing rule never covered. So a
gated section's enable row read as loose, unboxed content above the (hidden)
options.
Extend the boxed-panel rule to the master-toggle wrappers (mail / install-mode /
generic / backup-remote). Element-qualified to the wrapper <div> because
renderToggleSection stamps the same class on the inner .checkbox-label pill too,
which must not get its own nested box.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
When the official catalog is hidden, its "Re-enable" action rendered as a
flat, greyed-out ~27px pill pinned to the top-left of the row — shorter than
the 44px URL field and not obviously clickable (the generic .btn-secondary
loses its nebula-theme polish outside .config-actions, so it fell back to an
unstyled base button).
- The action column now align-self:stretch, so its control matches the URL
field's height and sits inline (also vertically centres the compact × in the
enabled state).
- The Re-enable button gets a dedicated .catalog-reenable-btn treatment: accent
fill + accent border + hover state + an eye icon, filling the row height so it
lines up with the input and clearly reads as a button.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Backups tab's Configuration sub-tab showed only the warning banner and
an orphaned divider -- the actual config form (retention presets, engine,
keep-policies, Save/Reset) never appeared.
Root cause: a duplicate id. The Fleet Overview, the app grid and per-app
detail all render the shared apps-unified-layout, which keeps a hidden
#config-tab > #config-section in the DOM. The Backups Configuration sub-tab
created its OWN #config-section, and configManager.renderConfig() resolved
the target with getElementById('config-section') -- which returns the FIRST
(hidden) node in document order. So the 21KB form painted into the invisible
per-app container while the backup tab's container stayed empty. Because the
/backup routes now redirect into /apps/overview/backups, this fired every
time -- the form had effectively never been visible.
Fix:
- renderConfig(category, target) now accepts an explicit container (element
or id) and falls back to #config-section; the error-retry button re-targets
via configSection.id.
- The Backups Configuration sub-tab uses a unique id (#backup-config-section)
and passes it to renderConfig; retention-preset and engine-details
selectors are scoped to it.
Also: the Refresh button now spins + locks while refreshAll()+render() run,
so the click has visible feedback instead of appearing to do nothing.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The global Backups tab rendered its snapshot list (and the "No backups
yet." empty state) as a bare flex column directly on the tab panel, so
the list section had no visual container -- unlike the Dashboard tab's
cards and the per-app Backups tab's recessed panel. Wrap it in a
.backup-snapshot-panel box (translucent fill + hairline border, matching
.backup-card) so the section reads as a bounded surface.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The embedded backup center deep-links *in* fine (mountBackupCenter parses the
sub-segment), but clicking a sub-tab never wrote the URL: BackupPage disables
pushTabToUrl() when embedded and nothing on the fleet side stepped in, so the
address bar stayed stale and the sub-tabs weren't shareable/deep-linkable —
unlike the sibling Migrate sub-tabs.
Give BackupPage an onTabChange hook fired on every switch; the OverviewManager
supplies it to replaceState /apps/overview/backups/<sub> (replaceState, matching
Migrate, so the on-entry deep-link re-sync can't stack a duplicate history
entry). Standalone /backup keeps using pushTabToUrl() unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
The badge centres on the input box (top:50%), but the single-line input renders
its text a hair low, so the pill read as slightly above the URL. +2px lands it
optically inline.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
Feedback: the removal warning was an overwhelming paragraph and made it look like
updates might break. Keep the official catalog removable, but present the effects
as a clear two-part list instead of prose:
✓ Still works: system updates & hotfixes, installed apps, re-enabling any time
✕ What changes: official apps hidden from the App Center "Available" list
Backed by a new optional messageHtml arg on the shared confirmation dialog (dev-
controlled HTML body) + styles for the effect groups. Required tickbox kept.
Signed-off-by: librelad <librelad@digitalangels.vip>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>