fix: add live age badge and zoom cursor to bot screenshots, bind monitor on startup, and retry feed assertions in e2e tests

This commit is contained in:
2026-06-14 15:20:35 +00:00
parent d75d5dc6a8
commit 9ed611097d
5 changed files with 88 additions and 24 deletions
+18
View File
@@ -42,6 +42,24 @@
object-fit: cover;
object-position: top center;
display: block;
cursor: zoom-in;
}
.bot-shot-age {
position: absolute;
right: 6px;
bottom: 6px;
padding: 2px 7px;
border-radius: 999px;
font-size: 0.72rem;
font-family: var(--font-mono);
font-weight: 600;
line-height: 1.4;
color: var(--text-primary);
background: rgba(0, 0, 0, 0.62);
backdrop-filter: blur(2px);
pointer-events: none;
letter-spacing: 0.02em;
}
.bot-shot-empty {
+48 -13
View File
@@ -16,6 +16,10 @@ export class BotMonitor {
immediate: false,
pauseHidden: true,
});
this.ticker = new Poller(() => this.tickAges(), 1000, {
immediate: true,
pauseHidden: true,
});
}
escape(value) {
@@ -24,6 +28,39 @@ export class BotMonitor {
return span.innerHTML;
}
ageLabel(seconds) {
const sec = Math.max(0, Math.floor(Number(seconds) || 0));
if (sec < 60) return `${sec}s`;
const minutes = Math.floor(sec / 60);
if (minutes < 60) return `${minutes}m`;
const hours = Math.floor(minutes / 60);
const rest = minutes % 60;
return rest ? `${hours}h${rest}m` : `${hours}h`;
}
tickAges() {
const now = Date.now();
for (const badge of this.root.querySelectorAll("[data-bot-age]")) {
let capturedMs = Number(badge.dataset.capturedMs);
if (!capturedMs) {
const ageSeconds = Number(badge.dataset.ageSeconds) || 0;
capturedMs = now - ageSeconds * 1000;
badge.dataset.capturedMs = String(capturedMs);
}
badge.textContent = this.ageLabel((now - capturedMs) / 1000);
}
}
shotMarkup(frame) {
if (!frame.has_image) {
return `<div class="bot-shot-empty">no frame yet</div>`;
}
const label = this.escape(frame.label || `bot${frame.slot}`);
const capturedMs = Date.now() - (Number(frame.age_seconds) || 0) * 1000;
return `<img src="${this.escape(frame.frame_url)}" alt="${label} screenshot" loading="lazy" data-lightbox>`
+ `<span class="bot-shot-age" data-bot-age data-captured-ms="${capturedMs}">${this.escape(this.ageLabel(frame.age_seconds))}</span>`;
}
async refresh() {
const payload = await Http.getJson(this.endpoint);
const frames = payload.frames || [];
@@ -69,16 +106,17 @@ export class BotMonitor {
card.className = `bot-card ${active}`;
const shot = card.querySelector(".bot-shot");
const image = shot.querySelector("img");
if (frame.has_image) {
if (image) {
if (image.getAttribute("src") !== frame.frame_url) {
image.src = frame.frame_url;
}
} else {
shot.innerHTML = `<img src="${this.escape(frame.frame_url)}" alt="${this.escape(frame.label || `bot${frame.slot}`)} screenshot" loading="lazy">`;
if (frame.has_image && image) {
if (image.getAttribute("src") !== frame.frame_url) {
image.src = frame.frame_url;
}
} else if (!shot.querySelector(".bot-shot-empty")) {
shot.innerHTML = `<div class="bot-shot-empty">no frame yet</div>`;
const badge = shot.querySelector("[data-bot-age]");
if (badge) {
badge.dataset.capturedMs = String(Date.now() - (Number(frame.age_seconds) || 0) * 1000);
badge.textContent = this.ageLabel(frame.age_seconds);
}
} else if (frame.has_image || !shot.querySelector(".bot-shot-empty")) {
shot.innerHTML = this.shotMarkup(frame);
}
card.querySelector(".bot-name").textContent = frame.label || `bot${frame.slot}`;
card.querySelector(".bot-persona").textContent = frame.persona || "-";
@@ -91,11 +129,8 @@ export class BotMonitor {
const persona = this.escape(frame.persona || "-");
const action = this.escape(frame.action || frame.status || "idle");
const active = frame.active ? "bot-active" : "bot-idle";
const shot = frame.has_image
? `<img src="${this.escape(frame.frame_url)}" alt="${label} screenshot" loading="lazy">`
: `<div class="bot-shot-empty">no frame yet</div>`;
return `<figure class="bot-card ${active}" data-slot="${slot}">
<div class="bot-shot">${shot}</div>
<div class="bot-shot">${this.shotMarkup(frame)}</div>
<figcaption class="bot-meta">
<span class="bot-name">${label}</span>
<span class="bot-persona">${persona}</span>