| <!doctype html> |
| <html lang="de"> |
| <head> |
| <meta charset="UTF-8" /> |
| <link rel="icon" type="image/svg+xml" href="/vite.svg" /> |
| <meta name="generator" content="Hostinger AI Builder" /> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> |
| <title>SecureTesting — VDE-Prüfungen für Geräte & Elektroinstallationen</title> |
| <script type="module" crossorigin src="/assets/index-CDQZGetR.js"></script> |
| <link rel="stylesheet" crossorigin href="/assets/index-BzlHbBrm.css"> |
| <script type="module"> |
| window.onerror = (message, source, lineno, colno, errorObj) => { |
| const errorDetails = errorObj ? JSON.stringify({ |
| name: errorObj.name, |
| message: errorObj.message, |
| stack: errorObj.stack, |
| source, |
| lineno, |
| colno, |
| }) : null; |
|
|
| window.parent.postMessage({ |
| type: 'horizons-runtime-error', |
| message, |
| error: errorDetails |
| }, '*'); |
| }; |
| </script> |
| <script type="module"> |
| const observer = new MutationObserver((mutations) => { |
| for (const mutation of mutations) { |
| for (const addedNode of mutation.addedNodes) { |
| if ( |
| addedNode.nodeType === Node.ELEMENT_NODE && |
| ( |
| addedNode.tagName?.toLowerCase() === 'vite-error-overlay' || |
| addedNode.classList?.contains('backdrop') |
| ) |
| ) { |
| handleViteOverlay(addedNode); |
| } |
| } |
| } |
| }); |
|
|
| observer.observe(document.documentElement, { |
| childList: true, |
| subtree: true |
| }); |
|
|
| function handleViteOverlay(node) { |
| if (!node.shadowRoot) { |
| return; |
| } |
|
|
| const backdrop = node.shadowRoot.querySelector('.backdrop'); |
|
|
| if (backdrop) { |
| const overlayHtml = backdrop.outerHTML; |
| const parser = new DOMParser(); |
| const doc = parser.parseFromString(overlayHtml, 'text/html'); |
| const messageBodyElement = doc.querySelector('.message-body'); |
| const fileElement = doc.querySelector('.file'); |
| const messageText = messageBodyElement ? messageBodyElement.textContent.trim() : ''; |
| const fileText = fileElement ? fileElement.textContent.trim() : ''; |
| const error = messageText + (fileText ? ' File:' + fileText : ''); |
|
|
| window.parent.postMessage({ |
| type: 'horizons-vite-error', |
| error, |
| }, '*'); |
| } |
| } |
| </script> |
| <script type="module"> |
| const originalConsoleError = console.error; |
| const MATCH_LINE_COL_REGEX = /:(\d+):(\d+)\)?\s*$/; // regex to match the :lineNum:colNum |
| const MATCH_AT_REGEX = /^\s*at\s+(?:async\s+)?(?:.*?\s+)?\(?/; // regex to remove the 'at' keyword and any 'async' or function name |
| const MATCH_PATH_REGEX = /^\//; // regex to remove the leading slash |
|
|
| function parseStackFrameLine(line) { |
| const lineColMatch = line.match(MATCH_LINE_COL_REGEX); |
| if (!lineColMatch) return null; |
| const [, lineNum, colNum] = lineColMatch; |
| const suffix = `:${lineNum}:${colNum}`; |
| const idx = line.lastIndexOf(suffix); |
| if (idx === -1) return null; |
| const before = line.substring(0, idx); |
| const path = before.replace(MATCH_AT_REGEX, '').trim(); |
| |
| if (!path) return null; |
|
|
| try { |
| const pathname = new URL(path).pathname; |
| const filePath = pathname.replace(MATCH_PATH_REGEX, '') || pathname; |
| return `${filePath}:${lineNum}:${colNum}`; |
| } catch (e) { |
| const filePath = path.replace(MATCH_PATH_REGEX, '') || path; |
| return `${filePath}:${lineNum}:${colNum}`; |
| } |
| } |
|
|
| function getFilePathFromStack(stack, skipFrames = 0) { |
| if (!stack || typeof stack !== 'string') return null; |
| const lines = stack.split('\n').slice(1); |
|
|
| const frames = lines.map(line => parseStackFrameLine(line.replace(/\r$/, ''))).filter(Boolean); |
|
|
| return frames[skipFrames] ?? null; |
| } |
|
|
| function formatConsoleMessage(args, skipStackFrames = 1) { |
| let messageString = ''; |
| let filePath = null; |
|
|
| for (let i = 0; i < args.length; i++) { |
| const arg = args[i]; |
| if (arg instanceof Error) { |
| filePath = getFilePathFromStack(arg.stack, 0); |
| messageString = `${arg.name}: ${arg.message}`; |
| if (filePath) { |
| messageString = `${messageString} at ${filePath}`; |
| } |
| break; |
| } |
| } |
|
|
| if (!messageString) { |
| messageString = args.map(arg => typeof arg === 'object' ? JSON.stringify(arg) : String(arg)).join(' '); |
| const stack = new Error().stack; |
| filePath = getFilePathFromStack(stack, skipStackFrames); |
| if (filePath) { |
| messageString = `${messageString} at ${filePath}`; |
| } |
| } |
|
|
| return messageString; |
| } |
|
|
| console.error = function(...args) { |
| originalConsoleError.apply(console, args); |
|
|
| window.parent.postMessage({ |
| type: 'horizons-console-error', |
| error: formatConsoleMessage(args, 1) |
| }, '*'); |
| }; |
|
|
| const originalConsoleWarn = console.warn; |
|
|
| console.warn = function(...args) { |
| originalConsoleWarn.apply(console, args); |
|
|
| window.parent.postMessage({ |
| type: 'horizons-console-warn', |
| warning: formatConsoleMessage(args, 1) |
| }, '*'); |
| }; |
| </script> |
| <script type="module"> |
| // Prevents authentication failures (PB returns 400) from crashing the app — use console.info, not console.error/PM2. |
| // [urlPattern, bodyPattern] tuples; align with sandboxErrorUtils ERROR_BLACKLIST. |
| const BENIGN_FETCH_ERRORS = [ |
| [/hcgi\/platform\/api\/collections\/.*auth-with-password.*/i, /Failed to authenticate/i], |
| [/hcgi\/api\//i, /Insufficient credits/i], |
| // A user-supplied integration secret (Stripe/PayPal/Twilio/...) is not set yet — an expected setup state, not a bug to fix. |
| [/hcgi\/api\//i, /INTEGRATION_NOT_CONFIGURED/i], |
| ]; |
|
|
| function isBenignFetchError(url, body) { |
| return BENIGN_FETCH_ERRORS.some(([urlPattern, bodyPattern]) => |
| urlPattern.test(url) && (!bodyPattern || bodyPattern.test(body))); |
| } |
|
|
| const PLATFORM_URL_PATTERN = /hcgi\/platform\//i; |
| const VALIDATION_CODE_TEXT_PATTERN = /validation_/; |
|
|
| function hasValidationCode(value) { |
| if (value == null) { |
| return false; |
| } |
| if (typeof value === 'string') { |
| return value.startsWith('validation_'); |
| } |
| if (Array.isArray(value)) { |
| return value.some(hasValidationCode); |
| } |
| if (typeof value === 'object') { |
| return Object.values(value).some(hasValidationCode); |
| } |
| return false; |
| } |
|
|
| function isValidationFetchWarning(url, body) { |
| if (!PLATFORM_URL_PATTERN.test(url)) { |
| return false; |
| } |
| try { |
| return hasValidationCode(JSON.parse(body)); |
| } catch { |
| return VALIDATION_CODE_TEXT_PATTERN.test(body); |
| } |
| } |
|
|
| const originalFetch = window.fetch; |
|
|
| window.fetch = function(...args) { |
| const url = args[0] instanceof Request ? args[0].url : args[0]; |
|
|
| // Skip WebSocket URLs |
| if (url.startsWith('ws:') || url.startsWith('wss:')) { |
| return originalFetch.apply(this, args); |
| } |
|
|
| return originalFetch.apply(this, args) |
| .then(async response => { |
| const contentType = response.headers.get('Content-Type') || ''; |
|
|
| // Exclude HTML document responses |
| const isDocumentResponse = |
| contentType.includes('text/html') || |
| contentType.includes('application/xhtml+xml'); |
|
|
| // no-cors requests (e.g. Google Analytics/Ads beacons) always return an opaque |
| const isOpaqueResponse = response.type === 'opaque'; |
|
|
| if (!response.ok && !isDocumentResponse && !isOpaqueResponse) { |
| const responseClone = response.clone(); |
| const errorFromRes = await responseClone.text(); |
| const requestUrl = response.url; |
| const errorMessage = `Fetch error from ${requestUrl}: ${errorFromRes}`; |
|
|
| if (isBenignFetchError(requestUrl, errorFromRes)) { |
| console.info(errorMessage); |
| } else if (isValidationFetchWarning(requestUrl, errorFromRes)) { |
| console.warn(errorMessage); |
| } else { |
| console.error(errorMessage); |
| } |
| } |
|
|
| return response; |
| }) |
| .catch(error => { |
| if (!url.match(/.html?$/i)) { |
| // Cancelled in-flight fetch (e.g. preview reload) — not a server failure. |
| if (error?.name === 'AbortError') { |
| console.info(error); |
| } else { |
| console.error(error); |
| } |
| } |
|
|
| throw error; |
| }); |
| }; |
| </script> |
| <script type="module"> |
| if (window.navigation && window.self !== window.top) { |
| window.navigation.addEventListener('navigate', (event) => { |
| const url = event.destination.url; |
|
|
| try { |
| const destinationUrl = new URL(url); |
| const destinationOrigin = destinationUrl.origin; |
| const currentOrigin = window.location.origin; |
|
|
| if (destinationOrigin === currentOrigin) { |
| return; |
| } |
| } catch (error) { |
| return; |
| } |
|
|
| window.parent.postMessage({ |
| type: 'horizons-navigation-error', |
| url, |
| }, '*'); |
| }); |
| } |
| </script> |
| </head> |
| <body> |
| <div id="root"></div> |
| </body> |
| </html> |
| |