UI improvements

This commit is contained in:
KingKDot
2026-05-30 22:44:55 -05:00
parent 5933297686
commit 2a90053eee
24 changed files with 1498 additions and 420 deletions
+50 -16
View File
@@ -219,8 +219,8 @@ export function upsertClientRow(
) {
const now = partial.lastSeen ?? Date.now();
db.run(
`INSERT INTO clients (id, hwid, role, ip, host, os, arch, version, user, nickname, custom_tag, custom_tag_note, monitors, country, last_seen, online, ping_ms, build_tag, built_by_user_id, enrollment_status, public_key, key_fingerprint, cpu, gpu, ram, is_admin, elevation, permissions)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, COALESCE(?, 0), ?, ?)
`INSERT INTO clients (id, hwid, role, ip, host, os, arch, version, user, nickname, custom_tag, custom_tag_note, monitors, country, last_seen, online, ping_ms, build_tag, built_by_user_id, enrollment_status, public_key, key_fingerprint, cpu, gpu, ram, battery_percent, battery_charging, is_admin, elevation, permissions)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, COALESCE(?, 0), ?, ?)
ON CONFLICT(id) DO UPDATE SET
hwid=COALESCE(excluded.hwid, clients.hwid),
role=COALESCE(excluded.role, clients.role),
@@ -246,6 +246,8 @@ export function upsertClientRow(
cpu=COALESCE(excluded.cpu, clients.cpu),
gpu=COALESCE(excluded.gpu, clients.gpu),
ram=COALESCE(excluded.ram, clients.ram),
battery_percent=COALESCE(excluded.battery_percent, clients.battery_percent),
battery_charging=COALESCE(excluded.battery_charging, clients.battery_charging),
is_admin=COALESCE(excluded.is_admin, clients.is_admin),
elevation=COALESCE(excluded.elevation, clients.elevation),
permissions=COALESCE(excluded.permissions, clients.permissions)
@@ -275,6 +277,8 @@ export function upsertClientRow(
partial.cpu ?? null,
partial.gpu ?? null,
partial.ram ?? null,
partial.batteryPercent ?? null,
partial.batteryCharging !== undefined && partial.batteryCharging !== null ? (partial.batteryCharging ? 1 : 0) : null,
partial.isAdmin !== undefined ? (partial.isAdmin ? 1 : 0) : null,
partial.elevation ?? null,
partial.permissions ? JSON.stringify(partial.permissions) : null,
@@ -618,6 +622,7 @@ export function markAllClientsOffline() {
export const SUSPICIOUS_FLOOD_THRESHOLD = Math.max(2, Number(process.env.OVERLORD_SUSPICIOUS_FLOOD_THRESHOLD || 40));
const SUSPICIOUS_IP_FLOOD_WINDOW_MS = Math.max(60_000, Number(process.env.OVERLORD_SUSPICIOUS_IP_WINDOW_MS || 300_000));
const SUSPICIOUS_SCAN_MAX_CLIENTS = Math.max(0, Number(process.env.OVERLORD_SUSPICIOUS_SCAN_MAX_CLIENTS || 50_000));
export function getFloodedHwids(threshold = SUSPICIOUS_FLOOD_THRESHOLD): Set<string> {
const rows = db
@@ -853,6 +858,12 @@ 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";
@@ -884,27 +895,48 @@ export function listClients(filters: ListFilters): ListResult {
})();
const totalRow = db
.query<{ c: number }>(`SELECT COUNT(*) as c FROM clients c LEFT JOIN client_groups g ON g.id = c.group_id ${whereSql}`)
.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 LEFT JOIN client_groups g ON g.id = c.group_id ${whereSql ? `${whereSql} AND c.online=1` : "WHERE c.online=1"}`,
`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 rows = db
.query<any>(
`SELECT 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.is_admin as isAdmin, c.elevation, c.permissions, c.disconnect_reason as disconnectReason, c.disconnect_detail as disconnectDetail, c.group_id as groupId, g.name as groupName, g.color as groupColor, c.notifications_muted as notificationsMuted, c.deny_reason as denyReason
FROM clients c
LEFT JOIN client_groups g ON g.id = c.group_id
${whereSql}
${orderBy}
LIMIT ? OFFSET ?`,
)
.all(...params, pageSize, offset);
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 { floodedHwids, floodedHardware, floodedIps } = getFloodSetsWithCache();
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}
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 suspiciousSets = shouldScanSuspicious
? getFloodSetsWithCache()
: { floodedHwids: new Set<string>(), floodedHardware: new Set<string>(), floodedIps: new Set<string>() };
const thumbnailSummaries = getThumbnailSummaries(rows.map((row: any) => row.id));
const items = rows.map((c: any) => {
@@ -936,6 +968,8 @@ export function listClients(filters: ListFilters): ListResult {
cpu: c.cpu || null,
gpu: c.gpu || null,
ram: c.ram || null,
batteryPercent: typeof c.batteryPercent === "number" ? c.batteryPercent : null,
batteryCharging: c.batteryCharging === null || c.batteryCharging === undefined ? null : c.batteryCharging === 1,
isAdmin: c.isAdmin === 1,
elevation: c.elevation || null,
permissions: c.permissions ? (() => { try { return JSON.parse(c.permissions); } catch { return null; } })() : null,
@@ -948,7 +982,7 @@ export function listClients(filters: ListFilters): ListResult {
denyReason: c.denyReason || null,
hasThumbnail: thumbnail?.hasThumbnail ?? false,
thumbnailVersion: thumbnail?.thumbnailVersion ?? 0,
suspiciousFlags: computeClientSuspiciousFlags(c, { floodedHwids, floodedHardware, floodedIps }),
suspiciousFlags: shouldScanSuspicious ? computeClientSuspiciousFlags(c, suspiciousSets) : [],
};
});
+12
View File
@@ -79,6 +79,12 @@ try {
try {
db.run(`ALTER TABLE clients ADD COLUMN ram TEXT`);
} catch {}
try {
db.run(`ALTER TABLE clients ADD COLUMN battery_percent INTEGER`);
} catch {}
try {
db.run(`ALTER TABLE clients ADD COLUMN battery_charging INTEGER`);
} catch {}
try {
db.run(`ALTER TABLE clients ADD COLUMN is_admin INTEGER NOT NULL DEFAULT 0`);
} catch {}
@@ -94,6 +100,12 @@ db.run(
db.run(
`CREATE INDEX IF NOT EXISTS idx_clients_online_last_seen ON clients(online, last_seen DESC);`,
);
db.run(
`CREATE INDEX IF NOT EXISTS idx_clients_enrollment_online_bookmark_id ON clients(enrollment_status, online DESC, bookmarked DESC, id ASC);`,
);
db.run(
`CREATE INDEX IF NOT EXISTS idx_clients_enrollment_online_last_seen ON clients(enrollment_status, online DESC, last_seen DESC, id ASC);`,
);
db.run(
`CREATE INDEX IF NOT EXISTS idx_clients_os_last_seen ON clients(os, last_seen DESC);`,
);
+2
View File
@@ -29,6 +29,8 @@ export type Hello = {
monitors: number;
monitorInfo?: { width: number; height: number }[];
country?: string;
batteryPercent?: number;
batteryCharging?: boolean;
publicKey?: string;
signature?: string;
publicIP?: string;
@@ -20,8 +20,8 @@ function acceptsGzip(req: Request): boolean {
return (req.headers.get("accept-encoding") ?? "").includes("gzip");
}
const STATIC_ASSET_CACHE = "public, max-age=86400, stale-while-revalidate=604800";
const MUTABLE_ASSET_CACHE = "public, max-age=3600, stale-while-revalidate=86400";
const STATIC_ASSET_CACHE = "public, max-age=31536000, immutable";
const MUTABLE_ASSET_CACHE = "public, max-age=86400, stale-while-revalidate=604800";
const NO_CACHE = "no-cache";
function assetCacheControl(relativePath: string): string {
@@ -75,7 +75,7 @@ export async function handleClientRoutes(
return new Response("Unauthorized", { status: 401 });
}
const page = Math.max(1, Number(url.searchParams.get("page") || 1));
const pageSize = Math.max(1, Number(url.searchParams.get("pageSize") || 12));
const pageSize = Math.min(200, Math.max(1, Number(url.searchParams.get("pageSize") || 12)));
const search = (url.searchParams.get("q") || "").toLowerCase().trim();
const sort = url.searchParams.get("sort") || "last_seen_desc";
const statusFilter = url.searchParams.get("status") || "all";
@@ -456,6 +456,12 @@ export async function handleWebSocketMessage(
if (typeof v !== "string") return undefined;
return v.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, "").slice(0, max);
};
const _percent = (v: unknown): number | undefined => {
if (typeof v !== "number" || !Number.isFinite(v)) return undefined;
const n = Math.round(v);
return n >= 0 && n <= 100 ? n : undefined;
};
const batteryPercent = _percent((payload as any).batteryPercent);
upsertClientRow({
id: resolvedId,
hwid: _s((payload as any).hwid) || resolvedId,
@@ -470,6 +476,8 @@ export async function handleWebSocketMessage(
cpu: _s((payload as any).cpu),
gpu: _s((payload as any).gpu),
ram: _s((payload as any).ram, 64),
batteryPercent,
batteryCharging: batteryPercent !== undefined ? !!(payload as any).batteryCharging : undefined,
country,
lastSeen: Date.now(),
online: 0 as any,
+2
View File
@@ -37,6 +37,8 @@ export type ClientInfo = {
cpu?: string;
gpu?: string;
ram?: string;
batteryPercent?: number | null;
batteryCharging?: boolean | null;
isAdmin?: boolean;
elevation?: string;
permissions?: Record<string, boolean>;
+14
View File
@@ -35,6 +35,13 @@ function sanitizeMonitorCount(val: unknown): number | undefined {
return n;
}
function sanitizePercent(val: unknown): number | undefined {
if (typeof val !== "number" || !Number.isFinite(val)) return undefined;
const n = Math.round(val);
if (n < 0 || n > 100) return undefined;
return n;
}
function sanitizeJsonField(
val: unknown,
opts: { maxJsonBytes: number; maxArrayLen?: number; maxKeys?: number },
@@ -101,6 +108,11 @@ export async function handleHello(
info.cpu = sanitizeInfoString((payload as any).cpu) || info.cpu;
info.gpu = sanitizeInfoString((payload as any).gpu) || info.gpu;
info.ram = sanitizeInfoString((payload as any).ram, 64) || info.ram;
const batteryPercent = sanitizePercent((payload as any).batteryPercent);
if (batteryPercent !== undefined) {
info.batteryPercent = batteryPercent;
info.batteryCharging = !!(payload as any).batteryCharging;
}
const geoip = await getGeoip();
const geo = info.ip ? geoip.lookup(info.ip) : undefined;
const countryRaw =
@@ -127,6 +139,8 @@ export async function handleHello(
cpu: info.cpu,
gpu: info.gpu,
ram: info.ram,
batteryPercent: info.batteryPercent,
batteryCharging: info.batteryCharging,
isAdmin: info.isAdmin,
elevation: info.elevation,
permissions: info.permissions,