The digest compare only ever asks about the tag already pinned, so it
answers "has my tag been rebuilt?" and can never answer "does a newer
version exist?". An app on v0.16 reports up to date forever while 0.17
ships. That is the gap between an app that updates and an app that is
current, and it silently affects every pinned app.
Adds tag enumeration for VERSIONED tags only (rolling tags already move
on their own): list the repo's tags, keep those sharing the current tag's
SHAPE, and pick the numerically greatest.
Shape matching is the whole safety story — v0.16 -> v#.# so it can never
"upgrade" you onto v0.16-alpine, 31-fpm-alpine onto 31-apache, or a date
tag onto a semver one. Comparison is component-wise numeric, so 0.10 > 0.9
and 1.0 > 0.99 (a string sort gets both wrong), with 10# forcing base ten
so an upstream "08" cannot be read as octal. 15 unit tests cover it.
Docker Hub only, deliberately: all three pinned apps live there, it needs
no auth, and the generic OCI tags/list wants a per-registry token dance.
Other registries stay quiet rather than guess. Throttled inside the
existing registry window and cached between windows so it cannot flicker.
Surfaced as INFORMATION, never an action: no button applies it, because a
version move can carry a data migration. `update_available` and the "up
to date" badge keep their exact meaning; the new state sits beside them
and points at the Version field.
Against the live registry: stalwart v0.16 is current, nextcloud is on
31-fpm-alpine with 34-fpm-alpine out, mastodon on v4.2.0 with v4.6.5 out.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every themed dropdown on the app config page was dead: it rendered, but
clicking did nothing. Only that page — every other dropdown in the WebUI
worked.
renderAppDetail captured the config section's own innerHTML right after
displayConfigForm() had rendered it...
const configHTML = document.getElementById('config-section')?.innerHTML;
...67 lines later...
configSection.innerHTML = configHTML;
...and wrote the same string straight back. That is a no-op for the
markup and a catastrophe for behaviour: re-assigning innerHTML re-parses
the subtree, so every listener in it is destroyed.
custom-select.js had already wrapped each <select>, so the captured
string contained the .custom-select wrapper and the custom-select-native
class. The re-inserted copy therefore looked enhanced — which made the
enhancer correctly skip it as already-done — while having no click
handler at all. A dropdown that renders perfectly and does nothing.
The container is never wholesale-rewritten in that function (it updates
header/config/console individually), so the capture-and-restore had no
purpose. Both lines removed, with a comment stating that anything added
there must mutate the section rather than re-assign its innerHTML.
Diagnosed in a real browser rather than by reading: instrumenting
CustomSelect.build() showed the enhancer DID build a widget for the field
while the wrapper in the DOM was not the one it built, and patching the
innerHTML setter named apps-manager.js:785 as the writer. Verified live:
the dropdown opens, and picking an option sets the value (auto) and the
label.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both found in a user's console log.
1. ReferenceError: setupMobileMenu is not defined (dashboard.js:98)
core/topbar/js/mobile-menu.js defines that global, and index.html
never loaded it. dashboard.js called it unguarded as the FIRST line
of setupEventListeners, so dashboard init threw every page load and
took loadInstalledApps() with it — and the burger menu was dead on
mobile. system-loader already guarded its own call with a typeof
check, which is why this survived unnoticed.
Loads the script (before dashboard.js) and guards the call, so
optional nav chrome can never take down the page below it again.
2. Endless 404s on /api/tasks/<id> for tasks that no longer exist
queue.json is append-only from the enqueue side and nothing ever
pruned it, so any task file removed afterwards left an id the WebUI
re-fetched forever, one 404 per poll per orphan. Adds
cleanupOrphanQueueEntries to the idle housekeeping pass: entries with
no task file are dropped and logged. Self-heals existing strays.
(Provoked by my own clean-up of two test tasks earlier in this
session, but the gap is real and predates it.)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
getFieldMappings/getConfigCategories fetched host-GENERATED files with
default caching, so a browser that had the page open before a release
kept rendering the previous release's config UI — a newly shipped field
(UPDATE_TYPE) simply never appeared, with nothing on screen to hint the
page was stale. Only a hard refresh fixed it.
Adds {cache:'no-store'} to both, plus the one configs.json read in this
file that was missing it while two others already had it. Cost is a
conditional request per config-page open.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Four fixes that make the auto-updater a trustworthy background system:
* CFG_UPDATER_WINDOW (default 06:00-08:00 host time, right after the
05:00 backup cron; HH:MM-HH:MM wraps midnight, 'always' = any time).
Gates only the enqueue — scans keep running all day, so the Updates
page stays current and pending updates visibly wait for the window.
Malformed values fail closed and are rejected by the WebUI validator.
* "Check now" actually checks: an explicit `updater check` sets
UPDATER_REGISTRY_FORCE=1. The flag existed but nothing ever set it,
so the button silently reused the 6h digest cache and could not find
a build the user knew had shipped. Force also overrides interval 0,
which now means "manual-only" as documented in the roadmap.
* Registry stamp moved from /tmp to <system>/logs: the task processor
runs under PrivateTmp, so daemon and CLI each kept a separate 6h
clock and the daemon's reset on every service restart.
* A failed automatic attempt is no longer invisible: the scan emits
auto_attempted_digest (the one-shot no-retry stamp), and when it
matches the available build the UI stops promising an install that
will never come — per-app detail explains, the fleet row gets an
"auto failed" chip, and the Overview board counts it as needing you.
Also corrects the CFG_TIMEZONE label: it sets the containers' TZ only;
scheduled tasks follow the host clock (timedatectl), and the old
"Timezone for scheduled tasks" wording promised a knob that never
existed. The window + auto_window display state plainly WHEN updates
land, answering "how does the user know when the next update happens".
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds the decision half of the app updater. Detection (P2) and the
snapshot-first apply/revert (P3) were already real, but nothing ever
pressed the button — every update waited for a click.
CFG_<APP>_UPDATE_TYPE=auto|manual per app, default auto (33 templates)
CFG_UPDATER_AUTO=true|false master switch, default true
updaterAppPolicy resolves the two the way backupResolveStrategy already
resolves backup strategy: the global switch can only make things more
manual. updaterApplyAuto runs at the end of `updater check` and enqueues
the ordinary updater_apply task for each auto app that has an update —
never applies inline, so an automatic update is the same code path, task
log, History entry and Roll back button as a manual one.
Safety: each attempt stamps its target digest under generated/auto/, so a
build that fails is rolled back and then left alone rather than retried on
every scan; in-flight updater tasks are skipped so scans can't stack.
Tracked end to end: updates.json carries each app's resolved update_type,
History entries carry trigger=manual|auto. The WebUI says whether updates
install themselves, chips only the apps that opted out, labels automatic
history, and — since an auto app's pending update needs no decision — keeps
it off the Overview board's "Needs action" view.
Also fixes artifactApplyAuto enqueueing without --detach: it runs inside
the single-threaded task processor's own poll, so following the new task in
the foreground waits for a task that cannot start until it returns.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The board landed on "Everything", so the rows that want a decision were
mixed in with the healthy one-liners on arrival. Default to the "Needs
action" chip instead, falling back to "Everything" when nothing is
pending (that view would otherwise be empty). An explicit chip click
still sticks for the session.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Tags and Paths were stacked full-width blocks, so a single tag chip and a
single path each burned a whole row and left the panel mostly empty. Wrap
them in a .bsm-blocks auto-fit grid that seats them side by side and falls
back to stacking under ~460px. The wrapper now owns the divider, so it
renders once for the pair instead of once per block, and is omitted
entirely when neither block has content.
Applied to both renderers of this markup: the global Backups view and the
per-app backup card.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
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>
"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>
The task-list row only paints the LibrePortal app-logo for commands matched by
isLibrePortalSystemTask's whitelist (appless tasks otherwise get just the type
icon, so arbitrary custom commands don't get a spurious logo). `libreportal
system health/network heal|check` weren't in it — only `system reclaim|image` —
so the heal rows showed a bare function icon instead of the logo like "Check for
Updates". Add health|network to the whitelist.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
Crash-loop detection keyed on `docker ps --filter status=restarting`, but a
backed-off loop sits "exited" between restarts once docker's backoff grows to
tens of seconds — so a slowed loop is missed. Detect via RestartCount CLIMBING
between scans (what a crash loop actually is), unioned with the instantaneous
restarting signal for fast loops. Baseline counts persist in
.health_restart_counts, written only by the throttled check so the heal's
re-scans don't disturb the delta.
Also give the system_health_heal / system_network_heal tasks proper display
(they fell through to the raw command + generic ⚙️): friendly titles in
formatCommandForUser, type icons (🩺 / 🌐) in getTaskTypeIcon, and action
labels in formatActionTitle — so they read as "LibrePortal - Repair Control
Plane" etc. with an icon, like install tasks.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
Follow-up to the backup-refresh throttle/dedupe, cutting the cost of the
remote pulls that do still happen.
* restic stats now runs in --mode raw-data (restic_check.sh). The default
restore-size mode walks every snapshot's tree to sum logical file sizes —
the slowest restic op — just to fill a size readout. raw-data reads the
index only and reports the repository's actual deduplicated on-disk size,
which is exactly what the dashboard already labels "deduplicated,
encrypted". raw-data omits total_file_count, so the per-location card now
shows that location's snapshot count (already loaded client-side, and more
useful for a backup repo) instead of a file count.
* engineLocationStats now shares the same per-refresh memoiser as
engineSnapshotsJson (engine_dispatch.sh). Both the locations and dashboard
generators call it per location, so repo stats went from two restic calls
per location per refresh to one. Factored the cache into _engineCachedPull.
* SSH connection reuse for SFTP locations (backup_ssh.sh): ControlMaster=auto
with a self-reaping ControlPersist master, so the several restic
subprocesses a refresh/backup spawns against one location share a single
authenticated connection instead of a fresh handshake each — the dominant
per-call cost on a high-latency link. Toggle via CFG_BACKUP_SSH_MULTIPLEX.
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>
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>
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>
.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>
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>
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>
formatSubcategoryName title-cased naively, so section headers read "Ssh",
"Dns", "Libreportal". Add a whole-word acronym/brand map so they render
professionally as "SSH", "DNS", "LibrePortal" (also covers VPN/API/IP/UI/URL/
HTTP(S)/TLS/SSL/WebUI for future files). Composes with stripCategoryPrefix,
which runs after and is case-insensitive.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
- Info banner: drop the borrowed traefik warning banner (icon sat above the
text, amber-warning styling) for a .catalog-note — icon inline with the text,
neutral boxed look matching the content sections.
- Official row (#1): render the release-host URL in a read-only input (same
element/height as the third-party rows) with the "✓ Official" badge overlaid,
instead of short text — so it shows the URL and its box height matches the
others. URL is read from the loaded config (CFG_RELEASE_BASE_URL).
Signed-off-by: librelad <librelad@digitalangels.vip>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Config sections (Basic, Mail, DNS, Firewall, …) rendered as a bare name. Add a
small data-driven icon before each: config files now carry a `# @icon <emoji>`
directive in their header, the config generator surfaces it as the subcategory's
`icon`, and the config editor prepends it to the section <h3> (covers every
render path — regular/master/toggle/domains/catalogs/whitelist — since they all
drop displaySubcategory straight into the heading).
- webui_generate_configs.sh: parse `# @icon` (first 10 lines) → emit "icon".
- config-manager.js: prepend subcategoryData.icon to the section title.
- configs/**: added an @icon to all 23 base config files (backup/general/network/
security/webui). The live install's config files get the same directive + a
configs.json regen so existing installs show the icons too.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
- Catalog block now shows the official catalog as a pinned row #1 (verified
badge + its URL from the generated data). Removing it opens the shared
confirmation dialog with a REQUIRED tickbox (generalized to accept custom
checkbox text) — it only hides official apps from browsing; system updates are
unaffected. Backed by a new CFG_CATALOG_OFFICIAL_ENABLED flag the scan honors.
- Trimmed the info-banner wording per feedback.
- Config option sections: the body (after the divider, before the buttons) now
sits in a darker boxed panel (bg + border), matching the content-box look used
elsewhere. Applies to regular grids + the block editors; scoped to the main
config editor.
Signed-off-by: librelad <librelad@digitalangels.vip>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds the Catalog Sources block to the config editor (General → App Catalogs),
mirroring the domains block UI: add/remove up to 9 catalog URL slots, saved by
the generic config-form (any CFG_* input) → configUpdateBatch, which rebuilds
registry_catalog.json. Official catalog stays source #1 (not editable here);
third-party rows carry an "unverified" note.
- new catalog-manager.js (lean CatalogManager; reuses the domains block CSS,
URL inputs, no DNS checks) + window.addCatalog/deleteCatalog globals.
- config-manager.js routes the general_catalogs subcategory to it.
- system-loader.js loads catalog-manager.js before config-manager.js.
Signed-off-by: librelad <librelad@digitalangels.vip>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Switching to the fleet Backups tab blanked the whole pane to a bare, headerless
lpLoadingBox() while the (script-heavy) backup center loaded, so it read as the
entire section reloading with no indication of where you'd landed — then the
header + content appeared at once when the fetch resolved.
Render the shared .ov-tab-header (💾 Backups + description + divider) up front and
drop the spinner into an .ov-tab-body beneath it, matching the per-app detail
tabs' header-first idiom (and the recent Admin board fix). The header stays put
and only the content area swaps in once BackupPage mounts.
The Migrate tab already does this — mountMigrate() injects its header and the
sub-tab strip is static, so its spinner is already confined to the Restore/Peers
sub-panel — so no change was needed there.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
The Admin Dashboard (OverviewPage) and Admin System (SystemPage) boards each
blanked the whole content pane to a bare headerless lpLoadingBox() on init(),
then rendered the .page-header + panel only once data arrived. So clicking
either tab flashed a full-pane spinner with no indication of which page you'd
landed on, then everything appeared at once.
Extract the page header into a _headerHtml() helper on each page and render it
up front in both the loading state and render(). Now navigating to a board
paints its header (breadcrumb + title + description) instantly and the spinner
is confined to the .admin-panel beneath it — the header stays put and only the
content swaps in when the fetch resolves.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
Replaces the extra on-card marketplace tags (the separate Official trust
badge) with a richer, more elegant flow: a registry card keeps a single
'Available' pill, and clicking Add (or the card) opens a modal outlining the
app — full description, publisher + trust, version, the add command, and a
'View full page on the marketplace ↗' link to the app's page on the source
it came from — with the actual add as the confirm. Gives a beat to review a
community app before pulling it in.
- webui_registry_scan.sh re-emits source{base,channel}; loadApps stashes it
(window.registryCatalogSource) + carries per-app version.
- openRegistryDetails() builds the eo-modal; addRegistryApp() is now just the
task dispatch (the modal's confirm / a fallback).
- The marketplace website supports a #<slug> deep-link (focus one app, with a
'Show all' bar), so the modal's link lands on that app's page.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
Reverts a4e65df. The maintainer prefers the more fluid model where registry
apps flow into the App Center grid as 'Available — Add' cards alongside
installed/local apps, rather than a separate /apps/marketplace destination.
Restores the Stage-4 merged-grid behaviour (loadApps merges
registry_catalog.json; createAppCard renders registry cards; addRegistryApp
dispatches app_add). The make_app.sh featured passthrough + the generator's
featured/source fields go with it (unused by the merged grid).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
Splits the catalog out of the main grid into its own destination, the
WordPress 'Add Plugins' vs 'Installed Plugins' model: the grid is now
purely 'your apps' (local definitions), and the new Marketplace section is
'get more apps' (the remote signed catalog).
- New MarketplacePage (components/apps/marketplace/) mounts at
/apps/marketplace inside the apps feature (same sub-dispatch pattern as
/apps/overview — no new top-level component). Pinned sidebar entry with a
live 'available to add' count badge.
- Status strip: signed/unsigned, available + catalog counts, serial, source
base+channel, freshness, and a Refresh that re-runs the host-side registry
scan via updater_check.
- Publisher-curated Featured shelf (meta.featured, set at publish time — no
tracking/popularity), category chips + search, per-app detail modal
(long description, publisher/trust/version, add command), and the chained
Add & set-up flow: dispatch app_add, and when the definition lands, hand
off to the app's config/install page.
- State-aware cards: Available (Add) / Added (Set up →) / Installed (Open).
- Backend: make_app.sh passes through meta.featured; webui_registry_scan.sh
emits featured + source{base,channel} in registry_catalog.json.
- Removed the grid's registry-merge + registry card path + its CSS (moved to
the namespaced marketplace surface); app_add task wiring + completion
handler retained and reused.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>