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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
Two fixes to the .lp-ui-advanced-toggle on the Services tab header:
1. The thumb flipped from --text-primary (white-ish) to --text-on-accent
(a dark navy on the default theme) when toggled on, which read as a
"black circle" inside the accent track. Other toggles in the project
(.eo-toggle in modal.css, .routing-toggle in routing.css) keep the
thumb white in both states — only the track changes colour. Dropping
the checked-state thumb fill brings this toggle in line.
2. The toggle was floating bare in the header row next to nothing,
which looked out of place compared to the contained button-style
controls in the same slot on Backups (Backup now / Open backup
center). Wrapped it in a chip: neutral rgba(text, 0.06) bg + 0.15
border + 6×12 padding, hover bumps both alphas. Same recipe a
.task-btn uses for its resting state, so the toggle visually reads
as a control sitting in line with the rest of the row's actions.
Signed-off-by: librelad <librelad@digitalangels.vip>
"Backup now" and "Open backup center" looked off compared to the rest of
the app page — the secondary link sat underlined with a trailing arrow
glyph instead of a real button, and neither carried an icon. Re-skins
both to use the .btn .btn-primary / .btn .btn-secondary pattern the
config Save / Reset buttons use, so the three action surfaces on an
app page read as one family.
Backup now .btn .btn-primary + upload-cloud SVG (16x16)
Open backup center .btn .btn-secondary + external-link SVG (16x16)
The "Open backup center" link is now SPA-routed (preventDefault + call
navigateToRoute) so clicking it doesn't trigger a full page reload —
same behaviour as the deep-link cells in the global Snapshots table.
href is still /backup so cmd/ctrl-click and right-click → open-in-new-tab
still work the natural way.
Applied to both apps-unified-layout.html and the legacy app-content.html
since the existing app-page surface lives in both templates.
Signed-off-by: librelad <librelad@digitalangels.vip>
Fixed-width tracks + cap formula kept the box pinned to "N cards at
328px" outer regardless of viewport size, so zooming out left a
massive empty band between the box's right edge and the layout edge.
The box was no longer "dynamic" in any real sense — it scaled with
the card count, not with the available content.
Switching grid-template-columns to repeat(auto-fit, minmax(--app-min,
1fr)) lets cards stretch to fill the row, and auto-fit collapses
trailing empty tracks so a 2-card row in a 3-track-wide viewport
doesn't leave a 328px hole at the end. Zoom in/out now just widens
or narrows the cards; the box always reaches the layout edge.
This drops the cross-category card-width uniformity that the earlier
fixed-width pass introduced — a 2-card category now lays out as two
wide cards while a 3-card category gets three narrower ones. That's
mutually exclusive with "box always full width" without leaving
holes, and the user has shifted priority to full-width-always.
JS cleanup: dropped updateAppsCount + its window-resize listener +
its callsites in renderApps/filterAppsByQuery — no more --app-count
or column-count measurement needed when the grid handles everything
natively.
Signed-off-by: librelad <librelad@digitalangels.vip>