From a639824fd09705e4b1e28cf613e58764348858c1 Mon Sep 17 00:00:00 2001 From: Sam Rolfe Date: Thu, 8 Oct 2026 21:56:42 +1100 Subject: [PATCH] fix(portal): fix card layout ejection bug & sync top-right status dot with live service state --- portal/static/app.css | 46 ++++++++++++++++++++---- portal/templates/partials/_sections.html | 32 ++++++++++++----- 2 files changed, 62 insertions(+), 16 deletions(-) diff --git a/portal/static/app.css b/portal/static/app.css index acf7f3e..efc2105 100644 --- a/portal/static/app.css +++ b/portal/static/app.css @@ -196,6 +196,7 @@ button.button, input[type="submit"].button { font-family: inherit; } @media (max-width: 600px) { .tool-grid { grid-template-columns: 1fr; } } .tool-card { + position: relative; display: flex; flex-direction: column; gap: var(--space-sm); @@ -212,6 +213,20 @@ button.button, input[type="submit"].button { font-family: inherit; } transform: translateY(-1px); text-decoration: none; } +.tool-card-link { + position: absolute; + inset: 0; + z-index: 1; + border-radius: inherit; +} +.tool-card-top, +.tool-card .tool-name, +.tool-card .tool-desc, +.tool-card-bottom { + position: relative; + z-index: 1; + pointer-events: none; +} .tool-card-top { display: flex; align-items: flex-start; justify-content: space-between; } .tool-icon { width: 40px; height: 40px; @@ -251,6 +266,8 @@ button.button, input[type="submit"].button { font-family: inherit; } .status-online { background: var(--sticker-green); } .status-starting { background: var(--sticker-orange); } .status-offline { background: var(--ink-faint); } +.status-failed { background: #d33a2b; } +[data-theme="dark"] .status-failed { background: #ff6b5e; } /* loading pulse for polling */ .tool-grid.htmx-request .tool-card { opacity: .6; transition: opacity .2s; } @@ -388,22 +405,37 @@ button.button, input[type="submit"].button { font-family: inherit; } /* ---------- ON/OFF service strip (ON_OFF.md) ---------- */ .svc-strip { + position: relative; + z-index: 2; + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + margin-top: 4px; + padding: 8px 12px; + background: var(--surface-2, #f6f5f4); + border: 1px solid var(--hairline); + border-radius: var(--rounded-md); + pointer-events: auto; +} +[data-theme="dark"] .svc-strip { + background: rgba(255, 255, 255, 0.04); +} +.svc-strip-left, +.svc-strip-right { display: flex; align-items: center; gap: 8px; - margin-top: 2px; - padding-top: 10px; - border-top: 1px dashed var(--hairline); } -.svc-state { display: inline-flex; align-items: center; gap: 6px; font: var(--type-caption); color: var(--ink-muted); } -.svc-dot { width: 8px; height: 8px; border-radius: var(--rounded-full); } +.svc-state { display: inline-flex; align-items: center; gap: 6px; font: var(--type-caption); color: var(--ink-muted); font-weight: 500; } +.svc-dot { width: 8px; height: 8px; border-radius: var(--rounded-full); flex-shrink: 0; } .svc-dot.svc-on { background: var(--accent-green, #1aae39); } .svc-dot.svc-off { background: var(--ink-faint, #a39e98); } .svc-dot.svc-trans { background: var(--accent-orange, #dd5b00); animation: pulse 1.2s ease-in-out infinite; } .svc-dot.svc-fail { background: #d33a2b; } .svc-dot.svc-part { background: var(--accent-orange, #dd5b00); } /* partial: some group members running */ @keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: .35; } } -.svc-ram { margin-left: auto; font: var(--type-caption); color: var(--ink-faint); white-space: nowrap; } -.button-sm { padding: 4px 10px; font-size: 12px; } +.svc-ram { font: var(--type-caption); color: var(--ink-faint); white-space: nowrap; font-size: 11px; } +.button-sm { padding: 4px 10px; font-size: 12px; height: 26px; line-height: 1; } [data-theme="dark"] .svc-dot.svc-fail { background: #ff6b5e; } .svc-logs { font: var(--type-caption); color: var(--sticker-orange, #dd5b00); text-decoration: underline; } diff --git a/portal/templates/partials/_sections.html b/portal/templates/partials/_sections.html index 0bc7d7c..29b1ea9 100644 --- a/portal/templates/partials/_sections.html +++ b/portal/templates/partials/_sections.html @@ -20,7 +20,8 @@
{% for tool in section.tools %} - +
+
{{ tool.name[:1]|upper }} @@ -39,16 +40,20 @@
- - … - - - - + onclick="event.stopPropagation();"> +
+ + … + + +
+
+ + +
{% endif %} - +
{% endfor %}
@@ -118,6 +123,8 @@ function paint(id, showPoll) { var strip = document.querySelector('[data-svc="' + id + '"]'); if (!strip) return; + var card = strip.closest('[data-dash-card]'); + var cardDot = card ? card.querySelector('.status-dot') : null; var st = normState(svcState[id]); var btn = strip.querySelector("[data-svc-btn]"); var lab = strip.querySelector("[data-svc-label]"); @@ -128,6 +135,13 @@ if (ram && svcState[id] && svcState[id].ram_mb) ram.textContent = "~" + svcState[id].ram_mb + "MB"; var cls = st === "running" ? "on" : st === "stopped" ? "off" : (st === "failed" ? "fail" : st === "partial" ? "part" : "trans"); dot.className = "svc-dot svc-" + cls; + + // Synchronize top-right card status dot with live service state + if (cardDot) { + var topCls = st === "running" ? "status-online" : st === "stopped" ? "status-offline" : (st === "failed" ? "status-failed" : "status-starting"); + cardDot.className = "status-dot " + topCls; + } + if (lab) lab.textContent = st === "running" ? "Running" : st === "stopped" ? "Stopped" : st === "failed" ? (svcState[id].error || "Failed") : st === "partial" ? "Partial" : (st[0].toUpperCase() + st.slice(1)); // partial behaves like running for the button (offered action: stop the rest) // failed -> offer the logs deep link (dozzle)