Files
Charon/docs/plans/archive/phase-6-user-management-ui.md
GitHub Actions 3169b05156 fix: skip incomplete system log viewer tests
- Marked 12 tests as skip pending feature implementation
- Features tracked in GitHub issue #686 (system log viewer feature completion)
- Tests cover sorting by timestamp/level/method/URI/status, pagination controls, filtering by text/level, download functionality
- Unblocks Phase 2 at 91.7% pass rate to proceed to Phase 3 security enforcement validation
- TODO comments in code reference GitHub #686 for feature completion tracking
- Tests skipped: Pagination (3), Search/Filter (2), Download (2), Sorting (1), Log Display (4)
2026-02-09 21:55:55 +00:00

712 lines
22 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Phase 6: User Management UI Implementation
> **Status**: Planning Complete
> **Created**: 2026-01-24
> **Estimated Effort**: L (Large) - Initially estimated 40-60 hours, **revised to 16-22 hours**
> **Priority**: P2 - Feature Completeness
> **Tests Targeted**: 19 skipped tests in `tests/settings/user-management.spec.ts`
> **Dependencies**: Phase 5 (TestDataManager Auth Fix) - Infrastructure complete, blocked by environment config
---
## Executive Summary
### Goals
Complete the User Management frontend to enable 19 currently-skipped Playwright E2E tests. This phase implements missing UI components including status badges with proper color classes, role badges, resend invite action, email validation, enhanced modal accessibility, and fixes a React anti-pattern bug.
### Key Finding
**Most UI components already exist.** After thorough analysis, the work is primarily:
1. Verifying existing functionality (toast test IDs already exist)
2. Implementing resend invite action (backend endpoint missing - needs implementation)
3. Adding email format validation with visible error
4. Fixing React anti-pattern in PermissionsModal
5. Verification and unskipping tests
**Revised Effort**: 16-22 hours (pending backend resend endpoint scope).
**Solution**: Add missing test selectors, implement resend invite, add email validation UI, fix React bugs, and systematically unskip tests as they pass.
### Test Count Reconciliation
The original plan stated 22 tests, but verification shows **19 skipped test declarations**. The discrepancy came from counting 4 conditional `test.skip()` calls inside test bodies (not actual test declarations). See Section 2 for the complete inventory.
---
## 1. Current State Analysis
### What EXISTS (in `UsersPage.tsx`)
The Users page at `frontend/src/pages/UsersPage.tsx` already contains substantial functionality:
| Component | Status | Notes |
|-----------|--------|-------|
| User list table | ✅ Complete | Columns: User, Role, Status, Permissions, Enabled, Actions |
| InviteModal | ✅ Complete | Email, role, permission mode, host selection, URL preview |
| PermissionsModal | ✅ Complete | Edit user permissions, host toggle |
| Role badges | ✅ Complete | Purple for admin, blue for user, rounded styling |
| Status indicators | ✅ Complete | Active (green), Pending (yellow), Expired (red) with icons |
| Enable/Disable toggle | ✅ Complete | Switch component per user |
| Delete button | ✅ Complete | Trash2 icon with confirmation |
| Settings/Permissions button | ✅ Complete | For non-admin users |
| React Query mutations | ✅ Complete | All CRUD operations |
| Copy invite link | ✅ Complete | With clipboard API |
| URL preview for invites | ✅ Complete | Shows invite URL before sending |
### What is PARTIALLY IMPLEMENTED
| Item | Issue | Fix Required |
|------|-------|--------------|
| Status badges | Class names may not match test expectations | Add explicit color classes |
| Modal keyboard nav | Escape key handling may be missing | Add keyboard event handler |
| PermissionsModal state init | **React anti-pattern: useState used like useEffect** | Fix to use useEffect (see Section 3.6) |
### What is MISSING
| Item | Description | Effort |
|------|-------------|--------|
| Email validation UI | Client-side format validation with visible error | 2 hours |
| Resend invite action | Button + API for pending users | 6-10 hours (backend missing) |
| Backend resend endpoint | `POST /api/v1/users/{id}/resend-invite` | See Phase 6.4 |
---
## 2. Test Analysis
### Summary: 19 Skipped Tests
**File**: `tests/settings/user-management.spec.ts`
| # | Test Name | Line | Category | Skip Reason | Status |
|---|-----------|------|----------|-------------|--------|
| 1 | should show user status badges | 70 | User List | Status badges styling | ✅ Verify |
| 2 | should display role badges | 110 | User List | Role badges selectors | ✅ Verify |
| 3 | should show pending invite status | 164 | User List | Complex timing | ⚠️ Complex |
| 4 | should open invite user modal | 217 | Invite | Outdated skip comment | ✅ Verify |
| 5 | should validate email format | 283 | Invite | No client validation | 🔧 Implement |
| 6 | should copy invite link | 442 | Invite | Toast verification | ✅ Verify |
| 7 | should open permissions modal | 494 | Permissions | Settings icon | 🔒 Auth blocked |
| 8 | should update permission mode | 538 | Permissions | Base URL auth | 🔒 Auth blocked |
| 9 | should add permitted hosts | 612 | Permissions | Settings icon | 🔒 Auth blocked |
| 10 | should remove permitted hosts | 669 | Permissions | Settings icon | 🔒 Auth blocked |
| 11 | should save permission changes | 725 | Permissions | Settings icon | 🔒 Auth blocked |
| 12 | should enable/disable user | 781 | Actions | TestDataManager | 🔒 Auth blocked |
| 13 | should change user role | 828 | Actions | Not implemented | ❌ Future |
| 14 | should delete user with confirmation | 848 | Actions | Delete button | 🔒 Auth blocked |
| 15 | should resend invite for pending user | 956 | Actions | Not implemented | 🔧 Implement |
| 16 | should be keyboard navigable | 1014 | A11y | Known flaky | ⚠️ Flaky |
| 17 | should require admin role for access | 1091 | Security | Routing design | By design |
| 18 | should show error for regular user access | 1125 | Security | Routing design | By design |
| 19 | should have proper ARIA labels | 1157 | A11y | ARIA incomplete | ✅ Verify |
### Legend
- ✅ Verify: Likely already works, just needs verification
- 🔧 Fix/Implement: Requires small code change
- 🔒 Auth blocked: Blocked by Phase 5 (TestDataManager)
- ⚠️ Complex/Flaky: Timing or complexity issues
- By design: Intentional skip (routing behavior)
- ❌ Future: Feature not prioritized
### Tests Addressable in Phase 6
**Without Auth Fix** (can implement now): 6 tests
- Test 1: Status badges styling (verify only)
- Test 2: Role badges (verify only)
- Test 4: Open invite modal (verify only - button IS implemented)
- Test 5: Email validation
- Test 6: Copy invite link (verify only - toast test IDs already exist)
- Test 19: ARIA labels (verify only)
**With Resend Invite**: 1 test
- Test 15: Resend invite
**After Phase 5 Auth Fix**: 6 tests
- Tests 7-12, 14: Permission/Action tests
### Detailed Test Requirements
#### Test 1: should show user status badges (Line 70)
**Test code**:
```typescript
const statusCell = page.locator('td').filter({
has: page.locator('span').filter({
hasText: /active|pending.*invite|invite.*expired/i,
}),
});
const activeStatus = page.locator('span').filter({ hasText: /^active$/i });
// Expects class to include 'green', 'text-green-400', or 'success'
const hasGreenColor = await activeStatus.first().evaluate((el) => {
return el.className.includes('green') ||
el.className.includes('text-green-400') ||
el.className.includes('success');
});
```
**Current code** (UsersPage.tsx line ~459):
```tsx
<span className="inline-flex items-center gap-1 text-green-400 text-xs">
<Check className="h-3 w-3" />
{t('common.active')}
</span>
```
**Analysis**: Current code already includes `text-green-400` class.
**Action**: ✅ **Verify only** - unskip and run test.
#### Test 2: should display role badges (Line 110)
**Test code**:
```typescript
const adminBadge = page.locator('span').filter({ hasText: /^admin$/i });
// Expects 'purple', 'blue', or 'rounded' in class
const hasDistinctColor = await adminBadge.evaluate((el) => {
return el.className.includes('purple') ||
el.className.includes('blue') ||
el.className.includes('rounded');
});
```
**Current code** (UsersPage.tsx line ~445):
```tsx
<span className={`inline-flex items-center px-2 py-1 rounded-full text-xs font-medium ${
user.role === 'admin' ? 'bg-purple-900/30 text-purple-400' : 'bg-blue-900/30 text-blue-400'
}`}>
{user.role}
</span>
```
**Analysis**: ✅ Classes include `rounded` and `purple`/`blue`.
**Action**: ✅ **Verify only** - unskip and run test.
#### Test 4: should open invite user modal (Line 217)
**Test code**:
```typescript
const inviteButton = page.getByRole('button', { name: /invite.*user/i });
await expect(inviteButton).toBeVisible();
await inviteButton.click();
// Verify modal is visible
const modal = page.getByRole('dialog');
await expect(modal).toBeVisible();
```
**Current state**: ✅ Invite button IS implemented in UsersPage.tsx. The skip comment is outdated.
**Action**: ✅ **Verify only** - unskip and run test.
#### Test 5: should validate email format (Line 283)
**Test code**:
```typescript
const sendButton = page.getByRole('button', { name: /send.*invite/i });
const isDisabled = await sendButton.isDisabled();
// OR error message shown
const errorMessage = page.getByText(/invalid.*email|email.*invalid|valid.*email/i);
```
**Current code**: Button disabled when `!email`, but no format validation visible.
**Action**: 🔧 **Implement** - Add email regex validation with error display.
#### Test 6: should copy invite link (Line 442)
**Test code**:
```typescript
const copiedToast = page.locator('[data-testid="toast-success"]').filter({
hasText: /copied|clipboard/i,
});
```
**Current state**: ✅ Toast component already has `data-testid={toast-${toast.type}}` at `Toast.tsx:31`.
**Action**: ✅ **Verify only** - unskip and run test. No code changes needed.
#### Test 15: should resend invite for pending user (Line 956)
**Test code**:
```typescript
const resendButton = page.getByRole('button', { name: /resend/i });
await resendButton.first().click();
await waitForToast(page, /sent|resend/i, { type: 'success' });
```
**Current state**: ❌ Resend action not implemented.
**Action**: 🔧 **Implement** - Add resend button for pending users + API call.
#### Test 19: should have proper ARIA labels (Line 1157)
**Test code**:
```typescript
const inviteButton = page.getByRole('button', { name: /invite.*user/i });
// Checks for accessible name on action buttons
const ariaLabel = await button.getAttribute('aria-label');
const title = await button.getAttribute('title');
const text = await button.textContent();
```
**Current state**:
- Invite button: text content "Invite User" ✅
- Delete button: `aria-label={t('users.deleteUser')}`
- Settings button: `aria-label={t('users.editPermissions')}`
**Action**: ✅ **Verify only** - unskip and run test.
---
## 3. Implementation Phases
### Phase 6.1: Verify Existing Functionality (3 hours)
**Goal**: Confirm tests 1, 2, 4, 6, 19 pass without code changes.
**Tests in Batch**:
- Test 1: should show user status badges
- Test 2: should display role badges
- Test 4: should open invite user modal
- Test 6: should copy invite link (toast test IDs already exist)
- Test 19: should have proper ARIA labels
**Tasks**:
1. Temporarily remove `test.skip` from tests 1, 2, 4, 6, 19
2. Run tests individually
3. Document results
4. Permanently unskip passing tests
**Commands**:
```bash
# Test status badges
npx playwright test tests/settings/user-management.spec.ts \
--grep "should show user status badges" --project=chromium
# Test role badges
npx playwright test tests/settings/user-management.spec.ts \
--grep "should display role badges" --project=chromium
# Test invite modal opens
npx playwright test tests/settings/user-management.spec.ts \
--grep "should open invite user modal" --project=chromium
# Test copy invite link (toast test IDs already exist)
npx playwright test tests/settings/user-management.spec.ts \
--grep "should copy invite link" --project=chromium
# Test ARIA labels
npx playwright test tests/settings/user-management.spec.ts \
--grep "should have proper ARIA labels" --project=chromium
```
**Expected outcome**: 4-5 tests pass immediately.
---
### Phase 6.2: Email Validation UI (2 hours)
**Goal**: Add client-side email format validation with visible error.
**File to modify**: `frontend/src/pages/UsersPage.tsx` (InviteModal)
**Implementation**:
```tsx
// Add state in InviteModal component
const [emailError, setEmailError] = useState<string | null>(null)
// Email validation function
const validateEmail = (email: string): boolean => {
if (!email) {
setEmailError(null)
return false
}
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
if (!emailRegex.test(email)) {
setEmailError(t('users.invalidEmail'))
return false
}
setEmailError(null)
return true
}
// Update email input (replace existing Input)
<div>
<Input
label={t('users.emailAddress')}
type="email"
value={email}
onChange={(e) => {
setEmail(e.target.value)
validateEmail(e.target.value)
}}
placeholder="user@example.com"
aria-invalid={!!emailError}
aria-describedby={emailError ? 'email-error' : undefined}
/>
{emailError && (
<p
id="email-error"
className="mt-1 text-sm text-red-400"
role="alert"
>
{emailError}
</p>
)}
</div>
// Update button disabled logic
disabled={!email || !!emailError}
```
**Translation key to add** (to appropriate i18n file):
```json
{
"users.invalidEmail": "Please enter a valid email address"
}
```
**Validation**:
```bash
npx playwright test tests/settings/user-management.spec.ts \
--grep "should validate email format" --project=chromium
```
**Expected outcome**: Test 5 passes.
---
### Phase 6.3: Resend Invite Action (6-10 hours)
**Goal**: Add resend invite button for pending users.
#### Backend Verification
**REQUIRED**: Check if backend endpoint exists before proceeding:
```bash
grep -r "resend" backend/internal/api/handlers/
grep -r "ResendInvite" backend/internal/api/
```
**Result of verification**: Backend endpoint **does not exist**. Both grep commands return no results.
**Contingency**: If backend is missing (confirmed), effort increases to **8-10 hours** to implement:
- Endpoint: `POST /api/v1/users/{id}/resend-invite`
- Handler: Regenerate token, send email, return new token info
- Tests: Unit tests for the new handler
#### Frontend Implementation
**File**: `frontend/src/api/users.ts`
Add API function:
```typescript
/**
* Resends an invitation email to a pending user.
* @param id - The user ID to resend invite to
* @returns Promise resolving to InviteUserResponse with new token
*/
export const resendInvite = async (id: number): Promise<InviteUserResponse> => {
const response = await client.post<InviteUserResponse>(`/users/${id}/resend-invite`)
return response.data
}
```
**File**: `frontend/src/pages/UsersPage.tsx`
Add mutation:
```tsx
const resendInviteMutation = useMutation({
mutationFn: resendInvite,
onSuccess: (data) => {
queryClient.invalidateQueries({ queryKey: ['users'] })
if (data.email_sent) {
toast.success(t('users.inviteResent'))
} else {
toast.success(t('users.inviteCreatedNoEmail'))
}
},
onError: (error: unknown) => {
const err = error as { response?: { data?: { error?: string } } }
toast.error(err.response?.data?.error || t('users.resendFailed'))
},
})
```
Add button in user row actions:
```tsx
{user.invite_status === 'pending' && (
<button
onClick={() => resendInviteMutation.mutate(user.id)}
className="p-1.5 text-gray-400 hover:text-blue-400 hover:bg-gray-800 rounded"
title={t('users.resendInvite')}
aria-label={t('users.resendInvite')}
disabled={resendInviteMutation.isPending}
>
<Mail className="h-4 w-4" />
</button>
)}
```
**Translation keys**:
```json
{
"users.resendInvite": "Resend Invite",
"users.inviteResent": "Invitation resent successfully",
"users.inviteCreatedNoEmail": "New invite created. Email could not be sent.",
"users.resendFailed": "Failed to resend invitation"
}
```
**Validation**:
```bash
npx playwright test tests/settings/user-management.spec.ts \
--grep "should resend invite" --project=chromium
```
**Expected outcome**: Test 15 passes.
---
### Phase 6.4: Modal Keyboard Navigation (2 hours)
**Goal**: Ensure Escape key closes modals.
**File to modify**: `frontend/src/pages/UsersPage.tsx`
**Implementation** (add to InviteModal and PermissionsModal):
```tsx
// Add useEffect for keyboard handler
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
handleClose()
}
}
if (isOpen) {
document.addEventListener('keydown', handleKeyDown)
return () => document.removeEventListener('keydown', handleKeyDown)
}
}, [isOpen, handleClose])
```
**Note**: Test 16 (keyboard navigation) is marked as **known flaky** and may remain skipped.
---
### Phase 6.5: PermissionsModal useState Bug Fix (1 hour)
**Goal**: Fix React anti-pattern in PermissionsModal.
**File to modify**: `frontend/src/pages/UsersPage.tsx` (line 339)
**Bug**: `useState` is being used like a `useEffect`, which is a React anti-pattern:
```tsx
// WRONG - useState used like an effect (current code at line 339)
useState(() => {
if (user) {
setPermissionMode(user.permission_mode || 'allow_all')
setSelectedHosts(user.permitted_hosts || [])
}
})
```
**Fix**: Replace with proper `useEffect` with dependency:
```tsx
// CORRECT - useEffect with dependency
useEffect(() => {
if (user) {
setPermissionMode(user.permission_mode || 'allow_all')
setSelectedHosts(user.permitted_hosts || [])
}
}, [user])
```
**Why this matters**: The useState initializer only runs once on mount. The current code appears to work incidentally but:
1. Will not update state when `user` prop changes
2. May cause stale data bugs
3. Violates React's data flow principles
**Validation**:
```bash
# Run TypeScript check
cd frontend && npm run typecheck
# Run related permission tests (after Phase 5 auth fix)
npx playwright test tests/settings/user-management.spec.ts \
--grep "permissions" --project=chromium
```
---
## 4. Implementation Order
```
Week 1 (10-14 hours)
├── Phase 6.1: Verify Existing (3h) → Tests 1, 2, 4, 6, 19
├── Phase 6.2: Email Validation (2h) → Test 5
├── Phase 6.3: Resend Invite (6-10h) → Test 15
│ └── Includes backend endpoint implementation
└── Phase 6.5: PermissionsModal Bug Fix (1h) → Stability
Week 2 (2-3 hours)
└── Phase 6.4: Modal Keyboard Nav (2h) → Partial for Test 16
Validation & Cleanup (3 hours)
└── Run full suite, update skip comments
```
---
## 5. Files to Modify
### Priority 1: Required for Test Enablement
| File | Changes |
|------|---------|
| `frontend/src/pages/UsersPage.tsx` | Email validation, resend invite button, keyboard nav, PermissionsModal useState→useEffect fix |
| `frontend/src/api/users.ts` | Add `resendInvite` function |
| `tests/settings/user-management.spec.ts` | Unskip verified tests |
### Priority 2: Backend (REQUIRED - endpoint missing)
| File | Changes |
|------|---------|
| `backend/internal/api/handlers/user_handler.go` | Add resend-invite endpoint |
| `backend/internal/api/routes.go` | Register new route |
| `backend/internal/api/handlers/user_handler_test.go` | Add tests for resend endpoint |
### Priority 3: Translations
| File | Keys to Add |
|------|-------------|
| `frontend/src/i18n/locales/en.json` | `invalidEmail`, `resendInvite`, `inviteResent`, etc. |
### NOT Required (Already Implemented)
| File | Status |
|------|--------|
| `frontend/src/components/Toast.tsx` | ✅ Already has `data-testid={toast-${toast.type}}` |
---
## 6. Validation Strategy
### After Each Phase
```bash
# Run specific tests
npx playwright test tests/settings/user-management.spec.ts \
--grep "<test name>" --project=chromium
```
### Final Validation
```bash
# Run all user management tests
npx playwright test tests/settings/user-management.spec.ts --project=chromium
# Expected:
# - ~12-14 tests passing (up from ~5)
# - ~6-8 tests still skipped (auth blocked or by design)
```
### Test Coverage
```bash
cd frontend && npm run test:coverage
# Verify UsersPage.tsx >= 85%
```
---
## 7. Expected Outcomes
### Tests to Unskip After Phase 6
| Test | Expected Outcome |
|------|------------------|
| should show user status badges | ✅ Pass |
| should display role badges | ✅ Pass |
| should open invite user modal | ✅ Pass |
| should validate email format | ✅ Pass |
| should copy invite link | ✅ Pass |
| should resend invite for pending user | ✅ Pass |
| should have proper ARIA labels | ✅ Pass |
**Total**: 7 tests enabled
### Tests Remaining Skipped
| Test | Reason |
|------|--------|
| Tests 7-12, 14 (7 tests) | 🔒 TestDataManager auth (Phase 5) |
| Test 3: pending invite status | ⚠️ Complex timing |
| Test 13: change user role | ❌ Feature not implemented |
| Test 16: keyboard navigation | ⚠️ Known flaky |
| Tests 17-18: admin access | Routing design (intentional) |
**Total remaining skipped**: ~12 tests (down from 22)
---
## 8. Risk Assessment
| Risk | Likelihood | Impact | Mitigation |
|------|------------|--------|------------|
| Backend resend endpoint missing | **CONFIRMED** | High | Backend implementation included in Phase 6.3 (6-10h) |
| Tests pass locally, fail in CI | Medium | Medium | Run in both environments |
| Translation keys missing | Low | Low | Add to all locale files |
| PermissionsModal bug causes regressions | Low | Medium | Fix early in Phase 6.5 with testing |
---
## 9. Success Metrics
| Metric | Before Phase 6 | After Phase 6 | Target |
|--------|----------------|---------------|--------|
| User management tests passing | ~5 | ~12-14 | 15+ |
| User management tests skipped | 19 | 10-12 | <10 |
| Frontend coverage (UsersPage) | TBD | ≥85% | 85% |
---
## 10. Timeline
| Phase | Effort | Cumulative |
|-------|--------|------------|
| 6.1: Verify Existing (5 tests) | 3h | 3h |
| 6.2: Email Validation | 2h | 5h |
| 6.3: Resend Invite (backend included) | 6-10h | 11-15h |
| 6.4: Modal Keyboard Nav | 2h | 13-17h |
| 6.5: PermissionsModal Bug Fix | 1h | 14-18h |
| Validation & Cleanup | 3h | 17-21h |
| Buffer | 3h | **16-22h** |
**Total**: 16-22 hours (range depends on backend complexity)
---
## Change Log
| Date | Author | Change |
|------|--------|--------|
| 2026-01-24 | Planning Agent | Initial plan created with detailed test analysis |
| 2026-01-24 | Planning Agent | **REVISION**: Applied Supervisor corrections: |
| | | - Toast test IDs already exist (Phase 6.2 removed) |
| | | - Updated test line numbers to actual values (70, 110, 217, 283, 442, 956, 1014, 1157) |
| | | - Added Test 4 and Test 6 to Phase 6.1 verification batch (5 tests total) |
| | | - Added Phase 6.5: PermissionsModal useState bug fix |
| | | - Backend resend endpoint confirmed missing (grep verification) |
| | | - Corrected test count: 19 skipped tests (not 22) |
| | | - Updated effort estimates: 16-22h (was 17h) |