From 40b15de4714cc5768b0bfd401dfcfb30fee21ee3 Mon Sep 17 00:00:00 2001 From: librelad Date: Thu, 28 May 2026 00:45:00 +0100 Subject: [PATCH] ui(tools+services): brighten tool-run buttons on Nebula and split service Logs into Details + opt-in log tail MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../libreportal/frontend/css/services.css | 18 +++++ .../js/components/app/app-tabbed-manager.js | 11 ++- .../js/components/app/services-manager.js | 73 +++++++++++++++---- .../frontend/themes/nebula/theme.css | 27 ++++++- 4 files changed, 108 insertions(+), 21 deletions(-) diff --git a/containers/libreportal/frontend/css/services.css b/containers/libreportal/frontend/css/services.css index 17dc8db..4836a97 100644 --- a/containers/libreportal/frontend/css/services.css +++ b/containers/libreportal/frontend/css/services.css @@ -242,6 +242,24 @@ } body:not(.lp-ui--advanced) .service-rich { display: none; } +/* "Show logs" / "Hide logs" toggle sitting at the very bottom of the + open .task-details panel. Logs are off by default — the user opts in + per service so a row of expanded services doesn't open one SSE stream + each. Centred and given a little top breathing room so it reads as + a footer action rather than another inline button. */ +.service-logs-toggle { + display: flex; + justify-content: center; + margin-top: 8px; + margin-bottom: 4px; +} +.service-logs-toggle .service-show-logs { + padding: 6px 14px; +} +.service-item.logs-shown .service-logs-toggle { + margin-bottom: 8px; +} + /* ============================================================ Beginner / Advanced toggle in the Services tab title row. Project-wide visual; same component will be reused wherever diff --git a/containers/libreportal/frontend/js/components/app/app-tabbed-manager.js b/containers/libreportal/frontend/js/components/app/app-tabbed-manager.js index c4046a7..f6fe575 100755 --- a/containers/libreportal/frontend/js/components/app/app-tabbed-manager.js +++ b/containers/libreportal/frontend/js/components/app/app-tabbed-manager.js @@ -942,10 +942,13 @@ class AppTabbedManager { if (button.classList.contains('tab-button')) { return; } - // Logs toggle buttons stay clickable while a task runs — viewing - // log output is read-only and the whole point during a long task. - if (button.dataset.action === 'toggle-logs' || - button.classList.contains('service-logs') || + // Details + log-stream toggles stay clickable while a task runs — + // viewing service details and tailing logs is read-only and the + // whole point during a long task. + if (button.dataset.action === 'toggle-details' || + button.dataset.action === 'toggle-log-stream' || + button.classList.contains('service-details') || + button.classList.contains('service-show-logs') || button.classList.contains('toggle-details')) { return; } diff --git a/containers/libreportal/frontend/js/components/app/services-manager.js b/containers/libreportal/frontend/js/components/app/services-manager.js index 088754f..796eeda 100644 --- a/containers/libreportal/frontend/js/components/app/services-manager.js +++ b/containers/libreportal/frontend/js/components/app/services-manager.js @@ -428,7 +428,7 @@ class ServicesManager { return `
-
+
${iconHtml} ${escapeHtml(svc.serviceName)} @@ -446,11 +446,11 @@ class ServicesManager { Restart -
@@ -462,7 +462,14 @@ class ServicesManager {
State: ${escapeHtml(state)}
Status: ${escapeHtml(svc.statusText)}
-
+ ${this._renderRichDetail(info)} +
+ +
+ - ${this._renderRichDetail(info)}
`; } @@ -494,8 +500,10 @@ class ServicesManager { if (action === 'restart') { await this._restartService(serviceName, btn); - } else if (action === 'toggle-logs') { - this._toggleLogs(item, serviceName); + } else if (action === 'toggle-details') { + this._toggleDetails(item, serviceName); + } else if (action === 'toggle-log-stream') { + this._toggleLogStream(item, serviceName); } else if (action === 'resume-logs') { this._resumeLogs(item, serviceName); } @@ -593,26 +601,61 @@ class ServicesManager { } } - _toggleLogs(item, serviceName) { - // The task-list uses a .task-details-open class (not the `hidden` - // attribute) because .task-details has `display: none` baked in. + // Toggle the .task-details panel (meta + rich detail + log toggle). + // Logs are NOT auto-opened here — the user has to click "Show logs" + // explicitly. Closing the panel also tears down any open log stream + // and resets the inline log block back to its hidden state. + _toggleDetails(item, serviceName) { const details = item.querySelector('.task-details'); - const output = item.querySelector('.service-log-output'); - if (!details || !output) return; + if (!details) return; const isOpen = details.classList.contains('task-details-open'); if (isOpen) { details.classList.remove('task-details-open'); - this._closeLogStream(serviceName); - this._hideLogOverlay(output); + this._resetLogBlock(item, serviceName); + return; + } + details.classList.add('task-details-open'); + } + + // Show / hide the log block inside the open details panel. Opening + // starts the SSE stream so logs auto-update; closing tears it down. + _toggleLogStream(item, serviceName) { + const logsBlock = item.querySelector('.task-logs'); + const output = item.querySelector('.service-log-output'); + if (!logsBlock || !output) return; + + const showing = item.classList.contains('logs-shown'); + if (showing) { + this._resetLogBlock(item, serviceName); return; } - details.classList.add('task-details-open'); + logsBlock.style.display = ''; output.textContent = ''; this._hideLogOverlay(output); output.dataset.stream = 'connecting'; this._openLogStream(serviceName, output); + item.classList.add('logs-shown'); + this._setLogToggleLabel(item, 'Hide logs'); + } + + // Tear down the log stream and put the block back to its closed state. + // Used both when the user clicks "Hide logs" and when the parent + // details panel collapses (so reopening starts in the clean state). + _resetLogBlock(item, serviceName) { + const logsBlock = item.querySelector('.task-logs'); + const output = item.querySelector('.service-log-output'); + if (output) this._hideLogOverlay(output); + if (logsBlock) logsBlock.style.display = 'none'; + this._closeLogStream(serviceName); + item.classList.remove('logs-shown'); + this._setLogToggleLabel(item, 'Show logs'); + } + + _setLogToggleLabel(item, text) { + const lbl = item.querySelector('.service-show-logs .task-btn-label'); + if (lbl) lbl.textContent = text; } _openLogStream(serviceName, outputEl) { diff --git a/containers/libreportal/frontend/themes/nebula/theme.css b/containers/libreportal/frontend/themes/nebula/theme.css index 86d0efa..fc759b3 100644 --- a/containers/libreportal/frontend/themes/nebula/theme.css +++ b/containers/libreportal/frontend/themes/nebula/theme.css @@ -247,7 +247,8 @@ [data-theme="nebula"] .uninstall-btn, [data-theme="nebula"] .btn-uninstall, [data-theme="nebula"] .btn-danger, -[data-theme="nebula"] .backup-danger-btn { +[data-theme="nebula"] .backup-danger-btn, +[data-theme="nebula"] .tool-run-btn.destructive { background: rgba(var(--status-danger-rgb), 0.35) !important; color: var(--text-primary) !important; border: 1px solid rgba(var(--status-danger-rgb), 0.65) !important; @@ -259,12 +260,34 @@ [data-theme="nebula"] .uninstall-btn:hover:not(:disabled), [data-theme="nebula"] .btn-uninstall:hover:not(:disabled), [data-theme="nebula"] .btn-danger:hover:not(:disabled), -[data-theme="nebula"] .backup-danger-btn:hover:not(:disabled) { +[data-theme="nebula"] .backup-danger-btn:hover:not(:disabled), +[data-theme="nebula"] .tool-run-btn.destructive:hover:not(:disabled) { background: rgba(var(--status-danger-rgb), 0.50) !important; border-color: rgba(var(--status-danger-rgb), 0.85) !important; transform: translateY(-1px); } +/* Tools-page Run buttons — same translucent recipe as service-trigger + (the small "Open" pill on installed app cards). The 0.12/0.30 alphas + from tools.css read as muddy green/red against Nebula's cosmic + gradient, especially with --status-success/danger text — bump to + 0.35/0.65 with neutral text so they pop. .destructive picks up the + danger recipe above via the chained selector. */ +[data-theme="nebula"] .tool-run-btn { + background: rgba(var(--status-success-rgb), 0.35) !important; + color: var(--text-primary) !important; + border: 1px solid rgba(var(--status-success-rgb), 0.65) !important; + text-shadow: none; + font-weight: 600 !important; + transition: background 0.18s ease, border-color 0.18s ease, transform 0.15s ease !important; +} + +[data-theme="nebula"] .tool-run-btn:hover:not(:disabled):not(.destructive) { + background: rgba(var(--status-success-rgb), 0.50) !important; + border-color: rgba(var(--status-success-rgb), 0.85) !important; + transform: translateY(-1px); +} + [data-theme="nebula"] .manage-btn, [data-theme="nebula"] .btn-manage, [data-theme="nebula"] .btn-primary,