diff --git a/frontend/src/shell/DatabaseManagementPage.sync-cache.test.tsx b/frontend/src/shell/DatabaseManagementPage.sync-cache.test.tsx
new file mode 100644
index 00000000..fb8c73a3
--- /dev/null
+++ b/frontend/src/shell/DatabaseManagementPage.sync-cache.test.tsx
@@ -0,0 +1,171 @@
+import { render, screen, waitFor, within } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+import { http, HttpResponse } from "msw";
+import { server } from "../test/msw";
+import type {
+ CatalogDatabase,
+ CatalogSyncRun,
+ CatalogTable,
+} from "../api/catalog-databases";
+import { Toaster } from "../components/ui/sonner";
+import { DatabaseManagementPage } from "./DatabaseManagementPage";
+
+const databaseId = "11111111-1111-4111-8111-111111111111";
+
+const database: CatalogDatabase = {
+ id: databaseId,
+ workspaceId: "psd-clinical",
+ workspaceName: "Policlinico San Donato",
+ workspaceAvailable: true,
+ workspaceRevision: { commit: "a".repeat(40), blob: "b".repeat(40) },
+ workspaceEvidence: { sourceType: "filesystem", state: "materialized_current_revision" },
+ runtimeBinding: {
+ transport: "postgres_direct",
+ configurationState: "ready",
+ sessionTransportSupported: true,
+ },
+ configured: true,
+ engine: "postgres",
+ databaseName: "warehouse",
+ schema: "datawarehouse",
+ version: 3,
+ createdAt: "2026-08-27T08:00:00Z",
+ updatedAt: "2026-08-27T09:00:00Z",
+ binding: {
+ transport: "postgres_direct",
+ host: "db.internal",
+ port: 5432,
+ username: "reader",
+ },
+ connectionStatus: "reachable",
+ testedVersion: 3,
+ secrets: {
+ password: false,
+ apiKey: false,
+ sshPrivateKey: false,
+ sshPrivateKeyPassphrase: false,
+ sshKnownHosts: false,
+ tlsCa: false,
+ },
+};
+
+const patientsTable: CatalogTable = {
+ id: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
+ databaseId,
+ name: "patients",
+ sourceComment: "Patients imported from the clinical source",
+ description: null,
+ generatedDescription: null,
+ version: 1,
+ createdAt: "2026-08-27T08:00:00Z",
+ updatedAt: "2026-08-27T09:00:00Z",
+};
+
+const queuedRun: CatalogSyncRun = {
+ id: "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb",
+ databaseId,
+ scope: "tables",
+ tableIds: [],
+ state: "queued",
+ phase: "queued",
+ requestedDatabaseVersion: 3,
+ plannedDiff: null,
+ confirmationToken: null,
+ counts: {},
+ errorCode: null,
+ errorMessage: null,
+ cancelRequested: false,
+ createdAt: "2026-08-27T10:00:00Z",
+ startedAt: null,
+ updatedAt: "2026-08-27T10:00:00Z",
+ finishedAt: null,
+ heartbeatAt: null,
+};
+
+const completedRun: CatalogSyncRun = {
+ ...queuedRun,
+ state: "succeeded",
+ phase: "completed",
+ counts: { tables: 1 },
+ startedAt: "2026-08-27T10:00:00Z",
+ updatedAt: "2026-08-27T10:00:01Z",
+ finishedAt: "2026-08-27T10:00:01Z",
+};
+
+test("refreshes database metadata when a synchronization completes with its drawer closed", async () => {
+ const user = userEvent.setup();
+ let synchronized = false;
+ let tableReads = 0;
+
+ server.use(
+ http.get("/api/catalog/databases", () => HttpResponse.json([database])),
+ http.get("/api/catalog/databases/:databaseId/tables", () => {
+ tableReads += 1;
+ return HttpResponse.json(synchronized ? [patientsTable] : []);
+ }),
+ http.post("/api/catalog/databases/:databaseId/sync-runs", () => (
+ HttpResponse.json(queuedRun, { status: 202 })
+ )),
+ http.get("/api/catalog/sync-runs/:runId", () => (
+ HttpResponse.json(synchronized ? completedRun : queuedRun)
+ )),
+ http.get("/api/catalog/sync-runs/:runId/events-list", () => HttpResponse.json([])),
+ http.get("/api/catalog/databases/:databaseId/sync-runs", () => (
+ HttpResponse.json([synchronized ? completedRun : queuedRun])
+ )),
+ );
+
+ const client = new QueryClient({
+ defaultOptions: { queries: { retry: false, staleTime: 30_000 } },
+ });
+ render(
+
+
+
+ ,
+ );
+
+ const initialRow = await screen.findByRole("row", { name: /Policlinico San Donato/ });
+ await user.click(within(initialRow).getByRole("button", {
+ name: "View tables for Policlinico San Donato",
+ }));
+ expect(await screen.findByRole("button", { name: "Back to databases" })).toBeVisible();
+ await waitFor(() => expect(tableReads).toBe(1));
+ await user.click(screen.getByRole("button", { name: "Back to databases" }));
+
+ client.setQueryData(["catalog-columns", databaseId, patientsTable.id], []);
+ client.setQueryData(["catalog-columns", "another-database", patientsTable.id], []);
+
+ const databaseRow = await screen.findByRole("row", { name: /Policlinico San Donato/ });
+ await user.click(within(databaseRow).getByRole("checkbox", { name: /toggle row selection/i }));
+ await user.selectOptions(
+ screen.getByRole("combobox", { name: "Batch action" }),
+ "sync-tables",
+ );
+ await user.click(screen.getByRole("button", { name: "Run action" }));
+
+ const synchronizationDrawer = await screen.findByRole("dialog", {
+ name: "Schema synchronization",
+ });
+ await user.click(within(synchronizationDrawer).getByRole("button", {
+ name: "Close synchronization drawer",
+ }));
+ synchronized = true;
+
+ const restoredRow = await screen.findByRole("row", { name: /Policlinico San Donato/ });
+ await user.click(within(restoredRow).getByRole("button", {
+ name: "View tables for Policlinico San Donato",
+ }));
+
+ expect(await screen.findByRole("row", { name: /patients/ }, { timeout: 3_000 })).toBeVisible();
+ expect(tableReads).toBeGreaterThanOrEqual(2);
+ expect(client.getQueryState(["catalog-columns", databaseId, patientsTable.id])?.isInvalidated)
+ .toBe(true);
+ expect(client.getQueryState(["catalog-columns", "another-database", patientsTable.id])?.isInvalidated)
+ .toBe(false);
+});
diff --git a/frontend/src/shell/DatabaseManagementPage.tsx b/frontend/src/shell/DatabaseManagementPage.tsx
index 5d4a1c3d..faa834e0 100644
--- a/frontend/src/shell/DatabaseManagementPage.tsx
+++ b/frontend/src/shell/DatabaseManagementPage.tsx
@@ -899,20 +899,19 @@ export function DatabaseManagementPage({
} catch (error) { toast.error(apiErrorMessage(error)); }
}, [activeRow, rememberSyncRun]);
- const catalogChanged = useCallback(async () => {
- const databaseId = activeSyncRun?.databaseId;
- if (databaseId) {
- await Promise.all([
- queryClient.invalidateQueries({ queryKey: ["catalog-tables", databaseId] }),
- queryClient.invalidateQueries({ queryKey: ["catalog-relationships", databaseId] }),
- queryClient.invalidateQueries({ queryKey: ["catalog-sync-runs", databaseId] }),
- ]);
- }
+ const catalogChanged = useCallback(async (run: CatalogSyncRun) => {
+ const databaseId = run.databaseId;
+ await Promise.all([
+ queryClient.invalidateQueries({ queryKey: ["catalog-tables", databaseId] }),
+ queryClient.invalidateQueries({ queryKey: ["catalog-columns", databaseId] }),
+ queryClient.invalidateQueries({ queryKey: ["catalog-relationships", databaseId] }),
+ queryClient.invalidateQueries({ queryKey: ["catalog-sync-runs", databaseId] }),
+ ]);
await Promise.all([
queryClient.invalidateQueries({ queryKey: DATABASE_QUERY_KEY }),
invalidateCatalogMetrics(),
]);
- }, [activeSyncRun, invalidateCatalogMetrics, queryClient]);
+ }, [invalidateCatalogMetrics, queryClient]);
const formVisible = screen.kind !== "list" && screen.kind !== "tables" && screen.kind !== "relationships" && activeRow && draft;
const tablesVisible = screen.kind === "tables" && activeRow?.configured && activeRow.id;
@@ -975,7 +974,7 @@ export function DatabaseManagementPage({
onClose={() => setSyncDrawerOpen(false)}
onRunChange={rememberSyncRun}
onRunUpdate={updateTrackedSyncRun}
- onCatalogChanged={() => void catalogChanged()}
+ onCatalogChanged={(run) => void catalogChanged(run)}
/>
setSyncDrawerOpen(false)}
onRunChange={rememberSyncRun}
onRunUpdate={updateTrackedSyncRun}
- onCatalogChanged={() => void catalogChanged()}
+ onCatalogChanged={(run) => void catalogChanged(run)}
/>
void;
onRunChange: (run: CatalogSyncRun) => void;
onRunUpdate: (run: CatalogSyncRun) => void;
- onCatalogChanged: () => void;
+ onCatalogChanged: (run: CatalogSyncRun) => void;
}
const phases: CatalogSyncPhase[] = [
@@ -102,7 +102,7 @@ export function CatalogSyncDrawer({
const runQuery = useQuery({
queryKey: ["catalog-sync-run", runId],
queryFn: () => getCatalogSyncRun(runId!),
- enabled: Boolean(runId && open),
+ enabled: Boolean(runId),
retry: false,
refetchInterval: (query) => terminal(query.state.data) ? false : 1_000,
});
@@ -185,7 +185,7 @@ export function CatalogSyncDrawer({
useEffect(() => {
if (!run || run.state !== "succeeded" || notifiedRun.current === run.id) return;
notifiedRun.current = run.id;
- onCatalogChanged();
+ onCatalogChanged(run);
}, [onCatalogChanged, run]);
const currentPhase = run ? phases.indexOf(run.phase) : -1;