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
@@ -166,7 +166,7 @@ export function ResourcesClient() {
const departedFilter = resourceUrlFilters.departed as BooleanFilter;
// chapters stored as comma-separated string; empty string means "all chapters visible"
const chaptersParam = resourceUrlFilters.chapters;
// eslint-disable-next-line react-hooks/exhaustive-deps
const chapterFilter: string[] = useMemo(
() => (chaptersParam ? chaptersParam.split(",").filter(Boolean) : []),
[chaptersParam],
@@ -175,21 +175,32 @@ export function ResourcesClient() {
// Flush debounced search input to URL
useEffect(() => {
setResourceUrlFilters({ search: debouncedSearch });
// eslint-disable-next-line react-hooks/exhaustive-deps
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [debouncedSearch]);
// Keep local search input in sync when URL changes externally
useEffect(() => {
setSearchInput(resourceUrlFilters.search);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [resourceUrlFilters.search]);
const setIsActiveFilter = useCallback((v: ActiveFilter) => setResourceUrlFilters({ activeFilter: v }), [setResourceUrlFilters]);
const setRolledOffFilter = useCallback((v: BooleanFilter) => setResourceUrlFilters({ rolledOff: v }), [setResourceUrlFilters]);
const setDepartedFilter = useCallback((v: BooleanFilter) => setResourceUrlFilters({ departed: v }), [setResourceUrlFilters]);
const setChapterFilter = useCallback((v: string[]) => {
setResourceUrlFilters({ chapters: v.join(",") });
}, [setResourceUrlFilters]);
const setIsActiveFilter = useCallback(
(v: ActiveFilter) => setResourceUrlFilters({ activeFilter: v }),
[setResourceUrlFilters],
);
const setRolledOffFilter = useCallback(
(v: BooleanFilter) => setResourceUrlFilters({ rolledOff: v }),
[setResourceUrlFilters],
);
const setDepartedFilter = useCallback(
(v: BooleanFilter) => setResourceUrlFilters({ departed: v }),
[setResourceUrlFilters],
);
const setChapterFilter = useCallback(
(v: string[]) => {
setResourceUrlFilters({ chapters: v.join(",") });
},
[setResourceUrlFilters],
);
const [includeProposedChargeability, setIncludeProposedChargeability] = useState(false);
const [hiddenCountryIds, setHiddenCountryIds] = useState<string[]>([]);
@@ -412,7 +423,13 @@ export function ResourcesClient() {
function clearAll() {
setSearchInput("");
setResourceUrlFilters({ search: "", activeFilter: "active", rolledOff: DEFAULT_BOOLEAN_FILTER, departed: DEFAULT_BOOLEAN_FILTER, chapters: "" });
setResourceUrlFilters({
search: "",
activeFilter: "active",
rolledOff: DEFAULT_BOOLEAN_FILTER,
departed: DEFAULT_BOOLEAN_FILTER,
chapters: "",
});
setHiddenCountryIds([]);
setIncludeWithoutCountry(true);
setHiddenResourceTypes([...DEFAULT_HIDDEN_RESOURCE_TYPES]);
@@ -468,7 +485,9 @@ export function ResourcesClient() {
if (next.length === chapters.length) {
setChapterFilter([]);
} else {
setChapterFilter(next.sort((left, right) => chapters.indexOf(left) - chapters.indexOf(right)));
setChapterFilter(
next.sort((left, right) => chapters.indexOf(left) - chapters.indexOf(right)),
);
}
},
[chapters, chapterFilter, setChapterFilter],
@@ -533,13 +552,23 @@ export function ResourcesClient() {
{ header: "LCR (cents)", accessor: (r) => r.lcrCents },
{ header: "Currency", accessor: (r) => r.currency },
{ header: "Chargeability Target", accessor: (r) => r.chargeabilityTarget },
{ header: "Active", accessor: (r) => r.isActive ? "Yes" : "No" },
{ header: "Active", accessor: (r) => (r.isActive ? "Yes" : "No") },
]);
downloadCsv(csv, `resources-export-${new Date().toISOString().slice(0, 10)}.csv`);
}, [displayedResources, selection.selectedIds]);
const chips = [
...(search ? [{ label: `Search: "${search}"`, onRemove: () => { setSearchInput(""); setResourceUrlFilters({ search: "" }); } }] : []),
...(search
? [
{
label: `Search: "${search}"`,
onRemove: () => {
setSearchInput("");
setResourceUrlFilters({ search: "" });
},
},
]
: []),
...(chapterFilter.length > 0
? [
{
@@ -1303,7 +1332,12 @@ export function ResourcesClient() {
/>
</div>
{isOverflow && (
<span className="text-[9px] font-bold text-green-600 dark:text-green-400" title={`${actual}% actual`}>+</span>
<span
className="text-[9px] font-bold text-green-600 dark:text-green-400"
title={`${actual}% actual`}
>
+
</span>
)}
</div>
)}