941 Commits

Author SHA1 Message Date
librelad
5929ecb4c4 docs(install): clarify the signing-key comment now that it's the real key
The old comment implied the key was still a REPLACE_ME placeholder. Reword to
describe current behaviour (signature required for release installs) plus how to
rotate the key.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-28 20:22:04 +01:00
librelad
a2dd26b469 Merge claude/1 2026-05-28 20:20:38 +01:00
librelad
3b0b3a0a1f feat(release): activate release signing with the production minisign key
Replaces the REPLACE_ME placeholder public key in libreportal.pub and install.sh
with the real LibrePortal release-signing public key (id BC92526B3ECA7F41). The
secret half is held offline by the maintainer.

This activates the signature-required path everywhere it was wired but inert:
install.sh now REQUIRES a valid tarball signature on release installs, the
updater (fetch.sh) requires it on update, and the integrity check (verify.sh)
will report a real "Verified" state once a signed release is installed.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-28 20:20:38 +01:00
librelad
1c3531b932 Merge claude/2 2026-05-28 20:06:34 +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
271b489029 Merge claude/1 2026-05-28 19:41:22 +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
9324c6a4f4 Merge claude/2 2026-05-28 19:28:17 +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
f792cf55f6 Merge claude/2 2026-05-28 19:06:02 +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
816b96fe97 Merge claude/2 2026-05-28 18:50:28 +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
f6fecd023a Merge claude/1 2026-05-28 18:38:28 +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
93d4aaabbb Merge claude/1 2026-05-28 18:25:25 +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
7a302d1af0 Merge claude/1 2026-05-28 18:20:21 +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
dca885738d Merge claude/1 2026-05-28 16:47:17 +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
librelad
324240dd90 Merge claude/2 2026-05-28 16:46:23 +01:00
librelad
b7679bb384 fix(admin/system): clear the "no samples" overlay once live data arrives
The metric detail page showed the empty overlay ("No samples in this
range yet — check back in a minute") whenever the initial history fetch
returned zero points, but nothing ever hid it again. Live ticks then
pushed samples in, drew the chart and filled the now/peak/avg/min stats
— while the overlay stayed up, contradicting the visible data.

Make _renderChart the single authority for the overlay: hidden whenever
there are points, shown only when there are none. Live data clears it as
soon as the first sample lands; switching to a genuinely empty range
brings it back.

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-28 16:46:23 +01:00
librelad
284356228b Merge claude/2 2026-05-28 16:28:42 +01:00
librelad
7178dddae7 ux(admin/system): make the Load gauge capacity-aware, not alarmist
The Load ring was driven by load1_percent = min(100, load1/cores*100)
and coloured by the generic gauge thresholds (red at >=90%). On a
low-core box that pinned it red whenever load merely approached the core
count — which is normal "fully used" territory, not a problem.

Drive the ring from raw load1 with max = cores*2 (so load == cores sits
mid-gauge) and colour by load-per-core: green below capacity, orange
around capacity (>=1.0x), red only once load clearly exceeds it (>=1.7x,
tasks genuinely queuing). cpu.cores rides the live SSE payload, so the
colour is correct on live ticks too.

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-28 16:28:42 +01:00
librelad
ec5b1735dc Merge claude/2 2026-05-28 16:16:22 +01:00
librelad
9c6cef5a05 ux(admin/system): give the host info strip a "Host" section head
The OS/Kernel/Uptime/CPU/Swap strip was the only section on the System
page rendered without a .sys-section-head, so it had no title and butted
directly against the charts above it (no top spacing). Add a "Host" head
— matching the Docker / Per-app pattern — which supplies both the label
and the section's 26px top margin. "Host" rather than "System" since the
page H1 is already "System".

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-28 16:16:22 +01:00
librelad
280bb11d5e Merge claude/1 2026-05-28 16:12:48 +01:00
librelad
f0dc73e332 fix(admin): Manage backups button navigates via the real SPA router
The admin overview Manage backups action called window.librePortalSPA,
a global that is never assigned, so the optional-chaining call silently
no-op'd. The router is window.spaClean; point the call at it.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-28 16:12:48 +01:00
librelad
00de75ffc3 Merge claude/1 2026-05-28 14:49:18 +01:00
librelad
ed319b0f94 fix(backup): configs backup gets its own task identity, not "Backup All Apps"
System-config backups (libreportal backup system) carry no app slug, so the
notification descriptor resolved a blank subject + no icon, and a system-only
pick collapsed to `backup all` when no apps were installed. Give them the
LibrePortal icon + a "Configs" subject, add backup-system to the system-task
logo detection, and guard the whole-fleet collapse on having >=1 app. Rename
the visible subject from "System config" to "Configs" throughout the backup UI.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-28 14:49:18 +01:00
librelad
494dc499b3 Merge claude/2 2026-05-28 14:42:22 +01:00
librelad
e9ee4c7983 ux(services): gap between the stat chips and action buttons in the row
The shared .task-header has no gap, so a full-width chip row (status /
cpu / mem / ports / ip) left the last live chip butting against the
Restart button. Add a 12px gap on .service-item rows only (matching
.task-info's internal gap) so the resource chip and Restart no longer
touch, without affecting the Tasks page that reuses .task-header.

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-28 14:42:22 +01:00
librelad
ad8f1b2975 Merge claude/2 2026-05-28 14:38:01 +01:00
librelad
18e29983c5 ux(services): left-align the Show/Hide logs toggle in service details
Was centred in the open details panel; move it to the left edge so it
lines up with the rest of the panel content.

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-28 14:38:01 +01:00
librelad
38a930c0bd Merge claude/2 2026-05-28 14:27:48 +01:00
librelad
7b786aae45 ux(setup): align dev strip content with the cards above
Bump the dev strip's horizontal padding 14px -> 18px to match
.setup-level-card's content inset, so the strip icon/text sit on the
same left edge as the card titles above it instead of ~4px inboard.
Padding sides (not an icon margin) keeps the whole row aligned and
leaves the top/bottom entrance animation untouched.

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-28 14:27:48 +01:00
librelad
6bf7e29fe8 Merge claude/2 2026-05-28 14:19:03 +01:00
librelad
7ff21621d9 ux(setup): dedicated dev icon + richer reveal for the dev-mode strip
Swap the strip's shared 🛠️ emoji for the inline "tool" SVG used by the
topbar Developer-mode banner — a real, dedicated icon that ties the two
dev-mode surfaces together and no longer doubles the Advanced card's
glyph.

Enrich the entrance: the box grows in and settles, a one-shot accent
glow pulses for the "unlocked" beat, a subtle shine sweeps across, the
icon pops with a slight overshoot/wiggle, and the text slides in just
behind it. All gated behind prefers-reduced-motion.

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-28 14:19:03 +01:00
librelad
1c5ee82a31 Merge claude/2 2026-05-28 14:12:29 +01:00
librelad
5be49b67c6 feat(setup): dev-mode easter egg on the Experience step
Tap the Advanced card 10 times and a full-width "Dev mode activated"
strip slides in beneath the two cards — the same 10-tap pattern as the
topbar logo and services-manager unlocks, now at install time. The
choice rides the setup payload (dev_mode) so setup_apply.sh persists
CFG_DEV_MODE=true, and it's mirrored in-process via LpUi.dev so the
next surface already reflects it. 10 more taps toggles it back off.

Counting the Advanced radio's click (not the label's) sidesteps the
label->input double-fire; the radio is pointer-events:none, so each tap
reaches it exactly once. The strip is [hidden] by default (no phantom
gap in the flex column) and replays its entrance keyframes each reveal.

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-28 14:12:29 +01:00
librelad
af8a4cb22e Merge claude/2 2026-05-28 13:58:20 +01:00
librelad
bf176e7e56 ux(setup): comma instead of em-dash in Beginner card copy
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-28 13:58:19 +01:00
librelad
5d7b96062c Merge claude/2 2026-05-28 13:56:05 +01:00
librelad
5a51c5825d ux(setup): shorten Experience step copy to one punchy line per card
The Beginner/Advanced cards on the first setup step had three-sentence
descriptions that read as a wall of operator detail — the opposite of
the friendly first impression the step is meant to give. Trim each to a
single game-intro-style line; the reversibility note and the Advanced
toggle still cover the details for anyone who wants them.

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-28 13:56:04 +01:00
librelad
5b0a445751 Merge claude/2 2026-05-28 13:46:16 +01:00
librelad
b7d95f5e95 fix(webui): app-log removal uses runFileOp rm -f so uninstall can't hang
The uninstall branch of webuiUpdateAppLog removed the per-app WebUI log
with a bare `rm`. The log lives in the container data plane and is owned
by the container user, often without a write bit. A bare rm (run as root
via `sudo init.sh uninstall`) prompts interactively for write-protected
files — which hangs an otherwise-unattended deploy: the uninstall phase
of a `full` redeploy stopped dead at "rm: remove write-protected regular
file '.../frontend/logs/apps/<app>.log'?".

Route it through runFileOp rm -f (as the container-data owner, force) to
match the neighbouring uninstall_app.sh and the install branch's
owner-aware createTouch/runFileWrite helpers. No prompt, correct owner.

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-28 13:46:16 +01:00
librelad
b9dfbb89d1 Merge claude/2 2026-05-28 02:10:26 +01:00
librelad
5cac965d0d ux(config): dep-required cards lay out as two rows — content above, button below
The "<X> needs to be installed" feature cards (Enable Whitelist, Authelia
Integration, Headscale Integration, …) were rendering with broken
proportions inside narrow form-grid columns: the body squashed into a
~30-char column and the install button stretched vertically as the only
flex item with room to grow.

Switch to a 2-row CSS grid:

  ┌────────┬──────────────────────────┐
  │ icon   │ title                    │   row 1: who is this for
  │        │ reason                   │
  ├────────┴──────────────────────────┤
  │       [ Install <Service> ]        │   row 2: full-width fix-it
  └───────────────────────────────────┘

  icon  grid-row 1, col 1
  body  grid-row 1, col 2
  action grid-row 2, col 1 / -1, width 100%

Reads top-to-bottom regardless of how narrow the host column is, so the
Features tab's 3-column grid stops looking broken. The old @media
(max-width: 560px) responsive override is gone — the grid layout works
at every width, no breakpoint needed.

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-28 02:10:26 +01:00
librelad
5432f46fd0 Merge claude/2 2026-05-28 02:00:06 +01:00