dashboard performance and more coding safety checks in integrity analyser

This commit is contained in:
Oliver Kieselbach
2026-04-17 11:16:41 +02:00
parent ef1d57f32b
commit 64240249ac
6 changed files with 182 additions and 40 deletions
@@ -269,7 +269,11 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Telemetry.Analyzers
if (raw != null)
{
try { intValue = Convert.ToInt32(raw); }
catch { intValue = 0; }
catch (Exception ex) when (ex is InvalidCastException || ex is FormatException || ex is OverflowException)
{
_logger.Warning($"{Name}: LabConfig '{valueName}' has unparseable value (raw type={raw.GetType().Name}): {ex.Message}");
intValue = 0;
}
}
result.Values[valueName] = intValue;
if (intValue == 1)
@@ -301,7 +305,11 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Telemetry.Analyzers
return new MoSetupCheckResult { KeyExists = true, Value = 0 };
int intValue = 0;
try { intValue = Convert.ToInt32(raw); } catch { intValue = 0; }
try { intValue = Convert.ToInt32(raw); }
catch (Exception ex) when (ex is InvalidCastException || ex is FormatException || ex is OverflowException)
{
_logger.Warning($"{Name}: MoSetup 'AllowUpgradesWithUnsupportedTPMOrCPU' has unparseable value (raw type={raw.GetType().Name}): {ex.Message}");
}
return new MoSetupCheckResult { KeyExists = true, Value = intValue };
}
}
@@ -345,7 +353,12 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Telemetry.Analyzers
if (raw == null) continue;
int intValue = 0;
try { intValue = Convert.ToInt32(raw); } catch { intValue = 0; }
try { intValue = Convert.ToInt32(raw); }
catch (Exception ex) when (ex is InvalidCastException || ex is FormatException || ex is OverflowException)
{
_logger.Warning($"{Name}: PCHC UpgradeEligibility for SID {sid} has unparseable value (raw type={raw.GetType().Name}): {ex.Message}");
continue;
}
if (intValue == 1)
{
@@ -415,13 +428,17 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Telemetry.Analyzers
using (var searcher = new ManagementObjectSearcher(
@"root\CIMV2\Security\MicrosoftTpm",
"SELECT IsEnabled_InitialValue, IsActivated_InitialValue FROM Win32_Tpm"))
using (var results = searcher.Get())
{
foreach (ManagementObject tpm in searcher.Get())
foreach (ManagementObject tpm in results)
{
var isEnabled = tpm["IsEnabled_InitialValue"] != null
&& Convert.ToBoolean(tpm["IsEnabled_InitialValue"]);
// At least one Tpm instance exists — return its enabled state
return isEnabled;
using (tpm)
{
var isEnabled = tpm["IsEnabled_InitialValue"] != null
&& Convert.ToBoolean(tpm["IsEnabled_InitialValue"]);
// At least one Tpm instance exists — return its enabled state
return isEnabled;
}
}
}
// No Win32_Tpm instance returned — no TPM present
@@ -1 +1 @@
1031
1032
@@ -1,10 +1,11 @@
"use client";
import { useRouter } from "next/navigation";
import { useState, useEffect, useRef } from "react";
import { useState, useEffect, useRef, useMemo, useDeferredValue } from "react";
import { Session } from "../types";
import { trackEvent } from "@/lib/appInsights";
import { fuzzyContains } from "@/utils/fuzzy";
import { buildUniqueValuesByField } from "./uniqueValuesByField";
// Column definition for the session table
interface ColumnDef {
@@ -163,8 +164,12 @@ export function SessionTable({
{ key: "status", label: "Status" },
];
const searchSuggestions: SearchSuggestion[] = (() => {
const q = searchQuery.trim().toLowerCase();
// Defer expensive suggestion scans so rapid typing keeps the input responsive.
const deferredSearchQuery = useDeferredValue(searchQuery);
const deferredTenantIdFilter = useDeferredValue(tenantIdFilter);
const searchSuggestions = useMemo<SearchSuggestion[]>(() => {
const q = deferredSearchQuery.trim().toLowerCase();
if (q.length < 2 || sessions.length === 0) return [];
if (/^[><]=?\s*\d+$/.test(q)) return [];
@@ -204,16 +209,18 @@ export function SessionTable({
}
return [...exactResults, ...fuzzyResults];
})();
// SEARCH_FIELDS is a stable literal — intentionally omitted from deps.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [deferredSearchQuery, sessions]);
// Fuzzy-match tenants by domain name or tenant ID
const tenantSuggestions = (() => {
const q = tenantIdFilter.trim().toLowerCase();
const tenantSuggestions = useMemo(() => {
const q = deferredTenantIdFilter.trim().toLowerCase();
if (q.length < 2 || tenantList.length === 0) return [];
return tenantList
.filter((t) => t.domainName.toLowerCase().includes(q) || t.tenantId.toLowerCase().includes(q))
.slice(0, 8);
})();
}, [deferredTenantIdFilter, tenantList]);
// Persist visible columns to localStorage
useEffect(() => {
@@ -242,15 +249,16 @@ export function SessionTable({
}
}, [showColumnSelector, openFilterColumn, showTenantSuggestions, showSearchSuggestions]);
// Compute unique values for a filterable field from all sessions (not filtered)
const getUniqueValues = (field: keyof Session): string[] => {
const values = new Set<string>();
for (const s of sessions) {
const v = s[field];
if (v != null && v !== "") values.add(String(v));
}
return [...values].sort();
};
// Compute unique values for every filterable field in a single pass over sessions.
// Memoized so that header re-renders pay only a map lookup, not N × O(sessions).
const uniqueValuesByField = useMemo(
() =>
buildUniqueValuesByField(
sessions,
ALL_COLUMNS.map((c) => c.filterKey).filter((k): k is keyof Session => !!k),
),
[sessions],
);
const activeFilterCount = Object.values(columnFilters).reduce(
(sum, s) => sum + (s.size > 0 ? 1 : 0), 0
@@ -681,7 +689,7 @@ export function SessionTable({
onSort={onSort}
className={["eventCount", "duration", "started", "country", "agentVersion", "osName", "osBuild", "osDisplayVersion", "osEdition", "osLanguage"].includes(col.key) ? "px-3" : undefined}
filterKey={col.filterKey}
filterValues={col.filterKey ? getUniqueValues(col.filterKey) : undefined}
filterValues={col.filterKey ? uniqueValuesByField[col.filterKey] : undefined}
activeFilter={col.filterKey ? columnFilters[col.filterKey] : undefined}
isFilterOpen={openFilterColumn === col.key}
onFilterToggle={() => setOpenFilterColumn(openFilterColumn === col.key ? null : col.key)}
@@ -0,0 +1,68 @@
import { describe, it, expect } from "vitest";
import { buildUniqueValuesByField } from "../uniqueValuesByField";
import type { Session } from "@/types";
function s(partial: Partial<Session>): Session {
return partial as unknown as Session;
}
describe("buildUniqueValuesByField", () => {
it("returns an empty string[] for every requested field when sessions is empty", () => {
const result = buildUniqueValuesByField([], ["status", "osName"]);
expect(result.status).toEqual([]);
expect(result.osName).toEqual([]);
});
it("collects unique non-empty values per field", () => {
const sessions = [
s({ status: "success", osName: "Windows 11" }),
s({ status: "failed", osName: "Windows 11" }),
s({ status: "success", osName: "Windows 10" }),
];
const result = buildUniqueValuesByField(sessions, ["status", "osName"]);
expect(result.status).toEqual(["failed", "success"]);
expect(result.osName).toEqual(["Windows 10", "Windows 11"]);
});
it("skips null, undefined, and empty-string values", () => {
const sessions = [
s({ status: "success", osName: null as unknown as string }),
s({ status: undefined as unknown as string, osName: "" }),
s({ status: "", osName: "Windows 11" }),
];
const result = buildUniqueValuesByField(sessions, ["status", "osName"]);
expect(result.status).toEqual(["success"]);
expect(result.osName).toEqual(["Windows 11"]);
});
it("coerces non-string values to string (e.g. numbers)", () => {
const sessions = [
s({ eventCount: 10 }),
s({ eventCount: 3 }),
s({ eventCount: 10 }),
];
const result = buildUniqueValuesByField(sessions, ["eventCount"]);
expect(result.eventCount).toEqual(["10", "3"]);
});
it("returns sorted output", () => {
const sessions = [
s({ status: "zulu" }),
s({ status: "alpha" }),
s({ status: "mike" }),
];
const result = buildUniqueValuesByField(sessions, ["status"]);
expect(result.status).toEqual(["alpha", "mike", "zulu"]);
});
it("makes a single pass regardless of field count (behavioral: all fields populated from same session list)", () => {
const sessions = [
s({ status: "success", osName: "Win11", manufacturer: "Dell" }),
s({ status: "failed", osName: "Win10", manufacturer: "HP" }),
];
const result = buildUniqueValuesByField(sessions, ["status", "osName", "manufacturer"]);
expect(result.status).toEqual(["failed", "success"]);
expect(result.osName).toEqual(["Win10", "Win11"]);
expect(result.manufacturer).toEqual(["Dell", "HP"]);
});
});
@@ -0,0 +1,22 @@
import type { Session } from "../types";
export type UniqueValuesByField = Partial<Record<keyof Session, string[]>>;
export function buildUniqueValuesByField(
sessions: Session[],
fields: readonly (keyof Session)[],
): UniqueValuesByField {
const sets = new Map<keyof Session, Set<string>>();
for (const f of fields) sets.set(f, new Set<string>());
for (const s of sessions) {
for (const f of fields) {
const v = s[f];
if (v != null && v !== "") sets.get(f)!.add(String(v));
}
}
const result: UniqueValuesByField = {};
for (const f of fields) result[f] = [...sets.get(f)!].sort();
return result;
}
@@ -100,6 +100,16 @@ export function useDashboardSessions({
const loadingMoreRef = useRef(loadingMore);
loadingMoreRef.current = loadingMore;
// Synchronous lock: set true BEFORE the first await so two triggers in the same
// render cycle (pagination effect + debounced search effect in page.tsx) cannot
// both pass the guard. The state-mirrored loadingMoreRef above lags by one tick
// and is insufficient for that race.
const fetchLockRef = useRef(false);
// Cancellation token for the progressive loadAll() loop. Bumped whenever the
// session list is being reset (refetch/refetchWith/unmount) so an in-flight
// loop stops appending to a now-stale list.
const loadAllTokenRef = useRef(0);
const hasInitialFetch = useRef(false);
const hasGlobalModeInitialized = useRef(false);
const hasJoinedGroup = useRef(false);
@@ -223,45 +233,62 @@ export function useDashboardSessions({
}, [fetchSessionsBatch, fetchBlockedDevices]);
const refetch = useCallback(() => {
loadAllTokenRef.current++; // cancel any in-flight progressive loader
setLoading(true);
fetchSessions();
}, [fetchSessions]);
const refetchWith = useCallback((tenantIdOverride: string) => {
loadAllTokenRef.current++; // cancel any in-flight progressive loader
setLoading(true);
fetchSessions(undefined, tenantIdOverride);
}, [fetchSessions]);
const loadMore = useCallback(() => {
if (!cursorRef.current || loadingMoreRef.current) return;
if (!cursorRef.current || fetchLockRef.current) return;
fetchLockRef.current = true;
setLoadingMore(true);
fetchSessions(cursorRef.current);
fetchSessions(cursorRef.current).finally(() => {
fetchLockRef.current = false;
});
}, [fetchSessions]);
// Progressive loader — fetches ALL remaining sessions batch by batch.
// Used when search is active and local results are insufficient.
const loadAll = useCallback(async () => {
if (!cursorRef.current || loadingMoreRef.current) return;
if (!cursorRef.current || fetchLockRef.current) return;
fetchLockRef.current = true;
const myToken = ++loadAllTokenRef.current;
setLoadingMore(true);
let currentCursor: string | null = cursorRef.current;
try {
let currentCursor: string | null = cursorRef.current;
while (currentCursor && loadAllTokenRef.current === myToken) {
const result = await fetchSessionsBatch(currentCursor);
if (!result || loadAllTokenRef.current !== myToken) break;
while (currentCursor) {
const result = await fetchSessionsBatch(currentCursor);
if (!result) break;
setSessions((prev) => [...prev, ...result.sessions]);
setCursor(result.cursor);
setHasMore(result.hasMore);
currentCursor = result.hasMore ? result.cursor : null;
setSessions((prev) => [...prev, ...result.sessions]);
setCursor(result.cursor);
setHasMore(result.hasMore);
currentCursor = result.hasMore ? result.cursor : null;
}
} finally {
fetchLockRef.current = false;
setLoadingMore(false);
}
setLoadingMore(false);
}, [fetchSessionsBatch]);
const removeSession = useCallback((sessionId: string) => {
setSessions((prev) => prev.filter((s) => s.sessionId !== sessionId));
}, []);
// Cancel any in-flight progressive loadAll() loop on unmount so it
// doesn't call setSessions against a torn-down component.
useEffect(() => {
return () => {
loadAllTokenRef.current++;
};
}, []);
// Initial fetch — gated on user role + tenant readiness
useEffect(() => {
if (user && !user.isTenantAdmin && !user.isGlobalAdmin && user.role !== "Operator") {