885 Commits

Author SHA1 Message Date
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
librelad
d8f585aada ux(backup): global Backups tab matches the per-app card pattern
The /backup → Backups tab was the last surface still rendering snapshots
as a plain HTML table — every other backup-related list had moved to the
.task-item card pattern shared with Services. Cohesion-only refactor:
both surfaces now look identical, with the global view adding the
fields the per-app view doesn't need.

HTML: drops <table class="backup-snapshot-table"> + its <tbody>,
replaces with a single <div id="backup-snapshot-list"
class="backup-snapshot-rows"> that the same .backup-snapshot-flash
deep-link highlight already targets.

renderSnapshots() now emits .task-item cards via the new
_renderSnapshotRow() helper. Each card carries:

  app icon · "12h ago" title · app-name chip (linked) · location pill
  · timestamp chip · short-ID chip       Restore · Delete · Details

Extras vs the per-app card:
  - App-name chip — global list isn't scoped to one app, so each row
    needs to name the app it belongs to. The chip is the deep-link to
    /app/<name>/backups?snapshot=<id> (replaces the dashed-underline
    "link" treatment on the old App / ID table cells).
  - Delete button alongside Restore — destructive cleanup lives on the
    global view, not on the per-app card.
  - "System config" rows (snapshots without an app=<slug> tag) get the
    LibrePortal icon and no app-link (no per-app page to open).

Detail panel (expanded via header / Details button) shows App, Backup
ID, Location, full timestamp, Host, Tags, Paths — the same shape as
the per-app version, plus Host (relevant on the global multi-host view).

Click delegation:
  - [data-action="toggle-snapshot-row"] on the header + Details button
    toggles .task-details-open
  - Restore / Delete buttons now stopPropagation so clicking them
    doesn't also toggle the panel
  - Existing [data-deep-link] handler is reused by the app-name chip

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-28 02:00:05 +01:00
librelad
4492f79f73 Merge claude/2 2026-05-28 01:50:49 +01:00
librelad
2e7ab3235a ux(backup): next-run hint in the Backup status card header
The Backup status card sat with just a heading + tooltip on the right;
the Locations card on the same row already had a hint pill ("Active
destinations"). Mirror that pattern: show the next scheduled backup
time pushed to the right of the heading, so the user can see at a
glance when the daily run will fire without digging into Configuration.

Derived purely client-side from CFG_BACKUP_CRONTAB_APP (read off the
already-loaded window.systemConfigs map) — no backend surface needed:

  - nextCronFireTime(expr) parses a 5-field crontab (minute hour dom
    month dow) supporting *, N, lists (N,M,O), ranges (N-M), and
    steps (* /N, N-M/S). Walks one minute at a time from now+1, honours
    the POSIX OR rule for DOM+DOW, caps at 366 days so an unmatchable
    expression doesn't loop forever, returns null on bad syntax so the
    UI falls back gracefully.
  - formatRelativeFuture(when) — formatRelative's future-tense sibling:
    "in 6h", "tomorrow", "in 3d".
  - formatScheduleClock(when) — "at 05:00" today, "Mon 05:00" otherwise.

Hint slot rendered in #backup-next-run. Three states:
  - parseable + computable        "Next backup tomorrow · at 05:00"
                                  + title with absolute time + schedule
  - unparseable schedule          "Schedule: <raw>"  with title hint
  - empty CFG_BACKUP_CRONTAB_APP  "No schedule set"  with title hint

Smoke-tested the cron parser against "0 5 * * *", "*/15 * * * *",
"30 23 * * 0", "0 0 1 * *", "", "garbage", and "0 5 * *" (4 fields).

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-28 01:50:49 +01:00
librelad
3f3499a348 Merge claude/2 2026-05-28 01:42:35 +01:00
librelad
6af5eac4d9 ux(backup): "Open backup center" inherits the amber Back-to-Apps treatment
The Open-backup-center button was rendering as the raw .btn-secondary
fallback (muted grey) because the amber-tinted theme override was
scoped only to .config-actions and .console-actions — .backup-title-
actions wasn't in the selector list. Result: same shape as Back-to-Apps,
totally different colour, looked off.

Add .backup-title-actions .btn-secondary to both override blocks (the
nebula-theme rule and the default themes.css fallback) so Open backup
center now matches Back-to-Apps and the Config Reset button: solid
amber in default themes, translucent amber under nebula.

Comment also reframed — these aren't "Back to Apps"-specific anymore;
they're "step away from this page" secondary actions as a family.

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-28 01:42:35 +01:00
librelad
95d7bb93dd Merge claude/1 2026-05-28 01:39:04 +01:00