web: canonical inner-async wrappers for all fetch-effect lint sites (batch 1)

Convert every useEffect that directly invokes a state-setting fetch
callback to the canonical inner-async shape (const run = async () => {...};
void run();) — the react-hooks/set-state-in-effect rule traces direct
calls but accepts effect-scoped async functions. Semantic no-op: an async
body runs synchronously until its first await, so execution order is
byte-identical. Leading loading/error/data resets that are part of the
fetch flow move to the top of run in unchanged order.

52 files, ~60 sites. Lint 99 -> 36 warnings; remaining sites are
reset/derive effects and sensitive context/sidebar refactors (next
batches).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Oliver Kieselbach
2026-08-12 18:55:49 +02:00
parent fda52666d6
commit 9236aba46e
52 changed files with 365 additions and 213 deletions
@@ -60,7 +60,10 @@ function BackupDetailContent() {
}, [backupId, getAccessToken, setError]);
useEffect(() => {
void load();
const run = async () => {
await load();
};
void run();
}, [load]);
const openRestoreModal = async (tableName: string, pk: string, rk: string) => {
@@ -44,7 +44,10 @@ export default function BackupsListPage() {
}, [getAccessToken, setError]);
useEffect(() => {
void load();
const run = async () => {
await load();
};
void run();
}, [load]);
// Poll the active job until terminal, then refresh the list.
@@ -57,7 +60,10 @@ export default function BackupsListPage() {
activeJob.state === "BlockedTerminal"
) {
// Terminal — final refresh and stop polling.
void load();
const run = async () => {
await load();
};
void run();
return;
}
@@ -168,7 +168,10 @@ export function DistressReportsSection({
};
useEffect(() => {
fetchReports();
const run = async () => {
await fetchReports();
};
void run();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
@@ -74,7 +74,10 @@ export function FeedbackSection({ getAccessToken, setError }: FeedbackSectionPro
}, [getAccessToken, setError]);
useEffect(() => {
fetchFeedback();
const run = async () => {
await fetchFeedback();
};
void run();
}, [fetchFeedback]);
// Partition by tab. Server returns mixed; client splits + filters per active tab so the
@@ -59,7 +59,10 @@ export function IgnoredSoftwareTab({
// Lazy load on mount
useEffect(() => {
if (!ignoredLoaded) {
fetchIgnoredSoftware();
const run = async () => {
await fetchIgnoredSoftware();
};
void run();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ignoredLoaded]);
@@ -83,7 +83,10 @@ export function MappedSoftwareTab({
// Lazy load on first render
useEffect(() => {
if (!mappedLoaded) {
fetchCpeMappings();
const run = async () => {
await fetchCpeMappings();
};
void run();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [mappedLoaded]);
@@ -179,10 +179,13 @@ export function OpsEventsSection({
// re-fire after every successful page-N click, race a page-1 fetch against
// it, and snap the user back to page 1.
useEffect(() => {
setContinuation(null);
setContinuationStack([]);
setPageNumber(1);
fetchEvents(null);
const run = async () => {
setContinuation(null);
setContinuationStack([]);
setPageNumber(1);
await fetchEvents(null);
};
void run();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [categoryFilter, dateFromIso, dateToIso]);
@@ -68,7 +68,10 @@ export function TenantAdminSection({
};
useEffect(() => {
fetchTenantAdmins(tenantId);
const run = async () => {
await fetchTenantAdmins(tenantId);
};
void run();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [tenantId]);
@@ -112,17 +112,23 @@ export function UnmappedSoftwareTab({
// Load on mount + whenever the page index changes
useEffect(() => {
fetchPage(currentPage);
const run = async () => {
await fetchPage(currentPage);
};
void run();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentPage]);
// Refresh when parent signals (e.g. item restored from ignored tab) — reset to page 0
useEffect(() => {
if (refreshTrigger > 0) {
setSavedMappings(new Set());
setSelectedEntries(new Map());
if (currentPage !== 0) setCurrentPage(0);
else fetchPage(0);
const run = async () => {
setSavedMappings(new Set());
setSelectedEntries(new Map());
if (currentPage !== 0) setCurrentPage(0);
else await fetchPage(0);
};
void run();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [refreshTrigger]);
@@ -96,7 +96,10 @@ export function VulnerabilityDataSection({
}, [getAccessToken]);
useEffect(() => {
void refreshSyncStatus();
const run = async () => {
await refreshSyncStatus();
};
void run();
}, [refreshSyncStatus]);
const refreshSummary = useCallback(async () => {
@@ -120,7 +123,10 @@ export function VulnerabilityDataSection({
}, [getAccessToken, summaryDays]);
useEffect(() => {
void refreshSummary();
const run = async () => {
await refreshSummary();
};
void run();
}, [refreshSummary]);
// Sync local state when adminConfig changes (initial load)
@@ -58,7 +58,10 @@ function CustomsArchiveDetailContent() {
}, [tenantId, historyRowKey, getAccessToken, setError]);
useEffect(() => {
void load();
const run = async () => {
await load();
};
void run();
}, [load]);
const toggleExpand = useCallback(async (entry: CustomsArchiveEntrySummary) => {
@@ -43,7 +43,10 @@ export default function CustomsArchivePage() {
}, [filter, getAccessToken, setError]);
useEffect(() => {
void load();
const run = async () => {
await load();
};
void run();
}, [load]);
const confirmDelete = useCallback(async () => {
@@ -225,7 +225,10 @@ export function SectionAgentMetrics() {
// accounts refresh which would otherwise re-fire the effect after each
// request and ping-pong the loading spinner.
useEffect(() => {
fetchMetrics(sampleSize, windowDays);
const run = async () => {
await fetchMetrics(sampleSize, windowDays);
};
void run();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [sampleSize, windowDays]);
@@ -90,7 +90,10 @@ export function SectionMcpUsage() {
}, [getAccessToken]);
useEffect(() => {
fetchData(dateRange);
const run = async () => {
await fetchData(dateRange);
};
void run();
}, [fetchData, dateRange]);
// Aggregate by user
@@ -156,7 +156,10 @@ export function SectionPlatformUsage() {
useEffect(() => {
// Fetch metrics on mount
// Authorization is handled by admin layout ProtectedRoute
fetchMetrics();
const run = async () => {
await fetchMetrics();
};
void run();
}, [fetchMetrics]);
const formatDuration = (minutes: number) => {
@@ -62,10 +62,10 @@ export function DeletionManifestSummaryView({
useEffect(() => {
let cancelled = false;
setLoading(true);
setError(null);
setSummary(null);
(async () => {
setLoading(true);
setError(null);
setSummary(null);
try {
const resp = await authenticatedFetch(
api.sessionDeletions.storedManifest(sessionId, tenantId, manifestId, "summary"),
@@ -120,7 +120,10 @@ export function MaintenanceStatusBanner({
}, [getAccessToken, setError]);
useEffect(() => {
void fetchStatus();
const run = async () => {
await fetchStatus();
};
void run();
}, [fetchStatus, refreshKey]);
const triggerRun = useCallback(async () => {
@@ -85,8 +85,8 @@ export function RestoreBrowserTab({ getAccessToken, setError, setSuccessMessage
if (!onlyWithRestore || tenantsWithRestore !== null) return;
const controller = new AbortController();
let cancelled = false;
setLoadingRestoreFilter(true);
(async () => {
setLoadingRestoreFilter(true);
try {
const resp = await authenticatedFetch(
api.sessionDeletions.tenantsWithManifests(),
@@ -142,23 +142,24 @@ export function RestoreBrowserTab({ getAccessToken, setError, setSuccessMessage
// the operator never sees stale manifests / preview / restore-action that belong to the
// previous tenant. Codex P2.
useEffect(() => {
// Immediate clear: dropping the displayed data on switch means the operator never has a
// "this manifest from tenant A is selected while the dropdown says tenant B" moment.
setSessions(null);
setExpandedSessions(new Set());
setSelection(null);
setLoadedTenantId("");
if (!selectedTenantId) {
setLoadingSessions(false);
return;
}
const controller = new AbortController();
let cancelled = false;
setLoadingSessions(true);
(async () => {
// Immediate clear: dropping the displayed data on switch means the operator never has a
// "this manifest from tenant A is selected while the dropdown says tenant B" moment.
setSessions(null);
setExpandedSessions(new Set());
setSelection(null);
setLoadedTenantId("");
if (!selectedTenantId) {
setLoadingSessions(false);
return;
}
setLoadingSessions(true);
try {
const resp = await authenticatedFetch(
api.sessionDeletions.tenantManifests(selectedTenantId),
@@ -176,7 +176,10 @@ function InFlightTab({
}, [getAccessToken, setError]);
useEffect(() => {
void fetchAll();
const run = async () => {
await fetchAll();
};
void run();
}, [fetchAll, refreshKey]);
return (
@@ -253,7 +256,10 @@ function PoisonedTab({
}, [getAccessToken, setError]);
useEffect(() => {
void fetchPoisoned();
const run = async () => {
await fetchPoisoned();
};
void run();
}, [fetchPoisoned, refreshKey]);
return (
@@ -353,7 +359,10 @@ function StrandedTab({
}, [getAccessToken, setError]);
useEffect(() => {
void fetchStranded();
const run = async () => {
await fetchStranded();
};
void run();
}, [fetchStranded, refreshKey]);
return (
@@ -92,7 +92,10 @@ export default function PresencePage() {
// Initial load + whenever the window changes.
useEffect(() => {
fetchPresence();
const run = async () => {
await fetchPresence();
};
void run();
}, [fetchPresence, windowMinutes]);
// Auto-refresh on a fixed interval (reads the current window via ref).
@@ -80,7 +80,10 @@ export function SectionDelegatedAdmins() {
}, [getAccessToken]);
useEffect(() => {
fetchAssignments();
const run = async () => {
await fetchAssignments();
};
void run();
}, [fetchAssignments]);
const handleGrant = useCallback(async () => {
@@ -89,7 +89,10 @@ export function SectionTenantGroups() {
}, [getAccessToken]);
useEffect(() => {
fetchGroups();
const run = async () => {
await fetchGroups();
};
void run();
}, [fetchGroups]);
/** Runs a mutation with shared busy/error/refetch handling. `body` undefined ⇒ no JSON body. */
@@ -57,7 +57,10 @@ export function SectionUsagePlans() {
}, [getAccessToken]);
useEffect(() => {
fetchTiers();
const run = async () => {
await fetchTiers();
};
void run();
}, [fetchTiers]);
const handleSave = useCallback(async () => {
@@ -406,7 +406,10 @@ export function SectionTenantConfigReport() {
}, [selectedTenantId, getAccessToken]);
useEffect(() => {
fetchConfig();
const run = async () => {
await fetchConfig();
};
void run();
}, [fetchConfig]);
// Computed runtime
@@ -50,10 +50,10 @@ export default function InventoryTab({ scope }: { scope: SoftwareTabScope }) {
useEffect(() => {
if (!scopeInitialized) return;
if (!isGlobalAdmin && !tenantId) return;
if (needsTenantPick) { setResp(null); setLoading(false); return; }
let cancelled = false;
const run = async () => {
if (needsTenantPick) { setResp(null); setLoading(false); return; }
try {
setLoading(true);
const url = routeGlobal
@@ -358,8 +358,11 @@ function AppDetailContent() {
// wrong scope before the GA default-to-own-tenant has settled.
useEffect(() => {
if (!scopeInitialized) return;
setSessionsOffset(0);
Promise.all([fetchAnalytics(), fetchSessions(0, statusFilter)]);
const run = async () => {
setSessionsOffset(0);
await Promise.all([fetchAnalytics(), fetchSessions(0, statusFilter)]);
};
void run();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [scopeInitialized, days, scopeKey]);
@@ -156,10 +156,13 @@ export default function AuditPage() {
// delegated caller) so we don't fire a wasted request in the wrong scope. scopeKey changes on a
// tenant/GA-mode switch → refetch from page 1.
if (!scopeInitialized) return;
setContinuation(null);
setContinuationStack([]);
setPageNumber(1);
fetchPage(null, true);
const run = async () => {
setContinuation(null);
setContinuationStack([]);
setPageNumber(1);
await fetchPage(null, true);
};
void run();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [scopeKey, scopeInitialized, dateFromIso, dateToIso, excludeDeletions]);
@@ -201,8 +201,11 @@ export function useDashboardStats({
useEffect(() => {
if (disabled) return;
if (!globalAdminMode && !tenantId) return;
setStats(null);
fetchStats();
const run = async () => {
setStats(null);
await fetchStats();
};
void run();
// fetchStats is stable; intentionally not in deps to avoid re-running on identity flips.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [tenantId, globalAdminMode, submittedTenantIdFilter, days, disabled]);
@@ -21,12 +21,12 @@ export function useTenantList(
const [tenantList, setTenantList] = useState<TenantListItem[]>([]);
useEffect(() => {
if (!globalAdminMode) {
setTenantList([]);
return;
}
let cancelled = false;
(async () => {
const run = async () => {
if (!globalAdminMode) {
setTenantList([]);
return;
}
try {
const res = await authenticatedFetch(api.config.all(), getAccessToken);
if (!res.ok || cancelled) return;
@@ -40,7 +40,8 @@ export function useTenantList(
} catch {
// Non-critical — tenant autocomplete just won't work
}
})();
};
void run();
return () => { cancelled = true; };
}, [globalAdminMode, getAccessToken]);
@@ -158,33 +158,39 @@ function DiagnosisContent() {
useEffect(() => {
if (!sessionId) return;
if (!globalAdminMode && !tenantId) return; // wait for real tenant ID
sessionIdRef.current = sessionId;
if (lastFetchedSessionId.current !== sessionId) {
hasInitialFetch.current = false;
lastFetchedSessionId.current = sessionId;
sessionFetchDone.current = false;
analysisFetchDone.current = false;
setSessionTenantId(null);
setLoading(true);
}
if (hasInitialFetch.current) return;
hasInitialFetch.current = true;
const run = async () => {
sessionIdRef.current = sessionId;
if (lastFetchedSessionId.current !== sessionId) {
hasInitialFetch.current = false;
lastFetchedSessionId.current = sessionId;
sessionFetchDone.current = false;
analysisFetchDone.current = false;
setSessionTenantId(null);
setLoading(true);
}
if (hasInitialFetch.current) return;
hasInitialFetch.current = true;
// Performance: eager-set sessionTenantId from TenantContext when known, so
// the events/analysis effect fires in parallel with fetchSessionDetails
// instead of waiting for its roundtrip — eliminates the fetch waterfall.
// Global Admins in all-tenant view fall through with null (backend resolves
// the tenant from the session itself).
if (!globalAdminMode && isGuid(tenantId)) {
setSessionTenantId(tenantId);
}
// Performance: eager-set sessionTenantId from TenantContext when known, so
// the events/analysis effect fires in parallel with fetchSessionDetails
// instead of waiting for its roundtrip — eliminates the fetch waterfall.
// Global Admins in all-tenant view fall through with null (backend resolves
// the tenant from the session itself).
if (!globalAdminMode && isGuid(tenantId)) {
setSessionTenantId(tenantId);
}
fetchSessionDetails();
await fetchSessionDetails();
};
void run();
}, [sessionId, tenantId, globalAdminMode, fetchSessionDetails]);
useEffect(() => {
if (sessionTenantId && sessionId) {
Promise.all([fetchEvents(), fetchAnalysisResults()]);
const run = async () => {
await Promise.all([fetchEvents(), fetchAnalysisResults()]);
};
void run();
}
}, [sessionTenantId, sessionId, fetchEvents, fetchAnalysisResults]);
@@ -210,8 +210,11 @@ export function useFleetHealth({
// previous-scope numbers don't linger if the refetch errors out.
useEffect(() => {
if (!scopeInitialized) return;
setData(null);
fetchData();
const run = async () => {
setData(null);
await fetchData();
};
void run();
// fetchData is stable; scopeKey/days drive the refetch.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [scopeInitialized, scopeKey, days]);
@@ -103,7 +103,10 @@ export default function FleetHealthPage() {
// App install metrics are already backend-aggregated; fetch alongside fleet data.
useEffect(() => {
if (!scopeInitialized) return;
fetchAppMetrics(timeRange);
const run = async () => {
await fetchAppMetrics(timeRange);
};
void run();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [scopeInitialized, timeRange, scopeKey]);
@@ -37,16 +37,16 @@ export function useFleetSummaries(tenantIds: string[], days: number, homeTenantI
const key = [...tenantIds].sort().join(",");
useEffect(() => {
if (tenantIds.length === 0) {
setSummaries({});
setLoading(false);
return;
}
let cancelled = false;
setLoading(true);
const run = async () => {
if (tenantIds.length === 0) {
setSummaries({});
setLoading(false);
return;
}
setLoading(true);
const queue = [...tenantIds];
const result: Record<string, FleetSummary> = {};
@@ -79,7 +79,7 @@ export function useFleetSummaries(tenantIds: string[], days: number, homeTenantI
}
};
run();
void run();
return () => {
cancelled = true;
};
@@ -106,10 +106,10 @@ export default function GatherRulesPage() {
// badge showing the GA's HOME-tenant mode for every foreign tenant.)
useEffect(() => {
if (!effectiveTenantId) return;
// Reset first so a failed/forbidden fetch never carries the previous tenant's mode over.
setUnrestrictedMode(false);
let stale = false;
const fetchConfig = async () => {
const run = async () => {
// Reset first so a failed/forbidden fetch never carries the previous tenant's mode over.
setUnrestrictedMode(false);
try {
const response = await authenticatedFetch(api.config.featureFlags(effectiveTenantId), getAccessToken);
if (!stale && response.ok) {
@@ -120,7 +120,7 @@ export default function GatherRulesPage() {
// Silently default to restricted mode
}
};
fetchConfig();
void run();
return () => { stale = true; };
}, [effectiveTenantId, getAccessToken]);
@@ -187,12 +187,15 @@ export default function GeographicPerformancePage() {
useEffect(() => {
if (!scopeInitialized) return;
if (isTimeRangeMount.current) {
isTimeRangeMount.current = false;
} else {
setLoading(true);
}
fetchGeoMetrics(timeRange, groupBy);
const run = async () => {
if (isTimeRangeMount.current) {
isTimeRangeMount.current = false;
} else {
setLoading(true);
}
await fetchGeoMetrics(timeRange, groupBy);
};
void run();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [scopeInitialized, timeRange, groupBy, scopeKey]);
@@ -109,8 +109,10 @@ export default function HealthCheckPage() {
// fast backend checks render immediately while the MCP probe (possible cold start) catches up.
useEffect(() => {
if (user && !hasRun) {
performHealthCheck();
performMcpCheck();
const run = async () => {
await Promise.all([performHealthCheck(), performMcpCheck()]);
};
void run();
}
}, [user, hasRun, performHealthCheck, performMcpCheck]);
@@ -54,61 +54,64 @@ export function useProgressEvents({
// Reset per-session fetch guard + events when the selected session changes
useEffect(() => {
if (!session) {
lastFetchedSessionId.current = null;
setEvents([]);
return;
}
if (lastFetchedSessionId.current === session.sessionId) return;
lastFetchedSessionId.current = session.sessionId;
setEvents([]);
const fetchEvents = async () => {
try {
const response = await authenticatedFetch(
api.progress.sessionEvents(session.sessionId, tenantId),
getAccessToken,
);
if (response.ok) {
const data = await response.json();
const fetched: EnrollmentEvent[] = data.events || [];
setEvents((prev) => {
if (prev.length === 0) return fetched;
const existingIds = new Set(prev.map((e) => e.eventId));
const newEvents = fetched.filter((e) => !existingIds.has(e.eventId));
if (newEvents.length === 0) return prev;
return [...prev, ...newEvents].sort(
(a, b) => a.sequence - b.sequence,
);
});
} else {
addNotification(
"error",
"Backend Error",
`Failed to load enrollment events: ${response.statusText}`,
"progress-events-error",
);
}
} catch (error) {
if (error instanceof TokenExpiredError) {
addNotification(
"error",
"Session Expired",
error.message,
"session-expired-error",
);
} else {
console.error("Failed to fetch events:", error);
addNotification(
"error",
"Backend Not Reachable",
"Unable to load enrollment events. Please check your connection.",
"progress-events-error",
);
}
const run = async () => {
if (!session) {
lastFetchedSessionId.current = null;
setEvents([]);
return;
}
if (lastFetchedSessionId.current === session.sessionId) return;
lastFetchedSessionId.current = session.sessionId;
setEvents([]);
const fetchEvents = async () => {
try {
const response = await authenticatedFetch(
api.progress.sessionEvents(session.sessionId, tenantId),
getAccessToken,
);
if (response.ok) {
const data = await response.json();
const fetched: EnrollmentEvent[] = data.events || [];
setEvents((prev) => {
if (prev.length === 0) return fetched;
const existingIds = new Set(prev.map((e) => e.eventId));
const newEvents = fetched.filter((e) => !existingIds.has(e.eventId));
if (newEvents.length === 0) return prev;
return [...prev, ...newEvents].sort(
(a, b) => a.sequence - b.sequence,
);
});
} else {
addNotification(
"error",
"Backend Error",
`Failed to load enrollment events: ${response.statusText}`,
"progress-events-error",
);
}
} catch (error) {
if (error instanceof TokenExpiredError) {
addNotification(
"error",
"Session Expired",
error.message,
"session-expired-error",
);
} else {
console.error("Failed to fetch events:", error);
addNotification(
"error",
"Backend Not Reachable",
"Unable to load enrollment events. Please check your connection.",
"progress-events-error",
);
}
}
};
await fetchEvents();
};
fetchEvents();
void run();
}, [session, tenantId, getAccessToken, addNotification]);
const scheduleFetchEvents = useCallback(
@@ -72,12 +72,12 @@ export default function DeviceHistoryBanner({
const [expanded, setExpanded] = useState(false);
useEffect(() => {
if (!sessionId || !serialNumber) {
setData(null);
return;
}
let cancelled = false;
(async () => {
const run = async () => {
if (!sessionId || !serialNumber) {
setData(null);
return;
}
try {
const response = await authenticatedFetch(
api.metrics.deviceHistory(serialNumber, sessionId, effectiveTenantId),
@@ -89,7 +89,8 @@ export default function DeviceHistoryBanner({
} catch {
// fail-soft: no banner
}
})();
};
void run();
return () => {
cancelled = true;
};
@@ -68,7 +68,10 @@ export function useSessionAnalysis(
// delay the analysis render.
useEffect(() => {
if (!sessionId || !sessionTenantId || !isGuid(sessionTenantId)) return;
Promise.all([fetchAnalysisResults(), fetchVulnerabilityReport()]);
const run = async () => {
await Promise.all([fetchAnalysisResults(), fetchVulnerabilityReport()]);
};
void run();
}, [sessionId, sessionTenantId, fetchAnalysisResults, fetchVulnerabilityReport]);
return {
@@ -135,27 +135,30 @@ export function useSessionDetail({
if (!sessionId) return;
if (!globalAdminMode && !tenantId && !tenantIdOverride) return; // wait for a real target tenant
// Reset fetch flag only if navigating to a different session
if (lastFetchedSessionId.current !== sessionId) {
hasInitialFetch.current = false;
lastFetchedSessionId.current = sessionId;
setSessionTenantId(null);
}
const run = async () => {
// Reset fetch flag only if navigating to a different session
if (lastFetchedSessionId.current !== sessionId) {
hasInitialFetch.current = false;
lastFetchedSessionId.current = sessionId;
setSessionTenantId(null);
}
// Prevent duplicate fetches in React StrictMode (development double-mounting)
if (hasInitialFetch.current) return;
hasInitialFetch.current = true;
// Prevent duplicate fetches in React StrictMode (development double-mounting)
if (hasInitialFetch.current) return;
hasInitialFetch.current = true;
// Performance: eager-set sessionTenantId if we already know it from TenantContext.
// This lets dependent effects kick off fetchEvents/analysis/vulns/config in parallel
// with fetchSessionDetails instead of waiting for its roundtrip — eliminates a waterfall.
// Global Admins in all-tenant view fall through with null and keep the old behavior.
const knownTenantId = resolveEffectiveTenantId();
if (knownTenantId && isGuid(knownTenantId)) {
setSessionTenantId(knownTenantId);
}
// Performance: eager-set sessionTenantId if we already know it from TenantContext.
// This lets dependent effects kick off fetchEvents/analysis/vulns/config in parallel
// with fetchSessionDetails instead of waiting for its roundtrip — eliminates a waterfall.
// Global Admins in all-tenant view fall through with null and keep the old behavior.
const knownTenantId = resolveEffectiveTenantId();
if (knownTenantId && isGuid(knownTenantId)) {
setSessionTenantId(knownTenantId);
}
fetchSessionDetails();
await fetchSessionDetails();
};
void run();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [sessionId, tenantId, globalAdminMode, tenantIdOverride, user]);
@@ -39,10 +39,9 @@ export function useDecisionGraph({
if (!sessionId) return;
let aborted = false;
setLoading(true);
setError(null);
(async () => {
const run = async () => {
setLoading(true);
setError(null);
try {
const url = api.inspector.decisionGraph(sessionId, tenantId);
const response = await authenticatedFetch(url, getAccessToken);
@@ -63,7 +62,8 @@ export function useDecisionGraph({
} finally {
if (!aborted) setLoading(false);
}
})();
};
void run();
return () => {
aborted = true;
@@ -44,10 +44,9 @@ export function useSessionAnchorEvents({
if (!sessionId) return;
let aborted = false;
setLoading(true);
setError(null);
(async () => {
const run = async () => {
setLoading(true);
setError(null);
try {
const url = api.sessions.events(sessionId, tenantId);
const response = await authenticatedFetch(url, getAccessToken);
@@ -68,7 +67,8 @@ export function useSessionAnchorEvents({
} finally {
if (!aborted) setLoading(false);
}
})();
};
void run();
return () => {
aborted = true;
@@ -44,10 +44,9 @@ export function useSessionSignals({
if (!sessionId) return;
let aborted = false;
setLoading(true);
setError(null);
(async () => {
const run = async () => {
setLoading(true);
setError(null);
try {
const url = api.inspector.signals(sessionId, { tenantId, maxResults });
const response = await authenticatedFetch(url, getAccessToken);
@@ -69,7 +68,8 @@ export function useSessionSignals({
} finally {
if (!aborted) setLoading(false);
}
})();
};
void run();
return () => {
aborted = true;
@@ -655,7 +655,10 @@ export function TenantConfigProvider({ children }: { children: React.ReactNode }
useEffect(() => {
if (!tenantId) return;
if (!user?.isTenantAdmin && !user?.isGlobalAdmin) return;
fetchAdmins();
const run = async () => {
await fetchAdmins();
};
void run();
}, [tenantId, user?.isTenantAdmin, user?.isGlobalAdmin, fetchAdmins]);
// -----------------------------------------------------------------------
@@ -685,7 +688,10 @@ export function TenantConfigProvider({ children }: { children: React.ReactNode }
// the per-tenant GA flag is the additive Community enable.
const bootstrapAvailable = editionInfo.edition === "pro" || config?.bootstrapTokenEnabled;
if (!tenantId || !bootstrapAvailable) return;
fetchBootstrapSessions();
const run = async () => {
await fetchBootstrapSessions();
};
void run();
}, [tenantId, editionInfo.edition, config?.bootstrapTokenEnabled, fetchBootstrapSessions]);
// -----------------------------------------------------------------------
@@ -117,7 +117,10 @@ export default function HardwareRejectionInsights({
}, [getAccessToken]);
useEffect(() => {
fetchData();
const run = async () => {
await fetchData();
};
void run();
}, [fetchData]);
// Loading state
@@ -88,8 +88,10 @@ export default function McpUsersSection() {
}, [getAccessToken]);
useEffect(() => {
fetchMcpUsers();
fetchPlanTiers();
const run = async () => {
await Promise.all([fetchMcpUsers(), fetchPlanTiers()]);
};
void run();
}, [fetchMcpUsers, fetchPlanTiers]);
const handlePolicyChange = useCallback(async (newPolicy: McpPolicy) => {
@@ -62,7 +62,10 @@ export default function NotRegisteredDevicesInsights({
}, [getAccessToken]);
useEffect(() => {
fetchData();
const run = async () => {
await fetchData();
};
void run();
}, [fetchData]);
// Loading state
@@ -63,7 +63,10 @@ export default function TpmIncompatibleDevicesInsights({
}, [getAccessToken]);
useEffect(() => {
fetchData();
const run = async () => {
await fetchData();
};
void run();
}, [fetchData]);
if (!data || data.aggregated.length === 0) {
@@ -76,7 +76,10 @@ export function SectionMcpUsage() {
}, [getAccessToken]);
useEffect(() => {
fetchUsage(dateRange);
const run = async () => {
await fetchUsage(dateRange);
};
void run();
}, [fetchUsage, dateRange]);
// Aggregate records by date
@@ -63,7 +63,12 @@ export function SectionOptionalGraphCapabilities() {
}
}, [tenantId, getAccessToken]);
useEffect(() => { void fetchStatus(); }, [fetchStatus]);
useEffect(() => {
const run = async () => {
await fetchStatus();
};
void run();
}, [fetchStatus]);
const onRefresh = useCallback(async () => {
if (!tenantId) return;
@@ -176,7 +176,12 @@ export default function SlaPage() {
}
}, [effectiveTenantId, isGlobalOverride, months, getAccessToken, addNotification]);
useEffect(() => { fetchMetrics(); }, [fetchMetrics]);
useEffect(() => {
const run = async () => {
await fetchMetrics();
};
void run();
}, [fetchMetrics]);
useEffect(() => {
if (tenantId) trackEvent('sla_page_viewed', { months });
@@ -148,7 +148,10 @@ export default function UsageMetricsPage() {
useEffect(() => {
if (!effectiveTenantId) return;
fetchMetrics();
const run = async () => {
await fetchMetrics();
};
void run();
}, [effectiveTenantId, fetchMetrics]);
const selectedTenantName = tenants.find(t => t.tenantId === selectedTenantId)?.domainName;