diff --git a/Overlord-Server/public/assets/data.js b/Overlord-Server/public/assets/data.js
index bad3848..58f4a3a 100644
--- a/Overlord-Server/public/assets/data.js
+++ b/Overlord-Server/public/assets/data.js
@@ -4,14 +4,12 @@ import { digestData } from "./utils.js";
const POLL_INTERVAL_MS = 5000;
const FALLBACK_POLL_MS = 30000;
const PREF_REFRESH_KEY = "overlord_refresh_interval_seconds";
-const THUMBNAIL_COOLDOWN_MS = 60000;
let pollTimer = null;
let render = () => {};
let dashboardWs = null;
let dashboardWsConnected = false;
let wsReconnectTimer = null;
const manuallyDisconnecting = new Set();
-const thumbnailRequestedAt = new Map();
function moveClientCardImmediately(msg) {
const clientId = typeof msg?.clientId === "string" ? msg.clientId : "";
@@ -77,8 +75,6 @@ export async function loadWithOptions(options = {}) {
render(data, { reorder });
const pag = document.getElementById("pagination");
if (pag) pag.style.visibility = "";
-
- requestThumbnailsForClients(data.items);
} catch (err) {
console.error("load clients", err);
} finally {
@@ -93,19 +89,6 @@ export async function loadWithOptions(options = {}) {
}
}
-async function requestThumbnailsForClients(items) {
- const now = Date.now();
- const onlineClientsWithoutThumbnail = items.filter(c => {
- if (!c.online || c.thumbnail) return false;
- const last = thumbnailRequestedAt.get(c.id);
- return !last || now - last > THUMBNAIL_COOLDOWN_MS;
- });
- for (const client of onlineClientsWithoutThumbnail) {
- thumbnailRequestedAt.set(client.id, now);
- await requestThumbnail(client.id);
- }
-}
-
function updateOsFilter(items) {
const osSelect = document.getElementById("filter-os");
if (!osSelect) return;
@@ -183,7 +166,6 @@ function connectDashboardWs() {
console.warn("[dashboard] ws closed");
dashboardWs = null;
dashboardWsConnected = false;
- thumbnailRequestedAt.clear();
adjustPollingForWs();
scheduleDashboardReconnect();
};
diff --git a/Overlord-Server/public/assets/main.css b/Overlord-Server/public/assets/main.css
index 3d22999..5dea6d2 100644
--- a/Overlord-Server/public/assets/main.css
+++ b/Overlord-Server/public/assets/main.css
@@ -2335,6 +2335,16 @@ tbody tr:hover {
}
.cv-thumb-img { object-fit: cover; cursor: pointer; }
.cv-thumb { position: relative; }
+.cv-thumb-host { position: relative; isolation: isolate; }
+.cv-thumb-overlay {
+ position: absolute;
+ inset: 0;
+ width: 100%;
+ height: 100%;
+ border: 0;
+ border-radius: inherit;
+ z-index: 1;
+}
.cv-faux {
position: absolute;
diff --git a/Overlord-Server/public/assets/main.js b/Overlord-Server/public/assets/main.js
index 3278efc..3f8550e 100644
--- a/Overlord-Server/public/assets/main.js
+++ b/Overlord-Server/public/assets/main.js
@@ -11,6 +11,7 @@ import {
requestThumbnail,
markManualDisconnect,
} from "./data.js";
+import { ThumbnailLoader } from "./thumbnail-loader.js";
const grid = document.getElementById("grid");
const totalPill = document.getElementById("total-pill");
@@ -641,6 +642,60 @@ document.querySelectorAll("#layout-toggle .layout-toggle-btn").forEach((btn) =>
btn.addEventListener("click", () => setLayout(btn.dataset.layout));
});
+let dashboardThumbnailLoader = null;
+
+async function isDashboardThumbnailEnabled() {
+ try {
+ const res = await fetch("/api/settings/thumbnails", { credentials: "include" });
+ if (!res.ok) return true;
+ const data = await res.json();
+ return data?.thumbnails?.dashboardEnabled !== false;
+ } catch {
+ return true;
+ }
+}
+
+async function setupDashboardThumbnailLoader() {
+ if (!grid || dashboardThumbnailLoader) return;
+
+ if (!(await isDashboardThumbnailEnabled())) return;
+
+ const stored = Number(localStorage.getItem("overlord_dash_thumb_interval_ms"));
+ const refreshIntervalMs = Number.isFinite(stored) && stored >= 2000 ? stored : 15_000;
+
+ dashboardThumbnailLoader = new ThumbnailLoader({
+ refreshIntervalMs,
+ rootMargin: "300px",
+ threshold: 0.05,
+ });
+
+ const wire = (el) => {
+ const id = el.dataset.thumbClient;
+ if (!id) return;
+ if (el.dataset.thumbOnline !== "1") return;
+ const v = Number(el.dataset.thumbVersion) || 0;
+ dashboardThumbnailLoader.observe(el, id, v);
+ };
+ const unwire = (el) => dashboardThumbnailLoader.unobserve(el);
+
+ const mo = new MutationObserver((mutations) => {
+ for (const m of mutations) {
+ m.addedNodes.forEach((n) => {
+ if (!(n instanceof HTMLElement)) return;
+ if (n.matches?.("[data-thumb-host]")) wire(n);
+ n.querySelectorAll?.("[data-thumb-host]").forEach(wire);
+ });
+ m.removedNodes.forEach((n) => {
+ if (!(n instanceof HTMLElement)) return;
+ if (n.matches?.("[data-thumb-host]")) unwire(n);
+ n.querySelectorAll?.("[data-thumb-host]").forEach(unwire);
+ });
+ }
+ });
+ mo.observe(grid, { childList: true, subtree: true });
+ grid.querySelectorAll("[data-thumb-host]").forEach(wire);
+}
+
function initializeRenderer() {
const savedLayout = localStorage.getItem(PREF_LAYOUT_KEY) || "rows";
if (grid) grid.dataset.layout = ["rows", "table", "cards"].includes(savedLayout) ? savedLayout : "rows";
@@ -666,6 +721,7 @@ function initializeRenderer() {
});
rendererSetLayout = rSetLayout;
registerRenderer(renderMerge);
+ setupDashboardThumbnailLoader();
refreshGroupFilter();
loadWithOptions();
startAutoRefresh();
diff --git a/Overlord-Server/public/assets/nav.js b/Overlord-Server/public/assets/nav.js
index 5076059..dbee3f8 100644
--- a/Overlord-Server/public/assets/nav.js
+++ b/Overlord-Server/public/assets/nav.js
@@ -8,7 +8,7 @@ import {
import { mountNav } from "./nav/template.js";
import { createAdaptiveNavController } from "./nav/layout.js";
-import { applyUserRoleUI } from "./nav/role-ui.js";
+import { applyUserRoleUI, applyThumbnailWallVisibility } from "./nav/role-ui.js";
import { loadPluginNavItems } from "./nav/plugins-loader.js";
import { init as initCommandPalette } from "./command-palette.js";
@@ -29,6 +29,7 @@ if (host) {
const activeMap = {
"/": "nav-clients",
"/metrics": "metrics-link",
+ "/screenshots": "screenshots-link",
"/logs": "logs-link",
"/scripts": "scripts-link",
"/socks5-manager": "socks5-link",
@@ -134,6 +135,7 @@ if (host) {
}
const user = await res.json();
applyUserRoleUI(user, refs);
+ applyThumbnailWallVisibility(user);
if (user.role === "admin" || user.role === "operator") {
try {
diff --git a/Overlord-Server/public/assets/nav/role-ui.js b/Overlord-Server/public/assets/nav/role-ui.js
index 212c49f..16d648d 100644
--- a/Overlord-Server/public/assets/nav/role-ui.js
+++ b/Overlord-Server/public/assets/nav/role-ui.js
@@ -75,3 +75,14 @@ export function applyUserRoleUI(user, refs) {
unhide("file-share-link");
}
}
+
+export async function applyThumbnailWallVisibility(user) {
+ if (!user || user.role === "viewer") return;
+ try {
+ const res = await fetch("/api/settings/thumbnails", { credentials: "include" });
+ if (!res.ok) return;
+ const data = await res.json();
+ if (data?.thumbnails?.wallEnabled === false) return;
+ document.querySelectorAll("#screenshots-link").forEach((el) => el.classList.remove("hidden"));
+ } catch {}
+}
diff --git a/Overlord-Server/public/assets/nav/template.js b/Overlord-Server/public/assets/nav/template.js
index cadd0d0..f346ab7 100644
--- a/Overlord-Server/public/assets/nav/template.js
+++ b/Overlord-Server/public/assets/nav/template.js
@@ -63,6 +63,7 @@ export const NAV_GROUPS = [
iconColor: "text-emerald-400",
children: [
{ href: "/metrics", label: "Metrics", icon: "fa-chart-line", iconColor: "text-emerald-400", linkId: "metrics-link" },
+ { href: "/screenshots", label: "Screenshot Wall", icon: "fa-images", iconColor: "text-sky-400", linkId: "screenshots-link", hidden: true },
],
},
{
diff --git a/Overlord-Server/public/assets/render.js b/Overlord-Server/public/assets/render.js
index d909423..ac4da61 100644
--- a/Overlord-Server/public/assets/render.js
+++ b/Overlord-Server/public/assets/render.js
@@ -71,10 +71,32 @@ function fauxDesktopHtml(client, opts = {}) {
}
function thumbHtml(client, { width, height, small = false } = {}) {
- if (client.thumbnail) {
- return `
`;
- }
- return `
diff --git a/Overlord-Server/src/config.ts b/Overlord-Server/src/config.ts
index 5ddf214..e13908f 100644
--- a/Overlord-Server/src/config.ts
+++ b/Overlord-Server/src/config.ts
@@ -81,6 +81,10 @@ export interface Config {
buildSigning: {
banlist: string[];
};
+ thumbnails: {
+ dashboardEnabled: boolean;
+ wallEnabled: boolean;
+ };
}
const DEFAULT_CONFIG: Config = {
@@ -160,6 +164,10 @@ const DEFAULT_CONFIG: Config = {
buildSigning: {
banlist: [],
},
+ thumbnails: {
+ dashboardEnabled: true,
+ wallEnabled: true,
+ },
};
type SaveSecrets = {
@@ -558,6 +566,16 @@ export function loadConfig(): Config {
? fileConfig.buildSigning.banlist.filter((v: unknown): v is string => typeof v === "string" && v.length > 0)
: [...DEFAULT_CONFIG.buildSigning.banlist],
},
+ thumbnails: {
+ dashboardEnabled:
+ fileConfig.thumbnails?.dashboardEnabled !== undefined
+ ? Boolean(fileConfig.thumbnails.dashboardEnabled)
+ : DEFAULT_CONFIG.thumbnails.dashboardEnabled,
+ wallEnabled:
+ fileConfig.thumbnails?.wallEnabled !== undefined
+ ? Boolean(fileConfig.thumbnails.wallEnabled)
+ : DEFAULT_CONFIG.thumbnails.wallEnabled,
+ },
};
if (saveChanged) {
@@ -858,6 +876,33 @@ export async function updateBuildRateLimitConfig(
return next;
}
+export async function updateThumbnailsConfig(
+ updates: Partial,
+): Promise {
+ const current = getConfig();
+
+ const next: Config["thumbnails"] = {
+ dashboardEnabled:
+ updates.dashboardEnabled !== undefined
+ ? Boolean(updates.dashboardEnabled)
+ : current.thumbnails.dashboardEnabled,
+ wallEnabled:
+ updates.wallEnabled !== undefined
+ ? Boolean(updates.wallEnabled)
+ : current.thumbnails.wallEnabled,
+ };
+
+ configCache = {
+ ...current,
+ thumbnails: next,
+ };
+
+ const fileConfig = readFileConfigForUpdate();
+ fileConfig.thumbnails = next;
+ await writePersistentFileConfig(fileConfig);
+ return next;
+}
+
export function getBuildSigningSecrets(): BuildSigningSecrets | null {
const dataDir = ensureDataDir();
const savePath = resolve(dataDir, "save.json");
@@ -926,6 +971,7 @@ export function getExportableConfig(serverVersion: string): Record): Promise<{ app
applied.push("buildRateLimit");
}
+ if (data.thumbnails && typeof data.thumbnails === "object") {
+ await updateThumbnailsConfig(data.thumbnails);
+ applied.push("thumbnails");
+ }
+
if (data.auth && typeof data.auth === "object") {
const dataDir = ensureDataDir();
const savePath = resolve(dataDir, "save.json");
diff --git a/Overlord-Server/src/db.ts b/Overlord-Server/src/db.ts
index e9d092d..5ad75ab 100644
--- a/Overlord-Server/src/db.ts
+++ b/Overlord-Server/src/db.ts
@@ -1,7 +1,7 @@
import Database from "bun:sqlite";
import type { Statement } from "bun:sqlite";
import { ClientInfo, ListFilters, ListResult, ClientRole } from "./types";
-import { getThumbnail } from "./thumbnails";
+import { hasThumbnail, getThumbnailVersion } from "./thumbnails";
import { resolve } from "path";
import { ensureDataDir } from "./paths";
@@ -1186,7 +1186,8 @@ export function listClients(filters: ListFilters): ListResult {
groupName: c.groupName || null,
groupColor: c.groupColor || null,
notificationsMuted: c.notificationsMuted === 1,
- thumbnail: getThumbnail(c.id),
+ hasThumbnail: hasThumbnail(c.id),
+ thumbnailVersion: getThumbnailVersion(c.id),
}));
return { page, pageSize, total: totalRow.c, online: onlineRow.c, items };
diff --git a/Overlord-Server/src/server/routes/client-routes.ts b/Overlord-Server/src/server/routes/client-routes.ts
index b694b09..df56899 100644
--- a/Overlord-Server/src/server/routes/client-routes.ts
+++ b/Overlord-Server/src/server/routes/client-routes.ts
@@ -36,6 +36,7 @@ import {
listUserClientRuleIdsByAccess,
} from "../../users";
import { notifyDashboardViewers } from "../../sessions/sessionManager";
+import { clearThumbnail } from "../../thumbnails";
type RequestIpProvider = {
requestIP: (req: Request) => { address?: string } | null | undefined;
@@ -252,7 +253,7 @@ export async function handleClientRoutes(
if (!canUserAccessClient(user.userId, user.role, clientId)) {
return new Response("Forbidden: Client access denied", { status: 403 });
}
- const { generateThumbnail, markThumbnailRequested } = await import("../../thumbnails");
+ const { generateThumbnail, markThumbnailRequested, getThumbnailVersion } = await import("../../thumbnails");
markThumbnailRequested(clientId);
const target = clientManager.getClient(clientId);
if (target?.online) {
@@ -268,7 +269,40 @@ export async function handleClientRoutes(
metrics.recordCommand("screenshot");
}
const success = await generateThumbnail(clientId);
- return Response.json({ ok: true, updated: success }, { headers: deps.CORS_HEADERS });
+ const version = getThumbnailVersion(clientId);
+ return Response.json(
+ { ok: true, updated: success, version },
+ { headers: deps.CORS_HEADERS },
+ );
+ }
+
+ if (req.method === "GET" && thumbnailMatch) {
+ const user = await authenticateRequest(req);
+ if (!user) {
+ return new Response("Unauthorized", { status: 401 });
+ }
+ const clientId = thumbnailMatch[1];
+ if (!canUserAccessClient(user.userId, user.role, clientId)) {
+ return new Response("Forbidden: Client access denied", { status: 403 });
+ }
+ const { getThumbnailRecord } = await import("../../thumbnails");
+ const record = getThumbnailRecord(clientId);
+ if (!record) {
+ return new Response("Not Found", { status: 404 });
+ }
+ const etag = `"${clientId}-${record.version}"`;
+ if (req.headers.get("if-none-match") === etag) {
+ return new Response(null, { status: 304, headers: { ETag: etag } });
+ }
+ return new Response(record.bytes as unknown as BodyInit, {
+ status: 200,
+ headers: {
+ "Content-Type": record.contentType,
+ "Content-Length": String(record.bytes.byteLength),
+ ETag: etag,
+ "Cache-Control": "private, max-age=300",
+ },
+ });
}
if (req.method === "DELETE" && url.pathname === "/api/clients/offline") {
@@ -338,6 +372,7 @@ export async function handleClientRoutes(
}
deleteClientRow(targetId);
+ clearThumbnail(targetId);
notifyDashboardViewers();
const ip = server.requestIP(req)?.address || "unknown";
@@ -771,6 +806,7 @@ export async function handleClientRoutes(
metrics.recordCommand("uninstall");
clientManager.deleteClient(targetId);
deleteClientRow(targetId);
+ clearThumbnail(targetId);
logAudit({
timestamp: Date.now(),
username: user.username,
diff --git a/Overlord-Server/src/server/routes/misc-routes.ts b/Overlord-Server/src/server/routes/misc-routes.ts
index 2b7dfb2..b57f7da 100644
--- a/Overlord-Server/src/server/routes/misc-routes.ts
+++ b/Overlord-Server/src/server/routes/misc-routes.ts
@@ -1,7 +1,7 @@
import { authenticateRequest } from "../../auth";
import { AuditAction, getAuditLogs, logAudit } from "../../auditLog";
import { logger } from "../../logger";
-import { getConfig, updateSecurityConfig, updateTlsConfig, updateAppearanceConfig, updateChatConfig, getExportableConfig, importFullConfig, updateRegistrationConfig, updateBuildRateLimitConfig } from "../../config";
+import { getConfig, updateSecurityConfig, updateTlsConfig, updateAppearanceConfig, updateChatConfig, getExportableConfig, importFullConfig, updateRegistrationConfig, updateBuildRateLimitConfig, updateThumbnailsConfig } from "../../config";
import { getClientMetricsSummary, getClientMetricsSummaryForUser } from "../../db";
import { metrics } from "../../metrics";
import { requirePermission } from "../../rbac";
@@ -693,5 +693,40 @@ export async function handleMiscRoutes(
}
}
+ // ── Thumbnail toggles (GET: any user, PUT: admin only) ─────────────
+ if (url.pathname === "/api/settings/thumbnails") {
+ const user = await authenticateRequest(req);
+ if (!user) return new Response("Unauthorized", { status: 401 });
+
+ if (req.method === "GET") {
+ return Response.json({ thumbnails: getConfig().thumbnails }, { headers: deps.CORS_HEADERS });
+ }
+
+ if (req.method === "PUT") {
+ if (user.role !== "admin") return new Response("Forbidden: Admin access required", { status: 403 });
+
+ let body: any = {};
+ try { body = await req.json(); } catch {
+ return Response.json({ error: "Invalid JSON" }, { status: 400 });
+ }
+
+ const updated = await updateThumbnailsConfig({
+ dashboardEnabled: body?.dashboardEnabled !== undefined ? Boolean(body.dashboardEnabled) : undefined,
+ wallEnabled: body?.wallEnabled !== undefined ? Boolean(body.wallEnabled) : undefined,
+ });
+
+ logAudit({
+ timestamp: Date.now(),
+ username: user.username,
+ ip: deps.requestIP?.(req)?.address || "unknown",
+ action: AuditAction.COMMAND,
+ details: `Updated thumbnail settings (dashboard=${updated.dashboardEnabled}, wall=${updated.wallEnabled})`,
+ success: true,
+ });
+
+ return Response.json({ ok: true, thumbnails: updated }, { headers: deps.CORS_HEADERS });
+ }
+ }
+
return null;
}
diff --git a/Overlord-Server/src/server/routes/page-routes.ts b/Overlord-Server/src/server/routes/page-routes.ts
index 46fa861..11c9a6e 100644
--- a/Overlord-Server/src/server/routes/page-routes.ts
+++ b/Overlord-Server/src/server/routes/page-routes.ts
@@ -1,4 +1,5 @@
import { authenticateRequest } from "../../auth";
+import { getConfig } from "../../config";
import { requirePermission, type Permission } from "../../rbac";
import { canUserAccessClient, getUserById, type UserRole } from "../../users";
@@ -65,6 +66,7 @@ type QueryClientPageDef = {
/** Static pages: each entry maps a URL path to its HTML file + access rules. */
const STATIC_PAGES: StaticPageDef[] = [
{ path: "/metrics", file: "metrics.html", access: "any", checkPasswordChange: true },
+ { path: "/screenshots", file: "screenshots.html", access: "no-viewer", checkPasswordChange: true },
{ path: "/settings", file: "settings.html", access: "any", checkPasswordChange: true },
{ path: "/logs", file: "logs.html", access: "any", checkPasswordChange: true, permission: "audit:view" },
{ path: "/notifications", file: "notifications.html", access: "admin-or-operator", checkPasswordChange: true },
@@ -162,6 +164,10 @@ export async function handlePageRoutes(
const user = await authenticateRequest(req);
if (!user) return serveLoginOrUnauthorized(deps);
+ if (page.path === "/screenshots" && !getConfig().thumbnails.wallEnabled) {
+ return new Response("Screenshot wall is disabled by the administrator", { status: 403 });
+ }
+
if (page.checkPasswordChange) {
const maybeChange = await serveChangePasswordIfRequired(deps, user.userId);
if (maybeChange) return maybeChange;
diff --git a/Overlord-Server/src/thumbnails.ts b/Overlord-Server/src/thumbnails.ts
index 8785358..207df98 100644
--- a/Overlord-Server/src/thumbnails.ts
+++ b/Overlord-Server/src/thumbnails.ts
@@ -21,17 +21,50 @@ const MAX_THUMBNAIL_SOURCE_BYTES = Math.max(
256 * 1024,
Number(process.env.OVERLORD_THUMBNAIL_MAX_SOURCE_BYTES || 16 * 1024 * 1024),
);
+const THUMBNAIL_CACHE_MAX = Math.max(
+ 64,
+ Number(process.env.OVERLORD_THUMBNAIL_CACHE_MAX || 2000),
+);
-const thumbnails = new Map();
+type ThumbnailRecord = {
+ bytes: Uint8Array;
+ contentType: string;
+ version: number;
+ updatedAt: number;
+};
+
+const thumbnails = new Map();
const latestFrames = new Map();
const thumbnailRequests = new Map();
-export function setThumbnail(id: string, dataUrl: string) {
- thumbnails.set(id, dataUrl);
+function touchThumbnailLRU(id: string) {
+ const existing = thumbnails.get(id);
+ if (!existing) return;
+ thumbnails.delete(id);
+ thumbnails.set(id, existing);
}
-export function getThumbnail(id: string) {
- return thumbnails.get(id) ?? null;
+function evictThumbnailsIfFull() {
+ while (thumbnails.size > THUMBNAIL_CACHE_MAX) {
+ const oldestKey = thumbnails.keys().next().value;
+ if (oldestKey === undefined) break;
+ thumbnails.delete(oldestKey);
+ }
+}
+
+export function hasThumbnail(id: string): boolean {
+ return thumbnails.has(id);
+}
+
+export function getThumbnailRecord(id: string): ThumbnailRecord | null {
+ const rec = thumbnails.get(id);
+ if (!rec) return null;
+ touchThumbnailLRU(id);
+ return rec;
+}
+
+export function getThumbnailVersion(id: string): number {
+ return thumbnails.get(id)?.version ?? 0;
}
export function clearThumbnail(id: string) {
@@ -48,7 +81,7 @@ export function setLatestFrame(id: string, bytes: Uint8Array, format: string) {
latestFrames.set(id, { bytes, format, capturedAt: Date.now() });
}
-async function buildThumbnailDataUrl(bytes: Uint8Array, format: string): Promise {
+async function buildThumbnailBytes(bytes: Uint8Array, format: string): Promise {
if (!bytes || bytes.byteLength === 0) {
return null;
}
@@ -71,7 +104,7 @@ async function buildThumbnailDataUrl(bytes: Uint8Array, format: string): Promise
.webp({ quality: THUMBNAIL_QUALITY })
.toBuffer();
- return `data:image/webp;base64,${output.toString("base64")}`;
+ return new Uint8Array(output);
}
export async function generateThumbnail(id: string): Promise {
@@ -81,11 +114,20 @@ export async function generateThumbnail(id: string): Promise {
}
try {
- const dataUrl = await buildThumbnailDataUrl(frameData.bytes, frameData.format);
- if (!dataUrl) {
+ const out = await buildThumbnailBytes(frameData.bytes, frameData.format);
+ if (!out) {
return false;
}
- thumbnails.set(id, dataUrl);
+ const prior = thumbnails.get(id);
+ const now = Date.now();
+ if (prior) thumbnails.delete(id);
+ thumbnails.set(id, {
+ bytes: out,
+ contentType: "image/webp",
+ version: (prior?.version ?? 0) + 1,
+ updatedAt: now,
+ });
+ evictThumbnailsIfFull();
latestFrames.delete(id);
return true;
} catch (err) {
diff --git a/Overlord-Server/src/types.ts b/Overlord-Server/src/types.ts
index bda19b5..4d6be92 100644
--- a/Overlord-Server/src/types.ts
+++ b/Overlord-Server/src/types.ts
@@ -71,7 +71,8 @@ export type ListFilters = {
export type ListItem = Omit & {
online: boolean;
- thumbnail: string | null;
+ hasThumbnail: boolean;
+ thumbnailVersion: number;
};
export type ListResult = {
diff --git a/Overlord-Server/src/wsHandlers.ts b/Overlord-Server/src/wsHandlers.ts
index 367ae6a..c0233a4 100644
--- a/Overlord-Server/src/wsHandlers.ts
+++ b/Overlord-Server/src/wsHandlers.ts
@@ -10,7 +10,7 @@ async function getGeoip() {
import { ClientInfo } from "./types";
import {
consumeThumbnailRequest,
- getThumbnail,
+ hasThumbnail,
generateThumbnail,
setLatestFrame,
} from "./thumbnails";
@@ -297,8 +297,7 @@ export function handleFrame(info: ClientInfo, payload: any) {
if (safeFormat) {
const now = Date.now();
const thumbnailRequested = consumeThumbnailRequest(info.id);
- const hasThumbnail = Boolean(getThumbnail(info.id));
- if (thumbnailRequested || !hasThumbnail) {
+ if (thumbnailRequested || !hasThumbnail(info.id)) {
setLatestFrame(info.id, bytes, safeFormat);
void generateThumbnail(info.id);
}