diff --git a/frontend/src/pages/admin/projects/ProjectCockpitPage.tsx b/frontend/src/pages/admin/projects/ProjectCockpitPage.tsx index 4875f25e..61da0582 100644 --- a/frontend/src/pages/admin/projects/ProjectCockpitPage.tsx +++ b/frontend/src/pages/admin/projects/ProjectCockpitPage.tsx @@ -83,6 +83,16 @@ const KIND_ICON: Record> = hours: Clock, }; +/** Neutralise links for a read-only preview: force every anchor to target a + * new tab so the sandboxed iframe (no allow-popups) blocks the navigation + * entirely. Without this, clicking "Accept"/"Decline" in the preview would + * hit the live action URLs and actually change the quote's state. */ +function neutralizeLinks(html: string): string { + const base = ''; + if (/]*>/i.test(html)) return html.replace(/]*>/i, (m) => m + base); + return base + html; +} + function minutesToHours(min: number): string { const h = Math.floor(min / 60); const m = min % 60; @@ -445,13 +455,16 @@ export const ProjectCockpitPage: React.FC = () => { {t('projects.email.reRendered', 'Re-rendered from the current template — this email was sent before previews were captured, so it may differ slightly from what the recipient received.')} )} - {/* Render the email exactly as sent — it carries its own - background from the brand/email theme. isolate the - iframe's color-scheme so the admin's OS dark mode doesn't - tint a document that defines its own colors. */} + {/* Read-only preview: renders the email with its own brand + colors (color-scheme:normal stops the dark app theme from + tinting it), but `sandbox` (no allow-popups/scripts/forms) + + neutralizeLinks make every link inert — so the admin + can't accidentally trigger the live Accept/Decline URLs by + clicking inside the preview. Scrolling still works. */}