forked from retoor/devplacepy
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:
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user