fix(scep-intune): use useTrackedMutation for trust-anchor reload (M-009)

Phase 9 follow-up — the M-009 hard-zero regression guard in
.github/workflows/ci.yml flagged the SCEPAdminPage's reload mutation as
a bare useMutation() call. The repo's invalidation contract requires
every mutation to go through useTrackedMutation with explicit
invalidates: QueryKey[] | 'noop' so cached data never goes stale after
a write.

Swap the bare useMutation for useTrackedMutation with
invalidates: [['admin', 'scep', 'intune', 'stats']] — the trust-anchor
reload changes the per-profile trust pool reflected in IntuneStats, so
the stats query MUST refetch on success. The audit-log queries stay on
their own 60s timer (a SIGHUP-equivalent reload doesn't backfill new
audit rows; nothing to invalidate there).

Verification:
  * tsc --noEmit clean
  * vitest SCEPAdminPage.test.tsx: 13/13 still pass (the wrapper's
    onSuccess fires AFTER invalidation, so the modal-close + state
    reset assertions hold)
  * M-009 grep guard reproduced locally — bare useMutation sites = 0
This commit is contained in:
Shankar
2026-04-29 16:35:40 +00:00
parent 82276bd29e
commit 96e81b642a
+15 -4
View File
@@ -1,9 +1,10 @@
import { useState } from 'react'; import { useState } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { getAdminSCEPIntuneStats, reloadAdminSCEPIntuneTrust, getAuditEvents } from '../api/client'; import { getAdminSCEPIntuneStats, reloadAdminSCEPIntuneTrust, getAuditEvents } from '../api/client';
import PageHeader from '../components/PageHeader'; import PageHeader from '../components/PageHeader';
import ErrorState from '../components/ErrorState'; import ErrorState from '../components/ErrorState';
import { useAuth } from '../components/AuthProvider'; import { useAuth } from '../components/AuthProvider';
import { useTrackedMutation } from '../hooks/useTrackedMutation';
import { formatDateTime } from '../api/utils'; import { formatDateTime } from '../api/utils';
import type { IntuneStatsSnapshot, IntuneTrustAnchorInfo, AuditEvent } from '../api/types'; import type { IntuneStatsSnapshot, IntuneTrustAnchorInfo, AuditEvent } from '../api/types';
@@ -299,7 +300,6 @@ function RecentFailuresTable({ events }: { events: AuditEvent[] }) {
export default function SCEPAdminPage() { export default function SCEPAdminPage() {
const auth = useAuth(); const auth = useAuth();
const queryClient = useQueryClient();
const [reloadTarget, setReloadTarget] = useState<IntuneStatsSnapshot | null>(null); const [reloadTarget, setReloadTarget] = useState<IntuneStatsSnapshot | null>(null);
const [reloadError, setReloadError] = useState<string | undefined>(undefined); const [reloadError, setReloadError] = useState<string | undefined>(undefined);
@@ -328,12 +328,23 @@ export default function SCEPAdminPage() {
refetchInterval: 60_000, refetchInterval: 60_000,
}); });
const reloadMutation = useMutation({ // Bundle-8 / M-009 invalidation contract: trust-anchor reload changes
// both the per-profile trust pool (reflected in IntuneStats) AND every
// recently-failed Intune enrollment counter that might now succeed on
// retry. We invalidate the stats key so the per-profile trust-anchor
// panel reflects the new pool immediately; the audit log queries
// remain on their 60s timer (a SIGHUP-equivalent reload doesn't
// backfill new audit rows).
const reloadMutation = useTrackedMutation<
Awaited<ReturnType<typeof reloadAdminSCEPIntuneTrust>>,
Error,
string
>({
mutationFn: (pathID: string) => reloadAdminSCEPIntuneTrust(pathID), mutationFn: (pathID: string) => reloadAdminSCEPIntuneTrust(pathID),
invalidates: [['admin', 'scep', 'intune', 'stats']],
onSuccess: () => { onSuccess: () => {
setReloadTarget(null); setReloadTarget(null);
setReloadError(undefined); setReloadError(undefined);
void queryClient.invalidateQueries({ queryKey: ['admin', 'scep', 'intune', 'stats'] });
}, },
onError: (err: Error) => { onError: (err: Error) => {
setReloadError(err.message); setReloadError(err.message);