ux(services): global Beginner/Advanced UI mode + log block first in panel

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>
This commit is contained in:
librelad 2026-05-27 23:09:07 +01:00
parent 092c1c215e
commit 8001e678e0
2 changed files with 132 additions and 4 deletions

View File

@ -231,7 +231,8 @@
Rich container detail panel limits, image, healthcheck, Rich container detail panel limits, image, healthcheck,
networks, mounts. Rendered inside .task-details above the networks, mounts. Rendered inside .task-details above the
log container so it's discoverable from the existing "Logs" 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 { .service-rich {
display: flex; display: flex;
@ -239,6 +240,72 @@
gap: 14px; gap: 14px;
margin: 8px 0 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 { .service-rich-grid {
display: grid; display: grid;

View File

@ -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. // Services tab on the app detail page.
// //
// Each row renders a single docker compose service with: // Each row renders a single docker compose service with:
@ -87,10 +128,23 @@ class ServicesManager {
_titleBlock(appName) { _titleBlock(appName) {
const display = (appName || '').replace(/[-_]/g, ' ').replace(/\b\w/g, c => c.toUpperCase()); 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 ` return `
<div class="services-title"> <div class="services-title">
<h3> Services</h3> <div class="services-title-main">
<p>Inspect, restart and tail logs for the docker compose services that make up ${escapeHtml(display)}</p> <h3> Services</h3>
<p>Inspect, restart and tail logs for the docker compose services that make up ${escapeHtml(display)}</p>
</div>
<label class="lp-ui-advanced-toggle" title="Reveal container internals (limits, mounts, networks, healthcheck)">
<input type="checkbox" data-action="toggle-advanced" ${adv}>
<span class="lp-ui-advanced-toggle-track"><span class="lp-ui-advanced-toggle-thumb"></span></span>
<span class="lp-ui-advanced-toggle-label">Advanced</span>
</label>
</div>`; </div>`;
} }
@ -449,7 +503,6 @@ class ServicesManager {
<div class="meta-item"><strong>State:</strong> ${escapeHtml(state)}</div> <div class="meta-item"><strong>State:</strong> ${escapeHtml(state)}</div>
<div class="meta-item"><strong>Status:</strong> ${escapeHtml(svc.statusText)}</div> <div class="meta-item"><strong>Status:</strong> ${escapeHtml(svc.statusText)}</div>
</div> </div>
${this._renderRichDetail(info)}
<div class="task-logs" style="position: relative;"> <div class="task-logs" style="position: relative;">
<div class="log-container terminal-style service-log-output" data-stream="off" style="height: 200px; overflow-y: auto; border: 1px solid #444; border-radius: 4px; padding: 10px; background-color: #1a1a1a;"></div> <div class="log-container terminal-style service-log-output" data-stream="off" style="height: 200px; overflow-y: auto; border: 1px solid #444; border-radius: 4px; padding: 10px; background-color: #1a1a1a;"></div>
<div class="service-log-overlay" style="position: absolute; inset: 0; display: none; flex-direction: column; align-items: center; justify-content: center; gap: 10px; background: rgba(20, 20, 24, 0.85); backdrop-filter: blur(2px); border-radius: 4px;"> <div class="service-log-overlay" style="position: absolute; inset: 0; display: none; flex-direction: column; align-items: center; justify-content: center; gap: 10px; background: rgba(20, 20, 24, 0.85); backdrop-filter: blur(2px); border-radius: 4px;">
@ -460,6 +513,7 @@ class ServicesManager {
</button> </button>
</div> </div>
</div> </div>
${this._renderRichDetail(info)}
</div> </div>
</div>`; </div>`;
} }
@ -487,6 +541,13 @@ class ServicesManager {
this._resumeLogs(item, serviceName); 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) { async _restartService(serviceName, btn) {