328 Commits

Author SHA1 Message Date
librelad
e6e796311a feat(webui): migrate Dashboard + Tasks to feature modules
Two more pages on the feature-module contract (specs produced + adversarially
verified by workflow):
- features/dashboard/index.js: landing page; mount() folds in the data-reload
  that used to be a navigate() special-case (now deleted from spa.js, so it
  fires exactly once). No controller class — uses the eager dashboard.js globals.
- features/tasks/index.js: re-inits the system-loader tasksManager singleton;
  unmount() clears the 30s auto-refresh interval + open log streams WITHOUT
  stopping the shared SSE bus or nulling the singleton.

Verifier fixes applied: deleted the duplicate dashboard reload in navigate();
dropped a dead detachDashboardLive() call; fixed an invalid try{}while(false)
in the tasks unmount.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-29 23:29:31 +01:00
librelad
182be8c33d feat(webui): phase 3 (first feature) — migrate Backup to a feature module
Introduces the kernel lifecycle and migrates the first real page to the
feature-module contract:
- kernel/lifecycle.js: MountContext (loadScripts/loadFragment/setContent
  + an AbortController/unsub teardown ledger so mounts can't leak
  listeners or live streams).
- features/backup/index.js: Backup Center as a self-contained module
  (LP.features.register with mount/unmount); heavy backup-page.js stays
  lazy-loaded on first mount.
- spa.js: routes whose feature has a registered mount() are driven
  through the kernel; everything else still uses its legacy handleX().
  navigate() unmounts the current feature first. Both fall back to the
  legacy handler if a module is missing or mount throws.

Strangler step: /backup now flows manifest -> registry -> mount/unmount.
The other pages are untouched. handleBackup remains as the fallback.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-29 23:02:24 +01:00
librelad
57c17647e3 feat(webui): phase 1a — shared base token layer (tokens.css)
First slice of the per-module CSS strategy: introduce
shared/css/tokens.css as the always-present, theme-agnostic base token
layer, loaded before all other stylesheets.

- Defines --font-mono (ui-monospace stack). Several feature sheets used
  var(--font-mono, monospace) with no definition, falling back to bare
  monospace; this unifies them with the richer stack used elsewhere.
- Hoists the --page-* identity hues out of css/admin.css so they belong
  to the base layer rather than the admin stylesheet. Values unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-29 22:49:39 +01:00
librelad
7e051be196 feat(webui): phase 0b — route from the feature manifest
LibrePortalSPAClean now builds its route table from window.LP.features
(features/manifest.dev.json) instead of the hardcoded setupRoutes() Map.
Manifest order is preserved so findRouteHandler()'s wildcard precedence
(/apps* before /app*) is unchanged. All-or-nothing fallback to the
built-in table if the manifest is missing/empty or names an unknown
handler, so routing is never left half-wired.

Rendering is unchanged — handlers still do the work; only the routing
source moved.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-29 22:32:42 +01:00
librelad
2eaa5857a1 feat(webui): phase 0a — feature-module kernel scaffold (passive)
Adds the foundation of the feature-module architecture
(docs/frontend-modularization.md) as inert, additive code:
- kernel/feature-registry.js: window.LP.features — runtime register(),
  manifest loader, route-table + nav builders.
- features/manifest.dev.json: hand-committed manifest mirroring spa.js
  setupRoutes() exactly (route -> handler + navId).
- index.html loads the kernel before spa.js.

