Matrix was uninstalled and the Updates tab kept listing it as up to
date. Not an instance problem — updates.json and cves.json are
scan-time snapshots on a 30-minute cadence, and nothing rewrote them at
uninstall, so any removed app haunted every updater surface until the
next scan happened to run. The backend was never wrong: the DB, the
apps data and the app's own page all said uninstalled within seconds.
Fixed at both ends. Uninstall now deletes the app's rows from both
generated files, surgically — a full rescan re-runs CVE checks against
every image and has no place inside an uninstall. And the updater's
merge drops any row whose app window.apps does not list as installed,
which covers every other way the snapshot can go stale (a crashed
uninstall, a hand-edited file, the next bug). The filter only applies
when the installed list has actually loaded, preserving the page's
degrade-gracefully contract when it has not.
The stale Matrix rows on this install were purged the same surgical
way; the tab now shows 14 rows with the merge still intact.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Replaces the follow-the-task navigation from the previous commit —
carrying the user to the tasks page and back was feedback by relocation.
This is feedback where the click happened: the button becomes
"Updating…" with a spinner and stays disabled until the task reaches a
terminal state, then the data refetches and the row repaints with the
result. Update, Update all / selected, the stepped Upgrade and Roll back
all get it (verbs keep their own labels: Upgrading…, Rolling back…).
Two paths keep the face honest. A DOM patch flips the buttons the moment
the click lands — no waiting for a render pass — stashing the original
face on the element so a failed dispatch can restore it. And every
renderer now builds these buttons through one busy-aware helper, so a
repaint landing MID-task (the auto-refresh poll, a filter change)
reconstructs the spinner instead of silently re-enabling the button.
Sibling actions are held while a task runs: Roll back on an app that is
mid-update is disabled — without a spinner, which marks the action that
is running, not the ones waiting on it. Entry points also guard on the
inflight set, so a keyboard-triggered duplicate is inert.
Correlation is by task id (routeAction resolves to the created task), so
another task finishing cannot end this button's busy state early. Only
completed/failed/cancelled end it; listeners are removed on the first
terminal event, with a 30-minute timeout so a task that never reports
terminal cannot pin a spinner forever. cursor is "progress", not
"not-allowed" — the work is happening, the button is not refusing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Pressing Update produced a toast and nothing else. The row did not
change, nothing on the page moved, and the work was real but invisible —
so the button read as though it had not done anything.
Update, Update all / Update selected, and the stepped Upgrade now carry
you to the task they started and return you to the Updates tab when it
reaches a terminal state.
Correlated by task ID, not by app name: routeAction resolves to the
created task, so a second update started elsewhere cannot bring this one
back early, and a completion for an unrelated app cannot either. Only
completed/failed/cancelled end the wait — taskUpdated also fires
mid-run, and treating that as done would bounce the user out of a task
still in progress.
Navigation is deliberately NOT locked, which is the one part of the
request I did not build. The task is a background job with its own
snapshot and rollback; it does not need watching, and a lock would
strand someone here if a task ever hung. Leaving is therefore treated as
a choice — the return only fires while the user is still on a tasks
page, so nobody is yanked back from somewhere they chose to be.
Both listeners are removed on the first terminal event, and a 30-minute
timeout removes them anyway, so a task that never reports a terminal
status cannot leak a pair of window listeners per click.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Four fixes to the Updates tab, all reported from the same screenshot.
Upgrade is green and just says "Upgrade". It was accent-styled and carried the
target version, which duplicated the confirmation it opens — the dialog already
names the version, so the button only had to name the action. Green also stops
it reading as the same control as the cyan Update beside it.
The picker moved from the head of the row to the tail, matching the Tasks list.
It used to lead with an `ov-pick-empty` spacer holding the column open, so with
nothing updatable every row carried a left gutter for a checkbox that was never
coming — the gap in the report. Unselectable rows now render no picker at all.
Select all joins the toolbar, right-aligned above that column, again mirroring
Tasks. It acts on the selectable rows in view, so a narrowing filter chip cannot
leave it quietly picking rows nobody can see.
The confirmation dialog takes an optional icon and the upgrade flow passes the
app's, so it leads with the same icon-in-holder the instance modals use. Styled
in base.css rather than borrowing .app-card-icon: this dialog is global and
appears on pages that never load the apps stylesheet. Its title now uses the
display name too — "Upgrade matrix to…" beside the Matrix logo read as a
different thing.
Verified against the live WebUI: green Upgrade with the gutter gone; Select all
and the trailing checkboxes with two apps temporarily flagged (data restored
byte-identical after); the master toggling 0 -> 2 -> 0; and the dialog showing
matrix.svg under the title "Upgrade Matrix to v1.159.0?".
Updating one app and updating everything were both already there — a
per-row Update button and an Update all in the tab header. What was
missing is the middle: choosing three of eight. Both existing controls
are gated on update_available, so with a fully current fleet the tab
shows neither and reads as though it cannot update anything at all,
which is what prompted this.
Checkboxes appear only on rows that actually have something to apply.
Offering one on a current app would be a choice with no outcome, and
"selected 6, updated 2" is a worse answer than not offering the four.
The column is still reserved on every row, hidden — otherwise the icons
shift left and right as scans land and rows change state.
applySelected filters the chosen names against update_available rather
than trusting them: a selection can outlive the scan that justified it,
and asking to update an app with nothing to apply would spend a snapshot
to achieve nothing. Update all now routes through the same function, so
there is one path, one task, and the same per-app contract — snapshot
first, roll back on failure.
Two details that only show up when you use it. The checkbox sits inside
the row head, which is itself the expander target, so its handler has to
claim the click or picking an app also opens its detail panel. And the
count is refreshed in place instead of re-rendering the tab, because a
re-render rebuilds the checkboxes under the pointer mid-selection.
The bar holds its space at zero selected rather than appearing on the
first tick, which would shift the list down under the cursor.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
"06:00-08:00" is unambiguous on paper and ambiguous at a glance — the
strip said updates install "during 06:00–08:00" and left the reader
working out whether that meant morning or night. The window now renders
as 6am–8am. The config stays 24-hour so there is still one canonical way
to type it; only the display spells it out, and anything that is not a
plain HH:MM-HH:MM passes through untouched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
"Up to date" answers one question — has the tag I track moved? — and an
abandoned project answers it reassuringly forever. The tag stays put, the
digest never changes, and the app reports as current while receiving no
security patches at all. Nothing in the UI could tell a healthy stable
app from a dead one.
An audit of all 34 anchor images found five in exactly that state:
speedtest (4.4y since rebuild), focalboard (2.8y — Mattermost dropped
support in 2023), pihole-unbound (2.3y), trilium (2.2y), unbound (1.8y).
The scan now records image_updated_at per app (one cheap Hub call inside
the existing registry window, cached between windows like everything
else) and emits stale_after_days from CFG_UPDATER_STALE_DAYS (365, 0
disables) so the UI and the config agree on one number.
Surfaced as an "unmaintained?" severity chip on the fleet row and a
dated explanation in the app detail. Phrased as an observation rather
than an accusation — plenty of small tools are simply finished — but it
does spell out the security consequence, because that is the part a user
cannot infer from "up to date".
Deliberately NOT a "needs action" row on the Overview board: it is not
fixable by pressing anything, and a permanently amber board teaches
people to ignore the board.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Puts the stepped engine behind the "34 available" chip so it is not
CLI-only. Wired end to end: updater_upgrade task type -> task-router ->
updaterUpgrade action -> `libreportal updater upgrade <app> [version]`,
with a label and icon in the tasks list.
The button always confirms, and the dialog states the plan and the
guarantee rather than asking "are you sure?" — which app, from which
version to which, that every step snapshots first and waits for the app
to confirm it is serving with no migration outstanding, that a failure
stops the ladder on the last version that verified, and that it can take
a long time because each release runs its own migration.
Both delegated dispatchers (per-app Updates tab and the fleet Overview
rows) learn the action, so the button works wherever the chip appears.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
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 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>
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 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>
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>
'Checking for improvements… this usually takes a minute.' implied an active
scan finishing imminently. The scan is periodic and runs in the background
(CFG_UPDATER_SCAN_INTERVAL, default 30m), so reword to reflect that. Kept the
cadence generic since the interval is admin-configurable and not exposed here.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
'No installed apps to track.' was terse and gave no next step. Point the user
to the App Center so the empty Updates tab is actionable.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
'No hotfix data yet — the automatic scan fetches the signed improvements index
within a couple of minutes.' leaked jargon (hotfix data, signed index) onto the
interface. Replace with a short, plain 'Checking for improvements…' line.
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>
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>
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>
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 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>
- HIGH: renderAppDetail gated the Roll back button on last_snapshot* fields no
generator emits, so it never rendered. Derive recoverability from update
history (updater_apply always snapshots first) so the affordance is reachable.
- MED: per-app Updates tab now repaints on update/rollback/check/hotfix task
completion (mirrors the backups card) instead of going stale until re-click.
- MED: in-page tab switches now sync spaClean.currentRoute, so the sidebar
Overview entry no longer no-ops after switching tabs.
- LOW: keyboard activation (Enter/Space) for the role=button expander heads,
backup tiles, and sidebar Overview entry.
- LOW: preserve ALL expanded Updates rows across a background repaint, not just
the single ?app= deep-link (split toggle into _openDetail/_closeDetail).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Introduce a per-fleet Overview area inside the apps shell, reachable from a new
'Overview' entry pinned above the apps sidebar search. Selecting it renders a
top-tabbed view in the main pane — Overview · Updates · Improvements · Backups —
mirroring the per-app tabbed layout, with the apps sidebar persistent.
- TabController: generic root-scoped show/hide tab host (core/ui-state).
- OverviewManager: drives the 4 tabs. Reuses a headless UpdaterPage for all
update/CVE/improvement data + rendering (its renderX() are pure HTML
producers) and reads backup/dashboard.json directly for backup health.
- Overview tab: combined update + backup health cards.
- Updates tab: per-app expander table (CVEs/recovery/history inline via the new
UpdaterPage.renderAppDetail) + All/Updates/Security filter chips.
- Improvements tab: reuses the updater's signed-hotfix renderer.
- Backups tab: fleet backup-health tiles; actions deep-link per app.
- Additive only: /overview* routes on the apps feature; old /updater and
/backup pages untouched. Cleanup (redirects, nav, Admin config move) is next.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Surfaces the hotfix channel in the WebUI. Primary home is the Updates &
Improvements page (the updater component) — its own "Improvements" tab — with a
secondary chip on the App detail page (fork 3 locality = both).
Updater component (components/updater):
- New "Improvements" sidebar tab + panel; renderImprovements() reads the host-
generated artifacts_available.json (severity badge, scope chip, applied/auto/
not-applicable badges, plain-English why). Apply/Revert buttons dispatch
artifact_apply / artifact_revert through the TASK system (services.tasks.route)
— no mutating API. Apply is disabled when the index is UNSIGNED.
- Overview gains an "Improvements" stat card; task-refresh now also repaints on
artifact_* task completion; URL tab routing + dispose teardown extended.
Task plumbing (core/tasks): artifactApply/artifactRevert action methods (id is
charset-guarded before it enters the command string) + artifact_apply/
artifact_revert routeAction cases. Task list/format gain icons + friendly labels.
Apps component: an amber "⚡ N improvements" chip on an installed app's detail
header (populated async from artifacts_available.json filtered by app, applicable
& not-applied), linking to /updater/improvements. Best-effort, never throws.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
Comment-only tidy from the feng-shui audit — no code behavior changes. The
features/ directory was renamed to components/ during modularization, but
several header banners and inline comments still named the old path:
- 6 component module headers (admin/tasks/backup/dashboard/updater/index.js +
updater/js/updater-page.js) now name their real components/<id>/… path
- core/kernel/js/spa.js + core/tasks/js/task-router.js comments
- backend/routes/features.js doc-banner (drop a components/<id>/ folder …)
- core/update-notifier/css/update-notifier.css header (js/update-notifier.js)
Guarded the rewrite so the LIVE /api/features/list endpoint name (feature-
registry.js sources + backend route) is untouched — only stale 'features/<path>'
directory references were updated.
Signed-off-by: librelad <librelad@digitalangels.vip>
De-clutter each component into sub-system folders (apps: core/ port-manager/
services/ tools/ routing/; admin: config/ overview/ system/ ssh/ peers/) with
the standard js/ css/ html/ icons/ layout inside; single-page components
(backup/dashboard/tasks/updater) get js/ css/ html/. Single-feature icon sets
moved into their sub-system (vpn -> apps/core/icons, config/cpu/os ->
admin/{config,system}/icons); shared app + category icons stay in core/icons.
feature.json + index.js stay at each component root (the scanned descriptor +
entry). Every controller/CSS/fragment/icon path reference rewritten; verified
no stale refs, all JS valid.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>