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>
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>
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>
User's empirical fix: on a 1280-class viewport (sidebar 220, content
~1010), --app-min 300 made the grid pick 3 cols because
floor((1010+20)/(300+20)) = 3, which left a 4-card category landing
as 3+1 with the orphan-row gap that's been the running visual
complaint. Bumping --app-min to 328 changes the floor to
floor((1010+20)/(328+20)) = 2, so the same 4-card category becomes
2+2 with no orphan.
Wider monitors are unaffected — a 1056px content area still fits 3
tracks of 328 (3*328 + 2*20 = 1024 ≤ 1056), and 1700px+ content
still fits 4. The cards-per-row count only drops on the narrow band
where 300 would otherwise have squeezed a third just-too-tight
column in.
Signed-off-by: librelad <librelad@digitalangels.vip>
Apply the same .services-rows / .tasks-container pocket pattern to the
Tools tab so the three app-page tabs (Services, Tools, Tasks/Backups)
share one visual language: rows live inside a sunken dark panel that
reads as a contained area inside the tab-pane's glass surface.
.tools-rows gains
background: rgba(var(--bg-rgb), 0.2)
border-radius: 8px
margin: 16px
padding: 16px (was 1rem 1.25rem 2rem)
For the multi-category tabs case the pocket sits flush below the tab
bar — drop the top margin via the .tools-tab-bar ~ .tools-cat-pane
sibling selector so the tabs and pocket read as one element.
Signed-off-by: librelad <librelad@digitalangels.vip>
The orphan-1 rebalance (4-on-3 → 2x2) cost too much: it dropped a
4-card category from 3 cards per row to 2 across the board, and would
do the same for any N where N % maxCols == 1. User feedback: 3
densely-packed cards with a small orphan-row gap reads better than
2 wider cards in a 2x2 layout — denser rows feel more compact and
let the eye scan more apps at once.
Back to the post-d4b7731 state: fixed-width tracks (auto-fill,
--app-min) so card widths line up across categories, plus the
sentinel that sets --app-count to 99 when visible cards meet the
natural full-width column count so the box reaches the layout edge.
The 4-on-3 case is now 3+1 again — the lone card on row 2 has empty
cells to its right, accepted as the lesser of two evils.
If the orphan ever becomes a real visual issue, the next move would
be JS-rendered last row (own sub-grid sized to its item count) rather
than reducing the column count globally.
Signed-off-by: librelad <librelad@digitalangels.vip>
Screenshot showed a 4-card category laying out as 3+1 (three cards on
row 1, Wireguard Easy alone on row 2 with two card-shaped empty cells
on its right). Fixed-width tracks + auto-fill kept the cards aligned
across categories but couldn't avoid the orphan — pure CSS grid has
no way to collapse partial-row trailing cells when the column above
them is filled.
apps-manager.js now picks --app-cols deliberately: the natural
column count for the viewport, reduced by one when the last row
would otherwise be exactly one orphan card. 4 cards on a 3-col
viewport becomes 2x2; 5 cards stays at 3+2; 6 stays at 3+3+0; 7
drops to 2-col so the last row gets a partner (still has one orphan
at the very end since 7 is prime, but never below 2 cols — a single
column stack reads worse than an orphan).
CSS swap: grid-template-columns now consumes the new --app-cols
custom property and uses minmax(--app-min, 1fr) so cards stretch
within their tracks (the orphan-prevention dance means widths can
vary across categories now — tradeoff for never having internal
gaps). 1-card view still shrinks the box via the existing formula
so a lone card isn't stretched across the full row.
Signed-off-by: librelad <librelad@digitalangels.vip>
The per-app Backups tab was the odd one out: snapshots and the
"Backup now" / "Open backup center" buttons all sat inside a single
flat .backup-app-card with no styling parity to Services or Tasks.
The Services tab uses .services-title (20px header + bottom border)
on top of a recessed .services-rows panel; Tasks uses the same recipe
with .tasks-title + .tasks-container. Backups now matches.
.backup-title is the header row — h3 + subtitle on the left,
Backup-now (primary) and Open-backup-center (secondary) buttons
pinned to the right so they stay reachable regardless of how long
the snapshot list grows. No pagination needed: the renderer already
soft-caps the displayed list at 50 with an "Open backup center"
overflow link, and per-app snapshot counts almost never exceed that.
.backup-snapshots-container is the dark panel (rgba bg 0.2, radius 8,
padding/margin 16) wrapping the existing status line + snapshot rows.
JS untouched — it still writes to #backup-app-card-status and
#backup-app-card-snapshots; only the outer shell changed.
Signed-off-by: librelad <librelad@digitalangels.vip>
Two bundled UI fixes.
1. Tools page Run / destructive buttons — the base recipe in tools.css
(rgba green/red 0.12 bg + 0.30 border + full-saturation text) reads
muddy against Nebula's cosmic gradient, same readability problem
.install-btn / .uninstall-btn had before the nebula overrides bumped
them to 0.35/0.65 with --text-primary text. .tool-run-btn and its
.destructive variant now ride those same overrides so Run pops as
green-tint and the dangerous variant pops as red-tint, both with
neutral text against the gradient.
2. Services tab row — the "Logs" button now reads "Details" because
that's what it actually toggles (meta + rich detail + log toggle).
The data-action moves from toggle-logs to toggle-details, and the
expanded panel no longer auto-opens a log stream. A small footer
"Show logs" / "Hide logs" toggle at the bottom of the open panel
explicitly opts in to tailing, kicking off the existing SSE stream
on click (auto-updates while shown). Closing the parent details
panel also resets the log block back to its hidden state so the
next reopen starts clean. app-tabbed-manager's task-running button
disable was taught about the new actions so they stay clickable
while a long task is running.
Signed-off-by: librelad <librelad@digitalangels.vip>
The auto-fill minmax(300px, 1fr) template stretched cards to fill the
glass box, so a 2-card category landed at ~301px each (the box
shrunk-and-stretched to a hair over 2*300) while a 3-card category
(box now full-width) landed at ~323px each. Cards visibly didn't
align between categories — the user spotted the 22px difference.
Switching the grid template to fixed-width tracks
(repeat(auto-fill, var(--app-min))) means cards are always exactly
--app-min (300px / 280px at ≤1024) regardless of how many are
visible. Card positions and widths line up across every category.
The natural-columns sentinel from the previous pass is no longer
load-bearing — with fixed-width cards, "full width" at high N gives
no extra card-width benefit, only trailing space inside the box.
updateAppsCount drops the measurement step and just sets the visible
count, letting the formula shrink the box around the cards.
Signed-off-by: librelad <librelad@digitalangels.vip>
Two follow-ups to the dynamic-width change:
1. The box was centred (margin: 22px auto), which moved cards out of
their original left position whenever the cap kicked in. Revert to
margin: 22px so the cards keep their left X — the box just shortens
on the right when there are few visible cards.
2. The formula assumed content-box, but style.css:4 sets
* { box-sizing: border-box } globally. With border-box max-width is
the outer width, so a 2-card cap of 664px gave content = 664 - 44
(padding) - 2 (border) = 618, just under the 620 needed to keep
2 columns at minmax(300px, 1fr) with gap 20 — grid silently dropped
to 1 column and the cards stacked. Formula now adds 46px (padding
+ border) plus 2px of sub-pixel buffer, so 2 cards have 622px of
content and reliably stay on one row.
Signed-off-by: librelad <librelad@digitalangels.vip>
Restores the per-app snapshot list (regressed during the backup-system
revamp) and rebuilds it on the same .task-item visual the Services tab
uses, so the two app-page tabs read as a matched pair. Wires the three-
level navigation the user asked for end-to-end:
/backup global dashboard + snapshot table
└─ click app tile → /app/<name>/backups
└─ click any snapshot row expands to detail in place
└─ click App / ID cell → /app/<name>/backups?snapshot=<id>
(auto-expands + scrolls + flashes)
Per-app Backups tab (BackupAppCard):
- Snapshots render as task-item rows: app icon, "12h ago" title,
location pill, full timestamp chip, short-ID monospace chip,
Restore + Details actions.
- Click the row header (or "Details") to toggle a .task-details panel
showing snapshot ID, location, full timestamp, host, tags, and the
paths the snapshot covers.
- Shows up to the 50 most recent; >50 surfaces a hint to the global
backup center for the full list.
- flattenSnapshots() now carries hostname/tags/paths through so the
detail panel has real content.
Cross-page navigation:
- Dashboard app-tile click navigates to /app/<name>/backups instead of
opening the pick-now modal. The pick-now action is preserved as an
explicit "Back up" pill that appears top-right on hover/focus.
System tile keeps the old modal click (no dedicated page yet).
- Global Snapshots table — the App and ID cells are now SPA-routed
links to /app/<name>/backups?snapshot=<id>. Snapshots without an
app=<slug> tag (system backups) stay plain text. Routed via
navigateToRoute so the SPA mounts in place instead of a full reload.
Deep-link mechanism:
- BackupAppCard._honorSnapshotDeepLink reads ?snapshot=<id> on render,
finds the matching .backup-snapshot-item, opens its details, scrolls
it into view, and applies a brief .backup-snapshot-flash (animated
box-shadow pulse) so the user's eye lands on it after the SPA jump.
CSS:
- backup.css gains .backup-snapshot-rows, the location pill, the
monospace ID chip, the tag chips, the deep-link flash keyframes,
the tile "Back up" pill (.backup-app-tile-action — only visible on
hover/focus to keep the dashboard calm at rest), and the dashed
underline link style for the snapshot-table deep-link cells.
Signed-off-by: librelad <librelad@digitalangels.vip>
Adds the install-time Beginner/Advanced choice the user described, with
the linked dev-mode escape hatch and global body-class machinery that
any surface can hang advanced/dev-only DOM off.
Three-tier mental model, two flags in the data model:
Beginner default. nothing extra shown.
Advanced .lp-advanced DOM revealed; advanced wizard steps shown
Adv+Dev .lp-dev DOM also revealed; dev-only fields visible
Linking rule (enforced inside LpUi):
- enabling dev auto-enables advanced (dev w/o advanced is incoherent)
- disabling advanced auto-disables dev
Wire shape:
CFG_INSTALL_LEVEL beginner | advanced (general_basic)
CFG_DEV_MODE existing, unchanged behaviour
window.LpUi.{advanced,dev} {get(), set(), apply()}
localStorage keys lp.ui.advanced, lp.ui.dev, lp.ui.seeded
body classes lp-ui--advanced, lp-ui--dev
events lp-ui-advanced-changed, lp-ui-dev-changed
global CSS gates body:not(.lp-ui--advanced) .lp-advanced { hide }
body:not(.lp-ui--dev) .lp-dev { hide }
Setup wizard:
- New step 1 "Choose your experience" with Beginner/Advanced cards.
Beginner is preselected so race-through gets the safe default.
- Picking a level updates totalSteps live (4 for beginner, 5 for
advanced) so the progress bar reflects the choice.
- Metrics step (Prometheus + Grafana) is gated to Advanced — beginner
never sees it, never gets asked, never installs them by accident.
- Submit payload now carries install_level; setup-routes.js validates
it against the enum (beginner|advanced).
- scripts/setup/setup_apply.sh writes it to CFG_INSTALL_LEVEL via
updateConfigOption.
- On submit, LpUi.advanced.set is called immediately so the next
surface (running-tasks page) is already in the right mode — no
refresh needed.
WebUI bootstrap:
- js/utils/lp-ui.js loads first thing in index.html (before any other
bootstrap) so body.lp-ui--advanced is applied pre-paint — no FOUC
of advanced content on a fresh tab.
- On first run, seeds lp.ui.advanced from CFG_INSTALL_LEVEL.
Subsequent loads honour the user's per-browser override.
- Mirrors CFG_DEV_MODE → lp.ui.dev on the seed pass.
Dev-mode unlock:
- Existing 10-click LibrePortal-logo easter egg unchanged.
- NEW: same 10-click unlock on the Advanced toggle (in services-manager).
Reuses the countdown-toast pattern; on the 10th click delegates to
the topbar's _setDevMode so there's one canonical setter and the
config_update task path stays singular.
- TopbarComponent now exposes its instance as window.topbar so the
toggle's tap handler can reach _setDevMode.
- topbar._setDevMode also calls LpUi.dev.set(enabled) so the body
class flips immediately (no reload needed to see dev-only DOM).
Convention rolled out:
- Services tab's .service-rich panel was already gated on
body.lp-ui--advanced.
- .lp-advanced / .lp-dev are now first-class hide classes any
component can tag DOM with — see style.css globals.
Signed-off-by: librelad <librelad@digitalangels.vip>
Adds a foundational global UI-mode flag — Beginner (default) vs Advanced —
gated by a single toggle on the Services tab title row. First foothold
of a project-wide pattern: any surface that grows extra-technical detail
(mounts, limits, internals, raw IDs, …) will gate it on the same flag,
so a newcomer doesn't see a wall of operator information while a power
user gets everything site-wide with one flip.
How it's wired:
window.LpUi.advanced — { get(), set(on), apply() }
localStorage key — lp.ui.advanced ('0' | '1')
body class — lp-ui--advanced
event — window 'lp-ui-advanced-changed' { advanced }
Surfaces gate their advanced-only DOM via CSS:
body:not(.lp-ui--advanced) .service-rich { display: none; }
So flipping the toggle is instant and DOM-free — no re-render needed.
The Services tab's rich container panel (limits, image, healthcheck,
networks, mounts) is the first thing behind the flag; live CPU%/memory
chips in each row stay visible always because they read just as easily
as a status colour and are useful to everyone.
Title row gets a small slider toggle styled in the project's accent —
unobtrusive, labelled "Advanced". Default OFF (Beginner).
The same _renderRow reorders the log block above the rich-detail block
inside .task-details, so when Advanced is on AND a row is expanded, the
live log appears right where the "Logs" click landed rather than below
a wall of metadata. Helps with the old simple-click feel even when the
extra detail is showing.
Plumbed deliberately to be project-wide so the upcoming first-install
"Beginner vs Advanced" wizard step can seed the flag (planned:
CFG_INSTALL_LEVEL in general config → emit body class server-side at
template render time → no FOUC on a fresh load).
Signed-off-by: librelad <librelad@digitalangels.vip>
The Admin → System area was growing a parallel per-container surface
(/admin/config/system/app/<name>) alongside the existing per-app Services
tab on the app page. Two pages onto the same thing is the kind of
duplication that rots fast — they drift, users have to remember which
one to use, and the next person adding a feature has to decide twice.
This commit consolidates onto the existing Services tab (which already
has compose-service awareness, docker socket access, restart actions via
the task system, and live log streaming) and decommissions the parallel
admin sub-page:
- Delete system-app-page.js and its lazyLoad entry. The dispatch in
admin-system.js for the 'app' view now redirects to the app page's
Services tab so old bookmarks still resolve cleanly.
- System index per-app rows navigate to /app/<name>/services (not
/admin/config/system/app/<name>) and the row hint copy is updated
to match.
- Services tab gains the rich container detail the old admin page
rendered, fed by /api/system/containers + /containers/:id +
/containers/:id/stats:
* Inline live chips in each service header: CPU% and memory
(with limit + percent if a limit is set). Memory chip flips
amber at 80% and red at 95% of the configured limit.
* New "service-rich" panel inside the existing expandable
details section (above the log block, so the existing Logs
toggle reveals both):
- Image + image-id + uptime + restart count
- Memory / CPU / PIDs limits + restart policy
- Healthcheck pill + last 3 probes (collapsible per-probe)
- Networks table (name, IP, gateway, MAC)
- Mounts table with type badges (volume/bind/tmpfs)
* Live stats refresh every 5 s; existing status refresh stays
on 10 s. Both gated on the Services tab being active.
- Backups for the app already live on the existing /app/<name>/backups
tab (loadAppBackups → BackupAppCard.render), so the navigational
promise of "one place per-app" is already met — System index just
needed to route there.
- CSS: services.css picks up .service-live-chip (with warn/danger
colour cues) and the full .service-rich block (grid, tables, mount
badges, healthcheck pills).
Signed-off-by: librelad <librelad@digitalangels.vip>
Promotes the admin → System area from a single index page with a transient
overlay into a real router with four addressable sub-pages, plus a docker-
api-backed read surface to drive them.
URLs:
/admin/config/system index (gauges + trends + per-app table)
/admin/config/system/metric/<key> single-metric deep-dive
/admin/config/system/app/<name> per-container app deep-dive
/admin/config/system/storage docker disk-usage breakdown
The path resolves to category=`system` in adminCategoryFromPath, so the
existing SPA dispatch still drops you into AdminSystem; AdminSystem then
reads the rest of the path and mounts the right sub-renderer into
config-section. Each sub-page owns its own DOM + lifecycle and is disposed
when the orchestrator re-mounts on the next navigation. Browser back, page
reload, and shareable URLs all work — no modal, no overlay state, no
fragile open/close lifecycle. Esc on the metric page navigates back to the
index.
Backend (containers/libreportal/backend):
- utils/docker.js — shared client for the bind-mounted Docker socket
(extracted from service-routes.js' inline copy). dockerRequest,
dockerStream, and a multiplex-log decoder for /containers/:id/logs.
- routes/docker-info-routes.js mounted at /api/system, contributes:
GET /containers full list, plus grouped-by-app shape
GET /containers/:id inspect projection (limits, mounts,
networks, ports, health, restart count)
GET /containers/:id/stats one-shot CPU% / memory / network /
blkio / pids (derived from precpu/cpu
deltas, like `docker stats`)
GET /containers/:id/logs last N lines, multiplex-decoded
GET /storage `docker system df` rolled up per
category, plus top-10 images +
top-10 volumes by size
Frontend (containers/libreportal/frontend/js/components/admin):
- admin-system.js — refactored into orchestrator + index view. _parsePath
drives dispatch; sub-views are window.SystemMetricPage /
SystemAppPage / SystemStoragePage classes mounted into config-section.
The per-app table is now keyboard-focusable rows that navigate to the
per-container page; the Docker strip grows a "Storage" tile that
navigates to the storage page.
- system-metric-page.js (renamed from system-detail.js, rewritten as an
in-flow page renderer). Same chart visuals as the old overlay — grid,
axis, area gradient, peak/min/now markers, hover crosshair + tooltip
scrubbing, per-metric accent theming — but rendered into the page
instead of a fixed-position panel. Range picker reflects to ?range=
so refresh preserves the selection. 1 Hz SSE feed splices into the
chart tail in real time.
- system-app-page.js — for each container in the app stack: status,
image, image-id, uptime; live stats card (cpu / mem with limit-pct /
rx / tx / blkio r-w / pids, polled every 2s with warn+danger colour
cues at 80% and 95% of memory limit); limits panel (memory, cpu,
pids, restart policy, restart count, started-ago); healthcheck
status + last 3 probes; networks table (name, IP, gateway, MAC);
published ports; mounts table with type badges; collapsible log tail
with refresh.
- system-storage-page.js — donut chart (cumulative-arc, hand-rolled
SVG) splits total in-use disk by images / volumes / containers /
build cache; per-category cards with size + reclaimable; top-10
images and top-10 volumes tables with "unused" / "orphan" badges.
CSS (containers/libreportal/frontend/css/admin.css):
Overlay-specific rules (.sys-detail wrapper, backdrop, panel, close
button, body lock) removed. Inner chart rules (stats grid, svg, grid,
axes, peak/min/now, crosshair, tooltip, foot) retained and reused by
the metric page. New blocks for .sys-metric-page, .sys-app-page (with
stat warn/danger colour states, health pills, mount-type badges, log
pre styling), .sys-storage-page (donut + legend + headline + per-
category cards + orphan/unused badges), .sys-app-row (clickable
rows with arrow + accent hover), .sys-stat-link (clickable Docker
strip tile).
Signed-off-by: librelad <librelad@digitalangels.vip>
Replaces the JSON history file behind /api/system/history with a fixed-size
binary ring buffer on disk and adds a second, downsampled tier so the chart
can now span seven days, not just twenty-four hours.
Two on-disk rings under frontend/data/system/:
metrics_ring_1m.bin 1440 pts @ 1 min ( 24 h)
metrics_ring_5m.bin 2016 pts @ 5 min ( 7 d)
Each point is 32 bytes (uint32 timestamp + 7 float32 metrics — cpu / mem /
swap / disk / load1 / net_rx / net_tx); files carry a 32-byte header with
magic, version, capacity, head, count, bucket seconds, and last bucket time
so they're self-describing and torn-write recoverable.
A persistent 1-minute ticker inside the backend (independent of whether
anyone's subscribed to /api/system/stream) composes points from /proc plus
the bash generator's latest snapshots and appends to the 1m ring; every
five minutes it averages the last five 1m points into the 5m ring. On
first run, the writer backfills the 1m ring from the legacy
metrics_history.json so first paint already has 24 h.
/api/system/history?range=N auto-selects the tier (≤1440 → 1m, else 5m),
keeps the existing { points, updated } shape, and additionally returns
`tier` for clients that care. Falls back to the legacy JSON on cold start.
Admin → System: 7d added to the range picker (now 1h / 6h / 24h / 7d),
swap + load1 promoted to their own trend cards, and every gauge / chart
card grows an Expand affordance that opens a fullscreen single-metric
deep-dive overlay:
- Big themed chart with grid, gradient area, peak/min/now markers, and
a live-pulsing "now" dot
- Hover crosshair + tooltip scrubs the series with formatted time +
value
- now / peak / avg / min stat strip with deltas
- Range picker (1h / 6h / 24h / 7d) re-fetches and re-themes per metric
- 1 Hz live SSE feed updates the overlay's now-stat in real time
- Escape / backdrop / close button all dismiss
- Per-metric accent colour (cpu=accent, mem=info, disk/swap=warning,
net_rx=success, net_tx=accent, load=accent) flows through gradient,
border, dot, and stats card
Zero new dependencies — hand-rolled SVG and pointer events throughout.
Signed-off-by: librelad <librelad@digitalangels.vip>
The glass box was a CSS Grid with auto-fill columns of minmax(300px,
1fr), so it always painted across the full content area. With only 2
apps on a wide row the third/fourth column slots remained inside the
border as empty space — visually a card-shaped hole.
Drive the box's max-width off a --app-count CSS variable, capped at
(100% - 44px) so it can't escape the layout's symmetric 22px gutter.
margin: 22px auto keeps the horizontal padding symmetric in both the
capped (auto-centers the smaller box) and full-width (auto collapses
to 22+22) cases. --app-min (300/280 at the ≤1024 breakpoint) feeds
both the grid template and the cap formula so the responsive column
width stays a single source of truth.
apps-manager.js sets --app-count to the count of visible .app-card
elements after every render and after the sidebar search filter, so
filtering down to 2 hits also collapses the box. Floor of 1 keeps the
empty state usable.
Mobile (≤768) overrides max-width to none — single column already
fills, and the 10px gutter shouldn't be auto-centered.
Signed-off-by: librelad <librelad@digitalangels.vip>
The previous commit added body.has-dev-banner shifts for .sidebar and
.apps-layout assuming they were position:fixed top:60 like the topbar.
They aren't — on desktop both sit in flex flow (.sidebar is
position:relative, .apps-layout is just a flex container), so
top:96px pushed the sidebar 96px down from its natural slot, leaving
a big visible gap above the category list.
Scope the sidebar nudge to the mobile media query where it actually
becomes fixed (also covers .sidebar-container, the unified apps
layout's mobile drawer). Replace the wrong .apps-layout top rule with
a height tweak — it sizes itself off (100vh - 60px) and was overflowing
the viewport by 36px when the banner was on; calc(100vh - 96px)
accounts for the banner.
Topbar shift (top:0 → 36) stays unchanged; that one was correct.
Signed-off-by: librelad <librelad@digitalangels.vip>
Banner was fixed at top: 60px (just below the 60px-tall topbar) at
z-index 999 — same vertical band as the sidebar (top: 60px, z-index
100) and the apps-layout subnav, so it covered the top 36px of both
when dev mode was on.
Moved to top: 0, z-index 1001 (above the topbar). When the banner is
visible, body.has-dev-banner now also shifts every other fixed-
positioned chrome element down by the banner's 36px:
.topbar 0 → 36
.sidebar 60 → 96
.apps-layout 60 → 96
.mobile-drawer 60 → 96 (already had this override)
Body padding-top stays at 96px (banner + topbar) — content offset is
unchanged. Standard environment-banner placement (Stripe test-mode,
GitHub staff-mode) and makes "you're in dev mode" actually visible
above your nav.
Signed-off-by: librelad <librelad@digitalangels.vip>
The custom-drawn green box + white tick was reading too utilitarian
against the row's other buttons (and the tick itself had defaulted
black against the dim green fill, hard to spot). Switches both
.task-select-box (per-row) and the master Select-all to the same
chrome the setup wizard uses for its app-pick cards:
- accent gradient fill on :checked (was status-success)
- 12px white SVG checkmark (inline data: URL, same one as
.setup-app input[type=checkbox]:checked::after)
- subtle inset border at rest, accent glow on hover/focus
- 0.22s setupCheckPop / taskCheckPop pop-in on tick
- indeterminate state on the master shows a horizontal dash,
drawn from a second inline SVG (still white on accent)
Sized to 18px so the row checkbox sits clean alongside the 22px-tall
.task-btn buttons. The master in the action bar reuses the same box
spec (no separate variant), matching the wizard's "one checkbox style,
many places" pattern.
Signed-off-by: librelad <librelad@digitalangels.vip>
Adds per-row checkboxes (right of the Delete button, per request), a
master "Select all" toggle in the action bar, and morphs Clear All into
"Delete Selected (N)" the moment 1+ rows are ticked. Both paths go
through the same _showClearAllModal redesigned in 1ccc4bb — same UX,
same "Cancel running too" toggle, same logic; only the title + eyebrow
shift to reflect which mode the user came in through:
all → "Delete all N tasks?" eyebrow "Delete Tasks"
selected → "Delete N selected tasks?" eyebrow "Delete Selected"
State lives in this.selectedTaskIds (Set<string>). The row checkboxes
fire toggleTaskSelection(id, checked); the master fires toggleSelectAll
which ticks/unticks every visible row's checkbox in one pass (visible,
not all-of-this.tasks — so category filters DTRT).
_updateSelectionUI() reconciles three things on every change:
- the Clear All button label + title attr
- the master checkbox's checked/indeterminate state (some-but-not-all
visible → indeterminate dash, all → checked, none → unchecked)
- hooked into renderTasks() so category-switches don't leave stale
UI
performClearAll(opts) now accepts opts.targets — the subset to operate
on. clearAllTasks() passes either the selection or this.tasks depending
on mode. The active-task cancel-or-skip logic (cancelRunning toggle) is
unchanged — runs identically over the smaller set.
CSS:
.task-select — 22×22 framed checkbox matching the .task-btn
buttons it sits next to (border, hover green,
focus outline)
.task-select-box — custom box with check + indeterminate dash
drawn via ::after, no SVG dependency
.task-select-all — text-style toggle in the action bar with the
same custom box
No new globals. Hooked up via the existing window.tasksManager.
Signed-off-by: librelad <librelad@digitalangels.vip>
.task-command was still using var(--status-success) (#28a745) which reads
muddy olive against the nebula gradient — the same dimming the status
pills and apps-installed pill already work around with #86efac. The
empty-state row ("$ No tasks found …") was the most visible offender.
Switches .task-command to the same bright mint already used elsewhere.
Same edit, while I was there: the empty-state copy interpolated
categoryName.toLowerCase() as `No ${cat} tasks found`, so the "All Tasks"
category produced "No all tasks tasks found". Special-cases the all
bucket and strips the trailing word when the category name already
includes it ("Running Tasks" → "No running tasks found", not "running
tasks tasks").
Signed-off-by: librelad <librelad@digitalangels.vip>
The Features section was a grab-bag of ~27 toggles, most of which are
either category-specific (firewall, SSL, Docker network, SSH hardening)
or install-time choices that brick the box if flipped on a live
install (the WebUI / config / CLI / Docker requirements). One page
made auditing easier but flattened the risk hierarchy.
Reorganised so each toggle lives where it conceptually belongs, and
the dangerous install-time set is double-gated:
network_docker (Advanced) DOCKER_NETWORK, DOCKER_NETWORK_PRUNE,
DOCKER_SWITCHER
network_firewall (Advanced) UFW, UFWD, WHITELIST_PORT_UPDATER [new]
network_domains (field-Adv) SSLCERTS
security_ssh (Advanced) SSHKEY_DOWNLOADER, SSH_DISABLE_PASSWORDS,
BCRYPT_SAVE, GLUETUN_FOR_ALL [new]
general_terminal (Advanced) CRONTAB, CONFIGS_CHECK,
CONFIGS_AUTO_UPDATE, CONFIGS_AUTO_DELETE,
MISSING_IPS, CONTINUE_PROMPT,
SUGGEST_INSTALLS, SUGGEST_METRICS
general_install (Adv+DEV) CONFIG, COMMAND, WEBUI, WEBUI_SERVICE,
DATABASE, PASSWORDS, DOCKER_CE,
DOCKER_COMPOSE
The install-time eight are marked **ADVANCED** **DEV** — invisible
unless Developer Mode is on AND "Show Advanced Options" is expanded.
Each field's description was updated to note "Disabling on an existing
install will brick the system" / "install-time choice only" so a user
who does get to the toggle understands the gun before pulling the
trigger.
Other cleanup that fell out:
- Removed `configs/features/` directory entirely.
- Added the two new subcategories to SUBCATEGORY_ORDER in
network/.category and security/.category.
- Dropped the `category === 'features'` Danger Zone header special-case
in config-manager.js and its .danger-zone-section--header-only CSS
variant (sole user).
- Trimmed an obsolete "Edit the features config" notice in
check_requirements.sh.
Signed-off-by: librelad <librelad@digitalangels.vip>
Two unrelated UI polish items.
1. Task notification titles. The "Config_update task completed!" toast
was leaking the literal task-type id because the friendly-name map
in the taskCompleted listener didn't list `config_update`,
`update_config`, or `system_update`, and the fallback only
capitalized the first letter. Same fallback was duplicated in
task-actions.js's started-toast path.
Extracted into `TasksManager.formatActionTitle(action)`:
- Adds the missing entries (`config_update`/`update_config` → "Update
Config", `system_update` → "Update System").
- Smarter fallback: snake/kebab → Title Case, so an unmapped future
type renders as e.g. "Foo Bar" instead of "Foo_bar".
- Both the started (task-actions.js) and completed/failed/cancelled
(tasks-manager.js) notification paths now route through it, so the
started/done pair always reads the same.
2. Dev-mode strip styling. Earlier amber-on-amber recipe read as a
warning state; the strip is just informational. Switched to a
neutral glass surface (rgba(text,0.04) + rgba(text,0.12) border),
text-primary copy, and the icon alone in var(--accent). Label is
now centered with the close button absolute-positioned on the
right (24px / 56px right padding so the centered text never sits
under the X).
Signed-off-by: librelad <librelad@digitalangels.vip>
Two small dev-mode UX changes.
1. Banner. When CFG_DEV_MODE is on, a 36px amber-tinted strip sits flush
under the topbar with "You are currently running in Developer mode"
and a dismiss X. Dismissal is remembered in localStorage and cleared
whenever dev mode is toggled back on, so re-enabling the mode brings
the banner back. Body picks up `.has-dev-banner` while visible to
bump padding-top by the strip's height (also adjusts the mobile
drawer's top/height).
2. Toast. The auto-enable message dropped the trailing
"Click the LibrePortal logo 10× to disable." — too noisy on every
git/local page load; the easter egg is still discoverable. New
message is just "Developer mode auto-enabled — you're on a <mode>
install."
Signed-off-by: librelad <librelad@digitalangels.vip>
Reshape the dashboard's Backup status grid into a click-to-pick UI:
- Removed the inline Back-up / Restore buttons from the System config
tile. Same shape as an app tile now; LibrePortal app icon instead of
the server-stack glyph.
- Grid is 2 columns (was auto-fill min 220px). Tiles are wider, read
better, and the System tile no longer needs to span a full row to fit
inline buttons.
- Click any tile (System or app) → opens a new "Back up" modal:
* System config first (key=__system__, LibrePortal icon)
* Every installed app, alphabetical
* Checkbox per row + 'Select all' / 'Clear' shortcuts
* The tile clicked is pre-ticked
- Confirm queues backup tasks:
* Everything ticked → single `libreportal backup all` (which also
runs `backup system`) — one task instead of N
* Subset → one task per ticked item (`backup system`
and/or `backup app create <slug>`)
Restore for System config used to live on the dashboard's inline
'Restore' button. It's now reachable via the Backups tab — system
snapshots appear in the snapshot list with the standard per-row
Restore action — same path apps already use. No new UI required;
just one fewer dashboard button.
Signed-off-by: librelad <librelad@digitalangels.vip>
The user Dashboard carried a small chevron link "Admin overview →" just
above the installed-apps grid. The topbar already has a top-level "Admin"
nav-item (topbar.html:34) that goes to the same /admin route. The
dashboard link was a redundant second entry point with no extra value;
removing it tightens the dashboard layout without losing navigation.
Drops:
- dashboard-content.html: the <a class="dashboard-admin-link"> block
- admin.css: the .dashboard-admin-link rule + :hover (now orphaned)
The /admin route, the topbar Admin nav-item, and the AdminOverview JS
component all stay as-is — only the dashboard-side entry point goes.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
- Add .backup-system-card { margin-top: 20px } — the card stands alone below the
two-column cards row (which has no bottom margin), so it was butting against it.
- Add a server-stack icon to the card header (matches the nebula stroke-icon style).
- DEVELOPMENT.md: document the dashboard "System config" card + its last-backup
status (tag system=config → `system` in the dashboard JSON), the CLI/auto paths,
and that the libreportal app is excluded from the per-app grid.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
The SSH Access page was boxed to max-width 860px and centered, unlike the
Overview and System admin pages (.admin-page) which span the full content
width. Drop the cap and match .admin-page padding so /admin/tools/ssh-access
looks like the rest of the Admin area.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
New 'System' admin page (sidebar Tools group) rendering the metrics the
collector now produces:
- live ring gauges for CPU, memory, disk and load
- SVG trend charts (CPU/mem/disk/network) with 1h/6h/24h range toggle
- host info + swap + docker summary strips
- per-app table: CPU/mem bars, network, status, CPU sparkline
Charts are hand-rolled SVG in charts.js (LPCharts) — no third-party libs or
CDN calls — themed entirely from the active theme's CSS variables. The
Overview System card now links here.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
- Sidebar now groups items: Overview at top, a 'Config' heading over the config
categories, and the existing 'Tools' heading over SSH Access.
- Breadcrumb reflects the group: config pages read 'Config' (was 'Admin'), SSH
reads 'Tools', Overview stays 'Admin'.
- SSH Access page restyled to the config page's section layout
(.config-category/.domains-wrapper sections) instead of backup-style cards, so
it matches the other Admin config pages.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
Add an Admin Overview as the Admin landing (default when you open Admin): an
ops/health board distinct from the user Dashboard. Four cards built from data
we already generate — Updates (update_status.json, with one-click update),
Backups (backup dashboard.json), SSH & Security (access.json), System
(disk/memory/system_info) — each with a Manage link into the right section.
Styled like the backup dashboard (tiles/status dots).
Wire-up: 'Overview' is the top sidebar item and the default category
(handleConfig + sidebar), rendered by AdminOverview into #config-section via a
renderConfig('overview') special case. Every Admin page now shows the same
'Admin' breadcrumb header (Overview, SSH Access, and the config categories) for
a consistent Admin → Section feel. User Dashboard gets an 'Admin overview →'
link.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
Rename the Config top-nav to 'Admin' and move SSH Access into its sidebar
under a 'Tools' group, instead of a separate top-level nav item. SSH Access is
rendered by SshPage into the config main pane via a renderConfig('ssh-access')
special case; the sidebar item (config-sidebar.js) routes there. SshPage now
mounts into any container (defaults to #config-section). /ssh redirects to
/config?=ssh-access for old links; the standalone ssh-content.html is removed.
Declutters the top bar and gives system/admin features one home that scales
(updates, users, Connect settings can become sidebar entries later).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
New /ssh page (topbar nav + SPA route + SshPage controller + ssh-content.html
+ ssh.css). Reads data/ssh/access.json and lets the admin: paste a public key
to authorize a machine, remove keys, and toggle key-only login — all via
'libreportal ssh ...' tasks through the backend's lockout guards. Reuses the
backup key-card styles for a consistent look. This is the inbound counterpart
to the backup location key card (outbound): same paste-a-key model, opposite
direction.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
When a location uses SSH key auth, show a key card: paste an existing private
key, or 'Generate keypair', then the card displays the public key to copy into
the remote server's authorized_keys (with Copy/Delete). Wires to the
ssh-key-set/generate/delete CLI; key mutations refresh locations.json so the
card reflects state immediately. applySshAuthVisibility toggles the card vs the
password field by auth mode. Private key only ever flows in (base64); only the
public key is ever shown.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
- Remove the per-tab 'How LibrePortal connects…' description lines; the tab
labels already say what each panel is, and the paragraphs read as misplaced
titles.
- Give the tab panels even, comfortable padding (tabs-content padding zeroed so
the panel owns it) instead of the cramped 2px sides.
- Round the tab strip's top corners (.tabs-list) so the strip + content read as
one card — .tabs-content already rounds the bottom, leaving the top square.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
Reuse the shared .tabs-wrapper/.tab-button/.tab-panel components (same as an
app's Config/Tasks tabs) for the location editor instead of bespoke tab CSS:
emoji + label buttons, equal-width strip, accent active state. Panels toggle
via the .active class like the rest of the UI; only the panel padding is
trimmed so it nests inside the backup row.
Also drop the now-dead 'No advanced options' empty state — every type has at
least Engine + append-only in the Advanced tab.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
The expanded location row was one long form. Split it into tabs so it opens
showing only the Connection fields. Retention moves from a stacked section
into its own tab, and the advanced overrides (URI/SSH port/append-only) get
their own tab instead of the inline disclosure from the previous pass.
Field grouping is metadata-driven: locFieldGroups partitions a type's fields
into Connection vs Advanced via the configs.json "advanced" flag (with
LOC_ADVANCED_SUFFIXES as the legacy fallback). Type changes rebuild both the
Connection and Advanced panels since advanced fields are type-dependent too.
Save still reads every field across all panels (hidden tabs stay in the DOM).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
The Locations editor now renders field metadata from configs.json
(window.configData) instead of relying on the hardcoded BACKUP_LOC_FIELD_DEFS,
which drops to a fallback. Fields flagged advanced (URI override, SSH port,
append-only) move out of the main grid into a full-width "Advanced"
disclosure that's collapsed by default, so the common case stays simple.
Also load the unified config once on the backup page into window.configData
(metadata) + a flat window.systemConfigs (values). Previously systemConfigs
was only populated after a save — and with the full nested JSON, while the
code reads it as a flat map — so default-engine lookups and save-time change
detection silently misbehaved on first load. Both are now correct.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
The config-grid toggle box used the input's 12px vertical padding, but its
24px pill made it render 48px tall vs the inputs' 44px, so it sat too tall
to read as inline. Trim vertical padding to 10px so the box is 44px.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
Divider: .domains-divider was a bold 2px accent bar under every section header,
which read as a stray line. Drop it to a subtle 1px low-opacity neutral rule so
it separates without shouting.
Toggle: the boxed config toggle (.checkbox-label) used a different radius (10px),
fill (0.04) and border (0.10) than the .form-control inputs beside it (8px /
0.05 / 0.20), so it looked off and out of line. Match it to the input field box
exactly so toggles and inputs read as the same surface. The app-config
borderless toggle override is unaffected.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
Give the per-location Save changes / Delete location row some breathing room
from the bottom of the expanded card.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
Buttons: the per-location Save changes / Delete location buttons had no icons,
unlike the apps-config action buttons. Add a save (floppy) icon and a trash
icon so they match the reference; colour comes from the nebula button groups
they already belong to.
Theme refactor: move the theme-specific [data-theme="nebula"] button/topbar/CTA
rules out of the shared css/themes.css and into themes/nebula/theme.css, where
the README says theme overrides belong. css/themes.css keeps only the generic,
non-theme-scoped defaults (solid status/accent buttons, danger-zone,
warning-banner) shared by dark-blue/light. No behaviour change: the nebula file
loads after css/themes.css so the moved rules still win.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
Two follow-ups to the button restyle:
- On the nebula theme, primary/danger CTAs are translucent (rgba accent/danger
fill + white text + border), not the solid generic .btn-primary. The earlier
change only added the backup classes to the generic groups, so on nebula the
Add location / Save changes / Delete buttons fell back to a solid fill with
dark text. Add .backup-primary-btn and .backup-danger-btn to the
[data-theme="nebula"] groups too, so they match the config-page buttons.
- The per-location action row used justify-content: space-between, throwing the
two buttons to opposite edges. Switch to flex-start with a gap (like
.config-actions) and put Save changes (primary) before Delete location.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
The backup pages' primary buttons (Add location, Save changes) and Delete
location button used a local gradient + glow in backup.css, so they didn't
match the flat solid-accent buttons on the config page. Add .backup-primary-btn
and .backup-danger-btn to the shared nebula button groups in themes.css
(.btn-primary / .btn-uninstall) so they get the same solid accent/danger fill
and hover with !important across themes, and drop the local gradient/shadow/lift
from backup.css. Top-right and bottom-of-page backup buttons now match config.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
Drop the Export button from the config-backup warning banner — it's now just the alert + dismiss (x). On the Configuration tab the top-right primary action becomes an 'Export' dropdown (first item: Repository Passwords, reusing the existing export-passwords action) so more export types can be added later. Other tabs keep Backup all apps / Add location. Menu opens from the trigger and closes on outside click, item click, or tab switch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
Add a large amber alert-triangle icon to the 'keep your config backed up offline' banner and a close (x) button in its top-right. Dismissal is stored in localStorage (libreportal:backup-config-warning-dismissed) — a per-browser UI nudge, not server config — and hides both the banner and its divider until cleared.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>