fix(portal): fix card layout ejection bug & sync top-right status dot with live service state
This commit is contained in:
@@ -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; }
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user