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
This commit is contained in:
senke 2026-01-15 19:52:48 +01:00
parent 0fa52a3776
commit 60dea49cf2
2 changed files with 85 additions and 20 deletions

View file

@ -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 🟢

View file

@ -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<HTMLDivElement, ErrorDisplayProps>(
// 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<HTMLDivElement, ErrorDisplayProps>(
}
}, [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<HTMLDivElement, ErrorDisplayProps>(
{isRetrying ? 'Retrying...' : 'Retry'}
</Button>
)}
{isServerError && (
<Button
variant="outline"
size={size === 'sm' ? 'sm' : 'default'}
onClick={handleReportIssue}
className="border-current text-current hover:bg-current/10"
>
<Bug className="w-4 h-4 mr-1.5" />
Report Issue
</Button>
{isServerError && apiError.request_id && (
<>
<Button
variant="outline"
size={size === 'sm' ? 'sm' : 'default'}
onClick={handleCopyRequestId}
className="border-current text-current hover:bg-current/10"
title="Copy Request ID to clipboard"
>
<Copy className="w-4 h-4 mr-1.5" />
Copy Request ID
</Button>
<Button
variant="outline"
size={size === 'sm' ? 'sm' : 'default'}
onClick={handleReportIssue}
className="border-current text-current hover:bg-current/10"
>
<Bug className="w-4 h-4 mr-1.5" />
Report Issue
</Button>
</>
)}
{actions.map((action, idx) => (
<Button
@ -624,11 +670,21 @@ export const ErrorDisplay = React.forwardRef<HTMLDivElement, ErrorDisplayProps>(
{isRetrying ? 'Retrying...' : 'Retry'}
</Button>
)}
{isServerError && (
<Button variant="outline" onClick={handleReportIssue}>
<Bug className="w-4 h-4 mr-1.5" />
Report Issue
</Button>
{isServerError && apiError.request_id && (
<>
<Button
variant="outline"
onClick={handleCopyRequestId}
title="Copy Request ID to clipboard"
>
<Copy className="w-4 h-4 mr-1.5" />
Copy Request ID
</Button>
<Button variant="outline" onClick={handleReportIssue}>
<Bug className="w-4 h-4 mr-1.5" />
Report Issue
</Button>
</>
)}
{actions.map((action, idx) => (
<Button