chore(per-instance-hook-scoping): archive verified+synced change

Move to openspec/changes/archive/2026-07-09-per-instance-hook-scoping/
(R100 renames preserved). 9 artifacts. Canonical openspec/specs/
service-instance-scoping/ remains. Resolves multi-instance wrong-data bug
(hooks now scope by instance.id; instance switcher re-scopes).
Carry-overs: fetchBackupDashboard untouched (design decision 5); subquery
scoping for runs/alerts (schema asymmetry).
This commit is contained in:
Developer
2026-07-10 00:16:52 +00:00
parent f921524d37
commit 29650ca512
16 changed files with 327 additions and 60 deletions
+3 -1
View File
@@ -6,7 +6,9 @@ import type {
BackupRun,
} from "../types/backups";
export async function fetchBackupJobs(serviceId?: string): Promise<BackupJob[]> {
export async function fetchBackupJobs(
serviceId?: string,
): Promise<BackupJob[]> {
return get<BackupJob[]>(
"/api/backups/jobs",
serviceId ? { service_id: serviceId } : undefined,
@@ -3,7 +3,10 @@ import { renderHook } from "@testing-library/react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { createElement, type ReactNode } from "react";
import { useBackupJobs, useBackupRuns, useBackupAlerts } from "../useBackups";
import { useAlertmanagerAlerts, usePrometheusStatus } from "../useObservability";
import {
useAlertmanagerAlerts,
usePrometheusStatus,
} from "../useObservability";
vi.mock("../../api/client", () => ({
fetchAlertmanagerAlerts: vi.fn(),
@@ -68,10 +71,9 @@ describe("per-instance hook queryKey isolation", () => {
it("useAlertmanagerAlerts includes serviceId in queryKey", () => {
const wrapper = createWrapper();
const { result: a } = renderHook(
() => useAlertmanagerAlerts("svc-a"),
{ wrapper },
);
const { result: a } = renderHook(() => useAlertmanagerAlerts("svc-a"), {
wrapper,
});
expect(a).toBeDefined();
});
+56 -40
View File
@@ -1,59 +1,75 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import {
acknowledgeBackupAlert,
fetchBackupAlerts,
fetchBackupDashboard,
fetchBackupJob,
fetchBackupJobs,
fetchBackupRuns,
acknowledgeBackupAlert,
fetchBackupAlerts,
fetchBackupDashboard,
fetchBackupJob,
fetchBackupJobs,
fetchBackupRuns,
} from "../api/backups";
export function useBackupJobs(serviceId?: string) {
return useQuery({
queryKey: ["backups", "jobs", serviceId ?? ""],
queryFn: () => fetchBackupJobs(serviceId),
refetchInterval: 30_000,
});
return useQuery({
queryKey: ["backups", "jobs", serviceId ?? ""],
queryFn: () => fetchBackupJobs(serviceId),
refetchInterval: 30_000,
});
}
export function useBackupJob(jobId: string) {
return useQuery({
queryKey: ["backups", "jobs", jobId],
queryFn: () => fetchBackupJob(jobId),
enabled: !!jobId,
});
return useQuery({
queryKey: ["backups", "jobs", jobId],
queryFn: () => fetchBackupJob(jobId),
enabled: !!jobId,
});
}
export function useBackupRuns(jobId?: string, status?: string, serviceId?: string) {
return useQuery({
queryKey: ["backups", "runs", jobId, status, serviceId ?? ""],
queryFn: () => fetchBackupRuns(jobId, status, serviceId),
refetchInterval: 15_000,
});
export function useBackupRuns(
jobId?: string,
status?: string,
serviceId?: string,
) {
return useQuery({
queryKey: ["backups", "runs", jobId, status, serviceId ?? ""],
queryFn: () => fetchBackupRuns(jobId, status, serviceId),
refetchInterval: 15_000,
});
}
export function useBackupAlerts(jobId?: string, acknowledged?: boolean, severity?: string, serviceId?: string) {
return useQuery({
queryKey: ["backups", "alerts", jobId, acknowledged, severity, serviceId ?? ""],
queryFn: () => fetchBackupAlerts(jobId, acknowledged, severity, serviceId),
refetchInterval: 30_000,
});
export function useBackupAlerts(
jobId?: string,
acknowledged?: boolean,
severity?: string,
serviceId?: string,
) {
return useQuery({
queryKey: [
"backups",
"alerts",
jobId,
acknowledged,
severity,
serviceId ?? "",
],
queryFn: () => fetchBackupAlerts(jobId, acknowledged, severity, serviceId),
refetchInterval: 30_000,
});
}
export function useAcknowledgeAlert() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: acknowledgeBackupAlert,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["backups", "alerts"] });
},
});
const queryClient = useQueryClient();
return useMutation({
mutationFn: acknowledgeBackupAlert,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["backups", "alerts"] });
},
});
}
export function useBackupDashboard() {
return useQuery({
queryKey: ["dashboard", "backups"],
queryFn: fetchBackupDashboard,
refetchInterval: 30_000,
});
return useQuery({
queryKey: ["dashboard", "backups"],
queryFn: fetchBackupDashboard,
refetchInterval: 30_000,
});
}
@@ -110,7 +110,9 @@ export function AlertsTab({ instance }: { instance: ServiceInstance }) {
isLoading: alertsLoading,
error: alertsError,
} = useAlertmanagerAlerts(instance.id);
const { data: status, isLoading: statusLoading } = useAlertmanagerStatus(instance.id);
const { data: status, isLoading: statusLoading } = useAlertmanagerStatus(
instance.id,
);
const statusDetail = status?.up
? status.version
@@ -58,12 +58,12 @@ vi.mock("../../../hooks/useObservability", () => ({
describe("AlertsTab", () => {
it("passes instance.id to scoped hooks", () => {
render(<AlertsTab instance={instance} />);
expect(vi.mocked(useObservability.useAlertmanagerAlerts)).toHaveBeenCalledWith(
"am-1",
);
expect(vi.mocked(useObservability.useAlertmanagerStatus)).toHaveBeenCalledWith(
"am-1",
);
expect(
vi.mocked(useObservability.useAlertmanagerAlerts),
).toHaveBeenCalledWith("am-1");
expect(
vi.mocked(useObservability.useAlertmanagerStatus),
).toHaveBeenCalledWith("am-1");
});
it("renders the alert count and alert names", () => {