mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-24 04:32:32 +00:00
web: fix all 16 exhaustive-deps lint warnings, ratchet cap 115 -> 99
Wrap ad-hoc fetch closures in useCallback and complete effect deps (SectionPlatformUsage, usage-metrics, diagnosis, TenantConfigContext, SignalRContext, dashboard, fleet-health, useProgressSignalR). Convert sla's initialLoad to a ref so the fresh-once flag can't retrigger the fetch effect. Document the three intentional live-ref bumps in unmount cleanups (cancellation counters) with justified disables. Also fixes a real stale-closure bug: TenantConfigContext's save callback serialized webhookNotifyOnHardwareRejection without depending on it, so toggling only that setting saved the old value. The 3 react-compiler preserve-manual-memoization infos remain (compiler bailouts, not defects) - cap ratcheted to the exact current count of 99. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -82,7 +82,7 @@ jobs:
|
||||
# in eslint.config.mjs + exhaustive-deps) — when you fix warnings, lower
|
||||
# the cap to match; never raise it.
|
||||
- name: Lint
|
||||
run: npm run lint -- --max-warnings 115
|
||||
run: npm run lint -- --max-warnings 99
|
||||
|
||||
- name: Tests
|
||||
run: npm test
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { api } from '@/lib/api';
|
||||
import { useAuth } from '../../../../contexts/AuthContext';
|
||||
@@ -112,7 +112,7 @@ export function SectionPlatformUsage() {
|
||||
const progress = useFetchProgress('platformUsage.lastFreshFetchMs');
|
||||
const { begin: progressBegin, finish: progressFinish } = progress;
|
||||
|
||||
const fetchMetrics = async (showRefreshing = false) => {
|
||||
const fetchMetrics = useCallback(async (showRefreshing = false) => {
|
||||
let recordEstimate = false;
|
||||
try {
|
||||
if (showRefreshing) {
|
||||
@@ -151,13 +151,13 @@ export function SectionPlatformUsage() {
|
||||
setLoading(false);
|
||||
setRefreshing(false);
|
||||
}
|
||||
};
|
||||
}, [getAccessToken, progressBegin, progressFinish]);
|
||||
|
||||
useEffect(() => {
|
||||
// Fetch metrics on mount
|
||||
// Authorization is handled by admin layout ProtectedRoute
|
||||
fetchMetrics();
|
||||
}, []);
|
||||
}, [fetchMetrics]);
|
||||
|
||||
const formatDuration = (minutes: number) => {
|
||||
if (minutes < 60) {
|
||||
|
||||
@@ -374,6 +374,9 @@ export function useDashboardSessions({
|
||||
// doesn't call setSessions against a torn-down component.
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
// Intentionally the LIVE ref at unmount time — bumping the counter is the
|
||||
// cancellation signal; a snapshot taken at mount would cancel nothing.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
loadAllTokenRef.current++;
|
||||
};
|
||||
}, []);
|
||||
|
||||
@@ -261,6 +261,9 @@ export function useDashboardStats({
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current);
|
||||
// Intentionally the LIVE ref at unmount time — bumping the generation
|
||||
// invalidates in-flight fetches; a snapshot taken at mount would not.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
fetchGenRef.current++;
|
||||
};
|
||||
}, []);
|
||||
|
||||
@@ -207,7 +207,7 @@ function HomeContent() {
|
||||
console.log('[Home] User has no platform scope, disabling global mode');
|
||||
setGlobalAdminMode(false);
|
||||
}
|
||||
}, [user, globalAdminMode]);
|
||||
}, [user, globalAdminMode, setGlobalAdminMode]);
|
||||
|
||||
// Clear the tenant filter when cross-tenant mode turns off (refetch is owned by useDashboardSessions).
|
||||
// Keyed on crossTenant (not raw globalAdminMode) so a delegated ("MSP") admin — whose crossTenant is
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { sessionUrl } from "@/lib/routes";
|
||||
import { Suspense, useEffect, useState, useRef } from "react";
|
||||
import { Suspense, useCallback, useEffect, useState, useRef } from "react";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { useSignalR } from "../../contexts/SignalRContext";
|
||||
import { useTenant } from "../../contexts/TenantContext";
|
||||
@@ -51,11 +51,11 @@ function DiagnosisContent() {
|
||||
const sessionFetchDone = useRef(false);
|
||||
const analysisFetchDone = useRef(false);
|
||||
|
||||
const finishLoadingWhenSettled = () => {
|
||||
const finishLoadingWhenSettled = useCallback(() => {
|
||||
if (sessionFetchDone.current && analysisFetchDone.current) {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const { on, off, isConnected, joinGroup, leaveGroup } = useSignalR();
|
||||
const { tenantId } = useTenant();
|
||||
@@ -66,7 +66,7 @@ function DiagnosisContent() {
|
||||
|
||||
// Declared ahead of the effects that call them (react-hooks/immutability:
|
||||
// a later declaration would freeze the effect's view of the binding).
|
||||
const fetchSessionDetails = async () => {
|
||||
const fetchSessionDetails = useCallback(async () => {
|
||||
try {
|
||||
// Fetch the single session directly instead of pulling the entire session
|
||||
// list and .find()-ing client-side. The backend resolves the tenant via
|
||||
@@ -100,9 +100,9 @@ function DiagnosisContent() {
|
||||
sessionFetchDone.current = true;
|
||||
finishLoadingWhenSettled();
|
||||
}
|
||||
};
|
||||
}, [sessionId, sessionTenantId, tenantId, globalAdminMode, getAccessToken, addNotification, finishLoadingWhenSettled]);
|
||||
|
||||
const fetchEvents = async () => {
|
||||
const fetchEvents = useCallback(async () => {
|
||||
const effectiveTenantId = sessionTenantId || tenantId;
|
||||
try {
|
||||
const response = await authenticatedFetch(
|
||||
@@ -123,9 +123,9 @@ function DiagnosisContent() {
|
||||
addNotification('error', 'Backend Not Reachable', 'Unable to load session events. Please check your connection.', 'diagnosis-events-error');
|
||||
}
|
||||
}
|
||||
};
|
||||
}, [sessionId, sessionTenantId, tenantId, getAccessToken, addNotification]);
|
||||
|
||||
const fetchAnalysisResults = async () => {
|
||||
const fetchAnalysisResults = useCallback(async () => {
|
||||
const effectiveTenantId = sessionTenantId || tenantId;
|
||||
try {
|
||||
const response = await authenticatedFetch(
|
||||
@@ -153,7 +153,7 @@ function DiagnosisContent() {
|
||||
analysisFetchDone.current = true;
|
||||
finishLoadingWhenSettled();
|
||||
}
|
||||
};
|
||||
}, [sessionId, sessionTenantId, tenantId, getAccessToken, addNotification, finishLoadingWhenSettled]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!sessionId) return;
|
||||
@@ -180,13 +180,13 @@ function DiagnosisContent() {
|
||||
}
|
||||
|
||||
fetchSessionDetails();
|
||||
}, [sessionId, tenantId, globalAdminMode]);
|
||||
}, [sessionId, tenantId, globalAdminMode, fetchSessionDetails]);
|
||||
|
||||
useEffect(() => {
|
||||
if (sessionTenantId && sessionId) {
|
||||
Promise.all([fetchEvents(), fetchAnalysisResults()]);
|
||||
}
|
||||
}, [sessionTenantId, sessionId]);
|
||||
}, [sessionTenantId, sessionId, fetchEvents, fetchAnalysisResults]);
|
||||
|
||||
// SignalR groups - subscribe-then-fetch pattern
|
||||
useEffect(() => {
|
||||
@@ -207,7 +207,7 @@ function DiagnosisContent() {
|
||||
hasJoinedGroups.current = false;
|
||||
}
|
||||
};
|
||||
}, [sessionId, isConnected, sessionTenantId, tenantId]);
|
||||
}, [sessionId, isConnected, sessionTenantId, tenantId, joinGroup, leaveGroup, fetchEvents, fetchAnalysisResults]);
|
||||
|
||||
// Real-time analysis updates
|
||||
useEffect(() => {
|
||||
|
||||
@@ -257,6 +257,9 @@ export function useFleetHealth({
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current);
|
||||
// Intentionally the LIVE ref at unmount time — bumping the generation
|
||||
// invalidates in-flight fetches; a snapshot taken at mount would not.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
fetchGenRef.current++;
|
||||
};
|
||||
}, []);
|
||||
|
||||
@@ -168,7 +168,7 @@ export default function FleetHealthPage() {
|
||||
hasJoinedGroup.current = false;
|
||||
}
|
||||
};
|
||||
}, [isConnected, effectiveTenantId]);
|
||||
}, [isConnected, effectiveTenantId, joinGroup, leaveGroup]);
|
||||
|
||||
// Server payload, with presentation-friendly defaults so the JSX renders
|
||||
// without null guards once loading clears. `avgDuration` keeps the card's
|
||||
@@ -199,7 +199,9 @@ export default function FleetHealthPage() {
|
||||
return `/dashboard?${params.toString()}`;
|
||||
};
|
||||
const topFailingModels = data?.topFailingModels ?? [];
|
||||
const failureReasons = data?.failureReasons ?? [];
|
||||
// Memoized (unlike the sibling defaults above) because it feeds the maxFailureCount
|
||||
// useMemo below — a fresh [] each render would invalidate that memo every time.
|
||||
const failureReasons = useMemo(() => data?.failureReasons ?? [], [data]);
|
||||
|
||||
// Timeline points with presentation labels derived from the UTC date string.
|
||||
const dailyData = useMemo(() => {
|
||||
|
||||
@@ -38,10 +38,15 @@ export function useProgressSignalR({
|
||||
}: UseProgressSignalRParams): void {
|
||||
const { on, off, isConnected, joinGroup, leaveGroup } = signalR;
|
||||
|
||||
useEffect(() => {
|
||||
if (!isConnected || !session) return;
|
||||
// Primitives extracted so the join effect keys on identity of the selected
|
||||
// session, not object identity — a refetched session object must not rejoin.
|
||||
const sessionId = session?.sessionId;
|
||||
const sessionTenantId = session?.tenantId;
|
||||
|
||||
const sessionGroup = `session-${session.tenantId}-${session.sessionId}`;
|
||||
useEffect(() => {
|
||||
if (!isConnected || !sessionId || !sessionTenantId) return;
|
||||
|
||||
const sessionGroup = `session-${sessionTenantId}-${sessionId}`;
|
||||
console.log("[Progress] Joining session group:", sessionGroup);
|
||||
joinGroup(sessionGroup);
|
||||
|
||||
@@ -49,7 +54,7 @@ export function useProgressSignalR({
|
||||
console.log("[Progress] Leaving session group:", sessionGroup);
|
||||
leaveGroup(sessionGroup);
|
||||
};
|
||||
}, [isConnected, session?.sessionId, session?.tenantId, joinGroup, leaveGroup]);
|
||||
}, [isConnected, sessionId, sessionTenantId, joinGroup, leaveGroup]);
|
||||
|
||||
useEffect(() => {
|
||||
const scheduleRefetch = (source: string, sessionId: string) => {
|
||||
|
||||
@@ -616,7 +616,7 @@ export function TenantConfigProvider({ children }: { children: React.ReactNode }
|
||||
};
|
||||
|
||||
fetchConfiguration();
|
||||
}, [tenantId]);
|
||||
}, [tenantId, getAccessToken, addNotification, user?.isTenantAdmin, user?.isGlobalAdmin]);
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Fetch admins
|
||||
@@ -813,7 +813,7 @@ export function TenantConfigProvider({ children }: { children: React.ReactNode }
|
||||
}
|
||||
}, [
|
||||
tenantId, config, canEditConfig, getAccessToken, addNotification,
|
||||
manufacturerWhitelist, modelWhitelist, validateAutopilotDevice, validateCorporateIdentifier, validateDeviceAssociation,
|
||||
manufacturerWhitelist, modelWhitelist, webhookNotifyOnHardwareRejection, validateAutopilotDevice, validateCorporateIdentifier, validateDeviceAssociation,
|
||||
dataRetentionDays, sessionTimeoutHours, enablePerformanceCollector, performanceCollectorInterval,
|
||||
helloWaitTimeoutSeconds, selfDestructOnComplete, keepLogFile, rebootOnComplete, rebootDelaySeconds,
|
||||
contactEmail, enableGeoLocation, enableTimezoneAutoSet, enableImeMatchLog, enableGatherRuleDebugLog, logLevel, showScriptOutput, showEnrollmentSummary,
|
||||
|
||||
@@ -118,7 +118,9 @@ export default function SlaPage() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [months, setMonths] = useState(3);
|
||||
const [initialLoad, setInitialLoad] = useState(true);
|
||||
// First fetch bypasses the server cache (fresh=true), all later ones use it. A ref, not
|
||||
// state: it is only read inside fetchMetrics and must not retrigger the fetch effect.
|
||||
const initialLoadRef = useRef(true);
|
||||
|
||||
// Global admin tenant scope (tenant list, selector state, override/effective tenant)
|
||||
const scope = useGlobalAdminScope();
|
||||
@@ -137,8 +139,8 @@ export default function SlaPage() {
|
||||
if (showRefreshing) setRefreshing(true);
|
||||
else setLoading(true);
|
||||
|
||||
const useFresh = initialLoad;
|
||||
if (initialLoad) setInitialLoad(false);
|
||||
const useFresh = initialLoadRef.current;
|
||||
initialLoadRef.current = false;
|
||||
|
||||
const url = isGlobalOverride
|
||||
? api.metrics.globalSla(effectiveTenantId, months, useFresh)
|
||||
|
||||
@@ -149,7 +149,7 @@ export default function UsageMetricsPage() {
|
||||
useEffect(() => {
|
||||
if (!effectiveTenantId) return;
|
||||
fetchMetrics();
|
||||
}, [effectiveTenantId]);
|
||||
}, [effectiveTenantId, fetchMetrics]);
|
||||
|
||||
const selectedTenantName = tenants.find(t => t.tenantId === selectedTenantId)?.domainName;
|
||||
|
||||
|
||||
@@ -222,7 +222,7 @@ export function SignalRProvider({ children }: { children: React.ReactNode }) {
|
||||
connectionRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [isAuthenticated, getAccessToken]);
|
||||
}, [isAuthenticated, getAccessToken, syncJoinedGroups]);
|
||||
|
||||
const on = (eventName: string, callback: SignalRHandler) => {
|
||||
if (connectionRef.current) {
|
||||
|
||||
Reference in New Issue
Block a user