(function () { const logs = []; const MAX_LOGS = 200; let bugModal = null; let screenshotDataUrl = null; let pendingScreenshotPromise = null; let isCapturingDisplayMedia = false; const pushLog = (type, args) => { try { logs.push({ type, message: (args || []).map((x) => { if (typeof x === 'string') return x; try { return JSON.stringify(x); } catch (_) { return String(x); } }).join(' '), timestamp: new Date().toISOString() }); if (logs.length > MAX_LOGS) { logs.splice(0, logs.length - MAX_LOGS); } } catch (_) { // no-op } }; ['log', 'warn', 'error'].forEach((type) => { const original = console[type]; console[type] = function (...args) { pushLog(type, args); original.apply(console, args); }; }); window.addEventListener('error', (event) => { logs.push({ type: 'error', message: event.message, url: event.filename, line: event.lineno, col: event.colno, timestamp: new Date().toISOString() }); if (logs.length > MAX_LOGS) { logs.splice(0, logs.length - MAX_LOGS); } }); function getCurrentUser() { const metaUser = document.querySelector('meta[name="user-id"]'); const userFromMeta = metaUser ? metaUser.getAttribute('content') : null; let userFromToken = null; const token = localStorage.getItem('access_token'); if (token) { try { const payload = JSON.parse(atob(token.split('.')[1])); userFromToken = payload.sub || payload.user_id || null; } catch (_) { userFromToken = null; } } return userFromToken || userFromMeta || null; } function getMetadata() { return { url: window.location.href, userAgent: navigator.userAgent, timestamp: new Date().toISOString(), screenSize: `${window.innerWidth}x${window.innerHeight}`, user: getCurrentUser(), }; } async function toDataUrl(file) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => resolve(String(reader.result || '')); reader.onerror = reject; reader.readAsDataURL(file); }); } function isCrossOriginUrl(url) { try { if (!url) return false; const parsed = new URL(url, window.location.href); return parsed.origin !== window.location.origin; } catch (_) { return false; } } function shouldIgnoreInScreenshot(el) { if (!el || !el.tagName) return false; const tag = String(el.tagName).toUpperCase(); if (tag === 'IFRAME' || tag === 'VIDEO' || tag === 'OBJECT' || tag === 'EMBED') { return true; } if (tag === 'IMG') { const src = el.getAttribute('src') || ''; return isCrossOriginUrl(src); } return false; } async function renderScreenshot(target, opts) { const canvas = await window.html2canvas(target, opts); return canvas.toDataURL('image/png'); } async function takeScreenshotViaDisplayMedia() { if (!navigator.mediaDevices || !navigator.mediaDevices.getDisplayMedia) { throw new Error('Display media API not supported'); } const stream = await navigator.mediaDevices.getDisplayMedia({ video: true, audio: false, }); try { const video = document.createElement('video'); video.srcObject = stream; video.muted = true; video.playsInline = true; await new Promise((resolve, reject) => { video.onloadedmetadata = () => resolve(); video.onerror = () => reject(new Error('Kunne ikke læse videostream')); }); await video.play(); const width = Math.max(1, video.videoWidth || window.innerWidth); const height = Math.max(1, video.videoHeight || window.innerHeight); const canvas = document.createElement('canvas'); canvas.width = width; canvas.height = height; const ctx = canvas.getContext('2d'); if (!ctx) { throw new Error('Canvas context unavailable'); } ctx.drawImage(video, 0, 0, width, height); return canvas.toDataURL('image/png'); } finally { stream.getTracks().forEach((t) => t.stop()); } } async function takeScreenshot() { if (!window.html2canvas) { throw new Error('Screenshot library not loaded'); } const doc = document.documentElement; const body = document.body; const fullWidth = Math.max( doc ? doc.scrollWidth : 0, doc ? doc.offsetWidth : 0, doc ? doc.clientWidth : 0, body ? body.scrollWidth : 0, body ? body.offsetWidth : 0, window.innerWidth || 0 ); const fullHeight = Math.max( doc ? doc.scrollHeight : 0, doc ? doc.offsetHeight : 0, doc ? doc.clientHeight : 0, body ? body.scrollHeight : 0, body ? body.offsetHeight : 0, window.innerHeight || 0 ); const common = { useCORS: true, allowTaint: false, logging: false, scale: 1, backgroundColor: '#ffffff', imageTimeout: 3000, ignoreElements: shouldIgnoreInScreenshot, removeContainer: true, }; // Strategy 1: Full page (most useful when it works) try { return await renderScreenshot(document.documentElement, { ...common, width: fullWidth, height: fullHeight, windowWidth: fullWidth, windowHeight: fullHeight, x: 0, y: 0, scrollX: 0, scrollY: 0, }); } catch (e1) { console.warn('Bug report screenshot strategy 1 failed', e1); if (String(e1?.message || '').toLowerCase().includes('unsupported color function "color"')) { throw new Error('Html2canvas understøtter ikke denne browsers farveprofil (color()).'); } } // Strategy 2: Main content only (explicit selectors avoid navbar-only captures) const contentRoot = document.querySelector('.container-fluid.px-4.py-4') || document.querySelector('[data-bugreport-root]') || document.querySelector('#main-content') || document.querySelector('#content') || document.querySelector('main') || document.querySelector('.content-wrapper') || document.documentElement; try { return await renderScreenshot(contentRoot, { ...common, width: Math.max(contentRoot.scrollWidth || 0, contentRoot.clientWidth || 0, window.innerWidth || 0), height: Math.max(contentRoot.scrollHeight || 0, contentRoot.clientHeight || 0, window.innerHeight || 0), scrollX: 0, scrollY: 0, }); } catch (e2) { console.warn('Bug report screenshot strategy 2 failed', e2); if (String(e2?.message || '').toLowerCase().includes('unsupported color function "color"')) { throw new Error('Html2canvas understøtter ikke denne browsers farveprofil (color()).'); } } throw new Error('Automatic screenshot failed'); } function setStatus(text, isError) { const el = document.getElementById('bugReportStatus'); if (!el) return; el.textContent = text || ''; el.className = isError ? 'small text-danger mt-2' : 'small text-muted mt-2'; } function setPreview(dataUrl) { const img = document.getElementById('bugScreenshotPreview'); const placeholder = document.getElementById('bugScreenshotPreviewPlaceholder'); if (!img || !placeholder) return; if (dataUrl) { img.src = dataUrl; img.style.display = ''; placeholder.style.display = 'none'; } else { img.removeAttribute('src'); img.style.display = 'none'; placeholder.style.display = ''; } } async function handlePasteScreenshot(event) { const modalVisible = document.getElementById('bugReportModal')?.classList.contains('show'); if (!modalVisible) return; const items = Array.from(event.clipboardData?.items || []); const imageItem = items.find((item) => item.kind === 'file' && item.type.startsWith('image/')); if (!imageItem) return; const blob = imageItem.getAsFile(); if (!blob) return; event.preventDefault(); try { const dataUrl = await toDataUrl(blob); screenshotDataUrl = dataUrl; setPreview(dataUrl); setStatus('Screenshot indsat fra clipboard.'); } catch (e) { console.warn('Clipboard screenshot parse failed', e); setStatus('Kunne ikke indsætte screenshot fra clipboard.', true); } } async function openBugReportModal() { if (!bugModal) { const modalEl = document.getElementById('bugReportModal'); if (!modalEl || !window.bootstrap) return; bugModal = new bootstrap.Modal(modalEl); } setStatus('Tager screenshot...'); screenshotDataUrl = null; setPreview(null); try { screenshotDataUrl = pendingScreenshotPromise ? await pendingScreenshotPromise : await takeScreenshot(); setPreview(screenshotDataUrl); setStatus('Screenshot klar. Udfyld felterne og send.'); } catch (e) { console.warn('Bug report screenshot failed', e); setStatus('Kunne ikke tage screenshot automatisk. Klik "Tag screenshot via skærmdeling" eller indsæt med Cmd+V.', true); } finally { pendingScreenshotPromise = null; } bugModal.show(); } function prepareScreenshotFromTrigger() { pendingScreenshotPromise = takeScreenshot(); return pendingScreenshotPromise; } async function captureScreenshotViaDisplayMediaFromUserGesture() { if (isCapturingDisplayMedia) return; const btn = document.getElementById('bugCaptureDisplayMediaBtn'); const originalHtml = btn ? btn.innerHTML : ''; isCapturingDisplayMedia = true; if (btn) { btn.disabled = true; btn.innerHTML = 'Venter på skærmvalg...'; } try { const dataUrl = await takeScreenshotViaDisplayMedia(); screenshotDataUrl = dataUrl; setPreview(dataUrl); setStatus('Screenshot taget via skærmdeling.'); } catch (e) { console.warn('Bug report display media capture failed', e); setStatus('Skærmdelings-screenshot mislykkedes. Prøv igen eller indsæt med Cmd+V.', true); } finally { isCapturingDisplayMedia = false; if (btn) { btn.disabled = false; btn.innerHTML = originalHtml || 'Tag screenshot via skærmdeling'; } } } async function submitBugReport() { const actual = (document.getElementById('bugActualInput')?.value || '').trim(); const expected = (document.getElementById('bugExpectedInput')?.value || '').trim(); const fileInput = document.getElementById('bugExtraFileInput'); const submitBtn = document.getElementById('bugReportSubmitBtn'); if (!actual || !expected) { setStatus('Udfyld både "Hvad gik galt" og "Hvad burde være sket".', true); return; } let extraFileName = null; let extraFileBase64 = null; if (fileInput && fileInput.files && fileInput.files[0]) { const f = fileInput.files[0]; extraFileName = f.name; try { extraFileBase64 = await toDataUrl(f); } catch (_) { setStatus('Kunne ikke læse ekstra fil.', true); return; } } const payload = { actual, expected, screenshot_base64: screenshotDataUrl, metadata: getMetadata(), logs, extra_file_name: extraFileName, extra_file_base64: extraFileBase64, }; const prevText = submitBtn ? submitBtn.innerHTML : ''; if (submitBtn) { submitBtn.disabled = true; submitBtn.innerHTML = 'Sender...'; } try { const endpoints = ['/api/v1/bug-reports', '/bug-reports']; let res = null; let data = {}; for (const endpoint of endpoints) { res = await fetch(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify(payload), }); data = await res.json().catch(() => ({})); // Try fallback endpoint only when path is missing. if (res.status === 404) { continue; } break; } if (!res || !res.ok) { const detail = (data && (data.detail || data.message)) || 'Kunne ikke sende fejlrapport'; throw new Error(detail); } setStatus('Fejl rapporteret.'); const target = data.case_url || '/sag'; setTimeout(() => { window.location.href = target; }, 500); } catch (e) { setStatus(e.message || 'Kunne ikke sende fejlrapport', true); } finally { if (submitBtn) { submitBtn.disabled = false; submitBtn.innerHTML = prevText; } } } document.addEventListener('DOMContentLoaded', () => { const btn = document.getElementById('bugReportBtn'); const submitBtn = document.getElementById('bugReportSubmitBtn'); const displayMediaBtn = document.getElementById('bugCaptureDisplayMediaBtn'); const modalEl = document.getElementById('bugReportModal'); if (btn) { btn.addEventListener('click', (e) => { e.preventDefault(); if (!pendingScreenshotPromise) { prepareScreenshotFromTrigger(); } openBugReportModal(); }); } if (submitBtn) { submitBtn.addEventListener('click', () => { submitBugReport(); }); } if (displayMediaBtn) { displayMediaBtn.addEventListener('click', (e) => { e.preventDefault(); captureScreenshotViaDisplayMediaFromUserGesture(); }); } if (modalEl) { modalEl.addEventListener('hidden.bs.modal', () => { const actual = document.getElementById('bugActualInput'); const expected = document.getElementById('bugExpectedInput'); const fileInput = document.getElementById('bugExtraFileInput'); if (actual) actual.value = ''; if (expected) expected.value = ''; if (fileInput) fileInput.value = ''; screenshotDataUrl = null; setPreview(null); setStatus(''); }); } document.addEventListener('paste', (e) => { handlePasteScreenshot(e); }); document.addEventListener('keydown', (e) => { const isTyping = ['INPUT', 'TEXTAREA'].includes((e.target?.tagName || '').toUpperCase()); if (isTyping) return; if (e.ctrlKey && e.shiftKey && (e.key === 'B' || e.key === 'b')) { e.preventDefault(); if (!pendingScreenshotPromise) { prepareScreenshotFromTrigger(); } openBugReportModal(); } }); }); })();