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
@@ -3,7 +3,7 @@
import { useEffect, useState } from "react";
import Link from "next/link";
import dynamic from "next/dynamic";
import { EstimateExportFormat } from "@capakraken/shared";
import type { EstimateExportFormat } from "@capakraken/shared";
import { clsx } from "clsx";
import { useSession } from "next-auth/react";
import type {
@@ -23,52 +23,80 @@ const TabSkeleton = () => (
);
const EstimateWorkspaceDraftEditor = dynamic(
() => import("~/components/estimates/EstimateWorkspaceDraftEditor.js").then((mod) => ({ default: mod.EstimateWorkspaceDraftEditor })),
() =>
import("~/components/estimates/EstimateWorkspaceDraftEditor.js").then((mod) => ({
default: mod.EstimateWorkspaceDraftEditor,
})),
{ loading: TabSkeleton },
);
const WeeklyPhasingView = dynamic(
() => import("~/components/estimates/WeeklyPhasingView.js").then((mod) => ({ default: mod.WeeklyPhasingView })),
() =>
import("~/components/estimates/WeeklyPhasingView.js").then((mod) => ({
default: mod.WeeklyPhasingView,
})),
{ loading: TabSkeleton },
);
const OverviewTab = dynamic(
() => import("~/components/estimates/tabs/OverviewTab.js").then((mod) => ({ default: mod.OverviewTab })),
() =>
import("~/components/estimates/tabs/OverviewTab.js").then((mod) => ({
default: mod.OverviewTab,
})),
{ loading: TabSkeleton },
);
const AssumptionsTab = dynamic(
() => import("~/components/estimates/tabs/AssumptionsTab.js").then((mod) => ({ default: mod.AssumptionsTab })),
() =>
import("~/components/estimates/tabs/AssumptionsTab.js").then((mod) => ({
default: mod.AssumptionsTab,
})),
{ loading: TabSkeleton },
);
const ScopeTab = dynamic(
() => import("~/components/estimates/tabs/ScopeTab.js").then((mod) => ({ default: mod.ScopeTab })),
() =>
import("~/components/estimates/tabs/ScopeTab.js").then((mod) => ({ default: mod.ScopeTab })),
{ loading: TabSkeleton },
);
const StaffingTab = dynamic(
() => import("~/components/estimates/tabs/StaffingTab.js").then((mod) => ({ default: mod.StaffingTab })),
() =>
import("~/components/estimates/tabs/StaffingTab.js").then((mod) => ({
default: mod.StaffingTab,
})),
{ loading: TabSkeleton },
);
const FinancialsTab = dynamic(
() => import("~/components/estimates/tabs/FinancialsTab.js").then((mod) => ({ default: mod.FinancialsTab })),
() =>
import("~/components/estimates/tabs/FinancialsTab.js").then((mod) => ({
default: mod.FinancialsTab,
})),
{ loading: TabSkeleton },
);
const VersionsTab = dynamic(
() => import("~/components/estimates/tabs/VersionsTab.js").then((mod) => ({ default: mod.VersionsTab })),
() =>
import("~/components/estimates/tabs/VersionsTab.js").then((mod) => ({
default: mod.VersionsTab,
})),
{ loading: TabSkeleton },
);
const ExportsTab = dynamic(
() => import("~/components/estimates/tabs/ExportsTab.js").then((mod) => ({ default: mod.ExportsTab })),
() =>
import("~/components/estimates/tabs/ExportsTab.js").then((mod) => ({
default: mod.ExportsTab,
})),
{ loading: TabSkeleton },
);
const CommentThread = dynamic(
() => import("~/components/comments/CommentThread.js").then((mod) => ({ default: mod.CommentThread })),
() =>
import("~/components/comments/CommentThread.js").then((mod) => ({
default: mod.CommentThread,
})),
{ loading: TabSkeleton },
);
@@ -137,20 +165,22 @@ export function EstimateWorkspaceClient({ estimateId }: { estimateId: string })
const createPlanningHandoffMutation = trpc.estimate.createPlanningHandoff.useMutation();
const estimateCommentTarget = { entityType: "estimate" as const, entityId: estimateId };
const canLoadCommentCount =
canViewCosts
&& !isPermissionsLoading
&& detailQuery.status === "success"
&& detailQuery.data != null;
canViewCosts &&
!isPermissionsLoading &&
detailQuery.status === "success" &&
detailQuery.data != null;
const commentCountQuery = trpc.comment.count.useQuery(
estimateCommentTarget,
{ enabled: canLoadCommentCount, staleTime: 30_000 },
);
const commentCountQuery = trpc.comment.count.useQuery(estimateCommentTarget, {
enabled: canLoadCommentCount,
staleTime: 30_000,
});
const commentCount = commentCountQuery.data ?? 0;
const estimate = (detailQuery.data as EstimateWorkspaceView | undefined) ?? null;
const hasWorkingVersion = estimate?.versions.some((version) => version.status === "WORKING") ?? false;
const editableTab = tab === "overview" || tab === "assumptions" || tab === "scope" || tab === "staffing";
const hasWorkingVersion =
estimate?.versions.some((version) => version.status === "WORKING") ?? false;
const editableTab =
tab === "overview" || tab === "assumptions" || tab === "scope" || tab === "staffing";
useEffect(() => {
setIsEditing(false);
@@ -258,19 +288,27 @@ export function EstimateWorkspaceClient({ estimateId }: { estimateId: string })
<div className="rounded-[28px] border border-gray-200 dark:border-gray-700 bg-gradient-to-br from-white via-white to-brand-50 dark:from-gray-900 dark:via-gray-900 dark:to-gray-900 p-6 shadow-sm dark:shadow-black/20">
<div className="flex flex-col gap-5 lg:flex-row lg:items-start lg:justify-between">
<div>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-brand-600 dark:text-sky-400">Estimate Workspace <InfoTooltip content="Central workspace for inspecting and editing an estimate's scope, staffing, financials, and version history." /></p>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-brand-600 dark:text-sky-400">
Estimate Workspace{" "}
<InfoTooltip content="Central workspace for inspecting and editing an estimate's scope, staffing, financials, and version history." />
</p>
<h1 className="mt-2 text-3xl font-semibold text-gray-900 dark:text-gray-50">
{estimate?.name ?? "Loading estimate"}
</h1>
<p className="mt-2 max-w-3xl text-sm text-gray-600 dark:text-gray-300">
Use the tabs below to inspect the connected estimate structure, version context, and staffing breakdown without relying on spreadsheet tabs.
Use the tabs below to inspect the connected estimate structure, version context, and
staffing breakdown without relying on spreadsheet tabs.
</p>
</div>
{estimate && (
<div className="flex flex-col gap-3 lg:items-end">
<div className="grid gap-2 text-sm text-gray-500 dark:text-gray-400 lg:text-right">
<span>{estimate.project ? `${estimate.project.shortCode} - ${estimate.project.name}` : "Standalone estimate"}</span>
<span>
{estimate.project
? `${estimate.project.shortCode} - ${estimate.project.name}`
: "Standalone estimate"}
</span>
<span>Updated {formatDateLong(estimate.updatedAt)}</span>
</div>
{canEdit && hasWorkingVersion && (
@@ -282,7 +320,11 @@ export function EstimateWorkspaceClient({ estimateId }: { estimateId: string })
}}
className="rounded-2xl border border-brand-200 dark:border-sky-700 bg-white dark:bg-gray-800 px-4 py-2 text-sm font-semibold text-brand-700 dark:text-sky-300 transition hover:border-brand-300 dark:hover:border-sky-600 hover:bg-brand-50 dark:hover:bg-gray-700"
>
{isEditing ? "Close editor" : editableTab ? "Edit working draft" : "Draft editor available in editable tabs"}
{isEditing
? "Close editor"
: editableTab
? "Edit working draft"
: "Draft editor available in editable tabs"}
</button>
)}
</div>
@@ -293,7 +335,9 @@ export function EstimateWorkspaceClient({ estimateId }: { estimateId: string })
{isPermissionsLoading ? (
<EmptyState>Loading estimate workspace...</EmptyState>
) : !canViewCosts ? (
<EmptyState>Your role can access the estimate list, but not the detailed financial workspace.</EmptyState>
<EmptyState>
Your role can access the estimate list, but not the detailed financial workspace.
</EmptyState>
) : detailQuery.isLoading ? (
<EmptyState>Loading estimate workspace...</EmptyState>
) : detailQuery.error ? (