fixed dashboard cards, add fuse js, fix load times, fix csp in scripts, fix remote desktop not working first try.

This commit is contained in:
KingKDot
2026-06-04 00:04:49 -05:00
parent 25b90005ea
commit 18321994d8
15 changed files with 974 additions and 118 deletions
+31 -2
View File
@@ -1,5 +1,5 @@
import { afterAll, describe, expect, test } from "bun:test";
import { deleteClientRow, listClients, setClientBookmark, upsertClientRow } from "./db";
import { deleteClientRow, listClients, setClientBookmark, setClientTag, upsertClientRow } from "./db";
const createdClientIds: string[] = [];
@@ -110,4 +110,33 @@ describe("client list ordering", () => {
cleanupCreatedClients();
}
});
});
test("search tolerates typos across client metadata", () => {
try {
const prefix = `fuse-${Date.now().toString(36)}`;
const id = `${prefix}-finance-terminal`;
createTempClient(id, {
online: true,
lastSeen: Date.now(),
host: "finance-terminal",
});
setClientTag(id, "Payroll Workstation", "Quarterly reporting machine");
const result = listClients({
page: 1,
pageSize: 12,
search: "payrol workstaton",
sort: "last_seen_desc",
statusFilter: "all",
osFilter: "all",
countryFilter: "all",
enrollmentFilter: "all",
});
expect(result.items.some((item) => item.id === id)).toBe(true);
} finally {
cleanupCreatedClients();
}
});
});
+262 -85
View File
@@ -1,4 +1,5 @@
import type { ClientInfo, ListFilters, ListResult, ClientRole } from "../types";
import Fuse from "fuse.js";
import { getThumbnailSummaries } from "../thumbnails";
import { db, dbPath } from "./connection";
import "./schema";
@@ -770,6 +771,119 @@ export function computeClientSuspiciousFlags(
return flags;
}
const CLIENT_SEARCH_KEYS = [
{ name: "nickname", weight: 0.32 },
{ name: "host", weight: 0.28 },
{ name: "user", weight: 0.18 },
{ name: "id", weight: 0.18 },
{ name: "customTag", weight: 0.2 },
{ name: "customTagNote", weight: 0.1 },
{ name: "groupName", weight: 0.14 },
{ name: "os", weight: 0.12 },
{ name: "ip", weight: 0.1 },
{ name: "hwid", weight: 0.08 },
{ name: "country", weight: 0.06 },
{ name: "version", weight: 0.06 },
{ name: "buildTag", weight: 0.06 },
{ name: "cpu", weight: 0.05 },
{ name: "gpu", weight: 0.05 },
{ name: "ram", weight: 0.04 },
] as const;
const CLIENT_SEARCH_SQL_COLUMNS = [
"COALESCE(c.nickname,'')",
"COALESCE(c.host,'')",
"COALESCE(c.user,'')",
"c.id",
"COALESCE(c.custom_tag,'')",
"COALESCE(c.custom_tag_note,'')",
"COALESCE(g.name,'')",
"COALESCE(c.os,'')",
"COALESCE(c.ip,'')",
"COALESCE(c.hwid,'')",
"COALESCE(c.country,'')",
"COALESCE(c.version,'')",
"COALESCE(c.build_tag,'')",
"COALESCE(c.cpu,'')",
"COALESCE(c.gpu,'')",
"COALESCE(c.ram,'')",
] as const;
function escapeLikeTerm(term: string): string {
return term.replace(/[\\%_]/g, (match) => `\\${match}`);
}
function buildFuzzyCandidateWhere(search: string): { sql: string; params: string[] } {
const query = search.trim().toLowerCase();
if (!query) return { sql: "", params: [] };
const terms = Array.from(new Set([
query,
...query.split(/[\s,;:|/\\]+/),
].map((term) => term.trim()).filter((term) => term.length >= 2))).slice(0, 8);
if (!terms.length) return { sql: "", params: [] };
const fieldClause = CLIENT_SEARCH_SQL_COLUMNS
.map((column) => `LOWER(${column}) LIKE ? ESCAPE '\\'`)
.join(" OR ");
const sql = terms.map(() => `(${fieldClause})`).join(" OR ");
const params = terms.flatMap((term) => {
const like = `%${escapeLikeTerm(term)}%`;
return CLIENT_SEARCH_SQL_COLUMNS.map(() => like);
});
return { sql: `(${sql})`, params };
}
function buildClientSearchFtsQuery(search: string): string {
const terms = Array.from(new Set(
search
.trim()
.split(/[\s,;:|/\\]+/)
.map((term) => term.replace(/["*]/g, "").trim())
.filter((term) => term.length >= 2),
)).slice(0, 8);
return terms.map((term) => `"${term}"*`).join(" OR ");
}
function getFtsClientSearchCandidateIds(search: string, limit: number): string[] | null {
const ftsQuery = buildClientSearchFtsQuery(search);
if (!ftsQuery) return [];
try {
return db
.query<{ id: string }>(
`SELECT id
FROM client_search_fts
WHERE client_search_fts MATCH ?
ORDER BY rank
LIMIT ?`,
)
.all(ftsQuery, limit)
.map((row) => row.id)
.filter(Boolean);
} catch {
return null;
}
}
function fuzzySearchClientRows(rows: any[], search: string): any[] {
const query = search.trim();
if (!query) return rows;
const fuse = new Fuse(rows, {
keys: CLIENT_SEARCH_KEYS as any,
threshold: 0.36,
distance: 120,
ignoreLocation: true,
shouldSort: true,
});
return fuse.search(query).map((result) => result.item);
}
export function listClients(filters: ListFilters): ListResult {
const {
page,
@@ -789,14 +903,6 @@ export function listClients(filters: ListFilters): ListResult {
const where: string[] = [];
const params: any[] = [];
if (search) {
where.push(
"(LOWER(COALESCE(c.host,'')) LIKE ? OR LOWER(COALESCE(c.user,'')) LIKE ? OR LOWER(COALESCE(c.nickname,'')) LIKE ? OR LOWER(COALESCE(c.custom_tag,'')) LIKE ? OR LOWER(COALESCE(c.custom_tag_note,'')) LIKE ? OR LOWER(c.id) LIKE ? OR LOWER(COALESCE(g.name,'')) LIKE ?)",
);
const needle = `%${search}%`;
params.push(needle, needle, needle, needle, needle, needle, needle);
}
if (statusFilter === "online") {
where.push("c.online=1");
} else if (statusFilter === "offline") {
@@ -859,11 +965,9 @@ export function listClients(filters: ListFilters): ListResult {
const whereSql = where.length ? `WHERE ${where.join(" AND ")}` : "";
const needsGroupJoinForFilter =
!!search ||
sort === "group_asc" ||
sort === "group_desc" ||
(groupFilter !== undefined && groupFilter !== "all");
const countJoinSql = needsGroupJoinForFilter ? "LEFT JOIN client_groups g ON g.id = c.group_id" : "";
const orderBy = (() => {
const online = "c.online DESC";
@@ -894,49 +998,99 @@ export function listClients(filters: ListFilters): ListResult {
}
})();
const totalRow = db
.query<{ c: number }>(`SELECT COUNT(*) as c FROM clients c ${countJoinSql} ${whereSql}`)
.get(...params) ?? { c: 0 };
const onlineRow = db
.query<{ c: number }>(
`SELECT COUNT(*) as c FROM clients c ${countJoinSql} ${whereSql ? `${whereSql} AND c.online=1` : "WHERE c.online=1"}`,
)
.get(...params) ?? { c: 0 };
const offset = (page - 1) * pageSize;
const clientFields =
"c.id, c.hwid, c.role, c.ip, c.host, c.os, c.arch, c.version, c.user, c.nickname, c.custom_tag as customTag, c.custom_tag_note as customTagNote, c.monitors, c.country, c.last_seen as lastSeen, c.online, c.ping_ms as pingMs, c.bookmarked, c.build_tag as buildTag, c.built_by_user_id as builtByUserId, c.enrollment_status as enrollmentStatus, c.public_key as publicKey, c.key_fingerprint as keyFingerprint, c.cpu, c.gpu, c.ram, c.battery_percent as batteryPercent, c.battery_charging as batteryCharging, c.is_admin as isAdmin, c.elevation, c.permissions, c.disconnect_reason as disconnectReason, c.disconnect_detail as disconnectDetail, c.group_id as groupId, c.notifications_muted as notificationsMuted, c.deny_reason as denyReason";
const rows = needsGroupJoinForFilter
? db
.query<any>(
`SELECT ${clientFields}, g.name as groupName, g.color as groupColor
FROM clients c
LEFT JOIN client_groups g ON g.id = c.group_id
${whereSql}
${orderBy}
LIMIT ? OFFSET ?`,
)
.all(...params, pageSize, offset)
: db
.query<any>(
`WITH page_clients AS (
SELECT ${clientFields}
let rows: any[] | undefined;
let totalCount = 0;
let onlineCount = 0;
if (search) {
const searchWhere = [...where];
const searchParams = [...params];
const candidateLimit = Math.min(5000, Math.max(1000, offset + pageSize * 8));
const ftsCandidateIds = getFtsClientSearchCandidateIds(search, candidateLimit);
if (ftsCandidateIds) {
if (!ftsCandidateIds.length) {
rows = [];
totalCount = 0;
onlineCount = 0;
} else {
searchWhere.push(`c.id IN (${ftsCandidateIds.map(() => "?").join(",")})`);
searchParams.push(...ftsCandidateIds);
}
} else {
const searchCandidate = buildFuzzyCandidateWhere(search);
if (searchCandidate.sql) {
searchWhere.push(searchCandidate.sql);
searchParams.push(...searchCandidate.params);
}
}
if (rows === undefined) {
const searchWhereSql = searchWhere.length ? `WHERE ${searchWhere.join(" AND ")}` : "";
const candidates = db
.query<any>(
`SELECT ${clientFields}, g.name as groupName, g.color as groupColor
FROM clients c
LEFT JOIN client_groups g ON g.id = c.group_id
${searchWhereSql}
${orderBy}`,
)
.all(...searchParams);
const matches = fuzzySearchClientRows(candidates, search);
totalCount = matches.length;
onlineCount = matches.filter((row: any) => row.online === 1).length;
rows = matches.slice(offset, offset + pageSize);
}
} else {
const countJoinSql = needsGroupJoinForFilter ? "LEFT JOIN client_groups g ON g.id = c.group_id" : "";
const totalRow = db
.query<{ c: number }>(`SELECT COUNT(*) as c FROM clients c ${countJoinSql} ${whereSql}`)
.get(...params) ?? { c: 0 };
const onlineRow = db
.query<{ c: number }>(
`SELECT COUNT(*) as c FROM clients c ${countJoinSql} ${whereSql ? `${whereSql} AND c.online=1` : "WHERE c.online=1"}`,
)
.get(...params) ?? { c: 0 };
totalCount = totalRow.c;
onlineCount = onlineRow.c;
rows = needsGroupJoinForFilter
? db
.query<any>(
`SELECT ${clientFields}, g.name as groupName, g.color as groupColor
FROM clients c
LEFT JOIN client_groups g ON g.id = c.group_id
${whereSql}
${orderBy}
LIMIT ? OFFSET ?
)
SELECT pc.*, g.name as groupName, g.color as groupColor
FROM page_clients pc
LEFT JOIN client_groups g ON g.id = pc.groupId`,
)
.all(...params, pageSize, offset);
LIMIT ? OFFSET ?`,
)
.all(...params, pageSize, offset)
: db
.query<any>(
`WITH page_clients AS (
SELECT ${clientFields}
FROM clients c
${whereSql}
${orderBy}
LIMIT ? OFFSET ?
)
SELECT pc.*, g.name as groupName, g.color as groupColor
FROM page_clients pc
LEFT JOIN client_groups g ON g.id = pc.groupId`,
)
.all(...params, pageSize, offset);
}
const shouldScanSuspicious = SUSPICIOUS_SCAN_MAX_CLIENTS === 0 || totalRow.c <= SUSPICIOUS_SCAN_MAX_CLIENTS;
const shouldScanSuspicious = !search && (SUSPICIOUS_SCAN_MAX_CLIENTS === 0 || totalCount <= SUSPICIOUS_SCAN_MAX_CLIENTS);
const suspiciousSets = shouldScanSuspicious
? getFloodSetsWithCache()
: { floodedHwids: new Set<string>(), floodedHardware: new Set<string>(), floodedIps: new Set<string>() };
rows = rows || [];
const thumbnailSummaries = getThumbnailSummaries(rows.map((row: any) => row.id));
const items = rows.map((c: any) => {
@@ -986,7 +1140,7 @@ export function listClients(filters: ListFilters): ListResult {
};
});
return { page, pageSize, total: totalRow.c, online: onlineRow.c, items };
return { page, pageSize, total: totalCount, online: onlineCount, items };
}
export type ClientMetricsSummary = {
@@ -998,7 +1152,56 @@ export type ClientMetricsSummary = {
byCountryOnline: Record<string, number>;
};
const CLIENT_METRICS_SUMMARY_TTL_MS = 4_000;
let clientMetricsSummaryCache: { expiresAt: number; summary: ClientMetricsSummary } | null = null;
const userClientMetricsSummaryCache = new Map<number, { expiresAt: number; summary: ClientMetricsSummary }>();
function cloneClientMetricsSummary(summary: ClientMetricsSummary): ClientMetricsSummary {
return {
total: summary.total,
online: summary.online,
byOS: { ...summary.byOS },
byCountry: { ...summary.byCountry },
byOSOnline: { ...summary.byOSOnline },
byCountryOnline: { ...summary.byCountryOnline },
};
}
function makeClientMetricsSummary(
counts: { total: number | null; online: number | null },
osRows: { key: string; total: number; online: number | null }[],
countryRows: { key: string; total: number; online: number | null }[],
): ClientMetricsSummary {
const byOS: Record<string, number> = {};
const byOSOnline: Record<string, number> = {};
for (const row of osRows) {
byOS[row.key] = Number(row.total) || 0;
byOSOnline[row.key] = Number(row.online) || 0;
}
const byCountry: Record<string, number> = {};
const byCountryOnline: Record<string, number> = {};
for (const row of countryRows) {
byCountry[row.key] = Number(row.total) || 0;
byCountryOnline[row.key] = Number(row.online) || 0;
}
return {
total: Number(counts.total) || 0,
online: Number(counts.online) || 0,
byOS,
byCountry,
byOSOnline,
byCountryOnline,
};
}
export function getClientMetricsSummary(): ClientMetricsSummary {
const now = Date.now();
if (clientMetricsSummaryCache && clientMetricsSummaryCache.expiresAt > now) {
return cloneClientMetricsSummary(clientMetricsSummaryCache.summary);
}
const counts = db
.query<{ total: number; online: number }>(
`SELECT COUNT(*) as total, SUM(CASE WHEN online=1 THEN 1 ELSE 0 END) as online FROM clients`,
@@ -1027,31 +1230,21 @@ export function getClientMetricsSummary(): ClientMetricsSummary {
)
.all();
const byOS: Record<string, number> = {};
const byOSOnline: Record<string, number> = {};
for (const row of osRows) {
byOS[row.key] = Number(row.total) || 0;
byOSOnline[row.key] = Number(row.online) || 0;
}
const byCountry: Record<string, number> = {};
const byCountryOnline: Record<string, number> = {};
for (const row of countryRows) {
byCountry[row.key] = Number(row.total) || 0;
byCountryOnline[row.key] = Number(row.online) || 0;
}
return {
total: Number(counts.total) || 0,
online: Number(counts.online) || 0,
byOS,
byCountry,
byOSOnline,
byCountryOnline,
const summary = makeClientMetricsSummary(counts, osRows, countryRows);
clientMetricsSummaryCache = {
expiresAt: now + CLIENT_METRICS_SUMMARY_TTL_MS,
summary,
};
return cloneClientMetricsSummary(summary);
}
export function getClientMetricsSummaryForUser(userId: number): ClientMetricsSummary {
const now = Date.now();
const cached = userClientMetricsSummaryCache.get(userId);
if (cached && cached.expiresAt > now) {
return cloneClientMetricsSummary(cached.summary);
}
const filter = `WHERE built_by_user_id = ?`;
const counts = db
@@ -1082,28 +1275,12 @@ export function getClientMetricsSummaryForUser(userId: number): ClientMetricsSum
)
.all(userId);
const byOS: Record<string, number> = {};
const byOSOnline: Record<string, number> = {};
for (const row of osRows) {
byOS[row.key] = Number(row.total) || 0;
byOSOnline[row.key] = Number(row.online) || 0;
}
const byCountry: Record<string, number> = {};
const byCountryOnline: Record<string, number> = {};
for (const row of countryRows) {
byCountry[row.key] = Number(row.total) || 0;
byCountryOnline[row.key] = Number(row.online) || 0;
}
return {
total: Number(counts.total) || 0,
online: Number(counts.online) || 0,
byOS,
byCountry,
byOSOnline,
byCountryOnline,
};
const summary = makeClientMetricsSummary(counts, osRows, countryRows);
userClientMetricsSummaryCache.set(userId, {
expiresAt: now + CLIENT_METRICS_SUMMARY_TTL_MS,
summary,
});
return cloneClientMetricsSummary(summary);
}
export function getOnlineClientCountForUser(userId: number): number {
+76
View File
@@ -148,6 +148,82 @@ try {
} catch {}
db.run(`CREATE INDEX IF NOT EXISTS idx_clients_group_id ON clients(group_id);`);
try {
db.run(`
CREATE VIRTUAL TABLE IF NOT EXISTS client_search_fts USING fts5(
id UNINDEXED,
host,
user,
nickname,
custom_tag,
custom_tag_note,
os,
ip,
hwid,
country,
version,
build_tag,
cpu,
gpu,
ram,
tokenize = 'unicode61'
);
`);
const counts = db
.query<{ clients: number; indexed: number }>(
`SELECT
(SELECT COUNT(*) FROM clients) as clients,
(SELECT COUNT(*) FROM client_search_fts) as indexed`,
)
.get() ?? { clients: 0, indexed: 0 };
if (counts.clients !== counts.indexed) {
db.run(`DELETE FROM client_search_fts`);
db.run(`
INSERT INTO client_search_fts(
rowid, id, host, user, nickname, custom_tag, custom_tag_note,
os, ip, hwid, country, version, build_tag, cpu, gpu, ram
)
SELECT
rowid, id, host, user, nickname, custom_tag, custom_tag_note,
os, ip, hwid, country, version, build_tag, cpu, gpu, ram
FROM clients
`);
}
db.run(`
CREATE TRIGGER IF NOT EXISTS clients_search_ai AFTER INSERT ON clients BEGIN
INSERT INTO client_search_fts(
rowid, id, host, user, nickname, custom_tag, custom_tag_note,
os, ip, hwid, country, version, build_tag, cpu, gpu, ram
)
VALUES (
new.rowid, new.id, new.host, new.user, new.nickname, new.custom_tag, new.custom_tag_note,
new.os, new.ip, new.hwid, new.country, new.version, new.build_tag, new.cpu, new.gpu, new.ram
);
END;
`);
db.run(`
CREATE TRIGGER IF NOT EXISTS clients_search_ad AFTER DELETE ON clients BEGIN
DELETE FROM client_search_fts WHERE rowid = old.rowid;
END;
`);
db.run(`
CREATE TRIGGER IF NOT EXISTS clients_search_au AFTER UPDATE ON clients BEGIN
DELETE FROM client_search_fts WHERE rowid = old.rowid;
INSERT INTO client_search_fts(
rowid, id, host, user, nickname, custom_tag, custom_tag_note,
os, ip, hwid, country, version, build_tag, cpu, gpu, ram
)
VALUES (
new.rowid, new.id, new.host, new.user, new.nickname, new.custom_tag, new.custom_tag_note,
new.os, new.ip, new.hwid, new.country, new.version, new.build_tag, new.cpu, new.gpu, new.ram
);
END;
`);
} catch {}
db.run(`
CREATE TABLE IF NOT EXISTS banned_ips (
ip TEXT PRIMARY KEY,
@@ -21,6 +21,7 @@ describe("SECURITY_HEADERS", () => {
const csp = SECURITY_HEADERS["Content-Security-Policy"];
expect(csp).toContain("default-src 'self'");
expect(csp).toContain("script-src 'self'");
expect(csp).toContain("font-src 'self' data:");
expect(csp).toContain("frame-ancestors 'none'");
});
+1 -1
View File
@@ -5,7 +5,7 @@ export const CORS_HEADERS = {
};
export const SECURITY_HEADERS = {
"Content-Security-Policy": "default-src 'self'; script-src 'self'; worker-src 'self' blob:; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https://cdn.discordapp.com https://a.basemaps.cartocdn.com https://b.basemaps.cartocdn.com https://c.basemaps.cartocdn.com https://d.basemaps.cartocdn.com; font-src 'self'; connect-src 'self'; object-src blob:; frame-ancestors 'none'",
"Content-Security-Policy": "default-src 'self'; script-src 'self'; worker-src 'self' blob:; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https://cdn.discordapp.com https://a.basemaps.cartocdn.com https://b.basemaps.cartocdn.com https://c.basemaps.cartocdn.com https://d.basemaps.cartocdn.com; font-src 'self' data:; connect-src 'self'; object-src blob:; frame-ancestors 'none'",
"X-Content-Type-Options": "nosniff",
"X-Frame-Options": "DENY",
"X-XSS-Protection": "1; mode=block",
@@ -160,6 +160,7 @@ function broadcastFrameToViewers(
}
const rdSendStats = { lastLog: 0, frames: 0, sendMs: 0, bytes: 0 };
const rdDebugFrameLogAt = new Map<string, number>();
export const rdStreamingState = new Map<string, {
isStreaming: boolean;
display: number;
@@ -223,9 +224,12 @@ function sendConsoleCommand(target: ClientInfo | undefined, commandType: string,
}
function sendDesktopCommandWithId(target: ClientInfo | undefined, commandType: string, payload: Record<string, unknown>, commandId: string) {
if (!target) return false;
if (!target) {
logger.warn(`[rd-debug] send command skipped, target missing command=${commandType} id=${commandId}`);
return false;
}
try {
logger.debug(`[rd] send command ${commandType} -> ${target.id}`);
logger.debug(`[rd-debug] send command command=${commandType} client=${target.id} id=${commandId} payload=${JSON.stringify(payload || {})}`);
target.ws.send(encodeMessage({ type: "command", commandType: commandType as any, payload, id: commandId }));
metrics.recordCommand(commandType);
return true;
@@ -326,6 +330,7 @@ export function handleRemoteDesktopViewerMessage(ws: ServerWebSocket<SocketData>
logger.debug(`[rd] inbound viewer msg type=${payload.type} client=${clientId}`);
switch (payload.type) {
case "desktop_start":
logger.debug(`[rd-debug] desktop_start requested client=${clientId} session=${ws.data.sessionId || ""} state=${JSON.stringify(state)} viewers=${sessionManager.getRdSessionsForClient(clientId).length} webrtc=${(payload as any).webrtc === true}`);
if (!state.isStreaming) {
if (target.os === "darwin" && target.permissions) {
const missing: string[] = [];
@@ -363,14 +368,15 @@ export function handleRemoteDesktopViewerMessage(ws: ServerWebSocket<SocketData>
sendDesktopCommand(target, "desktop_start", {});
state.isStreaming = true;
rdStreamingState.set(clientId, state);
logger.debug(`[rd] started streaming for client ${clientId}${(payload as any).webrtc === true ? " (webrtc)" : ""}`);
logger.debug(`[rd-debug] desktop_start forwarded client=${clientId} nextState=${JSON.stringify(state)} webrtc=${(payload as any).webrtc === true}`);
} else {
logger.debug(`[rd] ignoring duplicate desktop_start for client ${clientId}`);
logger.warn(`[rd-debug] desktop_start ignored duplicate client=${clientId} state=${JSON.stringify(state)} viewers=${sessionManager.getRdSessionsForClient(clientId).length}`);
}
break;
case "desktop_stop": {
const otherViewers = sessionManager.getRdSessionsForClient(clientId)
.filter(s => s.id !== ws.data.sessionId);
logger.debug(`[rd-debug] desktop_stop requested client=${clientId} session=${ws.data.sessionId || ""} otherViewers=${otherViewers.length} state=${JSON.stringify(state)}`);
if (otherViewers.length === 0) {
stopRemoteDesktopRecording(clientId, "desktop stopped");
sendDesktopCommand(target, "desktop_stop", {});
@@ -648,6 +654,12 @@ function handleRemoteDesktopFrame(payload: any) {
const frameFps = Number(header?.fps) || 0;
if (frameFps > 0) state.lastFps = frameFps;
rdStreamingState.set(clientId, state);
const now = Date.now();
const lastDebugFrameLog = rdDebugFrameLogAt.get(clientId) || 0;
if (!lastDebugFrameLog || now - lastDebugFrameLog >= 2000) {
rdDebugFrameLogAt.set(clientId, now);
logger.debug(`[rd-debug] frame received client=${clientId} bytes=${bytes?.byteLength || 0} fps=${frameFps || 0} viewers=${sessionManager.getRdSessionsForClient(clientId).length} header=${JSON.stringify(header || {})}`);
}
broadcastRemoteDesktopFrame(clientId, bytes, header);
}
@@ -660,7 +672,9 @@ function broadcastRemoteDesktopFrame(clientId: string, bytes: Uint8Array, header
}
recordRemoteDesktopFrame(clientId, header, bytes);
const buf = buildViewerFrameBuffer(bytes, header);
return broadcastFrameToViewers(sessionManager.getRdSessionsForClient(clientId), buf, header);
const sessions = sessionManager.getRdSessionsForClient(clientId);
const sent = broadcastFrameToViewers(sessions, buf, header);
return sent;
}
(globalThis as any).__rdBroadcast = (clientId: string, bytes: Uint8Array, header?: any): boolean => {