feat(dashboard): tighten explainability detail views

This commit is contained in:
2026-03-31 22:50:47 +02:00
parent db50e2e555
commit 7ace137d16
11 changed files with 580 additions and 65 deletions
@@ -0,0 +1,37 @@
import { describe, expect, it } from "vitest";
import { createDefaultDashboardLayout } from "@capakraken/shared/schemas";
import { shouldHydrateDashboardFromDb } from "./useDashboardLayout.js";
describe("shouldHydrateDashboardFromDb", () => {
it("hydrates from the database on first load when no local changes happened yet", () => {
expect(shouldHydrateDashboardFromDb({
remoteLayout: createDefaultDashboardLayout(),
hasHydratedFromDb: false,
hasLocalChangesBeforeHydration: false,
})).toBe(true);
});
it("skips initial database hydration after a local first-load change", () => {
expect(shouldHydrateDashboardFromDb({
remoteLayout: createDefaultDashboardLayout(),
hasHydratedFromDb: false,
hasLocalChangesBeforeHydration: true,
})).toBe(false);
});
it("does not re-hydrate once the initial database handoff already happened", () => {
expect(shouldHydrateDashboardFromDb({
remoteLayout: createDefaultDashboardLayout(),
hasHydratedFromDb: true,
hasLocalChangesBeforeHydration: false,
})).toBe(false);
});
it("does not hydrate when the backend has no stored layout", () => {
expect(shouldHydrateDashboardFromDb({
remoteLayout: null,
hasHydratedFromDb: false,
hasLocalChangesBeforeHydration: false,
})).toBe(false);
});
});
+41 -4
View File
@@ -37,9 +37,23 @@ function saveToStorage(config: DashboardLayoutConfig) {
} catch {}
}
export function shouldHydrateDashboardFromDb(params: {
remoteLayout: DashboardLayoutConfig | null | undefined;
hasHydratedFromDb: boolean;
hasLocalChangesBeforeHydration: boolean;
}): boolean {
const { remoteLayout, hasHydratedFromDb, hasLocalChangesBeforeHydration } = params;
return remoteLayout !== null
&& remoteLayout !== undefined
&& !hasHydratedFromDb
&& !hasLocalChangesBeforeHydration;
}
export function useDashboardLayout() {
const [config, setConfig] = useState<DashboardLayoutConfig>(() => loadFromStorage() ?? createDefaultDashboardLayout());
const saveTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const hasHydratedFromDbRef = useRef(false);
const hasLocalChangesBeforeHydrationRef = useRef(false);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const { data: dbData } = trpc.user.getDashboardLayout.useQuery(undefined, {
@@ -50,14 +64,37 @@ export function useDashboardLayout() {
// Sync from DB on load (DB wins if it has data)
useEffect(() => {
if (dbData?.layout) {
const dbConfig = normalizeDashboardLayout(dbData.layout);
setConfig(dbConfig);
saveToStorage(dbConfig);
const remoteLayout = dbData?.layout ?? null;
if (remoteLayout === null || hasHydratedFromDbRef.current) {
return;
}
if (!shouldHydrateDashboardFromDb({
remoteLayout,
hasHydratedFromDb: hasHydratedFromDbRef.current,
hasLocalChangesBeforeHydration: hasLocalChangesBeforeHydrationRef.current,
})) {
hasHydratedFromDbRef.current = true;
return;
}
const dbConfig = normalizeDashboardLayout(remoteLayout);
hasHydratedFromDbRef.current = true;
setConfig(dbConfig);
saveToStorage(dbConfig);
}, [dbData]);
useEffect(() => () => {
if (saveTimeoutRef.current) {
clearTimeout(saveTimeoutRef.current);
saveTimeoutRef.current = null;
}
}, []);
const persist = useCallback((nextConfig: DashboardLayoutConfig) => {
if (!hasHydratedFromDbRef.current) {
hasLocalChangesBeforeHydrationRef.current = true;
}
const newConfig = normalizeDashboardLayout(nextConfig);
saveToStorage(newConfig);
if (saveTimeoutRef.current) clearTimeout(saveTimeoutRef.current);