diff --git a/docs/superpowers/plans/2026-03-18-admin-redesign.md b/docs/superpowers/plans/2026-03-18-admin-redesign.md new file mode 100644 index 0000000..e9f6b9a --- /dev/null +++ b/docs/superpowers/plans/2026-03-18-admin-redesign.md @@ -0,0 +1,950 @@ +# Admin Redesign Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Overhaul admin section UX/UI to match the design language of the rest of the application, fix critical bugs, and improve usability. + +**Architecture:** Mostly file edits — replacing custom implementations with design system composites (DataTable, Tabs), fixing tokens, reworking the user creation flow with provider awareness, and adding toast feedback + accessibility. + +**Tech Stack:** React 18, TypeScript, CSS Modules, Vitest + RTL + +**Spec:** `docs/superpowers/specs/2026-03-18-admin-redesign.md` + +--- + +## File Map + +### Modified files +``` +src/pages/Admin/Admin.tsx — Replace custom nav with Tabs, import Tabs +src/pages/Admin/Admin.module.css — Remove nav styles, fix padding +src/pages/Admin/AuditLog/AuditLog.tsx — Rewrite to use DataTable +src/pages/Admin/AuditLog/AuditLog.module.css — Replace with card + filter styles only +src/pages/Admin/AuditLog/auditMocks.ts — Change id to string +src/pages/Admin/OidcConfig/OidcConfig.tsx — Remove h2 title, add toolbar +src/pages/Admin/OidcConfig/OidcConfig.module.css — Remove header styles, center form +src/pages/Admin/UserManagement/UserManagement.tsx — Replace inline style +src/pages/Admin/UserManagement/UserManagement.module.css — Fix tokens, radii, shadow, add tabContent + empty/security styles +src/pages/Admin/UserManagement/UsersTab.tsx — Rework create form, add security section, toasts, accessibility, confirmations +src/pages/Admin/UserManagement/GroupsTab.tsx — Add toasts, accessibility, confirmations, empty state +src/pages/Admin/UserManagement/RolesTab.tsx — Replace emoji, add toasts, accessibility, empty state +src/pages/Admin/UserManagement/rbacMocks.ts — No changes needed (provider field already exists) +``` + +--- + +### Task 1: Fix critical token bug + visual polish + +**Files:** +- Modify: `src/pages/Admin/Admin.module.css` +- Modify: `src/pages/Admin/UserManagement/UserManagement.module.css` + +- [ ] **Step 1: Fix `--bg-base` token in Admin.module.css** + +In `Admin.module.css`, replace `var(--bg-base)` with `var(--bg-surface)` on line 6. + +Also fix the content padding on line 35: change `padding: 20px` to `padding: 20px 24px 40px`. + +- [ ] **Step 2: Fix `--bg-base` token and visual polish in UserManagement.module.css** + +Replace both `var(--bg-base)` occurrences (lines 12, 19) with `var(--bg-surface)`. + +Change `border-radius: var(--radius-md)` to `var(--radius-lg)` on `.splitPane` (line 7), `.listPane` (line 15), and `.detailPane` (line 22). + +Add `box-shadow: var(--shadow-card)` to `.splitPane`. + +Add these new classes at the end of the file: + +```css +.tabContent { + margin-top: 16px; +} + +.emptySearch { + padding: 32px; + text-align: center; + color: var(--text-faint); + font-size: 12px; + font-family: var(--font-body); +} + +.securitySection { + margin-top: 8px; + margin-bottom: 8px; +} + +.securityRow { + display: flex; + align-items: center; + gap: 12px; + font-size: 12px; + font-family: var(--font-body); + color: var(--text-primary); +} + +.passwordDots { + font-family: var(--font-mono); + letter-spacing: 2px; +} + +.resetForm { + display: flex; + gap: 8px; + align-items: center; + margin-top: 8px; +} + +.resetInput { + width: 200px; +} +``` + +- [ ] **Step 3: Commit** + +```bash +git add src/pages/Admin/Admin.module.css src/pages/Admin/UserManagement/UserManagement.module.css +git commit -m "fix: replace nonexistent --bg-base token with --bg-surface, fix radii and padding" +``` + +--- + +### Task 2: Replace admin nav with Tabs composite + +**Files:** +- Modify: `src/pages/Admin/Admin.tsx` +- Modify: `src/pages/Admin/Admin.module.css` + +- [ ] **Step 1: Rewrite Admin.tsx** + +Replace the entire file with: + +```tsx +import { useNavigate, useLocation } from 'react-router-dom' +import { AppShell } from '../../design-system/layout/AppShell/AppShell' +import { Sidebar } from '../../design-system/layout/Sidebar/Sidebar' +import { TopBar } from '../../design-system/layout/TopBar/TopBar' +import { Tabs } from '../../design-system/composites/Tabs/Tabs' +import { SIDEBAR_APPS } from '../../mocks/sidebar' +import styles from './Admin.module.css' +import type { ReactNode } from 'react' + +const ADMIN_TABS = [ + { label: 'User Management', value: '/admin/rbac' }, + { label: 'Audit Log', value: '/admin/audit' }, + { label: 'OIDC', value: '/admin/oidc' }, +] + +interface AdminLayoutProps { + title: string + children: ReactNode +} + +export function AdminLayout({ title, children }: AdminLayoutProps) { + const navigate = useNavigate() + const location = useLocation() + + return ( + }> + + navigate(path)} + /> +
+ {children} +
+
+ ) +} +``` + +- [ ] **Step 2: Clean up Admin.module.css** + +Remove `.adminNav`, `.adminTab`, `.adminTab:hover`, `.adminTabActive` styles entirely. Keep only `.adminContent`. + +- [ ] **Step 3: Commit** + +```bash +git add src/pages/Admin/Admin.tsx src/pages/Admin/Admin.module.css +git commit -m "refactor: replace custom admin nav with Tabs composite" +``` + +--- + +### Task 3: Fix AuditLog mock data + migrate to DataTable + +**Files:** +- Modify: `src/pages/Admin/AuditLog/auditMocks.ts` +- Modify: `src/pages/Admin/AuditLog/AuditLog.tsx` +- Modify: `src/pages/Admin/AuditLog/AuditLog.module.css` + +- [ ] **Step 1: Change AuditEvent id to string in auditMocks.ts** + +Change `id: number` to `id: string` in the `AuditEvent` interface. + +Change all mock IDs from numbers to strings: `id: 1` → `id: 'audit-1'`, `id: 2` → `id: 'audit-2'`, etc. through all 25 events. + +- [ ] **Step 2: Rewrite AuditLog.tsx to use DataTable** + +Replace the entire file with: + +```tsx +import { useState, useMemo } from 'react' +import { AdminLayout } from '../Admin' +import { Badge } from '../../../design-system/primitives/Badge/Badge' +import { DateRangePicker } from '../../../design-system/primitives/DateRangePicker/DateRangePicker' +import { Input } from '../../../design-system/primitives/Input/Input' +import { Select } from '../../../design-system/primitives/Select/Select' +import { MonoText } from '../../../design-system/primitives/MonoText/MonoText' +import { CodeBlock } from '../../../design-system/primitives/CodeBlock/CodeBlock' +import { DataTable } from '../../../design-system/composites/DataTable/DataTable' +import type { Column } from '../../../design-system/composites/DataTable/types' +import type { DateRange } from '../../../design-system/utils/timePresets' +import { AUDIT_EVENTS, type AuditEvent } from './auditMocks' +import styles from './AuditLog.module.css' + +const CATEGORIES = [ + { value: '', label: 'All categories' }, + { value: 'INFRA', label: 'INFRA' }, + { value: 'AUTH', label: 'AUTH' }, + { value: 'USER_MGMT', label: 'USER_MGMT' }, + { value: 'CONFIG', label: 'CONFIG' }, +] + +function formatTimestamp(iso: string): string { + return new Date(iso).toLocaleString('en-GB', { + year: 'numeric', month: '2-digit', day: '2-digit', + hour: '2-digit', minute: '2-digit', second: '2-digit', + hour12: false, + }) +} + +const COLUMNS: Column[] = [ + { + key: 'timestamp', header: 'Timestamp', width: '170px', sortable: true, + render: (_, row) => {formatTimestamp(row.timestamp)}, + }, + { + key: 'username', header: 'User', sortable: true, + render: (_, row) => {row.username}, + }, + { + key: 'category', header: 'Category', width: '110px', sortable: true, + render: (_, row) => , + }, + { key: 'action', header: 'Action' }, + { + key: 'target', header: 'Target', + render: (_, row) => {row.target}, + }, + { + key: 'result', header: 'Result', width: '90px', sortable: true, + render: (_, row) => ( + + ), + }, +] + +const now = Date.now() +const INITIAL_RANGE: DateRange = { + from: new Date(now - 7 * 24 * 3600_000).toISOString().slice(0, 16), + to: new Date(now).toISOString().slice(0, 16), +} + +export function AuditLog() { + const [dateRange, setDateRange] = useState(INITIAL_RANGE) + const [userFilter, setUserFilter] = useState('') + const [categoryFilter, setCategoryFilter] = useState('') + const [searchFilter, setSearchFilter] = useState('') + + const filtered = useMemo(() => { + const from = new Date(dateRange.from).getTime() + const to = new Date(dateRange.to).getTime() + return AUDIT_EVENTS.filter((e) => { + const ts = new Date(e.timestamp).getTime() + if (ts < from || ts > to) return false + if (userFilter && !e.username.toLowerCase().includes(userFilter.toLowerCase())) return false + if (categoryFilter && e.category !== categoryFilter) return false + if (searchFilter) { + const q = searchFilter.toLowerCase() + if (!e.action.toLowerCase().includes(q) && !e.target.toLowerCase().includes(q)) return false + } + return true + }) + }, [dateRange, userFilter, categoryFilter, searchFilter]) + + return ( + +
+ + setUserFilter(e.target.value)} + onClear={() => setUserFilter('')} + className={styles.filterInput} + /> + setSearchFilter(e.target.value)} + onClear={() => setSearchFilter('')} + className={styles.filterInput} + /> +
+ +
+
+ Audit Log +
+ + {filtered.length} events + + +
+
+ row.result === 'FAILURE' ? 'error' : undefined} + expandedContent={(row) => ( +
+
+
+ IP Address + {row.ipAddress} +
+
+ User Agent + {row.userAgent} +
+
+
+ Detail + +
+
+ )} + /> +
+
+ ) +} +``` + +- [ ] **Step 3: Rewrite AuditLog.module.css** + +Replace the entire file with: + +```css +.filters { + display: flex; + gap: 10px; + flex-wrap: wrap; + margin-bottom: 16px; +} + +.filterInput { + width: 200px; +} + +.filterSelect { + width: 160px; +} + +.tableSection { + background: var(--bg-surface); + border: 1px solid var(--border-subtle); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-card); + overflow: hidden; +} + +.tableHeader { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 16px; + border-bottom: 1px solid var(--border-subtle); +} + +.tableTitle { + font-size: 13px; + font-weight: 600; + color: var(--text-primary); +} + +.tableRight { + display: flex; + align-items: center; + gap: 10px; +} + +.tableMeta { + font-size: 11px; + color: var(--text-muted); +} + +.target { + display: inline-block; + max-width: 220px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.expandedDetail { + padding: 4px 0; +} + +.detailGrid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 12px; + margin-bottom: 12px; +} + +.detailField { + display: flex; + flex-direction: column; + gap: 4px; +} + +.detailLabel { + font-size: 10px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.8px; + color: var(--text-muted); + font-family: var(--font-body); +} + +.detailValue { + font-size: 12px; + color: var(--text-secondary); +} +``` + +- [ ] **Step 4: Verify build** + +Run: `npx vite build 2>&1 | tail -5` +Expected: Build succeeds + +- [ ] **Step 5: Commit** + +```bash +git add src/pages/Admin/AuditLog/ +git commit -m "refactor: migrate AuditLog to DataTable with card wrapper" +``` + +--- + +### Task 4: Remove duplicate titles from OidcConfig + +**Files:** +- Modify: `src/pages/Admin/OidcConfig/OidcConfig.tsx` +- Modify: `src/pages/Admin/OidcConfig/OidcConfig.module.css` + +- [ ] **Step 1: Replace h2 header with toolbar in OidcConfig.tsx** + +Replace the `.header` div (lines 74-84) with a compact toolbar: + +```tsx +
+ + +
+``` + +- [ ] **Step 2: Update OidcConfig.module.css** + +Remove `.header`, `.title`, `.headerActions`. Add: + +```css +.toolbar { + display: flex; + gap: 8px; + justify-content: flex-end; + margin-bottom: 20px; +} +``` + +Also add `margin: 0 auto` to the `.page` class to center the form. + +- [ ] **Step 3: Commit** + +```bash +git add src/pages/Admin/OidcConfig/ +git commit -m "refactor: remove duplicate title from OIDC page, center form" +``` + +--- + +### Task 5: Replace inline style + fix UserManagement + +**Files:** +- Modify: `src/pages/Admin/UserManagement/UserManagement.tsx` + +- [ ] **Step 1: Replace inline style with CSS class** + +Change line 20 from `
` to `
`. + +Add the `styles` import if not already present (it already imports from `./UserManagement.module.css` — wait, it doesn't currently. Add: +```tsx +import styles from './UserManagement.module.css' +``` + +- [ ] **Step 2: Commit** + +```bash +git add src/pages/Admin/UserManagement/UserManagement.tsx +git commit -m "refactor: replace inline style with CSS module class" +``` + +--- + +### Task 6: Add toasts to all RBAC tabs + +**Files:** +- Modify: `src/pages/Admin/UserManagement/UsersTab.tsx` +- Modify: `src/pages/Admin/UserManagement/GroupsTab.tsx` +- Modify: `src/pages/Admin/UserManagement/RolesTab.tsx` + +- [ ] **Step 1: Add useToast to UsersTab** + +Add import: `import { useToast } from '../../../design-system/composites/Toast/Toast'` + +Add `const { toast } = useToast()` at the top of the `UsersTab` function body. + +Add toast calls: +- After `setSelectedId(newUser.id)` in `handleCreate`: `toast({ title: 'User created', description: newUser.displayName, variant: 'success' })` +- After `setDeleteTarget(null)` in `handleDelete`: `toast({ title: 'User deleted', description: deleteTarget.username, variant: 'warning' })` + +- [ ] **Step 2: Add useToast to GroupsTab** + +Same pattern. Add toast calls: +- After create: `toast({ title: 'Group created', description: newGroup.name, variant: 'success' })` +- After delete: `toast({ title: 'Group deleted', description: deleteTarget.name, variant: 'warning' })` + +- [ ] **Step 3: Add useToast to RolesTab** + +Same pattern. Add toast calls: +- After create: `toast({ title: 'Role created', description: newRole.name, variant: 'success' })` +- After delete: `toast({ title: 'Role deleted', description: deleteTarget.name, variant: 'warning' })` + +- [ ] **Step 4: Commit** + +```bash +git add src/pages/Admin/UserManagement/UsersTab.tsx src/pages/Admin/UserManagement/GroupsTab.tsx src/pages/Admin/UserManagement/RolesTab.tsx +git commit -m "feat: add toast notifications to all RBAC mutations" +``` + +--- + +### Task 7: Rework user creation form + password management + +**Files:** +- Modify: `src/pages/Admin/UserManagement/UsersTab.tsx` + +This is the largest single task. It covers spec items 3.2 (provider-aware create form), 3.3 (password management in detail pane), and 3.6 (remove unused password field). + +- [ ] **Step 1: Rework the create form section** + +Replace the create form state variables (lines 23-26): +```tsx +const [newUsername, setNewUsername] = useState('') +const [newDisplay, setNewDisplay] = useState('') +const [newEmail, setNewEmail] = useState('') +const [newPassword, setNewPassword] = useState('') +``` +with: +```tsx +const [newUsername, setNewUsername] = useState('') +const [newDisplay, setNewDisplay] = useState('') +const [newEmail, setNewEmail] = useState('') +const [newPassword, setNewPassword] = useState('') +const [newProvider, setNewProvider] = useState<'local' | 'oidc'>('local') +``` + +Add imports for RadioGroup, RadioItem, and InfoCallout: +```tsx +import { RadioGroup, RadioItem } from '../../../design-system/primitives/Radio/Radio' +import { InfoCallout } from '../../../design-system/primitives/InfoCallout/InfoCallout' +``` + +Update `handleCreate` to use the provider selection and validate password for local: +```tsx +function handleCreate() { + if (!newUsername.trim()) return + if (newProvider === 'local' && !newPassword.trim()) return + const newUser: MockUser = { + id: `usr-${Date.now()}`, + username: newUsername.trim(), + displayName: newDisplay.trim() || newUsername.trim(), + email: newEmail.trim(), + provider: newProvider, + createdAt: new Date().toISOString(), + directRoles: [], + directGroups: [], + } + setUsers((prev) => [...prev, newUser]) + setCreating(false) + setNewUsername(''); setNewDisplay(''); setNewEmail(''); setNewPassword(''); setNewProvider('local') + setSelectedId(newUser.id) + toast({ title: 'User created', description: newUser.displayName, variant: 'success' }) +} +``` + +Replace the create form JSX (lines 100-114) with: +```tsx +{creating && ( +
+ setNewProvider(v as 'local' | 'oidc')} orientation="horizontal"> + + + +
+ setNewUsername(e.target.value)} /> + setNewDisplay(e.target.value)} /> +
+ setNewEmail(e.target.value)} /> + {newProvider === 'local' && ( + setNewPassword(e.target.value)} /> + )} + {newProvider === 'oidc' && ( + + OIDC users authenticate via the configured identity provider. Pre-register to assign roles/groups before their first login. + + )} +
+ + +
+
+)} +``` + +- [ ] **Step 2: Add Security section to detail pane** + +Add password reset state at the top of the component: +```tsx +const [resettingPassword, setResettingPassword] = useState(false) +const [newPw, setNewPw] = useState('') +``` + +Add the Security section after the metadata grid (after the `
` closing the `.metaGrid`), before the "Group membership" SectionHeader: + +```tsx +Security +
+ {selected.provider === 'local' ? ( + <> +
+ Password + •••••••• + {!resettingPassword && ( + + )} +
+ {resettingPassword && ( +
+ setNewPw(e.target.value)} + className={styles.resetInput} + /> + + +
+ )} + + ) : ( + <> +
+ Authentication + OIDC ({selected.provider}) +
+ + Password managed by the identity provider. + + + )} +
+``` + +- [ ] **Step 3: Verify build** + +Run: `npx vite build 2>&1 | tail -5` +Expected: Build succeeds + +- [ ] **Step 4: Commit** + +```bash +git add src/pages/Admin/UserManagement/UsersTab.tsx +git commit -m "feat: rework user creation with provider selection, add password management" +``` + +--- + +### Task 8: Keyboard accessibility for entity lists + +**Files:** +- Modify: `src/pages/Admin/UserManagement/UsersTab.tsx` +- Modify: `src/pages/Admin/UserManagement/GroupsTab.tsx` +- Modify: `src/pages/Admin/UserManagement/RolesTab.tsx` + +- [ ] **Step 1: Add keyboard support to UsersTab entity list** + +On the `.entityList` wrapper div, add: +```tsx +
+``` + +On each `.entityItem` div, add `role`, `tabIndex`, `aria-selected`, and `onKeyDown`: +```tsx +
setSelectedId(user.id)} + role="option" + tabIndex={0} + aria-selected={selectedId === user.id} + onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); setSelectedId(user.id) } }} +> +``` + +Add empty-search state after the list map: +```tsx +{filtered.length === 0 && ( +
No users match your search
+)} +``` + +- [ ] **Step 2: Add keyboard support to GroupsTab entity list** + +Same pattern — add `role="listbox"` to container, `role="option"` + `tabIndex={0}` + `aria-selected` + `onKeyDown` to items, and empty-search state. + +- [ ] **Step 3: Add keyboard support to RolesTab entity list** + +Same pattern. Also replace the lock emoji on line 113: +```tsx +{role.system && 🔒} +``` +with: +```tsx +{role.system && } +``` + +Add empty-search state. + +- [ ] **Step 4: Commit** + +```bash +git add src/pages/Admin/UserManagement/UsersTab.tsx src/pages/Admin/UserManagement/GroupsTab.tsx src/pages/Admin/UserManagement/RolesTab.tsx +git commit -m "feat: add keyboard accessibility and empty states to entity lists" +``` + +--- + +### Task 9: Add confirmation for cascading removals + +**Files:** +- Modify: `src/pages/Admin/UserManagement/UsersTab.tsx` +- Modify: `src/pages/Admin/UserManagement/GroupsTab.tsx` + +- [ ] **Step 1: Add group removal confirmation in UsersTab** + +Add state for tracking the removal target: +```tsx +const [removeGroupTarget, setRemoveGroupTarget] = useState(null) +``` + +Replace the direct `onRemove` on group Tags (in the "Group membership" section) with: +```tsx +onRemove={() => { + const group = MOCK_GROUPS.find((gr) => gr.id === gId) + if (group && group.directRoles.length > 0) { + setRemoveGroupTarget(gId) + } else { + updateUser(selected.id, { directGroups: selected.directGroups.filter((id) => id !== gId) }) + toast({ title: 'Group removed', variant: 'success' }) + } +}} +``` + +Add an AlertDialog (import from composites) for the confirmation: +```tsx + setRemoveGroupTarget(null)} + onConfirm={() => { + if (removeGroupTarget && selected) { + updateUser(selected.id, { directGroups: selected.directGroups.filter((id) => id !== removeGroupTarget) }) + toast({ title: 'Group removed', variant: 'success' }) + } + setRemoveGroupTarget(null) + }} + title="Remove group membership" + description={`Removing this group will also revoke inherited roles: ${MOCK_GROUPS.find((g) => g.id === removeGroupTarget)?.directRoles.join(', ') ?? ''}. Continue?`} + confirmLabel="Remove" + variant="warning" +/> +``` + +Add import: `import { AlertDialog } from '../../../design-system/composites/AlertDialog/AlertDialog'` + +- [ ] **Step 2: Add role removal confirmation in GroupsTab** + +Add state: +```tsx +const [removeRoleTarget, setRemoveRoleTarget] = useState(null) +``` + +Replace direct `onRemove` on role Tags with: +```tsx +onRemove={() => { + const memberCount = MOCK_USERS.filter((u) => u.directGroups.includes(selected.id)).length + if (memberCount > 0) { + setRemoveRoleTarget(r) + } else { + updateGroup(selected.id, { directRoles: selected.directRoles.filter((role) => role !== r) }) + toast({ title: 'Role removed', variant: 'success' }) + } +}} +``` + +Add AlertDialog: +```tsx + setRemoveRoleTarget(null)} + onConfirm={() => { + if (removeRoleTarget && selected) { + updateGroup(selected.id, { directRoles: selected.directRoles.filter((role) => role !== removeRoleTarget) }) + toast({ title: 'Role removed', variant: 'success' }) + } + setRemoveRoleTarget(null) + }} + title="Remove role from group" + description={`Removing ${removeRoleTarget} from ${selected?.name} will affect ${members.length} member(s) who inherit this role. Continue?`} + confirmLabel="Remove" + variant="warning" +/> +``` + +Add import: `import { AlertDialog } from '../../../design-system/composites/AlertDialog/AlertDialog'` + +- [ ] **Step 3: Commit** + +```bash +git add src/pages/Admin/UserManagement/UsersTab.tsx src/pages/Admin/UserManagement/GroupsTab.tsx +git commit -m "feat: add confirmation dialogs for cascading removals" +``` + +--- + +### Task 10: Add duplicate name validation to create forms + +**Files:** +- Modify: `src/pages/Admin/UserManagement/UsersTab.tsx` +- Modify: `src/pages/Admin/UserManagement/GroupsTab.tsx` +- Modify: `src/pages/Admin/UserManagement/RolesTab.tsx` + +- [ ] **Step 1: Add duplicate check in UsersTab** + +Add a computed `duplicateUsername`: +```tsx +const duplicateUsername = newUsername.trim() && users.some((u) => u.username.toLowerCase() === newUsername.trim().toLowerCase()) +``` + +Update the Create button `disabled` to include `|| duplicateUsername`. + +Show error text below the username Input when duplicate: +```tsx +{duplicateUsername && Username already exists} +``` + +- [ ] **Step 2: Add duplicate check in GroupsTab** + +Similar pattern with `duplicateGroupName` check. Disable Create button when duplicate. + +- [ ] **Step 3: Add duplicate check in RolesTab** + +Similar pattern with `duplicateRoleName` check (compare uppercase). Disable Create button when duplicate. + +- [ ] **Step 4: Commit** + +```bash +git add src/pages/Admin/UserManagement/UsersTab.tsx src/pages/Admin/UserManagement/GroupsTab.tsx src/pages/Admin/UserManagement/RolesTab.tsx +git commit -m "feat: add duplicate name validation to create forms" +``` + +--- + +### Task 11: Final verification + +- [ ] **Step 1: Run full test suite** + +Run: `npx vitest run` +Expected: All tests pass + +- [ ] **Step 2: Build the project** + +Run: `npx vite build` +Expected: Build succeeds + +- [ ] **Step 3: Fix any issues** + +If build fails, fix TypeScript errors. Common issues: +- Import path typos +- Missing props on components +- InfoCallout `variant` prop — check the actual prop name (may be `color` instead) + +- [ ] **Step 4: Commit fixes if needed** + +```bash +git add -A +git commit -m "fix: resolve build issues from admin redesign" +```