chore: add pre-commit hooks, tighten ESLint, activate Sentry DSN, publish CI coverage (Phase 1)

- Install husky v9 + lint-staged: pre-commit runs eslint --fix and prettier on staged files
- Tighten ESLint base config: no-console→error, ban-ts-comment (ts-ignore banned, ts-expect-error with description allowed), reportUnusedDisableDirectives→error
- Migrate web app from deprecated `next lint` to `eslint src/` with flat config and react-hooks plugin
- Convert all 5 @ts-ignore to @ts-expect-error with descriptions, remove stale disable comments
- Add NEXT_PUBLIC_SENTRY_DSN to docker-compose.prod.yml and .env.example
- Add coverage artifact upload step to CI test job
- Pre-existing violations (102 warnings) downgraded to warn in web config for Phase 2 cleanup

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-10 14:49:29 +02:00
parent 605fd7cea1
commit 82acc56b8d
38 changed files with 2901 additions and 1251 deletions
+93 -74
View File
@@ -1,10 +1,7 @@
"use client";
import { useState, useCallback, useEffect, useRef } from "react";
import {
type DashboardLayoutConfig,
type DashboardWidgetType,
} from "@capakraken/shared/types";
import { type DashboardLayoutConfig, type DashboardWidgetType } from "@capakraken/shared/types";
import {
createDashboardWidget,
createDefaultDashboardLayout,
@@ -46,14 +43,15 @@ export function shouldHydrateDashboardFromDb(params: {
hasLocalChangesBeforeHydration: boolean;
}): boolean {
const { remoteLayout, hasHydratedFromDb, hasLocalChangesBeforeHydration } = params;
return remoteLayout !== null
&& remoteLayout !== undefined
&& !hasHydratedFromDb
&& !hasLocalChangesBeforeHydration;
return (
remoteLayout !== null &&
remoteLayout !== undefined &&
!hasHydratedFromDb &&
!hasLocalChangesBeforeHydration
);
}
export function useDashboardLayout() {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const { data: meData } = trpc.user.me.useQuery() as { data: { id?: string } | null | undefined };
const userId = meData?.id ?? null;
@@ -74,7 +72,12 @@ export function useDashboardLayout() {
// Once userId is known, hydrate from user-scoped localStorage (if no DB data yet).
useEffect(() => {
if (!userId || hasHydratedFromStorageRef.current || hasHydratedFromDbRef.current || hasLocalChangesBeforeHydrationRef.current) {
if (
!userId ||
hasHydratedFromStorageRef.current ||
hasHydratedFromDbRef.current ||
hasLocalChangesBeforeHydrationRef.current
) {
return;
}
const stored = loadFromStorage(userId);
@@ -90,7 +93,6 @@ export function useDashboardLayout() {
setIsHydrated(true);
}, [userId]);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const { data: dbData } = trpc.user.getDashboardLayout.useQuery(undefined, {
staleTime: 30_000,
}) as { data: { layout: DashboardLayoutConfig | null; updatedAt: unknown } | null | undefined };
@@ -122,11 +124,13 @@ export function useDashboardLayout() {
return;
}
if (!shouldHydrateDashboardFromDb({
remoteLayout,
hasHydratedFromDb: hasHydratedFromDbRef.current,
hasLocalChangesBeforeHydration: hasLocalChangesBeforeHydrationRef.current,
})) {
if (
!shouldHydrateDashboardFromDb({
remoteLayout,
hasHydratedFromDb: hasHydratedFromDbRef.current,
hasLocalChangesBeforeHydration: hasLocalChangesBeforeHydrationRef.current,
})
) {
// DB data present but local changes already in-flight — keep local state, mark done.
hasHydratedFromDbRef.current = true;
setIsHydrated(true);
@@ -159,69 +163,84 @@ export function useDashboardLayout() {
// Flush any pending debounced DB save when the component unmounts so that
// navigating away within the 2-second window doesn't silently lose changes.
useEffect(() => () => {
if (saveTimeoutRef.current) {
clearTimeout(saveTimeoutRef.current);
saveTimeoutRef.current = null;
if (pendingLayoutSaveRef.current) {
saveMutationRef.current.mutate({ layout: pendingLayoutSaveRef.current });
pendingLayoutSaveRef.current = null;
useEffect(
() => () => {
if (saveTimeoutRef.current) {
clearTimeout(saveTimeoutRef.current);
saveTimeoutRef.current = null;
if (pendingLayoutSaveRef.current) {
saveMutationRef.current.mutate({ layout: pendingLayoutSaveRef.current });
pendingLayoutSaveRef.current = null;
}
}
}
}, []);
},
[],
);
const persist = useCallback((nextConfig: DashboardLayoutConfig) => {
if (!hasHydratedFromDbRef.current) {
hasLocalChangesBeforeHydrationRef.current = true;
}
const newConfig = normalizeDashboardLayout(nextConfig);
if (userId) saveToStorage(userId, newConfig);
pendingLayoutSaveRef.current = newConfig;
if (saveTimeoutRef.current) clearTimeout(saveTimeoutRef.current);
saveTimeoutRef.current = setTimeout(() => {
pendingLayoutSaveRef.current = null;
saveMutation.mutate({ layout: newConfig });
}, 2000);
}, [saveMutation, userId]);
const persist = useCallback(
(nextConfig: DashboardLayoutConfig) => {
if (!hasHydratedFromDbRef.current) {
hasLocalChangesBeforeHydrationRef.current = true;
}
const newConfig = normalizeDashboardLayout(nextConfig);
if (userId) saveToStorage(userId, newConfig);
pendingLayoutSaveRef.current = newConfig;
if (saveTimeoutRef.current) clearTimeout(saveTimeoutRef.current);
saveTimeoutRef.current = setTimeout(() => {
pendingLayoutSaveRef.current = null;
saveMutation.mutate({ layout: newConfig });
}, 2000);
},
[saveMutation, userId],
);
const addWidget = useCallback((type: DashboardWidgetType) => {
setConfig((prev) => {
const newConfig = {
...prev,
widgets: [
...prev.widgets,
createDashboardWidget(type, {
id: generateWidgetId(),
x: 0,
y: getNextDashboardWidgetY(prev.widgets),
}),
],
};
persist(newConfig);
return newConfig;
});
}, [persist]);
const addWidget = useCallback(
(type: DashboardWidgetType) => {
setConfig((prev) => {
const newConfig = {
...prev,
widgets: [
...prev.widgets,
createDashboardWidget(type, {
id: generateWidgetId(),
x: 0,
y: getNextDashboardWidgetY(prev.widgets),
}),
],
};
persist(newConfig);
return newConfig;
});
},
[persist],
);
const removeWidget = useCallback((id: string) => {
setConfig((prev) => {
const newConfig = { ...prev, widgets: prev.widgets.filter((w) => w.id !== id) };
persist(newConfig);
return newConfig;
});
}, [persist]);
const removeWidget = useCallback(
(id: string) => {
setConfig((prev) => {
const newConfig = { ...prev, widgets: prev.widgets.filter((w) => w.id !== id) };
persist(newConfig);
return newConfig;
});
},
[persist],
);
const updateWidgetConfig = useCallback((id: string, configUpdate: Record<string, unknown>) => {
setConfig((prev) => {
const newConfig = {
...prev,
widgets: prev.widgets.map((w) =>
w.id === id ? { ...w, config: { ...w.config, ...configUpdate } } : w,
),
};
persist(newConfig);
return newConfig;
});
}, [persist]);
const updateWidgetConfig = useCallback(
(id: string, configUpdate: Record<string, unknown>) => {
setConfig((prev) => {
const newConfig = {
...prev,
widgets: prev.widgets.map((w) =>
w.id === id ? { ...w, config: { ...w.config, ...configUpdate } } : w,
),
};
persist(newConfig);
return newConfig;
});
},
[persist],
);
const onLayoutChange = useCallback(
(layout: { i: string; x: number; y: number; w: number; h: number }[]) => {