Merge claude/1

This commit is contained in:
librelad 2026-05-28 00:45:00 +01:00
commit 1a1813f1ea
4 changed files with 108 additions and 21 deletions

View File

@ -242,6 +242,24 @@
} }
body:not(.lp-ui--advanced) .service-rich { display: none; } 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. Beginner / Advanced toggle in the Services tab title row.
Project-wide visual; same component will be reused wherever Project-wide visual; same component will be reused wherever

View File

@ -942,10 +942,13 @@ class AppTabbedManager {
if (button.classList.contains('tab-button')) { if (button.classList.contains('tab-button')) {
return; return;
} }
// Logs toggle buttons stay clickable while a task runs — viewing // Details + log-stream toggles stay clickable while a task runs —
// log output is read-only and the whole point during a long task. // viewing service details and tailing logs is read-only and the
if (button.dataset.action === 'toggle-logs' || // whole point during a long task.
button.classList.contains('service-logs') || 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')) { button.classList.contains('toggle-details')) {
return; return;
} }

View File

@ -428,7 +428,7 @@ class ServicesManager {
return ` return `
<div class="task-item service-item" data-service="${escapeHtml(svc.serviceName)}" data-state="${state}"> <div class="task-item service-item" data-service="${escapeHtml(svc.serviceName)}" data-state="${state}">
<div class="task-header" data-action="toggle-logs"> <div class="task-header" data-action="toggle-details">
<div class="task-info"> <div class="task-info">
${iconHtml} ${iconHtml}
<span class="task-title">${escapeHtml(svc.serviceName)}</span> <span class="task-title">${escapeHtml(svc.serviceName)}</span>
@ -446,11 +446,11 @@ class ServicesManager {
</svg> </svg>
<span class="task-btn-label">Restart</span> <span class="task-btn-label">Restart</span>
</button> </button>
<button class="task-btn toggle-details service-logs" data-action="toggle-logs" title="Toggle live logs"> <button class="task-btn toggle-details service-details" data-action="toggle-details" title="Show service details">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="6,9 12,15 18,9"></polyline> <polyline points="6,9 12,15 18,9"></polyline>
</svg> </svg>
<span class="task-btn-label">Logs</span> <span class="task-btn-label">Details</span>
</button> </button>
</div> </div>
</div> </div>
@ -462,7 +462,14 @@ 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>
<div class="task-logs" style="position: relative;"> ${this._renderRichDetail(info)}
<div class="service-logs-toggle">
<button type="button" class="task-btn service-show-logs" data-action="toggle-log-stream" title="Tail this container's logs">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/></svg>
<span class="task-btn-label">Show logs</span>
</button>
</div>
<div class="task-logs" style="position: relative; display: none;">
<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;">
<div class="service-log-overlay-msg" style="color: #ddd; font-size: 13px;"></div> <div class="service-log-overlay-msg" style="color: #ddd; font-size: 13px;"></div>
@ -472,7 +479,6 @@ class ServicesManager {
</button> </button>
</div> </div>
</div> </div>
${this._renderRichDetail(info)}
</div> </div>
</div>`; </div>`;
} }
@ -494,8 +500,10 @@ class ServicesManager {
if (action === 'restart') { if (action === 'restart') {
await this._restartService(serviceName, btn); await this._restartService(serviceName, btn);
} else if (action === 'toggle-logs') { } else if (action === 'toggle-details') {
this._toggleLogs(item, serviceName); this._toggleDetails(item, serviceName);
} else if (action === 'toggle-log-stream') {
this._toggleLogStream(item, serviceName);
} else if (action === 'resume-logs') { } else if (action === 'resume-logs') {
this._resumeLogs(item, serviceName); this._resumeLogs(item, serviceName);
} }
@ -593,26 +601,61 @@ class ServicesManager {
} }
} }
_toggleLogs(item, serviceName) { // Toggle the .task-details panel (meta + rich detail + log toggle).
// The task-list uses a .task-details-open class (not the `hidden` // Logs are NOT auto-opened here — the user has to click "Show logs"
// attribute) because .task-details has `display: none` baked in. // 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 details = item.querySelector('.task-details');
const output = item.querySelector('.service-log-output'); if (!details) return;
if (!details || !output) return;
const isOpen = details.classList.contains('task-details-open'); const isOpen = details.classList.contains('task-details-open');
if (isOpen) { if (isOpen) {
details.classList.remove('task-details-open'); details.classList.remove('task-details-open');
this._closeLogStream(serviceName); this._resetLogBlock(item, serviceName);
this._hideLogOverlay(output); 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; return;
} }
details.classList.add('task-details-open'); logsBlock.style.display = '';
output.textContent = ''; output.textContent = '';
this._hideLogOverlay(output); this._hideLogOverlay(output);
output.dataset.stream = 'connecting'; output.dataset.stream = 'connecting';
this._openLogStream(serviceName, output); 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) { _openLogStream(serviceName, outputEl) {

View File

@ -247,7 +247,8 @@
[data-theme="nebula"] .uninstall-btn, [data-theme="nebula"] .uninstall-btn,
[data-theme="nebula"] .btn-uninstall, [data-theme="nebula"] .btn-uninstall,
[data-theme="nebula"] .btn-danger, [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; background: rgba(var(--status-danger-rgb), 0.35) !important;
color: var(--text-primary) !important; color: var(--text-primary) !important;
border: 1px solid rgba(var(--status-danger-rgb), 0.65) !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"] .uninstall-btn:hover:not(:disabled),
[data-theme="nebula"] .btn-uninstall:hover:not(:disabled), [data-theme="nebula"] .btn-uninstall:hover:not(:disabled),
[data-theme="nebula"] .btn-danger: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; background: rgba(var(--status-danger-rgb), 0.50) !important;
border-color: rgba(var(--status-danger-rgb), 0.85) !important; border-color: rgba(var(--status-danger-rgb), 0.85) !important;
transform: translateY(-1px); 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"] .manage-btn,
[data-theme="nebula"] .btn-manage, [data-theme="nebula"] .btn-manage,
[data-theme="nebula"] .btn-primary, [data-theme="nebula"] .btn-primary,