927 Commits

Author SHA1 Message Date
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
librelad
9a87e3f894 ui(services): keep Advanced toggle thumb white and contain it in a chip wrapper
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>
2026-05-28 01:39:04 +01:00
librelad
160d7d1b3c Merge claude/2 2026-05-28 01:36:00 +01:00
librelad
713cba76f0 ux(backup): match per-app Backups tab action buttons to the config Save style
"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>
2026-05-28 01:36:00 +01:00
librelad
930be04e47 Merge claude/1 2026-05-28 01:31:56 +01:00
librelad
bfda700794 fix(apps): stretch cards to fill the row width so the box stays full-width on any zoom level
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>
2026-05-28 01:31:56 +01:00
librelad
0b32405f0a Merge claude/2 2026-05-28 01:30:46 +01:00
librelad
adf79db9e2 ux(tools): play icon on the Run button
Each tool row's Run button gains a small play-triangle SVG to the left
of the label, matching the iconography pattern the Services tab uses
for its Restart and Open buttons. Same green colour (currentColor), so
the icon inherits the success/destructive variants without extra CSS.

Button becomes a flex container with a 6px gap so icon + label stay
nicely centred regardless of label width.

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-28 01:30:46 +01:00
librelad
017a521065 Merge claude/2 2026-05-28 01:27:53 +01:00