Zero behaviour change: nothing consults the registry yet. Phase 0b flips
routing to be registry-driven with the spa.js Map as fallback.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-29 22:28:19 +01:00
librelad
b6fa9317bd ux(ssh): drop the redundant paste-key hint, equalise the two cards
Remove the "Paste a public key…" line (the section description already
explains it) and stretch the Login / Add-a-key cards to equal height
(.ssh-cols align-items: start -> stretch).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-29 01:32:54 +01:00
librelad
382e91f2a7 fix(webui): friendly title + icon for the verify task
The `libreportal verify` task showed its raw command and no icon. Add its
formatCommandForUser pattern ("LibrePortal - Verify System"), a 🛡️ type icon,
a formatActionTitle entry, and include it in isLibrePortalSystemTask so it shows
the LibrePortal logo like other system tasks.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-29 00:34:04 +01:00
librelad
4290c04a78 ux(admin): match System page header icon to the sidebar (activity pulse)
The System header used a cpu icon while the sidebar (and the Dashboard's System
card) use the activity-pulse icon. Swap the header to the same activity pulse so
System reads consistently everywhere.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-29 00:29:35 +01:00
librelad
38f04a4dd6 Merge claude/2 2026-05-29 00:21:31 +01:00
librelad
83b129fdad ux(system): put OS + CPU logos in a rounded tile (backup-tile style)
Wrap both the OS and CPU logos in a 36x36 rounded icon tile with a subtle
background — same treatment as the backup app-list tiles — at a uniform
size, with the logo centred inside.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-29 00:21:31 +01:00
librelad
33a749c8d1 ux(admin): rename Overview → Dashboard + add header icons
Renames the Admin landing to "Dashboard" in both the page title and the sidebar
label, and adds a leading header icon (the grid icon, matching the Backup
dashboard) via the shared .page-header-icon-slot. System gets a cpu icon in its
header too. The slot styles come from the globally-loaded backup.css, so no new
CSS is needed.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-29 00:20:09 +01:00
librelad
350d72f6aa ux(ssh): lay Login + Add-a-key side by side (50/50) on tablet/desktop
Both sections are light on content, so a two-column grid uses the space better
than full-width stacking. Wrapped them in .ssh-cols (1fr 1fr, stacks under
640px); Authorized keys stays full width below.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-29 00:14:40 +01:00
librelad
78519a398e ux(system): crop CPU wordmark logos + enlarge host-strip icons
The Intel/AMD logos are wide wordmarks that sat in a square 24x24 box, so
the actual mark rendered only a few px tall and looked invisible. Crop each
viewBox to the wordmark and size the CPU icon by height with auto width so
it shows at a legible scale. Bump the OS distro icon from 18 to 22px.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-29 00:11:15 +01:00
librelad
036fead047 ux(admin): tinted Overview buttons w/ white text; Verify→green, Backups→blue
Reverts the solid fills (too heavy) back to the translucent hue tint but keeps
white text — white on a tint-over-dark-card reads cleanly on nebula, which the
old coloured text didn't. Hues restored to the brighter originals. Verify now
gets its own green token (--page-verify) per the usual verify=green convention,
and Backups takes the blue.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-29 00:08:08 +01:00
librelad
378a4c1dd6 feat(system): official distro logos + Intel/AMD CPU logo with clean model text
Replace the hand-drawn distro marks with the official artwork (Simple
Icons, brand-coloured, bundled locally — no external calls) for
Debian/Ubuntu/Fedora/Arch + a generic Linux fallback. Add Intel/AMD logos
under /icons/cpu/ and show the vendor logo beside the CPU, with the model
string stripped of trademark noise (Intel(R) Core(TM), ®/™, "CPU") since
the logo conveys the vendor — e.g. "Core i5-8250U @ 1.60GHz".

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-29 00:04:59 +01:00
librelad
bd0256715c ux(admin): solid-fill Overview buttons for legibility on nebula
The translucent tinted buttons washed out against nebula's glassy aurora
background. Switch .admin-action-btn to a solid fill with white text (the bg can
no longer bleed through) and deepen the --page-* hues enough for white-text
contrast. Hover uses brightness() so it's theme-agnostic.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-28 23:59:15 +01:00
librelad
28e007d087 feat(system): distro icon beside the OS on the System host strip
Bundle a small set of distro marks (Debian/Ubuntu/Fedora/Arch) plus a
generic Linux/Tux fallback under /icons/os/, and show the icon next to the
OS value, keyed off the cleaned distro name. Locally bundled — no external
calls — and unknown distros fall back to the generic glyph.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-28 23:51:45 +01:00
librelad
3fd4c84707 ux(admin): give each Overview area a signature colour
Introduces per-area identity hues as reusable tokens (--page-updates/backups/
ssh/system + -rgb companions) and a generic .admin-action-btn that takes its
colour from --page set on the card. The Overview buttons now read in their
area's hue — Updates blue, Backups emerald, SSH violet, System amber — with the
icon following via currentColor; "Update now" is the filled (primary) variant.

