Instrukcja użyciaInstrukcja instalacji→ Zainstaluj rozszerzenie Tampermonkey w Chrome
→ Otwórz dashboard Tampermonkey → Utwórz nowy skrypt
→ Wklej poniższy kod i zapisz (Ctrl+S)
→ Odwiedź dowolną stronę → widget GTM Checker pojawi się w prawym dolnym rogu
→ Alt+Shift+A — ukrywa/przywraca widget na dowolnej domenie
Skrypt GTM Checker
// ==UserScript==
// @name GTM Checker
// @namespace https://pawelpiekarski.pl/
// @version 2.5
// @description GTM injection, symulator pushy ecommerce, detektor pixeli z consent mode i biblioteka komend — narzędzia web analityka
// @author Paweł Piekarski
// @match *://*/*
// @icon https://www.gstatic.com/analytics-suite/header/suite/v2/ic_tag_manager.svg
// @run-at document-start
// @grant GM_registerMenuCommand
// @grant GM_getValue
// @grant GM_setValue
// ==/UserScript==
(function () {
'use strict';
const WID = 'pk-gtmc-host';
const LS_COL = 'pkGtmcCol';
const LS_HIDDEN = 'pkGtmcHidden'; // per domena
const LS_POS = 'pkGtmcPos';
const KEY_RULES = 'pk_at_gtm_rules';
let collapsed = false;
try { collapsed = localStorage.getItem(LS_COL) === '1'; } catch (e) {}
let host = null, root = null;
let activeTab = 'pixels';
let bridgeReady = false;
let lastSnapshot = null;
let lastConsent = null;
const C = {
bg: '#111827', hover: '#1e293b',
accent: '#3b82f6', accentDk: '#1e3a5f',
green: '#22c55e', red: '#ef4444', yellow: '#eab308',
text: '#e5e7eb', dim: '#6b7280', border: '#374151',
};
// Hosty, na których widget NIE pokazuje się automatycznie (panele narzędzi,
// nie "normalne" strony). Alt+Shift+A nadal działa, gdyby był potrzebny.
const BLOCKED_HOSTS = [
/(^|\.)tagmanager\.google\.com$/,
/(^|\.)analytics\.google\.com$/,
/(^|\.)tagassistant\.google\.com$/,
/(^|\.)ads\.google\.com$/,
/(^|\.)adsense\.google\.com$/,
/(^|\.)merchants\.google\.com$/,
/(^|\.)marketingplatform\.google\.com$/,
/(^|\.)search\.google\.com$/, // Search Console
/(^|\.)lookerstudio\.google\.com$/,
/(^|\.)datastudio\.google\.com$/,
/(^|\.)optimize\.google\.com$/,
/(^|\.)accounts\.google\.com$/,
/(^|\.)mail\.google\.com$/,
/(^|\.)drive\.google\.com$/,
/(^|\.)docs\.google\.com$/,
/(^|\.)calendar\.google\.com$/,
/(^|\.)meet\.google\.com$/,
/(^|\.)console\.cloud\.google\.com$/,
/(^|\.)business\.facebook\.com$/, // Meta Business Suite
/(^|\.)adsmanager\.facebook\.com$/,
/(^|\.)events\.facebook\.com$/, // Events Manager
/(^|\.)clarity\.microsoft\.com$/,
/(^|\.)ads\.tiktok\.com$/,
/(^|\.)ads\.microsoft\.com$/,
/(^|\.)campaignmanager\.linkedin\.com$/,
/(^|\.)analytics\.tiktok\.com$/,
/(^|\.)app\.salesmanago\.com$/,
];
function isBlockedHost() {
const h = location.hostname;
return BLOCKED_HOSTS.some(re => re.test(h));
}
function escHtml(s) { return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'); }
function escAttr(s) { return escHtml(s).replace(/"/g, '"'); }
function fmtTime(ts) { const d = new Date(ts); return [d.getHours(), d.getMinutes(), d.getSeconds()].map(n => String(n).padStart(2, '0')).join(':'); }
/* ═══════════════════ STORAGE (reguły GTM) ═══════════════════ */
function getRules() {
try {
const raw = GM_getValue(KEY_RULES, '[]');
return typeof raw === 'string' ? JSON.parse(raw) : raw;
} catch (e) { return []; }
}
function saveRules(rules) { GM_setValue(KEY_RULES, JSON.stringify(rules)); }
/* ═══════════════════ BRIDGE (kod działający w kontekście strony) ═══════════════════ */
// Wstrzykiwany jako <script>, komunikacja przez CustomEvent — działa niezależnie
// od trybu sandboxa Tampermonkey. Owija dataLayer.push, wykonuje pushe
// symulatora i zwraca snapshot globali/consentu.
function pageBridge() {
if (window.__pkGtmcBridge) return;
window.__pkGtmcBridge = true;
function safeStringify(obj) {
var seen = typeof WeakSet !== 'undefined' ? new WeakSet() : null;
return JSON.stringify(obj, function (k, v) {
if (typeof v === 'function') return '[function]';
try {
if (typeof Node !== 'undefined' && v instanceof Node) return '[DOM <' + (v.nodeName || 'node').toLowerCase() + '>]';
if (typeof Window !== 'undefined' && v instanceof Window) return '[window]';
} catch (e) {}
if (typeof v === 'object' && v !== null && seen) {
if (seen.has(v)) return '[circular]';
seen.add(v);
}
return v;
});
}
function send(type, data) {
try {
document.dispatchEvent(new CustomEvent('pk-gtmc', { detail: safeStringify({ type: type, data: data, ts: Date.now() }) }));
} catch (e) {}
}
// ── snapshot globali ──
function snapshot() {
var names = ['google_tag_manager', 'gtag', 'fbq', 'clarity', 'ttq', 'hj', '_linkedin_partner_id', 'pintrk', 'uetq', 'twq', 'snaptr', 'criteo_q', '_hsq', '_paq', 'UC_UI', 'Cookiebot', 'OneTrust', '__cmp'];
var g = {};
names.forEach(function (k) { try { g[k] = !!window[k]; } catch (e) { g[k] = false; } });
var gtmKeys = []; try { gtmKeys = Object.keys(window.google_tag_manager || {}); } catch (e) {}
var fbqIds = []; try { if (window.fbq && window.fbq.getState) (window.fbq.getState().pixels || []).forEach(function (p) { if (p.id) fbqIds.push(String(p.id)); }); } catch (e) {}
var hjid = null; try { hjid = window._hjSettings && window._hjSettings.hjid ? String(window._hjSettings.hjid) : null; } catch (e) {}
var liId = null; try { liId = window._linkedin_partner_id ? String(window._linkedin_partner_id) : null; } catch (e) {}
send('snapshot', { g: g, gtmKeys: gtmKeys, fbqIds: fbqIds, hjid: hjid, liId: liId });
}
// ── consent mode ──
function consent() {
var out = null;
try {
var e = window.google_tag_data && window.google_tag_data.ics && window.google_tag_data.ics.entries;
if (e) {
out = {};
Object.keys(e).forEach(function (k) {
out[k] = { def: e[k]['default'], upd: e[k].update };
});
}
} catch (err) {}
send('consent', out);
}
// ── komendy z userscriptu ──
document.addEventListener('pk-gtmc-cmd', function (ev) {
var d; try { d = JSON.parse(ev.detail); } catch (e) { return; }
if (d.a === 'snapshot') snapshot();
else if (d.a === 'consent') consent();
else if (d.a === 'push') {
try {
window.dataLayer = window.dataLayer || [];
if (d.clear) window.dataLayer.push({ ecommerce: null });
window.dataLayer.push(d.data);
} catch (e) {}
}
});
send('ready', 1);
}
function injectBridge() {
try {
const s = document.createElement('script');
s.textContent = '(' + pageBridge.toString() + ')();';
(document.documentElement || document.head || document.body).appendChild(s);
s.remove();
} catch (e) {}
}
function sendCmd(a, extra) {
try {
document.dispatchEvent(new CustomEvent('pk-gtmc-cmd', { detail: JSON.stringify(Object.assign({ a: a }, extra || {})) }));
} catch (e) {}
}
/* ═══════════════════ ODBIÓR Z BRIDGE'A ═══════════════════ */
document.addEventListener('pk-gtmc', ev => {
let msg; try { msg = JSON.parse(ev.detail); } catch (e) { return; }
if (msg.type === 'ready') { bridgeReady = true; return; }
if (msg.type === 'snapshot') { lastSnapshot = msg.data; return; }
if (msg.type === 'consent') { lastConsent = msg.data; }
});
/* ═══════════════════ GTM INJECTION ═══════════════════ */
const injectedIds = new Set();
function injectGTM(containerId, dlName) {
dlName = dlName || 'dataLayer';
if (injectedIds.has(containerId) ||
document.querySelector('script[src*="googletagmanager.com/gtm.js?id=' + containerId + '"]')) {
console.log('%c[GTM Checker] ' + containerId + ' już istnieje — pomijam', 'color:#eab308;');
return false;
}
injectedIds.add(containerId);
const s = document.createElement('script');
s.textContent = `
(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','${dlName.replace(/[^A-Za-z0-9_$]/g, '')}','${containerId}');
`;
document.documentElement.appendChild(s);
const insertNoscript = () => {
const ns = document.createElement('noscript');
const iframe = document.createElement('iframe');
iframe.src = 'https://www.googletagmanager.com/ns.html?id=' + containerId;
iframe.height = '0'; iframe.width = '0';
iframe.style.display = 'none'; iframe.style.visibility = 'hidden';
ns.appendChild(iframe);
document.body.insertBefore(ns, document.body.firstChild);
};
if (document.body) insertNoscript();
else document.addEventListener('DOMContentLoaded', insertNoscript);
console.log('%c[GTM Checker] ✓ Wstrzyknięto: ' + containerId, 'color:#22c55e;font-weight:bold;');
return true;
}
let autoInjectedId = null;
if (!isBlockedHost()) injectBridge();
(function autoInject() {
if (isBlockedHost()) return;
const rules = getRules();
const url = location.href;
for (const rule of rules) {
if (!rule.enabled) continue;
try {
if (new RegExp(rule.pattern, 'i').test(url)) {
injectGTM(rule.gtmId);
autoInjectedId = rule.gtmId;
break;
}
} catch (e) { console.warn('[GTM Checker] Błędny pattern:', rule.pattern, e); }
}
})();
/* ═══════════════════ BIBLIOTEKA KOMEND ═══════════════════ */
const COMMANDS = [
{ label: 'dataLayer', code: 'dataLayer', desc: 'Klasyk — jakie eventy i parametry są pushowane (GTM)' },
{ label: 'dataLayer — tylko eventy', code: 'dataLayer.filter(e => e.event)', desc: 'Odfiltrowane pushe bez eventu' },
{ label: 'dataLayer — tabela eventów', code: "console.table(dataLayer.filter(e => e.event).map((e,i) => ({'#': i, event: e.event})))", desc: 'Czytelna lista kolejności eventów' },
{ label: 'dataLayer — ostatni ecommerce', code: 'dataLayer.filter(e => e.ecommerce).pop()', desc: 'Ostatni push e-commerce (items, value, currency)' },
{ label: 'dataLayer — kopiuj jako JSON', code: 'copy(JSON.stringify(dataLayer, null, 2))', desc: 'Cały dataLayer do schowka — idealne do wklejenia AI' },
{ label: 'Live monitor dataLayer', code: "(function(){var p=dataLayer.push;dataLayer.push=function(){console.log('%c[DL push]','color:#22c55e;font-weight:bold',...arguments);return p.apply(this,arguments)}})()", desc: 'Loguje na żywo każdy kolejny push do dataLayer' },
{ label: 'window.google_tag_manager', code: 'window.google_tag_manager', desc: 'Czy GTM jest obecny i które kontenery działają' },
{ label: 'Google Consent Mode', code: "window.google_tag_data && google_tag_data.ics ? google_tag_data.ics.entries : 'brak google_tag_data.ics'", desc: 'Stan consent mode per storage type' },
{ label: 'document.referrer', code: 'document.referrer', desc: 'Skąd przyszedł użytkownik' },
{ label: 'Parametry UTM z URL', code: "Object.fromEntries([...new URLSearchParams(location.search)].filter(([k]) => /^(utm_|gclid|fbclid|msclkid)/.test(k)))", desc: 'utm_*, gclid, fbclid, msclkid z bieżącego URL' },
{ label: 'document.cookie', code: 'document.cookie', desc: 'Wszystkie cookies dostępne z JS' },
{ label: 'Cookies GA (_ga*)', code: "document.cookie.split('; ').filter(c => /^_ga/.test(c))", desc: 'Client ID i cookies sesji GA4' },
{ label: 'Cookies Meta (_fbp / _fbc)', code: "document.cookie.split('; ').filter(c => /^_fb[pc]/.test(c))", desc: 'Browser ID i click ID Meta — kluczowe dla CAPI' },
{ label: 'Zasoby / requesty', code: 'performance.getEntriesByType("resource")', desc: 'Co faktycznie się załadowało (w tym trackery)' },
{ label: 'Requesty trackerów', code: 'performance.getEntriesByType("resource").map(r => r.name).filter(n => /collect|gtm\\.js|gtag|facebook|clarity|hotjar|tiktok|doubleclick/i.test(n))', desc: 'Tylko hity trackingowe — walidacja czy coś odpaliło' },
{ label: 'Consent (Usercentrics)', code: "localStorage.getItem('uc_settings')", desc: 'Stan zgody, jeśli stoi Usercentrics' },
{ label: 'Iframes', code: "document.querySelectorAll('iframe')", desc: 'Osadzone narzędzia i skrypty 3rd party' },
{ label: 'Ukryte pola formularzy', code: "document.querySelectorAll('input[type=\"hidden\"]')", desc: 'Czy formularz zbiera dane trackingowe' },
{ label: 'Skrypty zewnętrzne', code: '[...document.scripts].map(s => s.src).filter(Boolean)', desc: 'Lista wszystkich zewnętrznych <script>' },
{ label: 'Timing ładowania strony', code: "(({domContentLoadedEventEnd:d,loadEventEnd:l,responseStart:r}) => ({TTFB_ms: Math.round(r), DOMContentLoaded_ms: Math.round(d), Load_ms: Math.round(l)}))(performance.getEntriesByType('navigation')[0])", desc: 'TTFB, DOMContentLoaded, Load — szybki health check' },
{ label: 'Niestandardowe dataLayer', code: "Object.keys(window).filter(k => /datalayer/i.test(k))", desc: 'Znajdź niestandardowe nazwy dataLayer w window' },
];
/* ═══════════════════ SYMULATOR PUSHY E-COMMERCE ═══════════════════ */
function sampleItems() {
return [{
item_id: 'SKU-750W',
item_name: 'Wiertarka udarowa 750W',
item_brand: 'Domitech',
item_category: 'Elektronarzędzia',
item_category2: 'Wiertarki',
item_list_name: 'Bestsellery',
index: 0,
price: 299.99,
quantity: 1,
}];
}
function txId() { return 'TEST-' + Date.now(); }
const ECOM_PRESETS = [
{ label: 'view_item_list', desc: 'Wyświetlenie listy produktów', build: () => ({ event: 'view_item_list', ecommerce: { item_list_id: 'bestsellery', item_list_name: 'Bestsellery', items: sampleItems() } }) },
{ label: 'select_item', desc: 'Kliknięcie produktu na liście', build: () => ({ event: 'select_item', ecommerce: { item_list_id: 'bestsellery', item_list_name: 'Bestsellery', items: sampleItems() } }) },
{ label: 'view_item', desc: 'Wyświetlenie karty produktu', build: () => ({ event: 'view_item', ecommerce: { currency: 'PLN', value: 299.99, items: sampleItems() } }) },
{ label: 'add_to_wishlist', desc: 'Dodanie do listy życzeń', build: () => ({ event: 'add_to_wishlist', ecommerce: { currency: 'PLN', value: 299.99, items: sampleItems() } }) },
{ label: 'add_to_cart', desc: 'Dodanie do koszyka', build: () => ({ event: 'add_to_cart', ecommerce: { currency: 'PLN', value: 299.99, items: sampleItems() } }) },
{ label: 'remove_from_cart', desc: 'Usunięcie z koszyka', build: () => ({ event: 'remove_from_cart', ecommerce: { currency: 'PLN', value: 299.99, items: sampleItems() } }) },
{ label: 'view_cart', desc: 'Wyświetlenie koszyka', build: () => ({ event: 'view_cart', ecommerce: { currency: 'PLN', value: 299.99, items: sampleItems() } }) },
{ label: 'begin_checkout', desc: 'Rozpoczęcie checkoutu', build: () => ({ event: 'begin_checkout', ecommerce: { currency: 'PLN', value: 299.99, coupon: 'LATO10', items: sampleItems() } }) },
{ label: 'add_shipping_info', desc: 'Wybór dostawy', build: () => ({ event: 'add_shipping_info', ecommerce: { currency: 'PLN', value: 299.99, shipping_tier: 'Kurier DPD', items: sampleItems() } }) },
{ label: 'add_payment_info', desc: 'Wybór płatności', build: () => ({ event: 'add_payment_info', ecommerce: { currency: 'PLN', value: 299.99, payment_type: 'BLIK', items: sampleItems() } }) },
{ label: 'purchase', desc: 'Zakup (unikalny transaction_id)', build: () => ({ event: 'purchase', ecommerce: { transaction_id: txId(), value: 315.98, tax: 59.09, shipping: 15.99, currency: 'PLN', coupon: 'LATO10', items: sampleItems() } }) },
{ label: 'refund', desc: 'Zwrot (pełny)', build: () => ({ event: 'refund', ecommerce: { transaction_id: txId(), value: 315.98, currency: 'PLN', items: sampleItems() } }) },
{ label: 'generate_lead', desc: 'Wysłanie formularza / lead', build: () => ({ event: 'generate_lead', currency: 'PLN', value: 150, lead_source: 'formularz kontaktowy' }) },
{ label: 'qualified_lead', desc: 'Lead zakwalifikowany', build: () => ({ event: 'qualified_lead', currency: 'PLN', value: 150 }) },
{ label: 'sign_up', desc: 'Rejestracja konta', build: () => ({ event: 'sign_up', method: 'email' }) },
{ label: 'login', desc: 'Logowanie użytkownika', build: () => ({ event: 'login', method: 'email' }) },
];
/* ═══════════════════ DETEKTOR PIXELI ═══════════════════ */
const TRACKERS = [
{ name: 'Google Tag Manager', re: /googletagmanager\.com\/gtm\.js/i, global: 'google_tag_manager',
ids: (u, s) => (s ? s.gtmKeys.filter(k => /^GTM-/i.test(k)) : []) },
{ name: 'GA4 / gtag', re: /googletagmanager\.com\/gtag\/js|google-analytics\.com|analytics\.google\.com\/g\/collect/i, global: 'gtag',
ids: (urls, s) => {
const out = new Set(s ? s.gtmKeys.filter(k => /^G-/i.test(k)) : []);
urls.forEach(u => {
let m = u.match(/gtag\/js\?id=(G-[A-Z0-9]+)/i); if (m) out.add(m[1].toUpperCase());
m = u.match(/\/g\/collect\?[^ ]*tid=(G-[A-Z0-9]+)/i); if (m) out.add(m[1].toUpperCase());
});
return [...out];
} },
{ name: 'Google Ads / DoubleClick', re: /googleadservices\.com|doubleclick\.net|googlesyndication/i, global: null,
ids: (u, s) => (s ? s.gtmKeys.filter(k => /^(AW|DC)-/i.test(k)) : []) },
{ name: 'Meta Pixel', re: /connect\.facebook\.net|facebook\.com\/tr/i, global: 'fbq',
ids: (urls, s) => {
const out = new Set(s ? s.fbqIds : []);
urls.forEach(u => { const m = u.match(/facebook\.com\/tr[/?].*?id=(\d+)/i); if (m) out.add(m[1]); });
return [...out];
} },
{ name: 'Microsoft Clarity', re: /clarity\.ms/i, global: 'clarity',
ids: (urls) => { const out = new Set(); urls.forEach(u => { const m = u.match(/clarity\.ms\/tag\/([a-z0-9]+)/i); if (m) out.add(m[1]); }); return [...out]; } },
{ name: 'TikTok Pixel', re: /analytics\.tiktok\.com/i, global: 'ttq',
ids: (urls) => { const out = new Set(); urls.forEach(u => { const m = u.match(/sdkid=([A-Z0-9]+)/i); if (m) out.add(m[1]); }); return [...out]; } },
{ name: 'Hotjar', re: /static\.hotjar\.com|script\.hotjar\.com/i, global: 'hj',
ids: (u, s) => (s && s.hjid ? [s.hjid] : []) },
{ name: 'LinkedIn Insight', re: /snap\.licdn\.com|px\.ads\.linkedin\.com/i, global: '_linkedin_partner_id',
ids: (u, s) => (s && s.liId ? [s.liId] : []) },
{ name: 'Pinterest Tag', re: /ct\.pinterest\.com|s\.pinimg\.com\/ct/i, global: 'pintrk', ids: () => [] },
{ name: 'Microsoft Ads (UET)', re: /bat\.bing\.com/i, global: 'uetq', ids: () => [] },
{ name: 'X / Twitter Pixel', re: /static\.ads-twitter\.com|t\.co\/i\/adsct/i, global: 'twq', ids: () => [] },
{ name: 'Snap Pixel', re: /sc-static\.net|tr\.snapchat\.com/i, global: 'snaptr', ids: () => [] },
{ name: 'Criteo', re: /static\.criteo\.net|criteo\.com/i, global: 'criteo_q', ids: () => [] },
{ name: 'HubSpot', re: /js\.hs-scripts\.com|hs-analytics\.net|hsforms/i, global: '_hsq', ids: () => [] },
{ name: 'Matomo / Piwik', re: /matomo\.js|piwik\.js|matomo\.php/i, global: '_paq', ids: () => [] },
{ name: 'Usercentrics (CMP)', re: /usercentrics\.eu|usercentrics\.com/i, global: 'UC_UI', ids: () => [] },
{ name: 'Cookiebot (CMP)', re: /consent\.cookiebot\.com/i, global: 'Cookiebot', ids: () => [] },
{ name: 'OneTrust (CMP)', re: /cdn\.cookielaw\.org|onetrust/i, global: 'OneTrust', ids: () => [] },
{ name: 'CookieYes (CMP)', re: /cdn-cookieyes\.com/i, global: null, ids: () => [] },
{ name: 'Consentmanager (CMP)', re: /consentmanager\.net|delivery\.consentmanager/i, global: '__cmp', ids: () => [] },
];
function detectTrackers() {
sendCmd('snapshot'); // dispatch jest synchroniczny — lastSnapshot aktualny
const s = lastSnapshot;
let urls = [];
try { urls = performance.getEntriesByType('resource').map(r => r.name); } catch (e) {}
[...document.scripts].forEach(sc => { if (sc.src) urls.push(sc.src); });
document.querySelectorAll('iframe[src]').forEach(f => urls.push(f.src));
urls = [...new Set(urls)];
return TRACKERS.map(t => {
const urlHit = urls.some(u => t.re.test(u));
const globalHit = !!(s && t.global && s.g[t.global]);
let ids = [];
try { ids = t.ids(urls, s) || []; } catch (e) {}
return { name: t.name, detected: urlHit || globalHit || ids.length > 0, ids };
});
}
/* ═══════════════════ CLIPBOARD ═══════════════════ */
function copyText(txt) {
if (navigator.clipboard && navigator.clipboard.writeText) {
return navigator.clipboard.writeText(txt).catch(() => fallbackCopy(txt));
}
return Promise.resolve(fallbackCopy(txt));
}
function fallbackCopy(txt) {
const ta = document.createElement('textarea');
ta.value = txt;
ta.style.cssText = 'position:fixed;opacity:0';
document.body.appendChild(ta);
ta.select();
try { document.execCommand('copy'); } catch (e) {}
ta.remove();
}
/* ═══════════════════ WIDGET (Shadow DOM) ═══════════════════ */
const CSS = `
:host{all:initial}
*{box-sizing:border-box}
.wg{position:relative;width:360px;border-radius:14px;overflow:hidden;box-shadow:0 10px 36px rgba(0,0,0,.55);border:1px solid ${C.border};background:${C.bg};font-family:'Google Sans',system-ui,sans-serif;user-select:none;text-align:left;line-height:1.4;color:${C.text}}
.at-hdr{background:linear-gradient(135deg,${C.accentDk},${C.bg});padding:10px 14px;display:flex;align-items:center;gap:8px;cursor:move;border-bottom:1px solid ${C.border}}
.at-logo{width:22px;height:22px;flex-shrink:0}
.at-logo svg{display:block}
.at-title{flex:1;font-size:13px;font-weight:600;color:#fff;letter-spacing:.3px}
.at-badge{font-size:10px;background:${C.border};color:${C.dim};padding:2px 8px;border-radius:10px;max-width:120px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.at-hdr-btn{background:none;border:none;color:#fff;font-size:18px;cursor:pointer;padding:0 4px;line-height:1;opacity:.6;transition:opacity .15s;font-family:inherit}
.at-hdr-btn:hover{opacity:1}
.at-wrap{overflow:hidden;transition:max-height .25s ease}
.at-wrap.open{max-height:80vh}.at-wrap.closed{max-height:0!important}
.at-tabs{display:flex;border-bottom:1px solid ${C.border}}
.at-tab{flex:1;background:none;border:none;color:${C.dim};font-size:10px;font-weight:600;padding:8px 2px;cursor:pointer;font-family:inherit;border-bottom:2px solid transparent;transition:all .15s;white-space:nowrap}
.at-tab:hover{color:${C.text};background:${C.hover}}
.at-tab.on{color:${C.accent};border-bottom-color:${C.accent}}
.at-panel{padding:10px 14px;max-height:min(360px,55vh);overflow-y:auto}
.at-panel::-webkit-scrollbar{width:5px}.at-panel::-webkit-scrollbar-thumb{background:${C.border};border-radius:3px}
.at-label{font-size:10px;color:${C.dim};font-weight:700;text-transform:uppercase;letter-spacing:.5px;margin:8px 0 4px}
.at-label:first-child{margin-top:0}
.at-input{width:100%;background:${C.border};border:1px solid #4b5563;color:${C.text};padding:6px 9px;border-radius:7px;font-size:12px;outline:none;font-family:'Consolas','Monaco',monospace}
.at-input:focus{border-color:${C.accent}}
.at-input::placeholder{color:#6b7280;font-family:'Google Sans',system-ui,sans-serif}
.at-btn{border:none;border-radius:7px;padding:6px 12px;cursor:pointer;font-size:11px;font-weight:600;font-family:inherit;transition:.15s}
.at-btn-primary{background:${C.accent};color:#fff}
.at-btn-primary:hover{background:#2563eb}
.at-btn-sm{padding:3px 9px;font-size:10px;border-radius:6px}
.at-btn-ghost{background:transparent;border:1px dashed ${C.border};color:${C.dim};width:100%}
.at-btn-ghost:hover{border-color:${C.accent};color:${C.accent}}
.at-btn-sec{background:${C.border};color:${C.text}}
.at-btn-sec:hover{background:#4b5563}
.at-cmd{padding:7px 8px;border-radius:7px;cursor:pointer;transition:background .1s;margin-bottom:2px}
.at-cmd:hover{background:${C.hover}}
.at-cmd-label{font-size:11px;font-weight:600;color:${C.text};display:flex;align-items:center;gap:6px}
.at-copied{color:${C.green};font-size:10px;font-weight:700}
.at-cmd-code{font-family:'Consolas','Monaco',monospace;font-size:10px;color:#93c5fd;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin:2px 0}
.at-cmd-desc{font-size:10px;color:${C.dim}}
.at-px{display:flex;align-items:center;gap:9px;padding:5px 4px;border-radius:6px}
.at-px:hover{background:${C.hover}}
.at-px-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}
.at-px-name{flex:1;font-size:12px;color:${C.text};white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.at-px-ids{font-size:9px;font-family:'Consolas',monospace;color:${C.green};background:rgba(34,197,94,.1);padding:1px 6px;border-radius:6px;max-width:140px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;cursor:pointer}
.at-px-ids:hover{background:rgba(34,197,94,.25)}
.at-empty{font-size:12px;color:${C.dim};text-align:center;padding:14px 8px;line-height:1.5}
.at-warnbox{font-size:11px;color:${C.yellow};background:rgba(234,179,8,.08);border:1px solid rgba(234,179,8,.25);border-radius:8px;padding:6px 9px;margin-bottom:8px;line-height:1.45}
.at-consent{display:grid;grid-template-columns:1fr auto auto;gap:2px 8px;font-size:10px;margin-bottom:4px}
.at-consent .h{color:${C.dim};font-weight:700;text-transform:uppercase;letter-spacing:.4px;font-size:9px}
.at-consent .k{color:${C.text};font-family:'Consolas',monospace}
.at-consent .g{color:${C.green};font-weight:700}
.at-consent .d{color:${C.red};font-weight:700}
.at-consent .u{color:${C.dim}}
.at-push{display:flex;align-items:center;gap:8px;padding:6px 7px;border-radius:7px;margin-bottom:2px}
.at-push:hover{background:${C.hover}}
.at-push-name{flex:1;min-width:0}
.at-push-ev{font-size:11px;font-weight:600;color:${C.text};font-family:'Consolas',monospace}
.at-push-desc{font-size:10px;color:${C.dim};white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.at-rule{display:flex;gap:5px;align-items:center;margin-bottom:5px}
.at-rule .at-input{margin:0}
.at-toggle{width:24px;height:24px;border-radius:6px;border:none;font-size:12px;cursor:pointer;flex-shrink:0;transition:.15s;font-family:inherit}
.at-toggle.on{background:rgba(34,197,94,.2);color:${C.green}}
.at-toggle.off{background:rgba(229,231,235,.06);color:${C.dim}}
.at-del{background:transparent;border:none;color:${C.red};font-size:13px;cursor:pointer;padding:2px 5px;border-radius:5px;flex-shrink:0;font-family:inherit}
.at-del:hover{background:rgba(239,68,68,.15)}
.at-note{font-size:10px;color:${C.dim};margin:4px 0 0 2px;line-height:1.4}
.at-note.warn{color:${C.yellow}}
.at-note.ok{color:${C.green}}
.at-foot{border-top:1px solid ${C.border};padding:7px 10px 7px 14px;display:flex;justify-content:space-between;align-items:center;gap:6px}
.at-foot-txt{font-size:10px;color:${C.dim};white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:1}
.at-foot-btn{font-size:10px;color:${C.accent};background:none;border:none;cursor:pointer;font-weight:600;padding:2px 5px;border-radius:4px;flex-shrink:0;font-family:inherit}
.at-foot-btn:hover{background:rgba(59,130,246,.12)}
`;
function buildWidget() {
if (host) host.remove();
host = document.createElement('div');
host.id = WID;
host.style.cssText = 'position:fixed;bottom:16px;right:16px;z-index:2147483647;';
root = host.attachShadow({ mode: 'open' });
root.innerHTML = `
<style>${CSS}</style>
<div class="wg">
<div class="at-hdr" id="at-hdr">
<div class="at-logo"><svg width="22" height="22" viewBox="0 0 192 192" xmlns="http://www.w3.org/2000/svg"><rect width="192" height="192" rx="24" fill="#3b82f6"/><path d="M96 36L140 96l-44 60-44-60L96 36z" fill="#fff" opacity=".9"/><circle cx="96" cy="96" r="20" fill="#fff"/></svg></div>
<span class="at-title">GTM Checker</span>
<span class="at-badge" id="at-badge" title=""></span>
<button class="at-hdr-btn" id="at-hide" title="Ukryj na tej domenie (Alt+Shift+A przywraca)">✕</button>
<button class="at-hdr-btn" id="at-col">▾</button>
</div>
<div class="at-wrap ${collapsed ? 'closed' : 'open'}" id="at-wrap">
<div class="at-tabs">
<button class="at-tab" data-tab="pixels">🔍 Pixele</button>
<button class="at-tab" data-tab="push">🛒 Push</button>
<button class="at-tab" data-tab="cmds">📋 Cmd</button>
<button class="at-tab" data-tab="inject">💉 GTM</button>
</div>
<div class="at-panel" id="at-panel"></div>
<div class="at-foot">
<span class="at-foot-txt" id="at-foot-txt"></span>
<button class="at-foot-btn" id="at-audit" title="Skopiuj audyt trackingu jako markdown">⤓ Audyt</button>
<button class="at-foot-btn" id="at-refresh">↻</button>
</div>
</div>
</div>
`;
document.body.appendChild(host);
restorePosition();
const $ = sel => root.querySelector(sel);
const colBtn = $('#at-col');
colBtn.textContent = collapsed ? '▸' : '▾';
colBtn.addEventListener('click', () => {
collapsed = !collapsed;
try { localStorage.setItem(LS_COL, collapsed ? '1' : '0'); } catch (e) {}
updateExpandDirection();
$('#at-wrap').classList.toggle('open', !collapsed);
$('#at-wrap').classList.toggle('closed', collapsed);
colBtn.textContent = collapsed ? '▸' : '▾';
});
$('#at-hide').addEventListener('click', () => {
try { localStorage.setItem(LS_HIDDEN, '1'); } catch (e) {}
host.remove(); host = null; root = null;
});
root.querySelectorAll('.at-tab').forEach(t => t.addEventListener('click', () => {
activeTab = t.dataset.tab;
renderTabs();
}));
$('#at-refresh').addEventListener('click', renderTabs);
$('#at-audit').addEventListener('click', exportAudit);
makeDraggable(host, $('#at-hdr'));
renderTabs();
updateBadge();
setTimeout(updateBadge, 2000);
setTimeout(updateBadge, 5000);
}
function updateBadge() {
if (!root) return;
sendCmd('snapshot');
const b = root.querySelector('#at-badge');
const gtm = lastSnapshot ? lastSnapshot.gtmKeys.filter(k => /^GTM-/i.test(k)) : [];
if (autoInjectedId) { b.textContent = '💉 ' + autoInjectedId; b.title = 'Wstrzyknięty przez regułę'; }
else if (gtm.length) { b.textContent = gtm.join(', '); b.title = 'GTM na stronie: ' + gtm.join(', '); }
else { b.textContent = 'brak GTM'; b.title = ''; }
}
function renderTabs() {
if (!root) return;
root.querySelectorAll('.at-tab').forEach(t => t.classList.toggle('on', t.dataset.tab === activeTab));
const panel = root.querySelector('#at-panel');
if (activeTab === 'pixels') renderPixels(panel);
else if (activeTab === 'push') renderPush(panel);
else if (activeTab === 'cmds') renderCommands(panel);
else renderInjector(panel);
updateBadge();
}
/* ─────────── CONSENT MODE (live) ─────────── */
let lastConsentKey = '';
function renderConsentBox() {
const box = root && root.querySelector('#at-consent-box');
if (!box) return;
lastConsentKey = JSON.stringify(lastConsent);
if (!lastConsent || !Object.keys(lastConsent).length) { box.innerHTML = ''; return; }
const fmt = v => v === true ? '<span class="g">granted</span>' : v === false ? '<span class="d">denied</span>' : '<span class="u">—</span>';
let html = '<div class="at-label">Consent Mode <span style="color:' + C.green + ';font-size:8px;vertical-align:middle">● live</span></div>' +
'<div class="at-consent"><span class="h"></span><span class="h">default</span><span class="h">update</span>';
Object.keys(lastConsent).forEach(k => {
html += `<span class="k">${escHtml(k)}</span>${fmt(lastConsent[k].def)}${fmt(lastConsent[k].upd)}`;
});
box.innerHTML = html + '</div>';
}
function refreshConsent() {
if (!root || activeTab !== 'pixels') return;
sendCmd('consent'); // dispatch synchroniczny — lastConsent już zaktualizowany
if (JSON.stringify(lastConsent) !== lastConsentKey) renderConsentBox();
}
setInterval(refreshConsent, 1500);
/* ─────────── TAB: PIXELE ─────────── */
function renderPixels(panel) {
const found = detectTrackers().filter(r => r.detected);
sendCmd('consent');
panel.innerHTML = '';
const warns = [];
if (lastSnapshot) {
const gtms = lastSnapshot.gtmKeys.filter(k => /^GTM-/i.test(k));
if (gtms.length > 1) warns.push('⚠️ ' + gtms.length + ' kontenery GTM naraz: ' + gtms.join(', '));
const ga4s = lastSnapshot.gtmKeys.filter(k => /^G-/i.test(k));
if (ga4s.length > 1) warns.push('ℹ️ ' + ga4s.length + ' strumienie GA4: ' + ga4s.join(', ') + ' — sprawdź czy to celowe');
}
if (!bridgeReady) warns.push('⚠️ Bridge zablokowany (CSP?) — detekcja tylko po URL-ach, zakładka Push może nie działać.');
if (warns.length) {
const wb = document.createElement('div');
wb.className = 'at-warnbox';
wb.innerHTML = warns.map(escHtml).join('<br>');
panel.appendChild(wb);
}
const consentBox = document.createElement('div');
consentBox.id = 'at-consent-box';
panel.appendChild(consentBox);
renderConsentBox();
const label = document.createElement('div');
label.className = 'at-label';
label.textContent = `Wykryte trackery (${found.length})`;
panel.appendChild(label);
if (!found.length) {
const em = document.createElement('div');
em.className = 'at-empty';
em.innerHTML = 'Nie wykryto żadnych trackerów.<br>Spróbuj ↻ po pełnym załadowaniu strony.';
panel.appendChild(em);
}
found.forEach(r => {
const row = document.createElement('div');
row.className = 'at-px';
row.innerHTML = `<div class="at-px-dot" style="background:${C.green}"></div>` +
`<span class="at-px-name">${escHtml(r.name)}</span>` +
(r.ids.length ? `<span class="at-px-ids" title="Kliknij, aby skopiować">${escHtml(r.ids.join(', '))}</span>` : '');
const idsEl = row.querySelector('.at-px-ids');
if (idsEl) idsEl.addEventListener('click', () => {
copyText(r.ids.join(', '));
idsEl.textContent = '✓ skopiowano';
setTimeout(() => { idsEl.textContent = r.ids.join(', '); }, 900);
});
panel.appendChild(row);
});
root.querySelector('#at-foot-txt').textContent = `${found.length} trackerów · ${location.hostname}`;
}
/* ─────────── TAB: PUSH (symulator ecommerce) ─────────── */
function renderPush(panel) {
panel.innerHTML = `<div class="at-label">Symuluj eventy — push do dataLayer strony</div>
<div class="at-note" style="margin:0 0 6px 2px">Przed eventami ecommerce leci push {ecommerce: null}. Podgląd w GTM Preview / konsoli.</div>
<div id="at-push-list"></div>`;
const list = panel.querySelector('#at-push-list');
ECOM_PRESETS.forEach(p => {
const row = document.createElement('div');
row.className = 'at-push';
row.innerHTML = `
<div class="at-push-name">
<div class="at-push-ev">${escHtml(p.label)} <span class="at-copied" style="display:none">✓</span></div>
<div class="at-push-desc">${escHtml(p.desc)}</div>
</div>
<button class="at-btn at-btn-sec at-btn-sm" data-act="copy" title="Kopiuj snippet dataLayer.push">⧉</button>
<button class="at-btn at-btn-primary at-btn-sm" data-act="push">▶ Push</button>
`;
const flash = () => {
const c = row.querySelector('.at-copied');
c.style.display = ''; setTimeout(() => { c.style.display = 'none'; }, 900);
};
row.querySelector('[data-act="push"]').addEventListener('click', () => {
const payload = p.build();
sendCmd('push', { data: payload, clear: !!payload.ecommerce });
flash();
});
row.querySelector('[data-act="copy"]').addEventListener('click', () => {
const payload = p.build();
const snippet = 'window.dataLayer = window.dataLayer || [];\n' +
(payload.ecommerce ? 'dataLayer.push({ ecommerce: null });\n' : '') +
'dataLayer.push(' + JSON.stringify(payload, null, 2) + ');';
copyText(snippet);
flash();
});
list.appendChild(row);
});
root.querySelector('#at-foot-txt').textContent = `${ECOM_PRESETS.length} presetów GA4`;
}
/* ─────────── TAB: KOMENDY ─────────── */
function renderCommands(panel) {
panel.innerHTML = `<div class="at-label">Kliknij, aby skopiować do schowka</div>`;
COMMANDS.forEach(cmd => {
const row = document.createElement('div');
row.className = 'at-cmd';
row.innerHTML = `<div class="at-cmd-label">${escHtml(cmd.label)} <span class="at-copied" style="display:none">✓ skopiowano</span></div>` +
`<div class="at-cmd-code">${escHtml(cmd.code)}</div>` +
`<div class="at-cmd-desc">${escHtml(cmd.desc)}</div>`;
row.addEventListener('click', () => {
copyText(cmd.code);
const c = row.querySelector('.at-copied');
c.style.display = '';
setTimeout(() => { c.style.display = 'none'; }, 900);
});
panel.appendChild(row);
});
root.querySelector('#at-foot-txt').textContent = `${COMMANDS.length} komend`;
}
/* ─────────── TAB: GTM INJECTOR ─────────── */
function renderInjector(panel) {
panel.innerHTML = `
<div class="at-label">Wstrzyknij jednorazowo</div>
<div style="display:flex;gap:6px;margin-bottom:4px">
<input class="at-input" id="at-once-id" placeholder="GTM-XXXXXX" maxlength="16" style="flex:1">
<button class="at-btn at-btn-primary" id="at-once-go">💉</button>
</div>
<div class="at-note" id="at-once-note"></div>
<div class="at-label" style="margin-top:12px">Reguły auto-inject</div>
<div class="at-note" style="margin:0 0 6px 2px">Pattern = regex na URL. Pierwsza pasująca wygrywa (document-start).</div>
<div id="at-rules"></div>
<button class="at-btn at-btn-ghost" id="at-rule-add" style="margin-top:4px">+ Dodaj regułę</button>
<div style="display:flex;justify-content:flex-end;margin-top:8px">
<button class="at-btn at-btn-primary" id="at-rules-save">💾 Zapisz i przeładuj</button>
</div>
`;
const onceInput = panel.querySelector('#at-once-id');
const onceNote = panel.querySelector('#at-once-note');
onceInput.addEventListener('input', () => {
onceInput.value = onceInput.value.toUpperCase().replace(/[^A-Z0-9-]/g, '');
});
panel.querySelector('#at-once-go').addEventListener('click', () => {
const id = onceInput.value.trim();
if (!/^GTM-[A-Z0-9]+$/.test(id)) {
onceNote.className = 'at-note warn';
onceNote.textContent = 'Nieprawidłowy format — oczekiwano GTM-XXXXXX';
return;
}
const ok = injectGTM(id);
onceNote.className = 'at-note ' + (ok ? 'ok' : 'warn');
onceNote.textContent = ok ? '✓ Wstrzyknięto ' + id : id + ' już jest na stronie';
updateBadge();
});
let localRules = JSON.parse(JSON.stringify(getRules()));
const rulesBox = panel.querySelector('#at-rules');
function renderRules() {
rulesBox.innerHTML = '';
if (!localRules.length) rulesBox.innerHTML = '<div class="at-note">Brak reguł.</div>';
localRules.forEach((r, i) => {
const row = document.createElement('div');
row.className = 'at-rule';
row.innerHTML = `
<button class="at-toggle ${r.enabled ? 'on' : 'off'}" data-i="${i}">${r.enabled ? '●' : '○'}</button>
<input class="at-input" data-i="${i}" data-f="pattern" value="${escAttr(r.pattern || '')}" placeholder="example\\.com" style="flex:1.6">
<input class="at-input" data-i="${i}" data-f="gtmId" value="${escAttr(r.gtmId || '')}" placeholder="GTM-XXX" maxlength="16" style="flex:1">
<button class="at-del" data-i="${i}" title="Usuń">✕</button>
`;
rulesBox.appendChild(row);
});
rulesBox.querySelectorAll('.at-toggle').forEach(el => el.onclick = () => {
localRules[+el.dataset.i].enabled = !localRules[+el.dataset.i].enabled;
renderRules();
});
rulesBox.querySelectorAll('.at-input').forEach(el => el.oninput = () => {
let v = el.value;
if (el.dataset.f === 'gtmId') { v = v.toUpperCase().replace(/[^A-Z0-9-]/g, ''); el.value = v; }
localRules[+el.dataset.i][el.dataset.f] = v;
});
rulesBox.querySelectorAll('.at-del').forEach(el => el.onclick = () => {
localRules.splice(+el.dataset.i, 1);
renderRules();
});
}
renderRules();
panel.querySelector('#at-rule-add').addEventListener('click', () => {
localRules.push({ pattern: '', gtmId: '', enabled: true });
renderRules();
});
panel.querySelector('#at-rules-save').addEventListener('click', () => {
saveRules(localRules.filter(r => r.gtmId || r.pattern));
location.reload();
});
root.querySelector('#at-foot-txt').textContent =
autoInjectedId ? 'Auto-inject: ' + autoInjectedId : 'Brak auto-injectu na tej stronie';
}
/* ═══════════════════ AUDYT (markdown do schowka) ═══════════════════ */
function exportAudit() {
sendCmd('snapshot'); sendCmd('consent');
const found = detectTrackers().filter(r => r.detected);
const lines = [];
lines.push('# Audyt trackingu — ' + location.hostname);
lines.push('URL: ' + location.href);
lines.push('Data: ' + new Date().toLocaleString('pl-PL'));
lines.push('');
lines.push('## Wykryte trackery (' + found.length + ')');
found.forEach(r => lines.push('- ' + r.name + (r.ids.length ? ' — `' + r.ids.join(', ') + '`' : '')));
if (lastConsent && Object.keys(lastConsent).length) {
lines.push('');
lines.push('## Consent Mode');
lines.push('| typ | default | update |');
lines.push('|---|---|---|');
const f = v => v === true ? 'granted' : v === false ? 'denied' : '—';
Object.keys(lastConsent).forEach(k => lines.push('| ' + k + ' | ' + f(lastConsent[k].def) + ' | ' + f(lastConsent[k].upd) + ' |'));
}
copyText(lines.join('\n'));
const btn = root.querySelector('#at-audit');
const old = btn.textContent;
btn.textContent = '✓ skopiowano';
setTimeout(() => { btn.textContent = old; }, 1200);
}
/* ═══════════════════ POZYCJA / KIERUNEK / DRAG ═══════════════════ */
const WIDGET_W = 360;
function clampToViewport() {
if (!host) return;
const r = host.getBoundingClientRect();
// pozycjonowany przez left/top?
if (host.style.left && host.style.left !== 'auto') {
const l = Math.max(0, Math.min(r.left, window.innerWidth - WIDGET_W));
const t = Math.max(0, Math.min(r.top, window.innerHeight - 56));
host.style.left = l + 'px'; host.style.top = t + 'px';
host.style.right = 'auto'; host.style.bottom = 'auto';
}
}
window.addEventListener('resize', () => { clampToViewport(); });
function restorePosition() {
try {
const pos = JSON.parse(localStorage.getItem(LS_POS) || 'null');
if (pos && typeof pos.l === 'number' && typeof pos.t === 'number') {
const l = Math.max(0, Math.min(pos.l, window.innerWidth - WIDGET_W));
const t = Math.max(0, Math.min(pos.t, window.innerHeight - 56));
host.style.left = l + 'px'; host.style.top = t + 'px';
host.style.right = 'auto'; host.style.bottom = 'auto';
}
} catch (e) {}
}
function savePosition() {
try {
const r = host.getBoundingClientRect();
const l = Math.max(0, Math.min(r.left, window.innerWidth - WIDGET_W));
const t = Math.max(0, Math.min(r.top, window.innerHeight - 56));
localStorage.setItem(LS_POS, JSON.stringify({ l, t }));
} catch (e) {}
}
function updateExpandDirection() {
if (!host || !root) return;
const rect = host.getBoundingClientRect();
const hdrH = root.querySelector('#at-hdr').offsetHeight;
const hdrMid = rect.top + hdrH / 2;
if (hdrMid > window.innerHeight / 2) {
host.style.bottom = (window.innerHeight - rect.bottom) + 'px';
host.style.top = 'auto';
} else {
host.style.top = rect.top + 'px';
host.style.bottom = 'auto';
}
}
function makeDraggable(el, handle) {
let sx, sy, sl, st;
handle.addEventListener('mousedown', e => {
if (e.composedPath().some(n => n.tagName === 'BUTTON')) return;
e.preventDefault();
sx = e.clientX; sy = e.clientY; const r = el.getBoundingClientRect(); sl = r.left; st = r.top;
const mv = ev => { el.style.left = (sl + ev.clientX - sx) + 'px'; el.style.top = (st + ev.clientY - sy) + 'px'; el.style.right = 'auto'; el.style.bottom = 'auto'; };
const up = () => { document.removeEventListener('mousemove', mv); document.removeEventListener('mouseup', up); updateExpandDirection(); savePosition(); };
document.addEventListener('mousemove', mv); document.addEventListener('mouseup', up);
});
}
/* ═══════════════════ SPA / TOGGLE / MENU / INIT ═══════════════════ */
function toggleWidget() {
if (host) {
try { localStorage.setItem(LS_HIDDEN, '1'); } catch (e) {}
host.remove(); host = null; root = null;
} else {
try { localStorage.removeItem(LS_HIDDEN); } catch (e) {}
injectBridge(); // na zablokowanym hoście bridge mógł nie wejść przy starcie
buildWidget(); updateExpandDirection();
}
}
GM_registerMenuCommand('⚡ Wstrzyknij GTM (jednorazowo)', () => {
const id = prompt('GTM Container ID (np. GTM-XXXXXX):');
if (id && /^GTM-[A-Z0-9]+$/i.test(id.trim())) injectGTM(id.trim().toUpperCase());
else if (id !== null) alert('Nieprawidłowy format. Oczekiwano GTM-XXXXXX');
});
document.addEventListener('keydown', e => {
if (e.altKey && e.shiftKey && e.code === 'KeyA') toggleWidget();
});
// SPA: zmiana URL bez reloadu → odśwież aktywną zakładkę
// Watchdog: frameworki potrafią podmienić <body> i wywalić hosta z DOM —
// jeśli widget powinien być widoczny, a zniknął, odbuduj go.
let lastUrl = location.href;
setInterval(() => {
if (location.href !== lastUrl) {
lastUrl = location.href;
if (root) setTimeout(renderTabs, 800);
}
let hidden = false;
try { hidden = localStorage.getItem(LS_HIDDEN) === '1'; } catch (e) {}
if (!hidden && !isBlockedHost() && document.body) {
if (!host || !host.isConnected) {
buildWidget();
updateExpandDirection();
}
}
}, 1200);
function init() {
if (isBlockedHost()) return; // panele narzędzi — nie pokazuj (Alt+Shift+A wymusza)
let hidden = false;
try { hidden = localStorage.getItem(LS_HIDDEN) === '1'; } catch (e) {}
if (!hidden) {
buildWidget();
updateExpandDirection();
}
}
if (document.readyState === 'complete') setTimeout(init, 800);
else window.addEventListener('load', () => setTimeout(init, 800));
})();