librelad 6fd7b0bf61 fix(tasks): use canonical section loader for task-log loading state
The task-log "Loading logs..." state rendered as a bare left-aligned text
line (or a tiny ad-hoc 16px spinner overlay), which read as unfinished next
to the rest of the UI. Swap all three log-loading spots (initial placeholder,
toggle-open fetch, running-task stream placeholder) to the canonical
window.lpLoadingBox('Loading logs…'), and scope .lp-loading inside the log
terminal box to fill it and drop its own card chrome so the spinner sits dead
centre over the terminal surface instead of a box-in-a-box. Widen the stream
overlay-removal selector to also clear .lp-loading.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-18 20:04:56 +01:00

1050 lines
25 KiB
CSS

/* Tasks page styling. Extracted from tasks-content.html so theme
overrides and edits live alongside the rest of the CSS. All
colors reference theme variables — see themes/<name>/theme.css. */
/* Tasks Layout - Match Apps/Config Style */
.tasks-layout {
display: flex;
height: calc(100vh - 60px);
background: transparent;
}
/* Sidebar Styles - Match existing LibrePortal style */
.sidebar-container {
width: 220px;
background: var(--bg-primary);
border-right: 1px solid var(--border-color);
display: flex;
flex-direction: column;
}
.sidebar {
width: 220px;
height: 100%;
overflow-y: auto;
}
.sidebar h2 {
color: var(--text-primary);
font-size: 18px;
font-weight: 600;
margin-bottom: 16px;
padding-bottom: 8px;
border-bottom: 1px solid var(--border-color);
}
.sidebar-category {
margin-bottom: 24px;
padding: 0 20px;
}
.sidebar-category h3 {
color: var(--text-secondary);
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
margin-bottom: 12px;
display: flex;
align-items: center;
gap: 8px;
}
.sidebar-items {
display: flex;
flex-direction: column;
gap: 4px;
}
.sidebar-item {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 12px;
color: var(--text-secondary);
text-decoration: none;
border-radius: 6px;
transition: all 0.2s;
font-size: 14px;
}
.sidebar-item:hover {
background: var(--bg-hover);
color: var(--text-primary);
}
.sidebar-item.active {
background: var(--accent-color);
color: white;
}
.task-count {
margin-left: auto;
background: var(--bg-secondary);
padding: 2px 6px;
border-radius: 12px;
font-size: 11px;
font-weight: 600;
min-width: 20px;
text-align: center;
}
.sidebar-item.active .task-count {
background: rgba(255, 255, 255, 0.2);
color: white;
}
/* Main Content Area */
.main-content {
flex: 1;
display: flex;
flex-direction: column;
background: transparent;
overflow: hidden;
}
/* Status Bar — glassy strip matching the loading-screen system-card recipe. */
.terminal-status-bar {
background: rgba(var(--text-rgb), 0.04);
border-bottom: 1px solid rgba(var(--text-rgb), 0.08);
padding: 12px 20px;
display: flex;
align-items: center;
gap: 16px;
flex-wrap: wrap;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
}
.status-item {
display: flex;
align-items: center;
gap: 6px;
color: var(--text-muted);
font-size: 11px;
}
.status-indicator {
width: 8px;
height: 8px;
border-radius: 50%;
display: inline-block;
}
.status-queued { background: var(--status-warning); text-transform: uppercase !important; }
.status-running { background: var(--status-success); animation: pulse 1.5s infinite; text-transform: uppercase !important; }
.status-completed { background: var(--status-success); text-transform: uppercase !important; }
.status-failed { background: var(--status-danger); text-transform: uppercase !important; }
/* Force uppercase on all task status elements */
.task-status.status-queued,
.task-status.status-running,
.task-status.status-completed,
.task-status.status-failed,
.task-status.status-cancelled {
text-transform: uppercase !important;
}
.status-installed {
background: var(--status-success);
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.refresh-btn {
background: rgba(var(--text-rgb), 0.04);
border: 1px solid rgba(var(--text-rgb), 0.12);
color: var(--text-secondary);
padding: 4px 10px;
border-radius: 8px;
cursor: pointer;
display: flex;
align-items: center;
gap: 5px;
font-size: 11px;
margin-left: auto;
transition: background 0.18s ease, border-color 0.18s ease, color 0.18s ease;
}
.refresh-btn:hover {
background: rgba(var(--accent-rgb), 0.15);
border-color: rgba(var(--accent-rgb), 0.45);
color: var(--accent);
}
.clear-btn {
background: rgba(var(--text-rgb), 0.04);
border: 1px solid rgba(var(--text-rgb), 0.12);
color: var(--text-secondary);
padding: 4px 10px;
border-radius: 8px;
cursor: pointer;
display: flex;
align-items: center;
gap: 5px;
font-size: 11px;
margin-left: 8px;
transition: background 0.18s ease, border-color 0.18s ease, color 0.18s ease;
}
.clear-btn:hover {
background: rgba(var(--status-danger-rgb), 0.18);
border-color: rgba(var(--status-danger-rgb), 0.50);
color: var(--status-danger);
}
/* Tasks Terminal — the scroll viewport. It fills the remaining height and
scrolls when the list overflows, so the panel inside can hug its content
instead of stretching to the bottom of the page. */
.tasks-terminal {
flex: 1;
overflow-y: auto;
display: flex;
flex-direction: column;
}
/* Recessed dark panel holding the task list — same recipe as the fleet
Overview's .ov-tab-body / the per-app .tasks-container, so the list reads
as a contained box under the status-bar strip rather than floating on the
page gradient. Sizes to its content (no flex-grow) so the box ends at the
last task rather than overhanging past it. */
.tasks-list {
padding: 16px;
margin: 16px;
background: rgba(var(--bg-rgb), 0.2);
border-radius: 8px;
}
/* Hide scrollbar when not needed, show only when scrolling */
.tasks-terminal::-webkit-scrollbar {
width: 8px;
}
.tasks-terminal::-webkit-scrollbar-track {
background: transparent;
}
.tasks-terminal::-webkit-scrollbar-thumb {
background: var(--border);
border-radius: 4px;
}
.tasks-terminal::-webkit-scrollbar-thumb:hover {
background: var(--border-strong);
}
/* Hide scrollbar by default, show only on hover or when content overflows */
.task-highlighted {
border: 2px solid var(--accent);
background: var(--accent-soft);
}
.task-details-open {
display: block !important;
}
@keyframes blink {
0%, 50% { opacity: 1; }
51%, 100% { opacity: 0; }
}
/* Task Items — glass tiles in the loading-screen system-card style:
light translucent fill, soft white border, inset top highlight, hover
lifts the card with a cyan glow. */
.task-item {
background: rgba(var(--text-rgb), 0.05);
border: 1px solid rgba(var(--text-rgb), 0.10);
border-radius: 12px;
margin-bottom: 10px;
overflow: hidden;
box-shadow: inset 0 1px 0 rgba(var(--text-rgb), 0.06);
transition: background 0.2s ease, border-color 0.2s ease,
transform 0.2s ease, box-shadow 0.2s ease;
}
.task-item:hover {
background: rgba(var(--text-rgb), 0.08);
border-color: rgba(var(--accent-rgb), 0.40);
transform: translateY(-2px);
box-shadow: inset 0 1px 0 rgba(var(--text-rgb), 0.10);
}
/* The last tile's bottom margin would stack on the list's 16px bottom padding,
leaving more gap below the last row than above the first. Drop it so the dark
panel's inner padding reads equal top and bottom. */
.task-item:last-child {
margin-bottom: 0;
}
.task-header {
padding: 4px 16px;
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
}
.task-info {
display: flex;
align-items: center;
gap: 12px;
flex: 1;
}
.task-title {
font-size: 12px;
font-weight: 500;
color: var(--text-primary);
line-height: 1.3;
}
.task-status {
padding: 2px 6px;
border-radius: 3px;
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
}
/* Task-status PILL — glass tinted tag, matches the button language.
Running/completed use a bright mint (#86efac) instead of the theme
--status-success (#28a745) which reads as muddy olive against the
nebula gradient. Same treatment used on the setup wizard's valid
border + the apps "Installed" pill. */
.task-status.status-queued {
background: rgba(var(--status-warning-rgb), 0.22);
border: 1px solid rgba(var(--status-warning-rgb), 0.60);
color: #fcd34d;
}
.task-status.status-running {
background: rgba(var(--status-success-rgb), 0.35);
border: 1px solid rgba(var(--status-success-rgb), 0.70);
color: #86efac;
}
.task-status.status-completed {
background: rgba(var(--status-success-rgb), 0.35);
border: 1px solid rgba(var(--status-success-rgb), 0.70);
color: #86efac;
}
/* Services persist when they're running; the pulse only makes sense
for transient task state, so disable it on service rows. The .status-running
class (line ~134) sets animation: pulse on anything that wears it — this
overrides for service pills. Task pills still pulse. */
.service-item .task-status.status-running {
animation: none;
}
.task-status.status-failed {
background: rgba(var(--status-danger-rgb), 0.22);
border: 1px solid rgba(var(--status-danger-rgb), 0.60);
color: #fca5a5;
}
/* "Retried" tag — a muted, neutral pill sitting next to a failed status to show
the failure was superseded by a new run (the failed task is kept, not deleted,
as the failure record). Deliberately calm so it doesn't compete with FAILED. */
.task-status.task-retried {
background: rgba(148, 163, 184, 0.16);
border: 1px solid rgba(148, 163, 184, 0.45);
color: #cbd5e1;
animation: none;
}
.task-command {
/* Bright mint to match the .status-running / .status-completed pills.
The theme's --status-success (#28a745) reads muddy olive on nebula —
this is the same #86efac treatment used by the status pills + the
setup-wizard valid border + the apps "Installed" pill. */
color: #86efac;
font-family: 'Courier New', monospace;
font-size: 11px;
flex: 1;
}
.task-time {
color: var(--text-muted);
font-size: 10px;
margin-right: 8px;
}
.task-actions {
display: flex;
gap: 6px;
}
.task-btn {
background: rgba(var(--text-rgb), 0.04);
border: 1px solid rgba(var(--text-rgb), 0.12);
color: var(--text-secondary);
padding: 3px 8px;
border-radius: 6px;
cursor: pointer;
font-size: 10px;
display: flex;
align-items: center;
gap: 3px;
transition: background 0.18s ease, border-color 0.18s ease, color 0.18s ease;
}
.task-btn:hover {
background: var(--surface-hover);
color: var(--status-success);
border-color: var(--status-success);
}
.task-btn.retry:hover {
background: var(--status-warning);
color: #000;
border-color: var(--status-warning);
}
.task-btn.delete:hover {
background: var(--status-danger);
color: var(--text-on-accent);
border-color: var(--status-danger);
}
/* Multi-select checkbox sat to the right of the row's Delete button.
Mirrors the .setup-app checkbox from the setup wizard: accent fill +
white SVG check + pop-in, no separate frame. The label wrapper is just
a hit-target — visible chrome is all on .task-select-box. Sized down
to 18px so it sits inline with the 22px-tall .task-btn buttons. */
.task-select {
display: inline-flex;
align-items: center;
justify-content: center;
cursor: pointer;
padding: 2px;
}
.task-select input[type="checkbox"] {
position: absolute;
opacity: 0;
pointer-events: none;
width: 0; height: 0;
}
.task-select-box {
width: 18px;
height: 18px;
flex-shrink: 0;
border-radius: 5px;
background: rgba(var(--text-rgb), 0.04);
border: 1.5px solid rgba(var(--text-rgb), 0.18);
box-shadow: inset 0 0 0 1px rgba(var(--text-rgb), 0.02);
position: relative;
transition: background 0.18s ease, border-color 0.18s ease, box-shadow 0.18s ease;
}
.task-select:hover .task-select-box {
border-color: rgba(var(--accent-rgb), 0.55);
box-shadow: 0 0 0 3px rgba(var(--accent-rgb), 0.08);
}
.task-select input[type="checkbox"]:focus-visible + .task-select-box {
outline: none;
border-color: rgba(var(--accent-rgb), 0.85);
box-shadow: 0 0 0 3px rgba(var(--accent-rgb), 0.20);
}
.task-select input[type="checkbox"]:checked + .task-select-box {
background: linear-gradient(135deg, var(--accent), var(--accent));
border-color: rgba(var(--accent-rgb), 0.9);
box-shadow: 0 0 0 1px rgba(var(--accent-rgb), 0.35);
}
.task-select input[type="checkbox"]:checked + .task-select-box::after {
content: "";
position: absolute;
inset: 0;
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='3.2' stroke-linecap='round' stroke-linejoin='round'><polyline points='20 6 9 17 4 12'/></svg>");
background-repeat: no-repeat;
background-position: center;
background-size: 12px 12px;
animation: taskCheckPop 0.22s cubic-bezier(0.34, 1.56, 0.64, 1);
}
@keyframes taskCheckPop {
0% { transform: scale(0.4); opacity: 0; }
100% { transform: scale(1); opacity: 1; }
}
/* Master "Select all" toggle in the action bar (right of Clear All).
Reuses .task-select-box so the two checkboxes are visually identical.
The wrapping label adds the inline "Select all" text + a hover lift. */
.task-select-all {
display: inline-flex;
align-items: center;
gap: 8px;
cursor: pointer;
padding: 4px 10px;
border-radius: 6px;
font-size: 11px;
color: var(--text-secondary);
user-select: none;
transition: background 0.18s ease, color 0.18s ease;
}
.task-select-all:hover {
background: var(--surface-hover);
color: var(--text-primary);
}
.task-select-all input[type="checkbox"] {
position: absolute;
opacity: 0;
pointer-events: none;
width: 0; height: 0;
}
.task-select-all:hover .task-select-box {
border-color: rgba(var(--accent-rgb), 0.55);
box-shadow: 0 0 0 3px rgba(var(--accent-rgb), 0.08);
}
.task-select-all input[type="checkbox"]:checked + .task-select-box {
background: linear-gradient(135deg, var(--accent), var(--accent));
border-color: rgba(var(--accent-rgb), 0.9);
box-shadow: 0 0 0 1px rgba(var(--accent-rgb), 0.35);
}
.task-select-all input[type="checkbox"]:checked + .task-select-box::after {
content: "";
position: absolute;
inset: 0;
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='3.2' stroke-linecap='round' stroke-linejoin='round'><polyline points='20 6 9 17 4 12'/></svg>");
background-repeat: no-repeat;
background-position: center;
background-size: 12px 12px;
animation: taskCheckPop 0.22s cubic-bezier(0.34, 1.56, 0.64, 1);
}
/* Indeterminate state — some-but-not-all visible rows ticked. Swap the
check SVG for a horizontal dash, still white on accent. */
.task-select-all input[type="checkbox"]:indeterminate + .task-select-box {
background: linear-gradient(135deg, var(--accent), var(--accent));
border-color: rgba(var(--accent-rgb), 0.9);
box-shadow: 0 0 0 1px rgba(var(--accent-rgb), 0.35);
}
.task-select-all input[type="checkbox"]:indeterminate + .task-select-box::after {
content: "";
position: absolute;
inset: 0;
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='3.2' stroke-linecap='round' stroke-linejoin='round'><line x1='5' y1='12' x2='19' y2='12'/></svg>");
background-repeat: no-repeat;
background-position: center;
background-size: 12px 12px;
}
.task-select-all-label {
font-weight: 500;
}
.task-details {
border-top: 1px solid rgba(var(--text-rgb), 0.10);
background: transparent;
padding: 14px 16px 4px;
display: none;
}
.task-details.show {
display: block;
}
.task-output {
padding: 12px;
white-space: pre-wrap;
word-break: break-word;
color: var(--text-muted);
font-family: 'Courier New', monospace;
font-size: 12px;
line-height: 1.4;
max-height: 200px;
overflow-y: auto;
}
/* Mobile Responsive */
@media (max-width: 768px) {
.sidebar-container {
position: fixed;
left: -220px;
top: 0;
height: 100vh;
z-index: 1000;
transition: left 0.3s ease;
}
.sidebar-container.mobile-open {
left: 0;
}
.main-content {
margin-left: 0;
}
.mobile-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
z-index: 999;
display: none;
}
.mobile-overlay.active {
display: block;
}
}
/* Loading Categories */
.loading-categories {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
padding: 16px;
color: var(--text-secondary);
font-size: 12px;
}
.loading-spinner {
width: 16px;
height: 16px;
border: 2px solid rgba(var(--text-rgb), 0.3);
border-top: 2px solid var(--text-primary);
border-radius: 50%;
animation: spin 1s linear infinite;
}
/* Task metadata strip. style.css turns .task-meta into a grid of
auto-fit columns, so the items wrap horizontally instead of stacking.
Uses a dark-tint overlay (not a light-tint) so the white labels and
values pop against the strip on nebula's gradient — the previous
rgba(text, 0.10) lifted the strip towards white and washed out the
text it was supposed to highlight. */
.task-meta {
background: rgba(var(--bg-rgb), 0.30);
border-radius: 10px;
padding: 12px 16px;
margin-bottom: 14px;
border: 1px solid rgba(var(--text-rgb), 0.10);
}
/* Bump label/value contrast inside the metadata strip — the global
.meta-item uses --text-muted (65% alpha on nebula) which reads as
dim grey. --text-secondary (82%) keeps the hierarchy vs the white
<strong> labels but is actually readable. */
.task-meta .meta-item {
color: var(--text-secondary);
}
/* Soften the log/output terminal box. The .log-container default is
var(--surface-sunken) — on nebula that's rgba(0,0,0,0.22) which on
top of the cosmic dark stack reads as pitch black and feels foreign
to the rest of the glass UI. Anchor it to nebula's navy chrome with
moderate opacity so the gradient still bleeds through faintly. */
.task-logs .log-container.terminal-style,
.task-output .output-content.terminal-style {
background: rgba(15, 25, 50, 0.45);
border: 1px solid rgba(var(--text-rgb), 0.10);
}
/* Canonical section loader (window.lpLoadingBox) inside a task-log
terminal box: fill the whole container and centre the spinner over the
terminal surface, dropping the loader's own card chrome so it reads as
one panel rather than a box-in-a-box. */
.task-logs .log-container .lp-loading {
height: 100%;
min-height: 0;
margin: 0;
padding: 0;
background: none;
border: 0;
}
.meta-item {
display: flex;
align-items: baseline;
justify-content: center;
gap: 6px;
padding: 2px 0;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.meta-item > strong {
flex-shrink: 0;
}
.meta-item > a {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
}
.meta-item code {
background: var(--code-bg);
color: var(--code-text);
padding: 2px 6px;
border-radius: 4px;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 12px;
}
.task-duration {
background: var(--accent-soft);
color: var(--accent);
padding: 2px 8px;
border-radius: 12px;
font-size: 11px;
font-weight: 500;
}
.task-logs {
margin-bottom: 16px;
}
.task-logs h4 {
margin: 0 0 12px 0;
color: var(--accent);
font-size: 14px;
font-weight: 600;
}
.log-container {
background: var(--surface-sunken);
border-radius: 8px;
padding: 12px;
max-height: 200px;
overflow-y: auto;
border: 1px solid var(--border-subtle);
}
.log-entry {
display: flex;
align-items: flex-start;
padding: 4px 0;
border-bottom: 1px solid var(--border-subtle);
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 12px;
}
.log-entry:last-child {
border-bottom: none;
}
.log-timestamp {
color: var(--text-muted);
margin-right: 12px;
white-space: nowrap;
font-size: 11px;
}
.task-output h4,
.task-error h4 {
margin: 0 0 8px 0;
font-size: 14px;
font-weight: 600;
}
.task-output h4 {
color: var(--status-success);
}
.output-content,
.error-content {
background: rgba(var(--text-rgb), 0.04);
border: 1px solid rgba(var(--text-rgb), 0.10);
box-shadow: inset 0 1px 0 rgba(var(--text-rgb), 0.04);
border-radius: 10px;
padding: 12px;
margin: 0;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 12px;
line-height: 1.4;
overflow-x: auto;
white-space: pre-wrap;
word-break: break-word;
}
.error-content {
background: rgba(var(--status-danger-rgb), 0.1);
border-color: rgba(var(--status-danger-rgb), 0.3);
color: var(--status-danger);
}
/* Original "task is running…" placeholder panel styling. The
.task-running class is also used as a JS state marker on buttons
and tab buttons (see app-tabbed-manager.js / apps-manager.js); the
:not(...) chain keeps those out so they don't suddenly grow 20px
of padding (and visibly jump taller) when a task starts. */
.task-running:not(button):not(.tab-button):not(.btn):not(.task-btn) {
text-align: center;
padding: 20px;
}
.spinner {
width: 20px;
height: 20px;
border: 2px solid rgba(var(--status-warning-rgb), 0.3);
border-top: 2px solid var(--status-warning);
border-radius: 50%;
animation: spin 1s linear infinite;
}
.info-content {
text-align: center;
color: var(--text-muted);
padding: 20px;
font-style: italic;
}
/* Modal Styles */
.task-logs-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 10000;
display: flex;
align-items: center;
justify-content: center;
}
/* These rules are scoped to .task-logs-modal so they don't override the
generic modal styling in modal.css used by every other modal. */
.task-logs-modal .modal-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
backdrop-filter: blur(4px);
}
.task-logs-modal .modal-content {
position: relative;
background: var(--bg-secondary);
border-radius: 12px;
width: 90%;
max-width: 800px;
max-height: 80vh;
overflow: hidden;
box-shadow: var(--card-shadow-hover);
border: 1px solid var(--border-subtle);
}
.task-logs-modal .modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20px 24px;
border-bottom: 1px solid var(--border-subtle);
background: rgba(var(--text-rgb), 0.05);
}
.task-logs-modal .modal-header h3 {
margin: 0;
color: var(--text-primary);
font-size: 18px;
font-weight: 600;
}
.task-logs-modal .modal-close {
background: none;
border: none;
color: var(--text-muted);
font-size: 24px;
cursor: pointer;
padding: 0;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 6px;
transition: all 0.2s ease;
}
.task-logs-modal .modal-close:hover {
background: var(--surface-hover);
color: var(--text-primary);
}
.task-logs-modal .modal-body {
padding: 24px;
max-height: calc(80vh - 80px);
overflow-y: auto;
}
.task-info-summary {
background: rgba(var(--text-rgb), 0.05);
border-radius: 8px;
padding: 16px;
margin-bottom: 20px;
border: 1px solid var(--border-subtle);
}
.info-row {
display: flex;
align-items: center;
padding: 8px 0;
border-bottom: 1px solid var(--border-subtle);
}
.info-row:last-child {
border-bottom: none;
}
.info-row code {
background: var(--code-bg);
color: var(--code-text);
padding: 2px 6px;
border-radius: 4px;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 12px;
margin-left: 8px;
}
.logs-section,
.output-section,
.error-section {
margin-bottom: 20px;
}
.logs-section h4,
.output-section h4,
.error-section h4 {
margin: 0 0 12px 0;
font-size: 16px;
font-weight: 600;
color: var(--text-primary);
}
.log-viewer,
.output-viewer,
.error-viewer {
background: var(--surface-sunken);
border-radius: 8px;
padding: 16px;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 12px;
line-height: 1.4;
overflow-x: auto;
white-space: pre-wrap;
word-break: break-word;
border: 1px solid var(--border-subtle);
max-height: 300px;
overflow-y: auto;
}
.log-viewer {
max-height: 400px;
}
.log-line {
display: flex;
align-items: flex-start;
padding: 4px 0;
border-bottom: 1px solid var(--border-subtle);
}
.log-line:last-child {
border-bottom: none;
}
.log-line .timestamp {
color: var(--text-muted);
margin-right: 12px;
white-space: nowrap;
font-size: 11px;
min-width: 140px;
}
.log-line .message {
color: var(--text-primary);
flex: 1;
word-break: break-word;
}
.error-viewer {
background: rgba(var(--status-danger-rgb), 0.1);
border-color: rgba(var(--status-danger-rgb), 0.3);
color: var(--status-danger);
}
/* Button enhancements */
.task-btn.view-logs {
background: var(--accent-soft);
color: var(--accent);
}
.task-btn.view-logs:hover {
background: rgba(var(--accent-rgb), 0.3);
}
/* Responsive */
@media (max-width: 768px) {
.modal-content {
width: 95%;
max-height: 90vh;
}
.modal-header,
.modal-body {
padding: 16px;
}
.log-line {
flex-direction: column;
gap: 4px;
}
.log-line .timestamp {
min-width: auto;
}
/* Task + service rows: stack the row so info, status, and actions
no longer fight for horizontal space. */
.task-header {
flex-direction: column;
align-items: stretch;
gap: 8px;
padding: 10px 12px;
}
.task-info {
flex-wrap: wrap;
gap: 8px;
}
.task-title {
flex: 1 1 100%;
word-break: break-word;
}
.task-actions {
width: 100%;
justify-content: flex-end;
flex-wrap: wrap;
gap: 6px;
}
/* Status bar: compress padding, allow refresh/clear to wrap below. */
.terminal-status-bar {
padding: 10px 12px;
gap: 8px;
}
.refresh-btn,
.clear-btn {
margin-left: 0;
}
/* Task metadata strip: stack key/value pairs vertically. */
.task-meta {
padding: 10px 12px;
}
/* Services row container: trim outer margins so cards reach edge. */
.services-rows {
margin: 10px;
padding: 10px;
}
}