The tokens are the foundation to extend each area's identity (page headers,
accents) going forward, not just these buttons.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-28 23:46:39 +01:00
librelad
ba3f71cf7a refactor(routing): admin overview canonical URL is /admin/dashboard
The admin ops/health board now lives at /admin/dashboard (adminPath
('overview') emits it; the topbar Admin link points there). Bare /admin
still resolves to the same board — no redirect, both paths render it.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-28 23:41:30 +01:00
librelad
350dc5623a ux(admin): add a leading icon to each Overview card action button
Update/Verify (refresh, shield-check), Manage backups (archive), Manage SSH
(key), View system stats (activity). Icons inherit the button's text colour —
no per-button colour, so the footer stays calm; the icon plus the card's status
dot do the distinguishing.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-28 23:40:31 +01:00
librelad
bbbd035ab2 refactor(routing): move System out of /admin/config to /admin/system
System is live stats, not configuration, so it shouldn't live under
/admin/config. adminPath('system') now emits /admin/system; the path
parser locates 'system' positionally; all nav targets, breadcrumbs and the
dashboard disk-card link point at /admin/system{,/storage,/metric/<k>}.
adminCategoryFromPath already resolves /admin/<x> to that category, so
ConfigManager still mounts AdminSystem unchanged.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-28 23:37:28 +01:00
librelad
3e18a85572 ux(system): match the System-page storage links to the pill CTA
The "View/Open storage breakdown" links in the System storage summary were
plain text links; restyle them as the same accent pill CTA (icon + label)
used on the Disk metric page.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-28 23:28:37 +01:00
librelad
e825749041 ux(system): replace metric page's dev-jargon subline with a plain description
"Live · binary ring backed" meant nothing to a user; show a per-metric
sentence describing what the chart is (e.g. "Root-filesystem usage over
time.") instead.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-28 23:22:49 +01:00
librelad
4f46be9b16 ux(system): stack the storage-breakdown button under the range selector
Move the Disk page's "View storage breakdown" button beneath the time-range
picker (actions stack in a column, right-aligned) instead of beside it.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-28 23:21:36 +01:00
librelad
9fac544442 ux(system): style the storage-breakdown button as a pill CTA
The plain text link read as unstyled; give it the same pill shape as the
Reclaim Space button (icon + label) in the accent colour, so it looks
intentional without borrowing Reclaim's caution-orange.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-28 23:19:17 +01:00
librelad
66f5cb4af4 ux(system): link the Disk metric page to the storage breakdown
Add a "View storage breakdown →" button on the Disk metric page (disk
only) that opens /admin/config/system/storage — keeps the trend page and
the breakdown page focused but a click apart, instead of merging them.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-28 23:16:21 +01:00
librelad
533f1bb9e3 ux(system): drop the disk gauge sublabel, keep just the %
The "LibrePortal X GB" sublabel under the disk percentage read as clutter;
remove it. The % and the coloured LibrePortal portion of the ring stay.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-28 23:11:00 +01:00
librelad
d15f6b09ee ux(system): collapse apps into one "Applications" slice on the System summary
The System-page storage ring is an overview, so showing individual app
(compose-project) names was wrong — it now shows a single "Applications"
slice totalling all apps, alongside Images and Build cache. The per-app
breakdown stays on the full Storage page.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-28 23:06:42 +01:00
librelad
67a841299c ux(system): disk ring shows LibrePortal as a portion + LP line on disk page
Replace the disk gauge's concentric inner ring with a single ring whose
leading portion is coloured to mark the LibrePortal share of the disk
(one ring: total disk used overall, LibrePortal highlighted within it).
On the full-screen Disk metric page, add a flat reference line marking
LibrePortal's current share alongside the disk-usage trend. The gauge
gains a `segment` option; the chart line is a "now" value (no historical
LP series yet), so it's flat across the range.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-28 22:59:49 +01:00
librelad
3d51eda988 ux(system): storage breakdown polish — dual disk gauge, app icons, list design
- Disk gauge (System page) gains an inner ring for the LibrePortal slice of
  the disk, so it shows total disk used AND how much of that is us.
- System-page storage summary now shows the full LibrePortal breakdown
  (apps + images + build cache), not just the Docker engine categories.
- Fix the chart colours: Images use the Reclaim orange, build cache the
  deeper red (warm = reclaimable overhead), apps a cool palette.
- Images list: dark container, Clear All / Select all moved into the section
  head (count text dropped), each image shows its app's icon.
- Storage by app restyled to the same Tasks-style list (app icons,
  expandable folders), minus the selection controls.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-28 22:19:40 +01:00
librelad
bae9a79158 feat(webui): central task-refresh registry + close stale-UI gaps
Post-task UI refresh was scattered: every page added its own taskCompleted
listener and hard-coded which actions it cared about, so it was easy to add a
task and forget the refresh (stale UI), with no single place to see the wiring.

Adds TaskRefreshCoordinator (window.taskRefresh): one listener, with dedupe
(the SSE bus + synthetic fallbacks double-fire) and opt-in debounce (bursts
coalesce; per-task handlers run every time). Components now register a refresh
entry; window.taskRefresh.table() is the introspectable "what reloads when" map.

Migrated onto it: apps (install/uninstall/tool/config_update lifecycle +
restore/update/rebuild state), backups (backup/restore/delete), the update
badge, and the admin overview integrity badge. Gaps closed: restore/update/
rebuild now repaint app+service data. (start/stop/restart intentionally omitted —
no live status surface to refresh today; revisit if a running/stopped badge is
added. Storage reclaim/image-rm keep their own in-page refresh.)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-28 22:06:39 +01:00
librelad
7f034d3e02 fix(webui): reload app/service data after a config apply
config_update re-deploys apps (ports, subdomains, Open URLs, routing) but the
WebUI only unlocked the nav on completion — leaving stale URLs/routing until a
manual reload. apps-manager now refreshes apps + services and repaints the
current view when a config_update task completes, via a reusable
refreshAppsAndView() helper (also the basis for the upcoming task-refresh
registry).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-28 21:52:29 +01:00
librelad
e3454dd10e feat(dashboard): whole-disk donut with storage breakdown on the frontpage
Replace the frontpage liquid-fill disk circle with a real donut split into
Apps · Docker · Other · Free, keeping disk % used in the centre. Apps come
from the per-app generator (root-device bytes), Docker from system df; both
are clamped within "used" so skew can't overflow. Live disk ticks redraw it,
and the card now clicks through to the full Storage breakdown.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-28 21:47:04 +01:00
librelad
b020a3f43a feat(system): re-add Disk usage to the System trends grid
The trends grid lost its disk chart; add it back as a 6th card plotting
root-filesystem % over time, alongside CPU/Memory/Network/Load/Swap. The
'disk' history series and metric-detail entry already existed, so the
expand-to-detail flow works unchanged.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-28 21:38:47 +01:00
librelad
eef24582b2 Merge claude/1 2026-05-28 21:32:29 +01:00
librelad
9ca5cc6c7c feat(system): full, deletable images list on the Storage page
Replaces the read-only "Largest images" top-10 table with a Tasks-style list of
ALL Docker images, with select-one / select-multiple / clear-all removal that
mirrors the Tasks page UX (row checkboxes, master select-all, a button that
morphs Clear All ↔ Delete Selected (N), an eo confirm modal).

Deletion routes through the task system, NOT a new web API: a new
`libreportal system image rm [--force] <ids>` CLI subcommand (validates each
ref, loops runFileOp docker image rm, reports a tally) is invoked via the
system_image_rm task action — same pattern as Reclaim. The web backend change
is read-only (uncap the existing /storage image list). In-use images are
skipped by default with an opt-in "force-remove" toggle (warned). The page
stays put, toasts, and refreshes on the task's completion event.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-28 21:32:29 +01:00
librelad
e6cc84271c ux(system): one unified Storage donut — apps + Docker together
Put everything back in a single donut instead of an app-only one: a slice
per app plus Docker's images and build cache, with one legend listing them
all. Colours run continuously so app and Docker slices stay distinct, and
the Docker cards below reuse the same colours. The per-folder app
drill-down table stays. This is the "all the data, with app usage added"
view that was asked for.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-28 21:28:19 +01:00
librelad
e5cbfba417 ux(system): make per-app usage the Storage page headline
The Storage page now leads with on-disk usage by app: the headline donut
is split by app (each a coloured slice, total app data in the centre),
and the "Storage by app" table is its legend — swatch + bar colours match
the slices, rows expand to the per-folder breakdown. Docker's engine
figures (images + build cache) drop to a secondary section below. This is
the integration that was asked for; the donut is your data, not Docker's
overhead.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-28 20:59:13 +01:00
librelad
5f91d2717e ux(system): drop container writable-layer from the Storage view
Like named volumes, a container's writable layer is a near-zero scratch
number for LibrePortal (app data lives in bind mounts, shown per-app), so
sitting it next to per-app storage just confused things. Remove the
"Containers" slice/card and its backend summation, and reframe the Docker
breakdown as "Docker engine" overhead (images + build cache) — clearly
separate from your app data.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-28 20:51:06 +01:00
librelad
7a0477ff8b feat(system): per-folder breakdown under each app on the Storage page
Extend the app-storage generator to record every bind mount's size and
in-container path, grouped into a per-app folder list. The "Storage by
app" rows are now expandable: click an app to see where its space goes
(e.g. /var/lib/mysql vs /data), with external-drive folders flagged.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-28 20:33:21 +01:00
librelad
17fe4d6ed5 ux(system): drop the Volumes category from the Storage view
LibrePortal apps keep data in bind mounts, so Docker named-volume
accounting is always ~empty and just reads as a confusing "0 B". Now that
per-app on-disk usage covers the real "what's filling my disk" question,
remove volumes end to end: the donut slice, category card, "Largest
volumes" table and the System-page count, plus the backend's volume
summation and top_volumes payload. Reclaim copy no longer references
volumes (it reassures about app data instead).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-28 20:26:13 +01:00
librelad
4b39cf770b feat(system): per-app on-disk storage on the Storage page
Docker only tracks where an app's data lives (its bind mounts), not how
big a bind-mounted host dir is — so named-volume accounting reads ~0 for
LibrePortal, whose app data lives in bind mounts. Add a generator that
reads each app's mount map from `docker inspect` and `du`s the directories
(via runFileOp, so it runs as the data-owning user and isn't blocked by
rootless UID mapping). `du -x` keeps each measurement on its own
filesystem, so data on a separate disk is reported as a distinct
"external" total. The generator self-throttles to ~10 min since du is
heavier than the per-minute metrics. Surfaced as a "Storage by app"
section on the Storage page.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-28 20:06:34 +01:00
librelad
b28268a61f feat(system): "Verified" integrity check against the signed release manifest
Adds per-file integrity attestation on top of the existing signed-tarball
release flow. make_release now generates a SHA256SUMS manifest over the shipped
tree and (when a key is configured) signs it, riding both inside the release
tarball so they land in the install tree with no extra download.

lpVerifyInstall (scripts/source/verify.sh) re-hashes the install tree against
that manifest and verifies the manifest's minisign signature against the
root-owned footprint pubkey, yielding states: verified / modified / tampered /
unsigned / unverifiable / development. webuiSystemVerify writes verify_status.json
(throttled daily, force on demand, also after each update apply), surfaced as an
Integrity line + "Verify now" button on the Admin → Overview Updates card and a
row in the update details panel. `libreportal verify` exposes the same check on
the CLI.

Honest framing: this is a self-check (run by the software it verifies), so red
fires only for genuine modified/tampered states; the badge tooltip points to
out-of-band `minisign -Vm` for an independent guarantee.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-28 19:41:22 +01:00
librelad
89a2b300b8 ux(overview): slim the System card to 3 compact rows
Drop the OS row and compact Memory/Uptime so the System card matches
the density of its sibling cards. Full detail stays on the deep system
stats page.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-28 19:28:17 +01:00
librelad
49cf7e8bec ux(system): move Reclaim button top-right, make it actually free space
Three fixes from testing the storage page:

- Placement: the "Reclaim space" button moves into the page header,
  top-right (matching the metric page), instead of sitting in the body.

- It now actually reclaims: build cache needs -a to drop (docker reports
  0 B "reclaimable" without it, but it's pure cache — safe to clear), so
  the CLI uses `docker builder prune -af`. Previously the safe scope
  freed ~nothing on a box whose reclaimable was mostly cache.

- Honest "Reclaimable" number: /api/system/storage was counting the
  whole build cache AND unused tagged images, overstating what the safe
  prune frees (e.g. 340 MB shown, ~96 MB per docker, button cleared 0).
  Reclaimable now = dangling images + build cache only; stopped
  containers and volumes are never counted (the safe prune never touches
  them). Headline now matches the button's effect.

Also simplify the CLI output (drop the jargony scope notice and the
reclaimed-total greps) and re-enable the now-persistent header button
after the post-reclaim refreshes.

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-28 19:06:02 +01:00
librelad
3031c6cab9 feat(system): "Reclaim space" action on the Storage page
Adds a `libreportal system reclaim` CLI command and an orange "Reclaim
space" button on /admin/config/system/storage (the v2 prune control the
page always hinted at).

Scope is deliberately SAFE: build cache + dangling (untagged) images
only (docker builder prune -f + docker image prune -f via the
rootless-aware runFileOp). It never touches volumes (app data) or
tagged/in-use images, so nothing an app relies on is removed.

Wiring mirrors system_update: a systemReclaim() action + system_reclaim
route case run the command verbatim through the task processor. The
button confirms via showConfirmation, shows a spinner, and re-reads
storage usage as the prune lands. Button styled with --status-warning to
match the Reclaimable stat it sits under, with a note clarifying scope.

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-28 18:50:27 +01:00
librelad
7ba281a390 ux(backup): redesign the snapshot details panel
The expanded snapshot detail reused the shared .task-meta/.meta-item
layout, which forces each field onto one nowrap line and clips long
values (the full date string, repo paths) mid-string. Give the backup
snapshot its own scoped label-over-value grid plus full-width Tags/Paths
blocks that wrap, surface app=/host=/engine= tags as their own fields,
and show a readable date (full timestamp on hover). Applied to both the
global Snapshots tab and the per-app Backups card so they match.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-28 18:38:28 +01:00
librelad
d448d34f67 feat(backup): auto-refresh the backup page when a backup/restore finishes
The page has no live feed, so a completed backup/restore wasn't reflected
until a manual Refresh or re-navigation. Subscribe to the TaskEventBus
'taskCompleted' event and repaint on backup/restore completions. Debounced
to coalesce the burst when several per-app tasks finish together; only the
mounted instance reacts and a stale listener removes itself. The Refresh
button stays as a manual pull.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-28 18:25:24 +01:00
librelad
7dbeb307c9 ux(backup): drop the "Backup all apps" header action on Dashboard and Migrate
The primary header button defaulted to "Backup all apps" on every tab that
wasn't Locations/Configuration, so it showed on Dashboard and Migrate where
it isn't wanted (Dashboard backs up via the status-grid tiles; Migrate is
about moving hosts, not backing up). Keep it on the Backups tab and hide the
header action entirely on Dashboard and Migrate.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-28 18:20:21 +01:00
librelad
8e6691b7d3 feat(system): surface the Docker storage breakdown on the System page
Promote a compact Storage summary (breakdown donut + per-category legend
+ reclaimable) onto the System index, replacing the thin Docker strip and
its easily-missed "Open breakdown" link; it links through to the full
breakdown page. Drop the Disk usage trend chart, which duplicated the
Disk gauge's root-mount %.

Extract the donut + segment builders onto SystemStoragePage so the index
summary and the full page share one renderer. This also fixes a donut
stacking bug: the SVG used the final cumulative fraction for every
slice's dashoffset instead of each slice's own running offset, so the
ring only partially filled. It now fills proportionally.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-28 16:47:17 +01:00