From 60dea49cf2eeb28db14674bdcdb8fb006aa172d8 Mon Sep 17 00:00:00 2001 From: senke Date: Thu, 15 Jan 2026 19:52:48 +0100 Subject: [PATCH] security: add copy request ID button to ErrorDisplay - Added "Copy Request ID" button that copies request ID to clipboard - Button appears for server errors when request_id is available - Uses modern Clipboard API with fallback to execCommand - Shows success toast when copied - Added alongside existing "Report Issue" button - Fixed TypeScript error in isServerError calculation - Action 5.3.1.2 complete --- EXHAUSTIVE_TODO_LIST.md | 15 +++- apps/web/src/components/ui/ErrorDisplay.tsx | 90 +++++++++++++++++---- 2 files changed, 85 insertions(+), 20 deletions(-) diff --git a/EXHAUSTIVE_TODO_LIST.md b/EXHAUSTIVE_TODO_LIST.md index 92613d9e1..c37cd2a26 100644 --- a/EXHAUSTIVE_TODO_LIST.md +++ b/EXHAUSTIVE_TODO_LIST.md @@ -1715,11 +1715,20 @@ Critical path dependencies: - Note: ErrorDisplay.tsx already shows request ID without dev check (line 651), so this change affects formatErrorMessage function specifically - **Rollback**: Restore dev check -- [ ] **Action 5.3.1.2**: Add "Report Issue" button with request ID +- [x] **Action 5.3.1.2**: Add "Report Issue" button with request ID - **Scope**: `apps/web/src/components/ui/ErrorDisplay.tsx` - Add button, copy request ID - - **Dependencies**: Action 3.1.1.2 complete + - **Dependencies**: Action 3.1.1.2 complete ✅ - **Risk**: LOW - - **Validation**: Button copies request ID to clipboard + - **Validation**: ✅ Button copies request ID to clipboard: + - Added `handleCopyRequestId` callback that copies request ID to clipboard + - Added "Copy Request ID" button with Copy icon from lucide-react + - Button appears for server errors when request_id is available + - Button shown alongside existing "Report Issue" button + - Uses modern Clipboard API with fallback to execCommand + - Shows success toast when copied + - Added to both banner and modal variants + - Fixed TypeScript error in isServerError calculation (use normalized error instead of apiError.status) + - No TypeScript errors - **Rollback**: Remove button ### Sub-Epic 5.4: Rate Limit UI 🟢 diff --git a/apps/web/src/components/ui/ErrorDisplay.tsx b/apps/web/src/components/ui/ErrorDisplay.tsx index 2ac880df9..0bccd5ba3 100644 --- a/apps/web/src/components/ui/ErrorDisplay.tsx +++ b/apps/web/src/components/ui/ErrorDisplay.tsx @@ -7,6 +7,7 @@ import { ChevronDown, ChevronUp, Bug, + Copy, } from 'lucide-react'; import { cn } from '@/lib/utils'; import { Button } from './button'; @@ -244,11 +245,12 @@ export const ErrorDisplay = React.forwardRef( // Determine if this is a server error that should show report issue button const isServerError = React.useMemo(() => { + const normalized = normalizeError(error); return ( errorCategory === 'server_error' || - (apiError.status !== undefined && apiError.status >= 500) + (normalized.status !== undefined && normalized.status >= 500) ); - }, [errorCategory, apiError]); + }, [errorCategory, error]); // Determine if details should be shown const isDev = import.meta.env.DEV; @@ -417,6 +419,38 @@ export const ErrorDisplay = React.forwardRef( } }, [error, context]); + // Action 5.3.1.2: Copy request ID to clipboard + const handleCopyRequestId = React.useCallback(async () => { + if (!apiError.request_id) return; + + try { + if ( + typeof navigator !== 'undefined' && + navigator.clipboard && + navigator.clipboard.writeText + ) { + await navigator.clipboard.writeText(apiError.request_id); + toast.success('Request ID copied to clipboard'); + } else { + // Fallback: create temporary textarea + const textarea = document.createElement('textarea'); + textarea.value = apiError.request_id; + textarea.style.position = 'fixed'; + textarea.style.opacity = '0'; + document.body.appendChild(textarea); + textarea.select(); + try { + document.execCommand('copy'); + toast.success('Request ID copied to clipboard'); + } finally { + document.body.removeChild(textarea); + } + } + } catch (err) { + toast.error('Failed to copy request ID'); + } + }, [apiError.request_id]); + // Determine if dismissible const isDismissible = dismissible ?? (onDismiss !== undefined || variant === 'modal'); @@ -525,16 +559,28 @@ export const ErrorDisplay = React.forwardRef( {isRetrying ? 'Retrying...' : 'Retry'} )} - {isServerError && ( - + {isServerError && apiError.request_id && ( + <> + + + )} {actions.map((action, idx) => ( )} - {isServerError && ( - + {isServerError && apiError.request_id && ( + <> + + + )} {actions.map((action, idx) => (