Add screenshot page, add polling screenshots, better screenshot memory cleanup, better visualization/no jitter when screenshots are shown now.

This commit is contained in:
KingKDot
2026-05-17 22:02:23 -05:00
parent 7574988e3b
commit bab5054b47
21 changed files with 1209 additions and 45 deletions
@@ -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,
@@ -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;
}
@@ -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;