(() => { 'use strict'; // Stable bootstrap installed in GetCourse once. URL exclusions and the // Foxplan/Naverivo fallback stay here; product behavior stays in Pixel. const loader = document.currentScript; if (!(loader instanceof HTMLScriptElement)) { return; } const pixelKey = loader.dataset.pixel; if (!pixelKey || !/^[0-9a-f-]{36}$/i.test(pixelKey)) { return; } const normalizePath = value => value.replace(/\/+$/, '') || '/'; const currentPage = new URL(window.location.href); let excludedUrls = []; try { excludedUrls = JSON.parse(loader.dataset.exclude || '[]'); } catch { excludedUrls = []; } const excluded = excludedUrls.some(value => { try { const page = new URL(value, currentPage.origin); return page.hostname.toLowerCase() === currentPage.hostname.toLowerCase() && normalizePath(page.pathname) === normalizePath(currentPage.pathname) && [...page.searchParams].every(([name, expected]) => currentPage.searchParams.get(name) === expected ); } catch { return false; } }); if (excluded || window.__foxplanLoaderStarted) { return; } window.__foxplanLoaderStarted = true; const load = url => new Promise(resolve => { const started = performance.now(); const script = document.createElement('script'); let finished = false; const finish = error => { if (finished) { return; } finished = true; window.clearTimeout(timeout); script.onload = null; script.onerror = null; if (error) { script.remove(); } resolve({ error, duration_ms: Math.round(performance.now() - started), }); }; const timeout = window.setTimeout(() => finish('timeout'), 8000); script.src = url; script.async = true; script.onload = () => finish(null); script.onerror = () => finish('network_or_block'); document.head.appendChild(script); }); const showReportButton = tests => { if (currentPage.searchParams.get('foxplan_debug') !== '1') { return; } const button = document.createElement('button'); let publicIp = null; let ipLookup = 'pending'; const report = () => JSON.stringify({ generated_at: new Date().toISOString(), page: currentPage.origin + currentPage.pathname, public_ip: publicIp, ip_lookup: ipLookup, online: navigator.onLine, user_agent: navigator.userAgent, language: navigator.language || null, timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || null, tests, }, null, 2); button.type = 'button'; button.disabled = true; button.textContent = 'Подготовка отчёта Foxplan…'; button.style.cssText = 'position:fixed;right:16px;bottom:16px;z-index:2147483647;padding:10px 14px;border:0;border-radius:8px;background:#f59e0b;color:#111827;font:bold 14px Arial;cursor:pointer'; button.onclick = () => { const contents = report(); const copy = navigator.clipboard?.writeText(contents); if (!copy) { window.prompt('Скопируйте отчёт', contents); return; } copy .then(() => { button.textContent = 'Отчёт скопирован'; }) .catch(() => window.prompt('Скопируйте отчёт', contents)); }; const mount = () => document.body.appendChild(button); if (document.body) { mount(); } else { document.addEventListener('DOMContentLoaded', mount, {once: true}); } const controller = new AbortController(); const timeout = window.setTimeout(() => controller.abort(), 3000); fetch('https://api64.ipify.org?format=json', { cache: 'no-store', credentials: 'omit', referrerPolicy: 'no-referrer', signal: controller.signal, }) .then(response => response.ok ? response.json() : Promise.reject(new Error('http'))) .then(result => { const value = String(result.ip || '').trim(); if (!/^[0-9a-f:.]{3,45}$/i.test(value)) { throw new Error('invalid_ip'); } publicIp = value; ipLookup = 'ok'; }) .catch(error => { ipLookup = error.name === 'AbortError' ? 'timeout' : 'unavailable'; }) .finally(() => { window.clearTimeout(timeout); button.disabled = false; button.textContent = 'Скопировать отчёт Foxplan'; }); }; const run = async () => { const direct = await load(`https://foxplan.online/assets/runtime/${pixelKey}.js`); if (!direct.error) { return; } const fallbackUrl = new URL(`https://naverivo.com/assets/runtime/${pixelKey}.js`); fallbackUrl.searchParams.set('direct_error', direct.error); fallbackUrl.searchParams.set('direct_ms', String(direct.duration_ms)); const fallback = await load(fallbackUrl.toString()); if (fallback.error) { showReportButton({direct, fallback}); } }; void run(); })();