fix(portal): fix card layout ejection bug & sync top-right status dot with live service state

This commit is contained in:
2026-10-08 21:56:42 +11:00
parent 24227c527a
commit a639824fd0
2 changed files with 62 additions and 16 deletions

View File

@@ -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; }

View File

@@ -20,7 +20,8 @@
</div>
<div class="tool-grid">
{% for tool in section.tools %}
<a class="tool-card" href="/tool/{{ tool.id }}" data-dash-card>
<div class="tool-card" data-dash-card>
<a class="tool-card-link" href="/tool/{{ tool.id }}" title="Open {{ tool.name }}"></a>
<div class="tool-card-top">
<span class="tool-icon" style="background:{{ section.color }}">{{ tool.name[:1]|upper }}</span>
<span class="status-dot status-{{ tool.status }}"></span>
@@ -39,16 +40,20 @@
<div class="svc-strip" data-svc="{{ tool.service_id }}"
data-ram="{{ tool.ram_mb or '' }}" data-name="{{ tool.name }}"
data-logs="{{ tool.logs_url or '' }}"
onclick="event.preventDefault();event.stopPropagation();">
onclick="event.stopPropagation();">
<div class="svc-strip-left">
<span class="svc-state" data-svc-state>
<span class="svc-dot" data-svc-dot></span><span data-svc-label>…</span>
</span>
<a class="svc-logs" data-svc-logs style="display:none" target="_blank" rel="noopener">logs</a>
</div>
<div class="svc-strip-right">
<span class="svc-ram" data-svc-ram></span>
<button type="button" class="button button-utility button-sm" data-svc-btn disabled>…</button>
</div>
</div>
{% endif %}
</a>
</div>
{% endfor %}
</div>
</section>
@@ -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)