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
@@ -27,7 +27,8 @@ const PERMISSION_LABELS: Record<string, string> = {
const PERMISSION_DESCRIPTIONS: Record<string, string> = {
viewPlanning: "Read project and allocation planning views without mutation access",
viewCosts: "Access to cost data, budget views, and financial reports",
useAssistantAdvancedTools: "Unlocks advanced AI assistant workflows for complex cross-entity analyses",
useAssistantAdvancedTools:
"Unlocks advanced AI assistant workflows for complex cross-entity analyses",
exportData: "Export data to Excel, CSV, or PDF formats",
importData: "Import data from external sources (Dispo, Excel)",
approveVacations: "Approve or reject vacation requests",
@@ -101,8 +102,7 @@ export function SystemRolesClient() {
staleTime: 10_000,
});
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore TS2589: tRPC infers union type too deeply for the role config update payload
// @ts-expect-error TS2589: tRPC infers union type too deeply for the role config update payload
const updateMutation = trpc.systemRoleConfig.update.useMutation({
onSuccess: async () => {
await utils.systemRoleConfig.list.invalidate();
@@ -164,9 +164,12 @@ export function SystemRolesClient() {
return (
<div className="p-6 max-w-4xl mx-auto">
<div className="mb-6">
<h1 className="text-2xl font-bold text-gray-900 dark:text-gray-50">System Role Management</h1>
<h1 className="text-2xl font-bold text-gray-900 dark:text-gray-50">
System Role Management
</h1>
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
Configure default permissions for each system role. Changes apply to all users with that role.
Configure default permissions for each system role. Changes apply to all users with that
role.
</p>
</div>
@@ -179,7 +182,11 @@ export function SystemRolesClient() {
{isLoading && (
<div className="space-y-3">
{[...Array(5)].map((_, i) => (
<div key={i} className="h-24 shimmer-skeleton rounded-xl animate-row-enter" style={{ animationDelay: `${i * 50}ms` }} />
<div
key={i}
className="h-24 shimmer-skeleton rounded-xl animate-row-enter"
style={{ animationDelay: `${i * 50}ms` }}
/>
))}
</div>
)}
@@ -197,7 +204,9 @@ export function SystemRolesClient() {
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center gap-3 mb-1">
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-semibold ${ROLE_BADGE_MAP[color] ?? ROLE_BADGE_MAP.gray}`}>
<span
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-semibold ${ROLE_BADGE_MAP[color] ?? ROLE_BADGE_MAP.gray}`}
>
{config.label}
</span>
<span className="text-xs text-gray-400 dark:text-gray-500 font-mono">
@@ -211,7 +220,9 @@ export function SystemRolesClient() {
)}
<div className="flex flex-wrap gap-1 mt-2">
{perms.length === 0 ? (
<span className="text-xs text-gray-400 dark:text-gray-500 italic">No default permissions</span>
<span className="text-xs text-gray-400 dark:text-gray-500 italic">
No default permissions
</span>
) : (
perms.map((p) => (
<span
@@ -241,15 +252,21 @@ export function SystemRolesClient() {
{configs.length > 0 && (
<div className="mt-8">
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-50 mb-3 flex items-center">
Permission Matrix <InfoTooltip content="Overview of which permissions each role has by default." />
Permission Matrix{" "}
<InfoTooltip content="Overview of which permissions each role has by default." />
</h2>
<div className="bg-white dark:bg-gray-900 rounded-xl border border-gray-200 dark:border-gray-700 overflow-x-auto">
<table className="w-full text-xs">
<thead>
<tr className="border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800/50">
<th className="px-3 py-2 text-left font-medium text-gray-600 dark:text-gray-400 sticky left-0 bg-gray-50 dark:bg-gray-800/50">Permission</th>
<th className="px-3 py-2 text-left font-medium text-gray-600 dark:text-gray-400 sticky left-0 bg-gray-50 dark:bg-gray-800/50">
Permission
</th>
{configs.map((c) => (
<th key={c.role} className="px-3 py-2 text-center font-medium text-gray-600 dark:text-gray-400 min-w-[80px]">
<th
key={c.role}
className="px-3 py-2 text-center font-medium text-gray-600 dark:text-gray-400 min-w-[80px]"
>
{c.label}
</th>
))}
@@ -269,7 +286,11 @@ export function SystemRolesClient() {
{has ? (
<span className="inline-flex items-center justify-center w-5 h-5 rounded-full bg-green-100 dark:bg-green-900/40 text-green-600 dark:text-green-400">
<svg className="w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
</span>
) : (
@@ -303,7 +324,10 @@ export function SystemRolesClient() {
</div>
<button
type="button"
onClick={() => { setEditingRole(null); setActionError(null); }}
onClick={() => {
setEditingRole(null);
setActionError(null);
}}
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 text-2xl leading-none"
>
&times;
@@ -370,7 +394,8 @@ export function SystemRolesClient() {
<div>
<div className="flex items-center justify-between mb-2">
<label className="text-sm font-medium text-gray-700 dark:text-gray-300">
Default Permissions ({editingRole.permissions.size}/{ALL_PERMISSION_KEYS.length})
Default Permissions ({editingRole.permissions.size}/{ALL_PERMISSION_KEYS.length}
)
</label>
<div className="flex gap-2">
<button
@@ -403,19 +428,31 @@ export function SystemRolesClient() {
: "bg-gray-50 border-gray-200 dark:bg-gray-800/50 dark:border-gray-700 hover:bg-gray-100 dark:hover:bg-gray-800"
}`}
>
<span className={`flex-shrink-0 w-4 h-4 rounded border flex items-center justify-center ${
isActive
? "text-green-600 border-green-300 bg-green-100 dark:bg-green-900/40"
: "border-gray-300 dark:border-gray-600"
}`}>
<span
className={`flex-shrink-0 w-4 h-4 rounded border flex items-center justify-center ${
isActive
? "text-green-600 border-green-300 bg-green-100 dark:bg-green-900/40"
: "border-gray-300 dark:border-gray-600"
}`}
>
{isActive && (
<svg className="w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
)}
</span>
<div className="flex-1 min-w-0">
<span className={isActive ? "text-gray-900 dark:text-gray-100 font-medium" : "text-gray-500 dark:text-gray-400"}>
<span
className={
isActive
? "text-gray-900 dark:text-gray-100 font-medium"
: "text-gray-500 dark:text-gray-400"
}
>
{PERMISSION_LABELS[key] ?? key}
</span>
<p className="text-[11px] text-gray-400 dark:text-gray-500 mt-0.5 truncate">
@@ -432,7 +469,10 @@ export function SystemRolesClient() {
<div className="flex items-center justify-end gap-3 px-6 py-4 border-t border-gray-200 dark:border-gray-700">
<button
type="button"
onClick={() => { setEditingRole(null); setActionError(null); }}
onClick={() => {
setEditingRole(null);
setActionError(null);
}}
className="px-4 py-2 text-sm text-gray-600 hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-200"
>
Cancel