diff --git a/.github/workflows/crowdsec-integration.yml b/.github/workflows/crowdsec-integration.yml index bc9ef2f4..6ea05b29 100644 --- a/.github/workflows/crowdsec-integration.yml +++ b/.github/workflows/crowdsec-integration.yml @@ -35,7 +35,7 @@ jobs: # Determine the correct image tag based on trigger context # For PRs: pr-{number}-{sha}, For branches: {sanitized-branch}-{sha} - name: Determine image tag - id: image + id: determine-tag env: EVENT: ${{ github.event.workflow_run.event }} REF: ${{ github.event.workflow_run.head_branch }} @@ -101,7 +101,7 @@ jobs: max_attempts: 3 retry_wait_seconds: 10 command: | - IMAGE_NAME="ghcr.io/${{ github.repository_owner }}/charon:${{ steps.image.outputs.tag }}" + IMAGE_NAME="ghcr.io/${{ github.repository_owner }}/charon:${{ steps.determine-tag.outputs.tag }}" echo "Pulling image: $IMAGE_NAME" docker pull "$IMAGE_NAME" docker tag "$IMAGE_NAME" charon:local @@ -113,12 +113,12 @@ jobs: if: steps.pull_image.outcome == 'failure' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - SHA: ${{ steps.image.outputs.sha }} + SHA: ${{ steps.determine-tag.outputs.sha }} run: | echo "⚠️ Registry pull failed, falling back to artifact..." # Determine artifact name based on source type - if [[ "${{ steps.image.outputs.source_type }}" == "pr" ]]; then + if [[ "${{ steps.determine-tag.outputs.source_type }}" == "pr" ]]; then PR_NUM=$(echo '${{ toJson(github.event.workflow_run.pull_requests) }}' | jq -r '.[0].number') ARTIFACT_NAME="pr-image-${PR_NUM}" else @@ -142,7 +142,7 @@ jobs: # Validate image freshness by checking SHA label - name: Validate image SHA env: - SHA: ${{ steps.image.outputs.sha }} + SHA: ${{ steps.determine-tag.outputs.sha }} run: | LABEL_SHA=$(docker inspect charon:local --format '{{index .Config.Labels "org.opencontainers.image.revision"}}' | cut -c1-7) echo "Expected SHA: $SHA" diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index fb610835..36c21732 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -54,10 +54,6 @@ on: - firefox - webkit - all - image_tag: - description: 'Docker image tag to test (e.g., pr-123-abc1234, latest)' - required: false - type: string env: NODE_VERSION: '20' diff --git a/.version b/.version index 6b60281a..0ffcf198 100644 --- a/.version +++ b/.version @@ -1 +1 @@ -v0.17.0 +v0.17.1 diff --git a/docs/issues/created/20260204-modal_dropdown_handoff_contract.md b/docs/issues/created/20260204-modal_dropdown_handoff_contract.md index b168345a..7112a565 100644 --- a/docs/issues/created/20260204-modal_dropdown_handoff_contract.md +++ b/docs/issues/created/20260204-modal_dropdown_handoff_contract.md @@ -1,8 +1,8 @@ # Modal Dropdown Fix - Local Environment Handoff Contract -**Date**: 2026-02-04 -**Status**: Implementation Complete - Testing Required -**Environment**: Codespace → Local Development Environment +**Date**: 2026-02-04 +**Status**: Implementation Complete - Testing Required +**Environment**: Codespace → Local Development Environment --- @@ -12,7 +12,7 @@ All 7 P0 critical modal components have been updated with the 3-layer modal architecture: 1. ✅ **ProxyHostForm.tsx** - ACL selector, Security Headers dropdowns fixed -2. ✅ **UsersPage.tsx** - InviteUserModal role/permission dropdowns fixed +2. ✅ **UsersPage.tsx** - InviteUserModal role/permission dropdowns fixed 3. ✅ **UsersPage.tsx** - EditPermissionsModal dropdowns fixed 4. ✅ **Uptime.tsx** - CreateMonitorModal & EditMonitorModal type dropdowns fixed 5. ✅ **RemoteServerForm.tsx** - Provider dropdown fixed @@ -74,7 +74,7 @@ docker-compose -f .docker/compose/docker-compose.yml up -d # Navigate to: http://localhost:8080/proxy-hosts # 1. Click "Add Proxy Host" # 2. Test ACL dropdown - should open and allow selection -# 3. Test Security Headers dropdown - should open and allow selection +# 3. Test Security Headers dropdown - should open and allow selection # 4. Fill form and submit - should work normally # 5. Edit existing proxy host - repeat dropdown tests ``` @@ -82,7 +82,7 @@ docker-compose -f .docker/compose/docker-compose.yml up -d **B. User Management Modals** ```bash # Navigate to: http://localhost:8080/users -# 1. Click "Invite User" +# 1. Click "Invite User" # 2. Test Role dropdown (User/Admin) - should work # 3. Test Permission Mode dropdown - should work # 4. Click existing user "Edit Permissions" @@ -94,7 +94,7 @@ docker-compose -f .docker/compose/docker-compose.yml up -d # Navigate to: http://localhost:8080/uptime # 1. Click "Create Monitor" # 2. Test Monitor Type dropdown (HTTP/TCP) - should work -# 3. Save monitor, then click "Configure" +# 3. Save monitor, then click "Configure" # 4. Test Monitor Type dropdown in edit mode - should work ``` @@ -130,7 +130,7 @@ npx playwright test --grep "dropdown|select|acl|security.headers" --project=chro ```bash # Test in each browser for compatibility npx playwright test tests/integration/proxy-acl-integration.spec.ts --project=chromium -npx playwright test tests/integration/proxy-acl-integration.spec.ts --project=firefox +npx playwright test tests/integration/proxy-acl-integration.spec.ts --project=firefox npx playwright test tests/integration/proxy-acl-integration.spec.ts --project=webkit ``` @@ -152,7 +152,7 @@ npx playwright test tests/integration/proxy-acl-integration.spec.ts --project=we # Frontend type check cd frontend && npm run type-check -# Backend tests (should be unaffected) +# Backend tests (should be unaffected) cd backend && go test ./... # Full test suite @@ -217,7 +217,7 @@ native select dropdown menus from being blocked by modal overlays. Components fixed: - ProxyHostForm: ACL selector and Security Headers dropdowns -- User management: Role and permission mode selection +- User management: Role and permission mode selection - Uptime monitors: Monitor type selection (HTTP/TCP) - Remote servers: Provider selection dropdown - CrowdSec: IP ban duration selection @@ -238,7 +238,7 @@ the UI interface. ### Definition of Done - [ ] Manual testing completed for all 7 components - [ ] All E2E tests passing -- [ ] Cross-browser verification complete +- [ ] Cross-browser verification complete - [ ] No console errors or TypeScript issues - [ ] Code review approved (if applicable) - [ ] Commit message follows conventional format @@ -254,4 +254,4 @@ the UI interface. All implementation work is complete. The modal dropdown z-index fix has been applied comprehensively across all 7 affected components. Testing in the local Docker environment will validate the fix works as designed. -**Next Actions**: Move to local environment, run the testing checklist above, and merge when all success criteria are met. \ No newline at end of file +**Next Actions**: Move to local environment, run the testing checklist above, and merge when all success criteria are met. diff --git a/docs/plans/comprehensive_modal_fix_spec.md b/docs/plans/comprehensive_modal_fix_spec.md index 1c822658..10461180 100644 --- a/docs/plans/comprehensive_modal_fix_spec.md +++ b/docs/plans/comprehensive_modal_fix_spec.md @@ -1,9 +1,9 @@ # Comprehensive Modal Z-Index Fix Plan -**Date**: 2026-02-04 -**Issue**: Widespread modal overlay z-index pattern breaking dropdown interactions -**Scope**: 11 modal components across the application -**Fix Strategy**: Unified 3-layer modal restructuring +**Date**: 2026-02-04 +**Issue**: Widespread modal overlay z-index pattern breaking dropdown interactions +**Scope**: 11 modal components across the application +**Fix Strategy**: Unified 3-layer modal restructuring --- @@ -73,10 +73,10 @@ With the 3-layer pattern: <> {/* Layer 1: Background overlay (z-40) */}
- + {/* Layer 2: Form container (z-50, pointer-events-none) */}
- + {/* Layer 3: Form content (pointer-events-auto) */}
@@ -96,7 +96,7 @@ With the 3-layer pattern: **Priority Order** (most business-critical first): 1. **ProxyHostForm.tsx** (30 min) - Security policy assignment 2. **UsersPage.tsx** - InviteUserModal (20 min) - User management -3. **UsersPage.tsx** - EditPermissionsModal (30 min) - Permission management +3. **UsersPage.tsx** - EditPermissionsModal (30 min) - Permission management 4. **Uptime.tsx** - Both modals (45 min) - Monitor management 5. **RemoteServerForm.tsx** (20 min) - Infrastructure management 6. **CrowdSecConfig.tsx** - BanIPModal (20 min) - Security management @@ -108,7 +108,7 @@ Analysis and fix of remaining interactive modals if needed. ### Phase 3: Testing & Validation (2-3 hours) - Manual testing of all dropdown interactions -- E2E test updates +- E2E test updates - Cross-browser verification **Total Estimated Time: 7-11 hours** @@ -121,7 +121,7 @@ Analysis and fix of remaining interactive modals if needed. For each P0 component: - [ ] Modal opens correctly -- [ ] Background overlay click-to-close works +- [ ] Background overlay click-to-close works - [ ] All dropdown menus open and respond to clicks - [ ] Dropdown options are selectable - [ ] Form submission works with selected values @@ -147,15 +147,15 @@ For each P0 component: **Risk Level: LOW-MEDIUM** **Mitigating Factors:** -✅ Non-breaking change (only CSS/DOM structure) -✅ Identical fix pattern across all components -✅ Well-understood solution (already documented in ConfigReloadOverlay) -✅ Only affects modal presentation layer +✅ Non-breaking change (only CSS/DOM structure) +✅ Identical fix pattern across all components +✅ Well-understood solution (already documented in ConfigReloadOverlay) +✅ Only affects modal presentation layer **Risk Areas:** -⚠️ Multiple files being modified simultaneously -⚠️ Modal close behavior could be affected -⚠️ CSS specificity or responsive behavior could change +⚠️ Multiple files being modified simultaneously +⚠️ Modal close behavior could be affected +⚠️ CSS specificity or responsive behavior could change **Mitigation Strategy:** - Fix components one at a time @@ -197,10 +197,10 @@ For each P0 component: ## Post-Implementation Actions 1. **Documentation Update**: Update modal component patterns in design system docs -2. **Code Review Guidelines**: Add z-index modal pattern to code review checklist +2. **Code Review Guidelines**: Add z-index modal pattern to code review checklist 3. **Linting Rule**: Consider ESLint rule to detect problematic modal patterns 4. **Design System**: Create reusable Modal component with correct z-index pattern --- -*This comprehensive fix addresses the root cause across the entire application, preventing future occurrences of the same issue.* \ No newline at end of file +*This comprehensive fix addresses the root cause across the entire application, preventing future occurrences of the same issue.* diff --git a/docs/plans/current_spec.md b/docs/plans/current_spec.md index 89666a94..7aafed45 100644 --- a/docs/plans/current_spec.md +++ b/docs/plans/current_spec.md @@ -1,974 +1,92 @@ -# Modal Z-Index Fix Implementation Plan +# Remediation Plan: Stability & E2E Regressions -**Date**: 2026-02-04 -**Issue**: Modal overlay z-index conflicts preventing dropdown interactions -**Scope**: 7 P0 critical modal components with native ` {/* BROKEN: Dropdown can't render above z-50 overlay */} -
-
-
-``` - -### Solution: 3-Layer Modal Architecture - -Replace single-layer pattern with 3 distinct layers: - -```tsx -{/* FIXED: 3-layer architecture */} -<> - {/* Layer 1: Background overlay (z-40) - Click to close */} -
- - {/* Layer 2: Container (z-50, pointer-events-none) - Centers content */} -
- - {/* Layer 3: Content (pointer-events-auto) - Interactive form */} -
-
- -
-
-
- -``` - -**Key CSS Classes**: -- `pointer-events-none` on container: Passes clicks through to overlay -- `pointer-events-auto` on content: Re-enables form interactions -- Background overlay at `z-40`: Lower than form container at `z-50` -3. **Native Dropdown Conflict**: Native HTML ` onChange(parseInt(e.target.value) || null)} - className="w-full bg-gray-900 border border-gray-700 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-blue-500" -> - - {accessLists?.filter((acl) => acl.enabled).map((acl) => ( - - ))} - -``` - -**Security Headers Dropdown** ([frontend/src/components/ProxyHostForm.tsx](frontend/src/components/ProxyHostForm.tsx#L797-L830)): -```tsx - -``` - -**Problems**: -- Native ` onChange(parseInt(e.target.value) || null)} // ✅ Handler is defined - className="w-full bg-gray-900 border border-gray-700 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-blue-500" - > - - {accessLists?.filter((acl) => acl.enabled).map((acl) => ( - - ))} - - {/* ... display selected ACL details ... */} -
- ); -} -``` - -**Event Handler Analysis**: -- ✅ `onChange` handler is correctly defined -- ✅ Handler parses the selected value correctly -- ✅ Handler calls parent's `onChange` prop with the correct value -- ❌ **THE PROBLEM**: Parent modal overlay prevents click event from reaching the ` { - const value = e.target.value === "0" ? null : parseInt(e.target.value) || null - setFormData({ ...formData, security_header_profile_id: value }) // ✅ Handler is defined - }} - className="w-full bg-gray-900 border border-gray-700 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-blue-500" - > - - - {securityProfiles?.filter(p => p.is_preset) - .sort((a, b) => a.security_score - b.security_score) - .map(profile => ( - - ))} - - {/* ... more profiles ... */} - - ); -} -``` - -**Event Handler Analysis**: -- ✅ `onChange` handler is correctly defined -- ✅ Handler correctly parses "0" as null (unselect) -- ✅ Handler correctly parses numeric values -- ✅ Handler updates `formData` state correctly -- ❌ **THE PROBLEM**: Parent modal overlay prevents click event from reaching the ` - {/* Security Headers dropdown */} - - -
- -) -``` - -**Z-Index Analysis**: -- Outer wrapper: `z-50` with `fixed inset-0` (covers viewport) -- Native `` dropdown menu in a popup layer -2. This popup layer has default z-index (often 0 or auto) -3. Parent's z-index: 50 creates a new stacking context -4. Dropdown menu tries to render in this context -5. If dropdown z-index < parent z-index, it's hidden behind the overlay - ---- - -## Test Evidence & References - -### E2E Test File Evidence - -**File**: [tests/integration/proxy-acl-integration.spec.ts](tests/integration/proxy-acl-integration.spec.ts#L130-L155) - -**Test Attempting to Select ACL** (currently failing): -```typescript -await test.step('Assign IP-based whitelist ACL to proxy host', async () => { - // Find the proxy host row and click edit - const proxyRow = page.locator(SELECTORS.proxyRow).filter({ - hasText: createdDomain, - }); - await expect(proxyRow).toBeVisible(); - - const editButton = proxyRow.locator(SELECTORS.proxyEditBtn).first(); - await editButton.click(); - await waitForModal(page, /edit|proxy/i); - - // Try to select ACL from dropdown - const aclDropdown = page.locator(SELECTORS.aclSelectDropdown); - const aclCombobox = page.getByRole('combobox') - .filter({ hasText: /No Access Control|whitelist/i }); - - // Build the pattern to match the ACL name - const aclNamePattern = aclConfig.name; - - if (await aclDropdown.isVisible()) { - const option = aclDropdown.locator('option').filter({ hasText: aclNamePattern }); - const optionValue = await option.getAttribute('value'); - if (optionValue) { - await aclDropdown.selectOption({ value: optionValue }); - // ❌ THIS FAILS: selectOption doesn't work because overlay blocks interaction - } - } -}); -``` - -**Test Selector References** ([tests/integration/proxy-acl-integration.spec.ts](tests/integration/proxy-acl-integration.spec.ts#L52-L54)): -```typescript -const SELECTORS = { - aclSelectDropdown: '[data-testid="acl-select"], select[name="access_list_id"]', - // ... -} -``` - ---- - -### Known Pattern Reference: ConfigReloadOverlay - -**Similar problematic pattern** in [frontend/src/components/LoadingStates.tsx](frontend/src/components/LoadingStates.tsx#L251-L287): - -```tsx -export function ConfigReloadOverlay({ - message = 'Ferrying configuration...', - submessage = 'Charon is crossing the Styx', - type = 'charon', -}: { - message?: string - submessage?: string - type?: 'charon' | 'coin' | 'cerberus' -}) { - return ( -
- {/* Content */} -
- ) -} -``` - -**Test Helper Workaround** ([tests/utils/ui-helpers.ts](tests/utils/ui-helpers.ts#L247-L275)): - -```typescript -/** - * ✅ FIX P0: Wait for ConfigReloadOverlay to disappear before clicking - * The ConfigReloadOverlay component (z-50) intercepts pointer events - * during Caddy config reloads, blocking all interactions. - */ -export async function clickSwitch( - locator: Locator, - options: SwitchOptions = {} -): Promise { - // ... - const page = locator.page(); - const overlay = page.locator('[data-testid="config-reload-overlay"]'); - - // Wait for overlay to disappear before clicking - await overlay.waitFor({ state: 'hidden', timeout: 10000 }).catch(() => { - // Overlay not present - continue - }); - // ... click the element ... -} -``` - -This confirms the pattern is **known to cause problems** and requires workarounds. - ---- - -## Detailed Fix Plan - -### Solution Strategy - -**Overall Approach**: Restructure the modal DOM to separate the overlay from the form content, using proper z-index layering and pointer-events management. - ---- - -### Fix 1: Restructure Modal Z-Index Hierarchy (CRITICAL) - -**File to Modify**: [frontend/src/components/ProxyHostForm.tsx](frontend/src/components/ProxyHostForm.tsx#L514-L525) - -**Current Code** (Lines 514-525): -```tsx -return ( -
-
-
-

- {host ? 'Edit Proxy Host' : 'Add Proxy Host'} -

-
- -
-``` - -**Proposed Fix**: -```tsx -return ( - <> - {/* Layer 1: Overlay (z-40) - separate from form to allow form to float above */} +- **Component**: `frontend/src/components/ProxyHostForm.tsx` +- **Change**: + - Remove manual overlay logic: + ```tsx
- - {/* Layer 2: Form container (z-50) with pointer-events-none on wrapper */} -
- {/* Layer 3: Form content with pointer-events-auto to re-enable interactions */} -
-
-

+
...
+ ``` + - Implement `Dialog` component (Shadcn UI): + ```tsx + !open && onCancel()}> + + + {host ? 'Edit Proxy Host' : 'Add Proxy Host'} -

-
- - -``` - -**Why This Works**: - -1. **Separate Overlay Layer** (`z-40`): - - Handles backdrop click-to-close - - Doesn't interfere with form's z-index stacking - - Can receive click events without blocking - -2. **Form Container Layer** (`z-50` with `pointer-events-none`): - - Higher z-index than overlay ensures form is on top visually - - `pointer-events-none` means this div doesn't capture clicks - - Clicks pass through to children - -3. **Form Content** (`pointer-events-auto`): - - Re-enables pointer events on actual form elements - - Allows native ` dropdowns -couldn't open or receive clicks because the overlay intercepted events. - -Changes: -- Restructured modal DOM to separate overlay (z-40) from form (z-50) -- Added pointer-events-none to form container, pointer-events-auto to - form content to re-enable child interactions -- This allows native