From 8001e678e091b8098e69d266eeedc7b3e5b0c7af Mon Sep 17 00:00:00 2001 From: librelad Date: Wed, 27 May 2026 23:09:07 +0100 Subject: [PATCH] ux(services): global Beginner/Advanced UI mode + log block first in panel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../libreportal/frontend/css/services.css | 69 ++++++++++++++++++- .../js/components/app/services-manager.js | 67 +++++++++++++++++- 2 files changed, 132 insertions(+), 4 deletions(-) diff --git a/containers/libreportal/frontend/css/services.css b/containers/libreportal/frontend/css/services.css index b483c37..17dc8db 100644 --- a/containers/libreportal/frontend/css/services.css +++ b/containers/libreportal/frontend/css/services.css @@ -231,7 +231,8 @@ Rich container detail panel — limits, image, healthcheck, networks, mounts. Rendered inside .task-details above the log container so it's discoverable from the existing "Logs" - expand action. + expand action. Gated behind the global Advanced UI mode so + a Beginner doesn't see a wall of technical detail. ============================================================ */ .service-rich { display: flex; @@ -239,6 +240,72 @@ gap: 14px; margin: 8px 0 14px; } +body:not(.lp-ui--advanced) .service-rich { display: none; } + +/* ============================================================ + Beginner / Advanced toggle in the Services tab title row. + Project-wide visual; same component will be reused wherever + else surfaces grow an Advanced-only section. + ============================================================ */ +.services-title { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 16px; +} +.services-title-main { flex: 1; min-width: 0; } + +.lp-ui-advanced-toggle { + display: inline-flex; + align-items: center; + gap: 8px; + cursor: pointer; + user-select: none; + flex-shrink: 0; +} +.lp-ui-advanced-toggle input { + position: absolute; + opacity: 0; + pointer-events: none; +} +.lp-ui-advanced-toggle-track { + position: relative; + width: 34px; + height: 18px; + background: rgba(var(--text-rgb), 0.18); + border-radius: 999px; + transition: background .15s ease; + flex-shrink: 0; +} +.lp-ui-advanced-toggle-thumb { + position: absolute; + top: 2px; + left: 2px; + width: 14px; + height: 14px; + background: var(--text-primary); + border-radius: 50%; + transition: transform .15s ease, background .15s ease; +} +.lp-ui-advanced-toggle input:checked + .lp-ui-advanced-toggle-track { + background: var(--accent); +} +.lp-ui-advanced-toggle input:checked + .lp-ui-advanced-toggle-track .lp-ui-advanced-toggle-thumb { + transform: translateX(16px); + background: var(--text-on-accent, #0a1426); +} +.lp-ui-advanced-toggle-label { + font-size: 0.78rem; + font-weight: 600; + letter-spacing: 0.02em; + color: rgba(var(--text-rgb), 0.7); + transition: color .15s ease; +} +.lp-ui-advanced-toggle:hover .lp-ui-advanced-toggle-label { color: var(--text-primary); } +.lp-ui-advanced-toggle input:focus-visible + .lp-ui-advanced-toggle-track { + outline: 2px solid rgba(var(--accent-rgb), 0.6); + outline-offset: 2px; +} .service-rich-grid { display: grid; diff --git a/containers/libreportal/frontend/js/components/app/services-manager.js b/containers/libreportal/frontend/js/components/app/services-manager.js index 5c47559..5075f60 100644 --- a/containers/libreportal/frontend/js/components/app/services-manager.js +++ b/containers/libreportal/frontend/js/components/app/services-manager.js @@ -1,3 +1,44 @@ +// Global "UI mode" — Beginner vs Advanced. First foothold of a +// project-wide pattern: any surface that wants to render extra-technical +// detail (mounts, limits, internals, raw IDs, …) gates it on +// body.lp-ui--advanced. Default is OFF (Beginner) so a newcomer isn't +// overwhelmed; flipping the toggle reveals everything site-wide. +// +// The flag persists per-browser via localStorage. The install will later +// seed the initial value from CFG_INSTALL_LEVEL (Beginner | Advanced) +// chosen on the very first setup screen — once that lands, the body +// class is set server-side at template render time so there's no FOUC. +// Until then, the user picks it from any surface that exposes a toggle +// (currently the Services tab). +(function setupLpUi() { + if (window.LpUi && window.LpUi.advanced) return; + const KEY = 'lp.ui.advanced'; + const advanced = { + get() { + try { return localStorage.getItem(KEY) === '1'; } catch { return false; } + }, + set(on) { + const v = !!on; + try { localStorage.setItem(KEY, v ? '1' : '0'); } catch { /* private mode */ } + document.body && document.body.classList.toggle('lp-ui--advanced', v); + window.dispatchEvent(new CustomEvent('lp-ui-advanced-changed', { detail: { advanced: v } })); + }, + apply() { + if (!document.body) return; + document.body.classList.toggle('lp-ui--advanced', this.get()); + }, + }; + window.LpUi = { ...(window.LpUi || {}), advanced }; + // Apply as early as possible — once the body exists, we drop the class + // in so any already-rendered surface (e.g. another tab re-mounted with + // .service-rich present) reflects the saved mode immediately. + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', () => advanced.apply(), { once: true }); + } else { + advanced.apply(); + } +})(); + // Services tab on the app detail page. // // Each row renders a single docker compose service with: @@ -87,10 +128,23 @@ class ServicesManager { _titleBlock(appName) { const display = (appName || '').replace(/[-_]/g, ' ').replace(/\b\w/g, c => c.toUpperCase()); + const adv = window.LpUi?.advanced?.get() ? 'checked' : ''; + // The toggle is the visible surface for the global "Advanced UI" mode + // ([[window.LpUi.advanced]]). Flipping it here unhides the rich + // container detail (limits, mounts, networks, healthcheck) across + // every service row. The body class drives the CSS show/hide, so + // other surfaces that opt into the same mode get it for free. return `
-

⚡ Services

-

Inspect, restart and tail logs for the docker compose services that make up ${escapeHtml(display)}

+
+

⚡ Services

+

Inspect, restart and tail logs for the docker compose services that make up ${escapeHtml(display)}

+
+
`; } @@ -449,7 +503,6 @@ class ServicesManager {
State: ${escapeHtml(state)}
Status: ${escapeHtml(svc.statusText)}
- ${this._renderRichDetail(info)}
+ ${this._renderRichDetail(info)} `; } @@ -487,6 +541,13 @@ class ServicesManager { this._resumeLogs(item, serviceName); } }); + // Advanced toggle lives in the title, not on a row — bind change here + // so it works regardless of which service rows are present. + root.addEventListener('change', (ev) => { + const cb = ev.target.closest('[data-action="toggle-advanced"]'); + if (!cb || !window.LpUi?.advanced) return; + window.LpUi.advanced.set(cb.checked); + }); } async _restartService(serviceName, btn) {