A fresh install creates an empty containers root but leaves the rootless
daemon's own container state untouched. Restarting the daemon then runs its
container-restore pass, which resurrects the previous install's containers —
and Docker materialises each missing bind-mount source first, creating an empty
DIRECTORY even where the mount is a file.
That is the trigger behind the <app>.config stub directories: at 19:38:59 the
daemon re-created every missing mount source for a container built 40 minutes
earlier, runc then failed with "not a directory: Are you trying to mount a
directory onto a file", and the abandoned stubs collided with the install's own
copies 18 seconds later.
Add dockerRemoveStrandedContainers, run right after the rootless daemon
restart: remove containers whose compose project directory no longer exists, so
the next restart has nothing to resurrect. Scoped to project directories under
the LibrePortal containers root, so unrelated containers on the host are never
touched, and gated on the daemon answering.
Signed-off-by: librelad <librelad@digitalangels.vip>
Docker materialises a missing bind-mount source as an empty directory when a
container starts. The WebUI compose mounts ./libreportal.config as a file, so a
container start before the config landed left a directory at that path — and it
was self-perpetuating:
- copyFolder's tar extract aborted the whole source copy with
"libreportal/libreportal.config: Cannot open: File exists" (exit 2)
- dockerConfigSetupToContainer guards on [ ! -f ], which a directory fails, so
copyFile dropped the real config INSIDE the stub
- the closing -e / -r sanity checks both pass on a directory
The installer then reported success while libreportal-service crash-looped on
EISDIR reading /app/libreportal.config, leaving the WebUI unreachable.
Add repairStubDirForFile: promote a same-named file out of the stub, drop the
directory, and report if the path still isn't a regular file. Call it before the
WebUI source copy and before the per-app config copy (covers every app, not just
the WebUI), and tighten the closing existence check from -e to -f so a stub can
never pass validation again.
Signed-off-by: librelad <librelad@digitalangels.vip>
The Security triage implied 'fix available · update the app to clear these'
even when the app was already on the newest image — where updating does
nothing and the patch only lands when the maintainer rebuilds. That read as a
false to-do. Reframe honestly:
- Status line keyed on update_available, not Trivy's fixed_in: up to date =>
'nothing to apply; clears when the maintainer ships a rebuilt image'; update
available => 'updating may pull in patched packages'.
- Groups relabeled to describe reality: 'Patch released upstream' (hint: lands
on rebuild / may be cleared by updating) and 'No patch yet'. Counts go
neutral so a big number doesn't read as either alarm or all-clear.
- Row 'no fix yet' -> 'no patch'; use getAppDisplayName so it's 'Trivy' not
'trivy'.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Consistency pass across the App Center Overview area: no tab has a top-level
manual "Check" button anymore — scans are automatic.
- Overview tab: remove the header "Check now"; the hero already reports last-scan
time, and the unscanned sub-copy no longer points at a button that's gone.
- Improvements tab: remove the header "Check"; lead its body with the same
auto-check line (right-side Check-now nudge) as the Updates tab.
- Remove the now-unused checkBtn helper.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A user rightly noted the Security section read as a wall of unrelated
dependency CVEs against an 'Up to date' app — no cue for what, if anything,
to do. Make it answer 'is this my problem, and will updating fix it?':
- Scanner (trivy_scan.sh): stop discarding Trivy's Class/Type/Status at the
jq flatten — bind them onto each vuln so the UI can tell an OS package from
the app's own bundled dependency, and a real fix from a won't-fix.
- Security section (updater-page.js): explain these are vulnerabilities in the
packages bundled in the image (not the app version), tally 'N with a fix ·
M no fix yet', then split the list into a 'Fix available' group (worst-first,
each row tagged OS/dependency and showing installed -> fixed) and a dimmed
'No fix yet' group. No fabricated 'this update fixes N' claim — fixed_in vs
the image tag isn't a reliable join, so we only state fix availability.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extends the per-app Updates treatment to the fleet App Center → Overview →
Updates list, and folds the manual check into the status line so it's the one
canonical (secondary) affordance rather than a top-level tab button:
- updater-page.js: renderAutoCheckLine() now ends with a right-aligned "↻ Check
now" button (data-updater-action="check" — both surfaces already wire it).
- overview-manager.js: drop the "Check"/"Check now" button from the Updates tab
header (keep "Update all", only when updates exist); lead renderUpdates() with
the auto-check line.
- overview.css: .updater-autocheck wraps on narrow widths; .updater-autocheck-btn
sits right (margin-left:auto), smaller.
- app-tabbed-manager.js: friendlier no-data copy ("You're all caught up — no
updates found for this app.").
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The per-app Updates tab pushed a manual "Check" button (header + empty state)
even though scans run automatically on CFG_UPDATER_SCAN_INTERVAL — so an app with
nothing to update read like an empty/actionable page. Replace the manual Check
with a calm status line inside the panel: "Checked automatically · last checked X
· next check ~Y", backup-schedule style. The genuine Apply/Roll back actions stay
(applying is still manual and safe). No auto-apply.
- webui_updater_scan.sh: stamp scan_interval_minutes alongside generated_at in
updates.json so the display needs no separate config fetch (0 = auto off).
- updater-page.js: renderAutoCheckLine() + fmtRelFuture().
- app-tabbed-manager.js: drop the header/empty-state Check buttons; render the
auto-check line; friendlier no-data copy.
- overview.css: style .updater-autocheck (green dot live / muted when off).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The expanded-row deep-link pushed /overview/<tab>?app=<app> — wrong prefix
and a ?query the SPA's path-based router drops on a cold load, so the row
never reopened from a shared URL. Switch to /apps/overview/updates/<app>,
matching the Migrate/Backups sub-tab path pattern, and parse the app from
that path segment in _honorAppDeepLink so the row expands on cold load too.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Security section in the per-app detail panel dropped the CVE list flush
against the panel edges (no side padding, no visible frame). Wrap it in an
inset dark rounded .updater-cve-box (side padding + border) so it reads as a
contained block matching the app rows, and make the scroll thumb more present
(wider, higher-contrast, padded track) so a long list clearly scrolls.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reduce .ov-row-head vertical padding (12px -> 5px) and shrink the app icon
tile (32px -> 24px) so each Updates row is roughly half as tall.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Redesign the Fleet Overview Updates list rows to match the Tasks page:
each app now shows its icon tile, a status pill ('✓ Up to date' /
'↑ Update available' / '• Unscanned'), and a dedicated 'Details' toggle
button (chevron) in the row actions instead of the bare leading chevron.
Resolve the app icon from the slug (/core/icons/apps/<slug>.svg, hidden on
error) and prefer window.getAppDisplayName for a prettier title.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The task-log "Loading logs..." state rendered as a bare left-aligned text
line (or a tiny ad-hoc 16px spinner overlay), which read as unfinished next
to the rest of the UI. Swap all three log-loading spots (initial placeholder,
toggle-open fetch, running-task stream placeholder) to the canonical
window.lpLoadingBox('Loading logs…'), and scope .lp-loading inside the log
terminal box to fill it and drop its own card chrome so the spinner sits dead
centre over the terminal surface instead of a box-in-a-box. Widen the stream
overlay-removal selector to also clear .lp-loading.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The per-app Updates tab's recessed panel used padding: 4px 16px, so the first
section (VERSION) sat 4px from the top edge — looking unpadded — while the last
section ended 20px from the bottom. Match the sibling .backup-snapshots-container
idiom (uniform 16px) and zero the first/last sections' outer padding so the
container's inset sets an even top/bottom margin.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
For a generic app (no traefik files, not libreportal) fixPermissionsBeforeStart
prints nothing, so the "Updating file permissions before starting" step showed
its header followed by an empty line — looking like it did nothing. Add an
isSuccessful line after the call so the step reports like every other one.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A fresh install ran webuiLibrePortalUpdate twice back-to-back: once in
installLibrePortal step 11, then again in startScan at the end of preinstall.
The 30s time-debounce meant to collapse them is fragile (it never fired on a
recent install — >30s elapsed between the two), and debouncing is the wrong
lever anyway: startScan's pass runs AFTER scanConfigsForRandomPassword
finalises app passwords, so it — not the step-11 pass — is authoritative.
Defer the step-11 generation deterministically during a bootstrap install
(libreportal_bootstrap_install=true), leaving startScan's single pass to do the
work. Standalone reinstalls (no bootstrap flag) still generate in step 11. The
time-debounce stays as a general back-to-back backstop.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The authoritative icon source containers/trivy/trivy.svg held a generic
hand-drawn shield/magnifier placeholder, while the frontend copy had the
official brand mark. The icon sync (webuiSyncAppIcons) treats the source as
canonical, so it was clobbering the official logo with the placeholder on
every full WebUI update/install — leaving the setup wizard's app tile
showing the generic icon. Replace the source with the official logo so it
sticks.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
P0 anchor, P1 config-first pin, P2 registry detection + display, and P3 honest
pinned apply/revert are all built and deployed. Records the detection nuance
(digest-compare fully covers rolling apps; versioned newer-version enumeration
is a P5 add) and the remaining deferred work.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
Make the Update / Roll-back buttons tell the truth, closing the "new code on
old data" hole a floating tag creates.
updaterApplyApp:
- Anchor-correct: capture before/after from updaterPrimaryImage (the
<slug>-service image), not `grep -m1 image:` — fixes ollama et al.
- Records EXACT build refs in history from->to: repo:tag@sha256:<digest>
(via updaterRefDigest), so history is meaningful even when the tag doesn't
move (a rebuilt `latest`).
- Un-pins any digest a prior rollback pinned before pulling, so Update tracks
the channel again instead of freezing on the rolled-back build.
updaterRollbackApp:
- Before recreating, re-pins the anchor image to the pre-update build's digest
(from history's last update/ok `from`) via updaterSetAnchorRef, so `up` runs
the OLD code — not the current channel head. This is the fix for restoring a
data snapshot but recreating on a newer image.
New helpers (cli_updater_commands.sh): updaterRefDigest (local RepoDigest),
updaterSetAnchorRef (rewrite the anchor image line by service name, preserving
indent + the version sentinel; correct for companion-first apps like ollama),
updaterLastUpdateFrom (roll-back target from history).
Verified the helpers on nextcloud + ollama: pin adds @sha256 to the right
anchor only, sidecars untouched, sentinel preserved, unpin restores, YAML valid.
Caught and fixed a `local a=$1 b=...$a...` same-statement expansion bug that
would have silently no-op'd the rollback pin. End-to-end apply/revert not
exercised live here (no installed app has a pending update on this box).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
Trivy had no /core/icons/apps/trivy.svg (the canonical app-icon path used by
task rows, tiles, tools, services, routing), so it fell back to the default.
Add the official Trivy mark, white on its #1904DA brand tile so it stays legible
on the dark, near-transparent icon tiles across every theme.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
The per-app object build used `available_version:($available_version|select(.!=""))`
to omit the field when empty. But in jq, a `{key: (empty)}` makes the WHOLE
object construction emit nothing — so every app with no available update (empty
available_version) produced no object and was silently dropped from updates.json.
Only apps WITH a pending update survived; an all-up-to-date fleet showed an empty
list. (Missed in P2's sandbox test because both fixture apps had updates.)
Emit an explicit null instead: `(if $available_version=="" then null else … end)`.
Verified: an up-to-date app (trivy, local==registry digest) now emits with
available_version:null; apps with updates still carry the string.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
The type glyph (🩺/🔄/✅…) sat bare next to the framed .task-app-icon tile. Give
it the same 32px rounded tile (border + surface-elevated bg) so the two read as
a matched pair; spacing now comes from .task-info's gap like the app icon.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
webuiUpdaterScan now tells the truth instead of hardcoding update_available=false.
Per app it resolves three facts (see roadmap §2): the running RepoDigest (one
`docker inspect` of the anchor image), the display version (OCI label →
versioned tag → channel·shortdigest), and — throttled — the registry's current
index digest for the channel tag (`docker buildx imagetools inspect`, the same
identity as RepoDigest, verified exact). update_available = the two digests
differ. Emits type (versioned|rolling), channel, current/available digests +
versions, and a services[] array (every image line, anchor flagged).
- Registry lookups throttled separately from the scan: CFG_UPDATER_REGISTRY_
INTERVAL (min, default 360; 0 = local-only), own /tmp stamp, reuse of the
prior available_digest between windows so the app list still refreshes every
scan. UPDATER_REGISTRY_FORCE=1 forces a live pull (the Check-now button).
Registry failure (offline/rate-limited) = "unknown", never a false "changed".
- Digest-compare fully detects rolling apps' new builds; for versioned apps it
catches rebuilds of the pinned tag (newer-version enumeration is a later
step) — honest per type, and versioned apps are user-picked via P1b anyway.
- Fixes a P1b regression: updaterPrimaryImage now strips the trailing version
sentinel comment (`s/ #.*//`) via _updaterCleanImageRef — without this the
anchor ref (and the live CVE scanner's image arg) carried the comment.
- JSON built with jq for safe escaping; jq-less fallback keeps output valid.
Verified via a simulated 2-app install: navidrome → "0.62.0" (OCI label) +
detected update; rolling app → channel·shortdigest + update; throttled re-run
reuses the prior digest; all output valid JSON.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
Previously retry silently created a new task, left the failed one in place with
no link, and didn't take you to the retry. Now:
- The new task is linked to the failed one via `retryOf`; the server stamps the
original `retriedBy` and KEEPS it (its log is the failure record — never
deleted), so history survives and there's no confusing bare duplicate.
- The failed row shows a muted "↻ Retried" pill, hides its now-stale Retry
button, and offers "View retry" to jump to the new run. The new run shows a
"Retry of:" backlink to the original.
- After retrying, the UI selects the new task and opens its live log so you
follow the retry instead of hunting for it.
retryOf rides through the existing POST /api/tasks (no new endpoint); createTask
gained an optional extra-body arg.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
Add a `#LIBREPORTAL|<APP>_VERSION_TAG|<current-tag>` sentinel to each app's
anchor (`<slug>-service`) image line, so a `CFG_<APP>_VERSION` config drives
that tag through the existing tagging system — the config becomes the source,
the compose tag is derived (like ports/IPs/domains already are). This is the
"install/pin a specific version" knob.
- Placeholder is the literal current tag (e.g. `31-fpm-alpine`), NOT a `*_DATA`
token: an unset var leaves the line untouched (inert comment, keeps the real
tag) and never trips the up_app stale-tag scanner.
- Only the anchor line is tagged (sidecars mariadb/redis/nginx stay
tracked-by-digest, not user-version-picked). ollama correctly targets
`ollama/ollama`, not the companion open-webui.
- Untagged anchors normalized to `:latest` (semantic no-op) so they're
templatable too. The manager (libreportal) is skipped — it updates via its
release channel, not a docker tag.
32 apps wired. Verified: CFG_NEXTCLOUD_VERSION=32 → `nextcloud:32` (prefix
preserved, idempotent, sidecars untouched); all compose files still valid YAML.
Inert until a version config is set, so no behaviour change on existing installs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
"Control plane" is jargon. Rename the self-heal task and de-jargon all the
user-facing copy: task titles ("LibrePortal - Fix System Issues" / "System
Health Check"), the action label, the badge/banner/panel text and "Fix now"
button, the status summaries, and the heal task-log messages. Behaviour
unchanged; code comments keep the technical term where accurate.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>