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:
@@ -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"
|
||||
>
|
||||
×
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user