File: //tmp/ezdns_detail_scripts2.js
const params = new URLSearchParams(location.search);
const scanId = params.get('id');
const pathDomain = (!scanId && location.pathname.startsWith('/') && location.pathname.includes('.') ) ? location.pathname.slice(1) : null;
const apikeyInput = document.getElementById('apikey');
const refreshBtn = document.getElementById('refresh');
const deleteBtn = document.getElementById('delete');
const summary = document.getElementById('summary');
const findings = document.getElementById('findings');
const metrics = document.getElementById('metrics');
const title = document.getElementById('title');
const reportAuditForm = document.getElementById('reportAuditForm');
const reportAuditDomain = document.getElementById('reportAuditDomain');
const reportAuditEmail = document.getElementById('reportAuditEmail');
const reportAuditMessage = document.getElementById('reportAuditMessage');
const reportAuditStatus = document.getElementById('reportAuditStatus');
function badge(sev, text) {
const cls = sev === 'error' ? 'bg-red-900 text-red-200' : sev === 'warning' ? 'bg-amber-900 text-amber-200' : 'bg-emerald-900 text-emerald-200';
return `<span class="px-2 py-0.5 rounded ${cls}">${text || sev}</span>`;
}
function sevRank(sev) {
if (sev === 'error') return 3;
if (sev === 'warning') return 2;
if (sev === 'info') return 1;
return 0;
}
function worstSeverity(findings, categories) {
if (!findings || !findings.length) return null;
let worst = null;
for (const f of findings) {
if (categories && categories.length && !categories.includes(f.category)) continue;
if (worst === null || sevRank(f.severity) > sevRank(worst)) worst = f.severity;
}
return worst;
}
function perfSeverity(checks) {
const score = checks?.performance?.performance_score;
if (score === null || score === undefined) return null;
if (score >= 90) return null;
if (score >= 70) return 'warning';
return 'error';
}
function statusBadgeFromSeverity(sev) {
if (sev === 'error') return badge('error', 'bad');
if (sev === 'warning') return badge('warning', 'ok');
if (sev === 'info') return badge('warning', 'ok');
return badge('ok', 'good');
}
function sectionBadges(result) {
const checks = result?.checks || result || {};
const findings = result?.findings || [];
const out = {};
out.overall = worstSeverity(findings);
out.delegation = worstSeverity(findings, ['delegation','dns-hygiene','zone-transfer','recursion','reachability']);
out.mail = worstSeverity(findings, ['mail','spf','dmarc','caa','smtp']);
out.dnssec = worstSeverity(findings, ['dnssec']);
out.performance = perfSeverity(checks);
return out;
}
function trackEvent(category, action, name) {
if (window._paq) window._paq.push(['trackEvent', category, action, name || '']);
}
function currentReportDomain() {
const label = title && title.textContent ? title.textContent.replace(/^Scan:\s*/, '').trim().toLowerCase() : '';
return pathDomain || label || '';
}
async function recordLead(intent, plan, extra = {}) {
try {
const payload = Object.assign({
source: 'report',
intent,
plan,
domain: currentReportDomain(),
page: window.location.pathname + window.location.search
}, extra || {});
await fetch('/v1/leads', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
} catch (e) { /* lead tracking must never block sales */ }
}
const ORDER_URLS = {
priority_audit: 'https://support.panomity.com/cart/priority-support/DNS-Priority-Audit/&step=0',
quick_fix: 'https://support.panomity.com/cart/priority-support/DNS-Quick-Fix/&step=0',
cleanup: 'https://support.panomity.com/cart/priority-support/DNS-Mail-Security-Cleanup/&step=0',
managed_monitoring: 'https://support.panomity.com/?cmd=cart&action=add&id=856&cycle=m'
};
const ORDER_POSTS = {
priority_audit: { action: 'https://support.panomity.com/cart/priority-support/DNS-Priority-Audit/&step=0', id: '879', cycle: 'Once' },
quick_fix: { action: 'https://support.panomity.com/cart/priority-support/DNS-Quick-Fix/&step=0', id: '867', cycle: 'Once' },
cleanup: { action: 'https://support.panomity.com/cart/priority-support/DNS-Mail-Security-Cleanup/&step=0', id: '868', cycle: 'Once' }
};
function supportTicketUrl(intent, plan) {
const params = new URLSearchParams();
params.set('subject', `ezdns.support ${intent}: ${plan}`);
if (currentReportDomain()) params.set('domain', currentReportDomain());
params.set('source', 'ezdns.support-report');
return `https://support.panomity.com/tickets/new/?${params.toString()}`;
}
function orderUrlFor(plan) {
return ORDER_URLS[plan] || '';
}
function submitHostBillOrder(plan) {
const cfg = ORDER_POSTS[plan];
if (!cfg) return false;
const form = document.createElement('form');
form.method = 'post';
form.action = cfg.action;
for (const [name, value] of Object.entries({ action: 'add', id: cfg.id, cycle: cfg.cycle })) {
const input = document.createElement('input');
input.type = 'hidden';
input.name = name;
input.value = value;
form.appendChild(input);
}
document.body.appendChild(form);
form.submit();
return true;
}
async function requestService(plan, intent = 'report_fix') {
trackEvent('Revenue', intent, plan);
await recordLead(intent, plan);
if (submitHostBillOrder(plan)) return;
window.location.href = orderUrlFor(plan) || supportTicketUrl(intent, plan);
}
async function fetchJSON(url, opts={}) {
const headers = Object.assign({ 'x-api-key': apikeyInput.value || 'dev-key' }, opts.headers||{});
const res = await fetch(url, Object.assign({}, opts, { headers }));
if (!res.ok) throw new Error('HTTP '+res.status);
return await res.json();
}
function renderSummary(data) {
summary.innerHTML = '';
const created = data.created_at ? new Date(data.created_at).toLocaleString() : '';
const badges = sectionBadges(data);
const items = [
['Domain', data.domain],
['Overall', statusBadgeFromSeverity(badges.overall)],
['Delegation', statusBadgeFromSeverity(badges.delegation)],
['Mail', statusBadgeFromSeverity(badges.mail)],
['DNSSEC', statusBadgeFromSeverity(badges.dnssec)],
['Performance', statusBadgeFromSeverity(badges.performance)],
];
for (const [k,v] of items) {
const div = document.createElement('div');
div.innerHTML = `<div class="text-slate-400">${k}</div><div class="font-medium">${typeof v==='string'? v : v}</div>`;
summary.appendChild(div);
}
document.getElementById('timestamp').textContent = created ? `Last updated: ${created}` : '';
renderTopFixes(data.findings || []);
}
function canonicalFindingKey(f) {
const msg = (f && f.message) ? String(f.message) : '';
if (f.category === 'spf' && msg.includes('Kein SPF')) return 'spf.missing';
if (f.category === 'spf' && msg.includes('10 DNS-Lookups')) return 'spf.lookups';
if (f.category === 'dmarc' && msg.includes('Kein DMARC')) return 'dmarc.missing';
if (f.category === 'dmarc' && msg.includes('Policy ist none')) return 'dmarc.none';
if (f.category === 'dnssec' && msg.includes('DS vorhanden') && msg.includes('keine DNSKEY')) return 'dnssec.ds_no_dnskey';
if (f.category === 'dnssec' && msg.includes('DS passt')) return 'dnssec.ds_dnskey_mismatch';
if (f.category === 'dnssec' && msg.includes('RRSIG')) return 'dnssec.rrsig';
if (f.category === 'delegation' && msg.startsWith('Lame NS')) return 'delegation.lame_ns';
if (f.category === 'delegation' && msg.includes('Parent/Child NS')) return 'delegation.ns_mismatch';
if (f.category === 'delegation' && msg.includes('Glue')) return 'delegation.glue_mismatch';
if (f.category === 'zone-transfer' && msg.includes('AXFR')) return 'zone-transfer.axfr_open';
if (f.category === 'recursion' && msg.includes('Recursion')) return 'recursion.open';
if (f.category === 'dns-hygiene' && msg.includes('Apex')) return 'dns-hygiene.apex_cname';
if (f.category === 'mail' && msg.includes('Keine MX')) return 'mail.no_mx';
if (f.category === 'caa' && msg.includes('Keine CAA')) return 'caa.missing';
if (f.category === 'smtp' && msg.includes('STARTTLS')) return 'smtp.starttls';
if (f.category === 'smtp' && msg.includes('SMTP nicht erreichbar')) return 'smtp.reachability';
return `${f.category}:${msg}`;
}
function impactScoreForFinding(f) {
const sev = (f && f.severity) ? String(f.severity) : '';
if (sev === 'error') return 100;
if (sev === 'warning') return 70;
if (sev === 'info') return 40;
return 10;
}
function effortLabelForFinding(f) {
const key = canonicalFindingKey(f);
if (key.startsWith('spf.') || key.startsWith('dmarc.') || key === 'caa.missing') return { score: 1, label: 'niedrig' };
if (key.startsWith('delegation.ns_mismatch') || key.startsWith('delegation.glue_mismatch') || key.startsWith('recursion.') || key.startsWith('zone-transfer.') || key.startsWith('dns-hygiene.')) return { score: 2, label: 'mittel' };
if (key.startsWith('dnssec.')) return { score: 3, label: 'hoch' };
return { score: 2, label: 'mittel' };
}
function titleForFinding(f) {
const key = canonicalFindingKey(f);
if (key === 'delegation.ns_mismatch') return 'Parent/Child Nameserver angleichen';
if (key === 'delegation.glue_mismatch') return 'Glue-IPs korrigieren';
if (key === 'delegation.lame_ns') return 'Lame Nameserver entfernen/ersetzen';
if (key === 'zone-transfer.axfr_open') return 'AXFR (Zone Transfer) absichern';
if (key === 'recursion.open') return 'Open Recursion deaktivieren';
if (key === 'spf.missing') return 'SPF Record hinzufügen';
if (key === 'spf.lookups') return 'SPF auf ≤10 Lookups reduzieren';
if (key === 'dmarc.missing') return 'DMARC Record hinzufügen';
if (key === 'dmarc.none') return 'DMARC Policy schrittweise härten';
if (key === 'dnssec.ds_no_dnskey') return 'DNSSEC: DS/DNSKEY konsistent machen';
if (key === 'dnssec.ds_dnskey_mismatch') return 'DNSSEC: DS↔DNSKEY mismatch beheben';
if (key === 'dnssec.rrsig') return 'DNSSEC: Signatur-Ablauf (RRSIG) beheben';
if (key === 'dns-hygiene.apex_cname') return 'Apex-CNAME entfernen (DNS Hygiene)';
if (key === 'mail.no_mx') return 'MX Records prüfen/setzen';
if (key === 'caa.missing') return 'CAA Records setzen (optional)';
if (key === 'smtp.starttls') return 'STARTTLS für SMTP aktivieren';
if (key === 'smtp.reachability') return 'SMTP Erreichbarkeit prüfen';
return 'Empfohlene Maßnahme';
}
function landingPageForFinding(f) {
const key = canonicalFindingKey(f);
const guides = {
'dmarc.missing': { href: '/dmarc-record-fehlt', label: 'DMARC-Record-Problemseite öffnen', plan: 'priority_audit' },
'dmarc.none': { href: '/dmarc-p-none-fix', label: 'DMARC-p=none-Problemseite öffnen', plan: 'priority_audit' },
'spf.lookups': { href: '/spf-lookup-limit', label: 'SPF-Limit-Problemseite öffnen', plan: 'quick_fix' },
'dnssec.ds_no_dnskey': { href: '/dnssec-fix', label: 'DNSSEC-Fix-Seite öffnen', plan: 'quick_fix' },
'dnssec.ds_dnskey_mismatch': { href: '/dnssec-fix', label: 'DNSSEC-Fix-Seite öffnen', plan: 'quick_fix' },
'dnssec.rrsig': { href: '/dnssec-servfail-fix', label: 'DNSSEC-SERVFAIL-Seite öffnen', plan: 'quick_fix' }
};
const guide = guides[key];
return guide ? Object.assign({ key }, guide) : null;
}
function sortedCommercialFindings(findingsList) {
const grouped = new Map();
for (const f of (findingsList || [])) {
const key = canonicalFindingKey(f);
const prev = grouped.get(key);
if (!prev || sevRank(f.severity) > sevRank(prev.severity)) grouped.set(key, f);
}
const all = Array.from(grouped.values());
all.sort((a, b) => {
const ia = impactScoreForFinding(a);
const ib = impactScoreForFinding(b);
if (ib !== ia) return ib - ia;
const ea = effortLabelForFinding(a).score;
const eb = effortLabelForFinding(b).score;
if (ea !== eb) return ea - eb;
return String(a.category).localeCompare(String(b.category));
});
return all;
}
function renderReportAuditRequest(result) {
if (!reportAuditForm) return;
const domain = (result && result.domain) ? String(result.domain).trim().toLowerCase() : currentReportDomain();
if (reportAuditDomain && !reportAuditDomain.value && domain) reportAuditDomain.value = domain;
const top = sortedCommercialFindings((result && result.findings) || [])[0];
if (reportAuditMessage && !reportAuditMessage.value) {
const reportPath = window.location.pathname + window.location.search;
reportAuditMessage.value = top
? `${titleForFinding(top)}: ${top.message} — Report: ${reportPath}`
: `Bitte DNS-, SPF-, DKIM-, DMARC- und DNSSEC-Priority-Audit für diese Domain durchführen. Report: ${reportPath}`;
}
}
function renderTopFixes(findingsList) {
const host = document.getElementById('topFixes');
if (!host) return;
host.innerHTML = '';
const top = sortedCommercialFindings(findingsList).slice(0, 3);
const wrapper = document.createElement('div');
wrapper.className = 'border border-slate-800 rounded-xl p-3';
wrapper.innerHTML = `<div class="font-semibold">Top 3 Fixes</div><div class="mt-1 text-xs text-slate-400">Priorisiert nach Impact (Ausfall/Deliverability/Security) und Aufwand.</div>`;
if (!top.length) {
const ok = document.createElement('div');
ok.className = 'mt-3 text-sm text-slate-300';
ok.innerHTML = `${badge('ok', 'good')} Keine dringenden Fixes gefunden.`;
wrapper.appendChild(ok);
host.appendChild(wrapper);
return;
}
const list = document.createElement('div');
list.className = 'mt-3 grid grid-cols-1 gap-3';
for (const f of top) {
const eff = effortLabelForFinding(f);
const title = titleForFinding(f);
const guide = landingPageForFinding(f);
const guideLink = guide ? `<a class="inline-flex items-center rounded-lg bg-emerald-700 px-3 py-1.5 text-sm font-semibold text-white hover:bg-emerald-600" href="${guide.href}" data-finding-guide="${guide.key}" data-guide-plan="${guide.plan}">${guide.label}</a>` : '';
const item = document.createElement('div');
item.className = 'border border-slate-800 rounded-lg p-3';
const dataHelp = encodeURIComponent(JSON.stringify(f));
item.innerHTML = `
<div class="flex items-start justify-between gap-3">
<div>
<div class="flex items-center gap-2">
${badge(f.severity, f.severity)}
<div class="font-medium">${title}</div>
</div>
<div class="mt-2 text-sm text-slate-300">${explainWhy(f)}</div>
<div class="mt-2 text-xs text-slate-400">Aufwand: <span class="text-slate-300">${eff.label}</span></div>
${guideLink ? `<div class="mt-3 flex flex-wrap gap-2">${guideLink}</div>` : ''}
</div>
<button class="shrink-0 px-3 py-1.5 rounded-lg border border-slate-700 text-slate-200 hover:bg-slate-800" data-help="${dataHelp}">Details</button>
</div>
`;
list.appendChild(item);
}
wrapper.appendChild(list);
const cta = document.createElement('div');
cta.className = 'mt-4 bg-slate-950 border border-blue-500 rounded-lg p-3';
cta.innerHTML = `
<div class="font-semibold">Fix durch Panomity buchen</div>
<div class="mt-1 text-sm text-slate-300">Für DNS-, DNSSEC- oder Mail-Findings: Priority Audit ab €49, Quick Fix ab €149 oder vollständiger DNS/Mail Cleanup ab €499 über den Panomity-Warenkorb.</div>
<div class="mt-3 flex flex-wrap gap-2">
<button data-service-lead="priority_audit" class="px-3 py-1.5 rounded-lg bg-emerald-600 text-white">Audit €49</button>
<button data-service-lead="quick_fix" class="px-3 py-1.5 rounded-lg bg-blue-600 text-white">Quick Fix €149</button>
<button data-service-lead="cleanup" class="px-3 py-1.5 rounded-lg bg-green-600 text-white">Cleanup €499</button>
<button data-service-lead="managed_monitoring" class="px-3 py-1.5 rounded-lg border border-slate-700 text-slate-200">Monitoring €199/Monat</button>
</div>
`;
wrapper.appendChild(cta);
host.appendChild(wrapper);
}
function sectionCard(title, rows) {
const card = document.createElement('div');
card.className = 'border border-slate-800 rounded-lg';
card.innerHTML = `<div class="px-3 py-2 bg-slate-800 border-b border-slate-700 font-medium flex items-center justify-between"><span>${title}</span><span></span></div>`;
const table = document.createElement('table');
table.className = 'w-full text-sm';
table.innerHTML = `<tbody></tbody>`;
const tbody = table.querySelector('tbody');
for (const row of rows) {
const k = row[0];
const v = row[1];
const metric = row.length >= 3 ? row[2] : null;
const tr = document.createElement('tr');
const help = metric ? ` <button class="ml-2 underline text-blue-400" data-metric="${metric}">?</button>` : '';
tr.innerHTML = `<td class="px-3 py-2 text-slate-400 w-1/3">${k}${help}</td><td class="px-3 py-2">${v}</td>`;
tbody.appendChild(tr);
}
card.appendChild(table);
return card;
}
function explainMetric(metric){
switch(metric){
case 'a.answers':
return { why:'A-Records sind die Basis für die Erreichbarkeit (IPv4). Viele Systeme fallen bei fehlendem A auf (z.B. Web, Mail-Fallbacks).', what:'Welche IPv4-Adressen (`A`) für die Domain publiziert sind.', how:'Stelle sicher, dass die IP(s) korrekt sind, erreichbar sind und zum Hosting passen. Für moderne Setups ggf. zusätzlich AAAA pflegen.' };
case 'a.ttl':
return { why:'TTL beeinflusst, wie lange Resolver Antworten cachen – das wirkt sich auf Stabilität und Änderungsdauer aus.', what:'Time-To-Live (Sekunden) für A-Antworten.', how:'Wähle TTL passend: höher für stabile Records (weniger Last), niedriger vor geplanten Änderungen (schneller Rollout). Typisch: 300–3600s.' };
case 'a.authoritative':
return { why:'Autoritative Antworten zeigen, dass du tatsächlich von den zuständigen Nameservern Daten bekommst.', what:'Ob die Antwort autoritativ (AA) war.', how:'Wenn nicht autoritativ: prüfen ob du gegen die richtigen NS fragst; bei Delegationsproblemen Parent/Child-NS und Glue prüfen.' };
case 'aaaa.answers':
return { why:'AAAA-Records ermöglichen IPv6-Erreichbarkeit. Das wird zunehmend erwartet und kann Performance/Reachability verbessern.', what:'Welche IPv6-Adressen (`AAAA`) für die Domain publiziert sind.', how:'Wenn du IPv6 anbietest: AAAA korrekt setzen und testen. Wenn nicht: kein AAAA ist ok, aber dann bewusst IPv6 nicht anbieten.' };
case 'aaaa.ttl':
return { why:'TTL steuert Cache-Verhalten auch für IPv6. Bei Deployments/Providerwechseln ist das relevant.', what:'Time-To-Live (Sekunden) für AAAA-Antworten.', how:'Analog zu A-TTL: höher für stabil, niedriger vor Änderungen. Typisch 300–3600s.' };
case 'aaaa.authoritative':
return { why:'Wie bei A: autoritative Antworten erhöhen die Sicherheit, dass die Daten aus der Zone stammen.', what:'Ob die AAAA-Antwort autoritativ (AA) war.', how:'Bei „no“: Delegation/Resolver-Pfad prüfen. Oft ein Indikator für falsche NS/Glue oder Query gegen falsche Resolver.' };
case 'ns.answers':
return { why:'NS-Records bestimmen, welche Nameserver autoritativ für die Domain sind. Fehler hier führen zu kompletten Ausfällen.', what:'Welche Nameserver in der Zone (Child) publiziert sind.', how:'NS-Set konsistent halten: Child NS müssen zu Parent/Registry NS passen. Bei Änderungen TTL beachten und alte NS nicht zu früh abschalten.' };
case 'soa.present':
return { why:'SOA ist Pflicht für jede Zone. Ohne SOA sind viele Resolver/Tools irritiert oder behandeln die Zone als fehlerhaft.', what:'Ob ein SOA-Record vorhanden ist.', how:'Fehlt SOA: Zonefile/Provider-Konfiguration prüfen und SOA korrekt anlegen (MNAME/RNAME/Serial/Refresh/Retry/Expire/Minimum).' };
case 'soa.ttl':
return { why:'TTL beim SOA beeinflusst Caching und wird u.a. bei negativen Antworten (NXDOMAIN) relevant.', what:'TTL des SOA-Records.', how:'Typisch 300–3600s. Achte darauf, dass negative Caching nicht zu lang ist (besonders wenn du häufig Subdomains änderst).' };
case 'delegation.child_ns':
return { why:'Die Nameserver im Child (Zone) bestimmen, wer autoritativ antwortet.', what:'Welche NS-Records die Zone selbst angibt.', how:'Stelle sicher, dass die Child-NS korrekt sind und zu den beim Parent eingetragenen NS passen.' };
case 'delegation.parent_ns':
return { why:'Der Parent (Registry) delegiert die Zone an diese Nameserver.', what:'Welche NS beim Parent (z.B. Registry) hinterlegt sind.', how:'Parent-NS müssen korrekt sein und mit den Child-NS übereinstimmen.' };
case 'delegation.glue_mismatch':
return { why:'Falsche Glue-IPs können Resolution-Ausfälle verursachen.', what:'Glue-IPs (Parent) weichen von A/AAAA der NS-Hosts ab.', how:'Glue-Records korrigieren oder A/AAAA der NS-Hosts anpassen.' };
case 'delegation.ns_mismatch':
return { why:'Mismatch führt oft zu inkonsistenter Delegation und Debug-Aufwand.', what:'Parent und Child listen unterschiedliche Nameserver.', how:'NS-Set angleichen (Registry + Zonefile) und TTL abwarten.' };
case 'delegation.lame_ns':
return { why:'Lame NS sind als autoritativ delegiert, liefern aber keine autoritativen Antworten.', what:'Ein delegierter NS antwortet nicht autoritativ für die Zone.', how:'NS reparieren/ersetzen und Delegation aktualisieren.' };
case 'delegation.axfr_open':
return { why:'Offene Zone-Transfers leaken die komplette Zone.', what:'AXFR ist von außen möglich.', how:'AXFR deaktivieren oder nur für Secondary/Trusted IPs erlauben.' };
case 'delegation.open_recursion':
return { why:'Open Recursion kann für Amplification/Abuse missbraucht werden.', what:'Autoritativer NS erlaubt Recursion.', how:'Recursion deaktivieren oder strikt auf interne Netze beschränken.' };
case 'delegation.soa_ok':
return { why:'SOA-Sanity ist ein schneller Plausibilitätscheck, ob die Zone „gesund“ wirkt (z.B. Serial/Timer nicht völlig aus der Reihe).', what:'Ob grundlegende SOA-Heuristiken ok sind.', how:'Bei Problemen: SOA Parameter prüfen (Serial korrekt, Refresh/Retry/Expire sinnvoll), Provider-Templates checken und Änderungen konsistent ausrollen.' };
case 'mail.mx':
return { why:'MX bestimmt, wohin E-Mails für die Domain zugestellt werden.', what:'Liste der Mail-Exchanger.', how:'MX korrekt setzen; ohne MX wird ggf. A/AAAA fallback genutzt (unerwünscht).' };
case 'mail.spf':
return { why:'SPF hilft, Spoofing zu reduzieren und Zustellbarkeit zu verbessern.', what:'Vorhandensein und Inhalt des SPF Records.', how:'`v=spf1 ... -all` sauber pflegen und autorisierte Sender eintragen.' };
case 'mail.spf_lookups':
return { why:'Zu viele Lookups führen dazu, dass SPF-Prüfung fehlschlägt.', what:'Anzahl DNS-Lookups, die SPF triggert.', how:'Includes reduzieren/flatten, Ziel ≤10 Lookups.' };
case 'mail.dmarc':
return { why:'DMARC setzt Policy und Reporting für SPF/DKIM.', what:'Vorhandensein und Policy (none/quarantine/reject).', how:'Start mit `p=none`, dann schrittweise `quarantine/reject` nach Monitoring.' };
case 'mail.dkim':
return { why:'DKIM signiert E-Mails kryptografisch und unterstützt DMARC.', what:'Gefundene DKIM-Selectoren (best effort).', how:'DKIM im Mail-System aktivieren, Selector publizieren, Rotation einplanen.' };
case 'mail.caa':
return { why:'CAA begrenzt, welche CAs Zertifikate ausstellen dürfen.', what:'CAA Records (issue/issuewild/iodef).', how:'CAA setzen (z.B. Let’s Encrypt / eigene CA) um Mississuance zu reduzieren.' };
case 'dnssec.ds_present':
return { why:'DS beim Parent aktiviert DNSSEC-Chain-of-Trust.', what:'Ob DS Records beim Parent vorhanden sind.', how:'Bei DNSSEC: DS korrekt setzen; ohne DNSSEC: DS darf nicht gesetzt sein.' };
case 'dnssec.dnskey_present':
return { why:'DNSKEYs sind nötig, damit Signaturen verifizierbar sind.', what:'Ob DNSKEY Records in der Zone vorhanden sind.', how:'DNSSEC korrekt konfigurieren, Keys publizieren.' };
case 'dnssec.ds_dnskey_match':
return { why:'Mismatch bricht Validierung und führt zu SERVFAIL bei Resolvern.', what:'Ob DS zu einer DNSKEY passt.', how:'DS neu publishen (Registry) passend zum aktuellen Key (KSK).' };
case 'dnssec.rrsig_expiry':
return { why:'Abgelaufene Signaturen machen Antworten ungültig.', what:'Frühestes Ablaufdatum von RRSIGs.', how:'Signer so konfigurieren, dass rechtzeitig resigniert wird; Monitoring auf Expiry.' };
case 'dnssec.negative_signed':
return { why:'Signierte negative Antworten (NSEC/NSEC3) sind Teil von DNSSEC-Integrität.', what:'Ob negative Antworten signiert sind.', how:'DNSSEC-Setup prüfen; NSEC/NSEC3 korrekt aktivieren.' };
case 'perf.p50':
return { why:'Latenz beeinflusst UX und Resolver-Cache Hit-Rates.', what:'Median Antwortzeit der autoritativen Nameserver.', how:'Anycast/Provider verbessern, Netz/NS-Standorte optimieren.' };
case 'perf.p95':
return { why:'p95 zeigt Worst-Case und Ausreißer (oft die spürbaren).', what:'95%-Perzentil der Antwortzeiten.', how:'Bottlenecks/Packetloss/überlastete NS identifizieren, Provider/Setup verbessern.' };
case 'perf.score':
return { why:'Ein einfacher Gesamtscore erleichtert Quick-Triage.', what:'Heuristische Bewertung aus p95 und Erfolgsrate.', how:'Verbessere p95 und Erfolgsrate; defekte/slow NS ersetzen.' };
default:
return { why:'Diese Metrik beeinflusst typischerweise Erreichbarkeit, Integrität oder Zustellbarkeit.', what:'Details dieser Kennzahl im Report.', how:'Wenn hier Auffälligkeiten angezeigt werden: DNS-Provider/Zonendaten prüfen und nach Best Practices korrigieren.' };
}
}
function renderMetrics(result) {
metrics.innerHTML = '';
const checks = result.checks || {};
const badges = sectionBadges(result);
// Example: A/AAAA
if (checks.A) {
const rows = [
['A answers', (checks.A.answers||[]).map(a=>a.rr).join(', ') || '-', 'a.answers'],
['TTL', checks.A.ttl ?? '-', 'a.ttl'],
['Authoritative', checks.A.authoritative? 'yes':'no', 'a.authoritative'],
];
metrics.appendChild(sectionCard('A records', rows));
}
if (checks.AAAA) {
const rows = [
['AAAA answers', (checks.AAAA.answers||[]).map(a=>a.rr).join(', ') || '-', 'aaaa.answers'],
['TTL', checks.AAAA.ttl ?? '-', 'aaaa.ttl'],
['Authoritative', checks.AAAA.authoritative? 'yes':'no', 'aaaa.authoritative'],
];
metrics.appendChild(sectionCard('AAAA records', rows));
}
if (checks.NS) {
const rows = [
['NS', (checks.NS.answers||[]).map(a=>a.rr).join(', ') || '-', 'ns.answers'],
];
metrics.appendChild(sectionCard('NS', rows));
}
if (checks.SOA) {
const rows = [
['SOA present', checks.SOA.answers && checks.SOA.answers.length ? 'yes' : 'no', 'soa.present'],
['TTL', checks.SOA.ttl ?? '-', 'soa.ttl'],
];
metrics.appendChild(sectionCard('SOA', rows));
}
if (checks.delegation) {
const d = checks.delegation;
const rows = [
['Child NS', (d.child_ns||[]).join(', ') || '-', 'delegation.child_ns'],
['Parent NS', (d.parent_ns||[]).join(', ') || '-', 'delegation.parent_ns'],
['Glue mismatch', d.glue_mismatch? badge('warning','yes'):'no', 'delegation.glue_mismatch'],
['NS mismatch', d.ns_mismatch? badge('warning','yes'):'no', 'delegation.ns_mismatch'],
['Lame NS', (d.lame_nameservers||[]).join(', ') || '-', 'delegation.lame_ns'],
['AXFR open', (d.axfr_open||[]).join(', ') || '-', 'delegation.axfr_open'],
['Open recursion', (d.open_recursion||[]).join(', ') || '-', 'delegation.open_recursion'],
['SOA sanity', d.soa_ok? badge('ok','good'):'check', 'delegation.soa_ok'],
];
const card = sectionCard('Delegation & hygiene', rows);
card.querySelector('div span:last-child').innerHTML = statusBadgeFromSeverity(badges.delegation);
metrics.appendChild(card);
}
if (checks.mail) {
const m = checks.mail;
const rows = [
['MX', (m.mx.records||[]).map(r=>`${r.exchange} (pref ${r.preference})`).join('<br>') || '-', 'mail.mx'],
['SPF', m.spf.present? m.spf.record : 'none', 'mail.spf'],
['SPF lookups', m.spf.dns_lookups ?? '-', 'mail.spf_lookups'],
['DMARC', m.dmarc.present? m.dmarc.record : 'none', 'mail.dmarc'],
['DKIM', (m.dkim.selectors_found||[]).join(', ') || '–', 'mail.dkim'],
['CAA', (m.caa.records||[]).map(r=>`${r.tag}=${r.value}`).join(', ') || 'none', 'mail.caa'],
];
const card = sectionCard('Mail & domain security', rows);
card.querySelector('div span:last-child').innerHTML = statusBadgeFromSeverity(badges.mail);
metrics.appendChild(card);
}
if (checks.dnssec) {
const d = checks.dnssec;
const rows = [
['DS present', d.ds_present? 'yes':'no', 'dnssec.ds_present'],
['DNSKEY present', d.dnskey_present? 'yes':'no', 'dnssec.dnskey_present'],
['DS↔DNSKEY match', d.ds_dnskey_match? 'yes':'no', 'dnssec.ds_dnskey_match'],
['Earliest RRSIG expiry', d.rrsig_earliest_expiry_epoch? new Date(d.rrsig_earliest_expiry_epoch*1000).toLocaleString() : '-', 'dnssec.rrsig_expiry'],
['Negative signed', d.negative_signed===true? 'yes': (d.negative_signed===false? 'no':'n/a'), 'dnssec.negative_signed'],
];
const card = sectionCard('DNSSEC', rows);
card.querySelector('div span:last-child').innerHTML = statusBadgeFromSeverity(badges.dnssec);
metrics.appendChild(card);
}
if (checks.performance) {
const p = checks.performance;
const rows = [
['p50 (ms)', p.summary?.p50_ms ?? '-', 'perf.p50'],
['p95 (ms)', p.summary?.p95_ms ?? '-', 'perf.p95'],
['Score', p.performance_score ?? '-', 'perf.score'],
];
const card = sectionCard('Performance', rows);
card.querySelector('div span:last-child').innerHTML = statusBadgeFromSeverity(badges.performance);
metrics.appendChild(card);
}
}
function renderFindings(list) {
findings.innerHTML = '';
if (!list || !list.length) {
findings.innerHTML = '<li class="text-slate-400">No findings</li>';
return;
}
for (const f of list) {
const li = document.createElement('li');
const sev = f.severity;
li.innerHTML = `<div>${badge(sev, sev)} <span class="font-medium">${f.category}</span> – ${f.message} <button class="ml-2 underline text-blue-400" data-help="${encodeURIComponent(JSON.stringify(f))}">Mehr Info</button></div>`;
findings.appendChild(li);
}
}
// Simple modal for contextual help
const modal = document.createElement('div');
modal.className = 'fixed inset-0 hidden items-center justify-center bg-black/60';
modal.innerHTML = `
<div class="bg-slate-900 border border-slate-700 rounded-xl max-w-2xl w-[90%] p-4">
<div class="flex items-center justify-between">
<h3 class="font-semibold">Guidance</h3>
<button id="modalClose" class="text-slate-400">✕</button>
</div>
<div id="modalBody" class="mt-3 text-sm space-y-2"></div>
</div>`;
document.body.appendChild(modal);
document.body.addEventListener('click', async (e)=>{
const serviceBtn = e.target.closest('[data-service-lead]');
const guideLink = e.target.closest('[data-finding-guide]');
const btn = e.target.closest('[data-help]');
const metricBtn = e.target.closest('[data-metric]');
if (serviceBtn) {
e.preventDefault();
requestService(serviceBtn.dataset.serviceLead, 'report_fix');
return;
}
if (guideLink) {
e.preventDefault();
const key = guideLink.dataset.findingGuide || '';
const plan = guideLink.dataset.guidePlan || 'priority_audit';
trackEvent('Revenue', 'report_problem_guide', key);
await recordLead('report_problem_guide', plan);
window.location.href = guideLink.href;
return;
}
if (btn) {
const f = JSON.parse(decodeURIComponent(btn.dataset.help));
const body = document.getElementById('modalBody');
body.innerHTML = '';
const why = document.createElement('div');
const what = document.createElement('div');
const how = document.createElement('div');
why.innerHTML = `<div class="text-slate-400">Warum relevant</div><div>${explainWhy(f)}</div>`;
what.innerHTML = `<div class="text-slate-400">Bedeutung</div><div>${explainWhat(f)}</div>`;
how.innerHTML = `<div class="text-slate-400">Was tun</div><div>${explainHow(f)}</div>`;
body.appendChild(why); body.appendChild(what); body.appendChild(how);
modal.classList.remove('hidden');
modal.classList.add('flex');
}
if (metricBtn) {
const metric = metricBtn.dataset.metric;
const exp = explainMetric(metric);
const body = document.getElementById('modalBody');
body.innerHTML = '';
const why = document.createElement('div');
const what = document.createElement('div');
const how = document.createElement('div');
why.innerHTML = `<div class="text-slate-400">Warum relevant</div><div>${exp.why || '-'}</div>`;
what.innerHTML = `<div class="text-slate-400">Bedeutung</div><div>${exp.what || '-'}</div>`;
how.innerHTML = `<div class="text-slate-400">Was tun</div><div>${exp.how || '-'}</div>`;
body.appendChild(why); body.appendChild(what); body.appendChild(how);
modal.classList.remove('hidden');
modal.classList.add('flex');
}
if (e.target.id==='modalClose' || e.target===modal) {
modal.classList.add('hidden');
modal.classList.remove('flex');
}
});
function explainWhy(f){
switch(f.category){
case 'dns-hygiene': return 'Sauberes DNS reduziert Fehlkonfigurationen, Kompatibilitätsprobleme und unerwartetes Cache-Verhalten.';
case 'delegation': return 'Delegationsfehler können die Erreichbarkeit der gesamten Domain/Zone brechen.';
case 'zone-transfer': return 'Offene Zone-Transfers (AXFR) können die komplette Zonendatei offenlegen.';
case 'recursion': return 'Open Recursion kann für Abuse/Amplification missbraucht werden.';
case 'reachability': return 'Ohne passende A/AAAA-Records ist die Domain ggf. nicht erreichbar oder Services funktionieren nicht erwartungsgemäß.';
case 'mail': return 'Mail-Konfiguration beeinflusst Zustellbarkeit und verhindert Fehlzustellungen.';
case 'spf': return 'SPF hilft gegen Spoofing und verbessert Zustellbarkeit; fehlerhafte SPF-Checks können Mails ablehnen lassen.';
case 'dmarc': return 'DMARC setzt Policy und Reporting auf Basis von SPF/DKIM – wichtig für Marken-Schutz und Deliverability.';
case 'caa': return 'CAA kann Mississuance reduzieren, indem nur ausgewählte CAs Zertifikate ausstellen dürfen.';
case 'smtp': return 'Erreichbarkeit/STARTTLS von SMTP ist relevant für zuverlässigen Mail-Empfang und Security.';
case 'dnssec': return 'DNSSEC schützt vor Manipulation; fehlerhafte Signaturen führen bei validierenden Resolvern zu SERVFAIL.';
default: return 'Dieses Thema beeinflusst typischerweise Integrität, Verfügbarkeit oder Zustellbarkeit.';
}
}
function explainWhat(f){
return f.message || 'Siehe Details im Report.';
}
function explainHow(f){
const msg = (f && f.message) ? String(f.message) : '';
if (f && f.category === 'dns-hygiene' && msg.includes('Apex')) {
return 'Am Zonen-Apex keinen CNAME verwenden. Vorgehen: (1) Ziel des CNAME identifizieren, (2) A/AAAA Records auf die richtigen IP(s) setzen (oder ALIAS/ANAME beim Provider nutzen, falls erforderlich), (3) CNAME entfernen, (4) nach TTL erneut testen.';
}
if (f && f.category === 'delegation' && msg.includes('Parent/Child NS')) {
return 'Parent/Child NS angleichen: (1) Child-NS in der Zone prüfen, (2) Parent-NS bei Registry/Registrar prüfen, (3) NS-Set identisch machen, (4) alte NS erst nach TTL/Propagation abschalten, (5) erneut scannen.';
}
if (f && f.category === 'delegation' && msg.includes('Glue')) {
return 'Glue prüfen/korrigieren: (1) A/AAAA der NS-Hostnames ermitteln, (2) Glue-IPs beim Parent/Registrar damit abgleichen, (3) Glue aktualisieren oder NS-Host-A/AAAA korrigieren, (4) TTL/Propagation abwarten, (5) erneut scannen.';
}
if (f && f.category === 'delegation' && msg.startsWith('Lame NS')) {
return 'Lame NS beheben: (1) betroffenen Nameserver prüfen (antwortet er autoritativ für die Zone?), (2) Konfiguration/Zone auf diesem NS reparieren oder NS entfernen, (3) Delegation (Parent+Child) aktualisieren, (4) erneut scannen.';
}
if (f && f.category === 'zone-transfer' && msg.includes('AXFR')) {
return 'AXFR absichern: (1) AXFR auf autoritativen NS deaktivieren oder nur Secondary/Trusted IPs erlauben, (2) ggf. TSIG einsetzen, (3) von extern erneut testen.';
}
if (f && f.category === 'recursion' && msg.includes('Recursion')) {
return 'Open Recursion abschalten: (1) Recursion auf autoritativem Nameserver deaktivieren, (2) wenn nötig nur intern erlauben, (3) von extern prüfen, dass kein RA/Recursive-Answer mehr möglich ist.';
}
if (f && f.category === 'mail' && msg.includes('Keine MX')) {
return 'Wenn die Domain E-Mails empfangen soll: (1) MX Records beim DNS-Provider setzen, (2) Ziel-Hosts müssen korrekt auflösbar sein (A/AAAA), (3) nach TTL erneut testen. Wenn bewusst keine Mail: Finding ignorieren.';
}
if (f && f.category === 'caa' && msg.includes('Keine CAA')) {
return 'Optional (empfohlen bei Security-Fokus): CAA Records setzen, um erlaubte Zertifikatsstellen zu definieren (z.B. `issue \"letsencrypt.org\"`). Vorher prüfen, welche CA du nutzt; danach Zertifikat-Ausstellung testen.';
}
if (f && f.category === 'smtp' && msg.includes('SMTP nicht erreichbar')) {
return 'SMTP Reachability: (1) Prüfen, ob MX-Hosts korrekt zeigen, (2) Firewall/Provider erlaubt eingehend Port 25 (oder genutzten Port), (3) Service läuft und hört, (4) erneut testen.';
}
if (f && f.category === 'smtp' && msg.includes('STARTTLS')) {
return 'STARTTLS aktivieren: (1) TLS-Zertifikat auf dem Mailserver korrekt konfigurieren, (2) STARTTLS für eingehende Verbindungen aktivieren, (3) Cipher/Protocols modern halten, (4) erneut testen.';
}
if (f && f.category === 'spf' && msg.includes('Kein SPF')) {
return 'Einen SPF TXT Record anlegen (z.B. `v=spf1 include:... -all`). Nur legitime Sender aufnehmen und danach testen.';
}
if (f && f.category === 'spf' && msg.includes('10 DNS-Lookups')) {
return 'SPF „flatten“/vereinfachen: Includes reduzieren, unnötige Mechanismen entfernen, Ziel ≤10 Lookups. Danach SPF-Check testen.';
}
if (f && f.category === 'spf' && msg.includes('ptr')) {
return 'SPF ohne `ptr` verwenden (veraltet/zu teuer). Record bereinigen und testen.';
}
if (f && f.category === 'dmarc' && msg.includes('Kein DMARC')) {
return 'DMARC TXT Record unter `_dmarc` anlegen (Start z.B. `v=DMARC1; p=none; rua=mailto:...`). Danach Reporting auswerten.';
}
if (f && f.category === 'dmarc' && msg.includes('Policy ist none')) {
return 'Nach Monitoring (rua Reports) schrittweise härten: erst `quarantine`, später `reject` (Alignment prüfen, legitime Sender fixen).';
}
if (f && f.category === 'dnssec' && msg.includes('DS vorhanden') && msg.includes('keine DNSKEY')) {
return 'DNSSEC-Konfiguration prüfen: Wenn DNSSEC aktiv sein soll, müssen DNSKEYs publiziert sein. Wenn nicht, DS beim Parent entfernen.';
}
if (f && f.category === 'dnssec' && msg.includes('DS passt')) {
return 'DS beim Parent/Registry auf den aktuellen KSK/DNSKEY aktualisieren (häufig nach Key-Rollover). Danach Validierung testen.';
}
if (f && f.category === 'dnssec' && msg.includes('RRSIG')) {
return 'Signer/Provider so konfigurieren, dass rechtzeitig resigniert wird (RRSIG nicht ablaufen lassen). Monitoring auf Expiry aktivieren.';
}
switch(f.category){
case 'dns-hygiene': return 'Apex nicht als CNAME betreiben; am Zonen-Apex A/AAAA verwenden. Allgemein: Zonendaten konsistent halten und ungewöhnliche Konstrukte vermeiden.';
case 'delegation': return 'Parent/Child NS angleichen, Glue-IPs korrigieren und lame Nameserver ersetzen. Nach Änderungen TTL/Propagation einplanen.';
case 'zone-transfer': return 'AXFR deaktivieren oder strikt auf Secondary/Trusted IPs beschränken.';
case 'recursion': return 'Recursion auf autoritativen Nameservern deaktivieren oder auf interne Netze beschränken.';
case 'reachability': return 'A/AAAA Records prüfen, Hosting/Loadbalancer korrekt konfigurieren und DNS-Änderungen mit passenden TTLs planen.';
case 'mail': return 'MX Records prüfen/korrigieren. Für Security: SPF/DMARC (und DKIM im Mailsystem) sauber einrichten und testen.';
case 'spf': return 'Includes/Mechanismen so reduzieren, dass ≤10 DNS-Lookups entstehen; ptr vermeiden; ein `all`-Mechanismus setzen (meist `-all` oder `~all`).';
case 'dmarc': return 'Mit `p=none` starten (Monitoring), dann schrittweise Richtung `quarantine/reject`. `rua` Reports aktivieren und Fehlkonfigurationen (Alignment) beheben.';
case 'caa': return 'CAA Records setzen (z.B. nur Let\'s Encrypt/gewünschte CA erlauben). Für Wildcards `issuewild` pflegen. Änderungen testen.';
case 'smtp': return 'Firewall/Ports prüfen (25/465/587 je nach Setup), MX-Hosts erreichbar machen, STARTTLS aktivieren und Zertifikate korrekt bereitstellen.';
case 'dnssec': return 'Signer so konfigurieren, dass RRSIGs rechtzeitig resigniert werden. Bei Key-Rollovers DS beim Parent korrekt aktualisieren.';
default: return 'Best Practices der jeweiligen Metrik anwenden und DNS-Provider/Zonefile-Konfiguration prüfen.';
}
}
async function load() {
const apiBase = '';
let result = {};
if (scanId) {
const data = await fetchJSON(`/v1/scan/${scanId}`);
result = data.result || {};
result.created_at = data.created_at;
title.textContent = `Scan: ${result.domain || data.domain_id}`;
} else if (pathDomain) {
const rep = await fetchJSON(`/v1/report/${pathDomain}`);
result = rep.result || {};
result.created_at = rep.created_at;
result.domain = rep.domain || pathDomain;
title.textContent = `Scan: ${result.domain}`;
}
renderSummary(result);
renderMetrics(result);
renderFindings(result.findings || []);
renderReportAuditRequest(result);
}
if (reportAuditForm) {
reportAuditForm.addEventListener('submit', async (event) => {
event.preventDefault();
const domain = reportAuditDomain && reportAuditDomain.value.trim() ? reportAuditDomain.value.trim().toLowerCase() : currentReportDomain();
const email = reportAuditEmail ? reportAuditEmail.value.trim().toLowerCase() : '';
const message = reportAuditMessage ? reportAuditMessage.value.trim() : '';
if (!email || !message) {
if (reportAuditStatus) reportAuditStatus.textContent = 'Bitte E-Mail und Problem ergänzen.';
return;
}
if (reportAuditStatus) reportAuditStatus.textContent = 'Audit-Anfrage wird gespeichert…';
trackEvent('Revenue', 'report_qualified_audit', 'priority_audit');
await recordLead('qualified_fix_request', 'priority_audit', {
domain,
contact_email: email,
message,
page: window.location.pathname + window.location.search + '#report-audit-request'
});
if (reportAuditStatus) reportAuditStatus.textContent = 'Gespeichert. Der Warenkorb wird geöffnet…';
setTimeout(() => {
if (submitHostBillOrder('priority_audit')) return;
window.location.href = orderUrlFor('priority_audit') || supportTicketUrl('qualified_fix_request', 'priority_audit');
}, 250);
});
}
refreshBtn.addEventListener('click', async () => {
const key = apikeyInput.value || 'dev-key';
const dom = (pathDomain ? pathDomain : title.textContent.replace(/^Scan:\s*/, ''))
await fetch('/v1/scan', { method:'POST', headers:{'x-api-key': key, 'Content-Type':'application/json'}, body: JSON.stringify({domain: dom}) });
const t = document.createElement('div');
t.className = 'fixed top-4 right-4 bg-emerald-900 text-emerald-100 border border-emerald-700 px-3 py-2 rounded shadow';
t.textContent = 'New scan started';
document.body.appendChild(t);
setTimeout(()=>{ t.remove(); }, 2500);
setTimeout(load, 1000);
});
deleteBtn.addEventListener('click', async () => {
const key = apikeyInput.value || 'dev-key';
const id = scanId;
if (!id) return;
try {
const res = await fetch(`/v1/scan/${id}`, { method: 'DELETE', headers: { 'x-api-key': key } });
if (!res.ok) throw new Error('HTTP '+res.status);
const t = document.createElement('div');
t.className = 'fixed top-4 right-4 bg-emerald-900 text-emerald-100 border border-emerald-700 px-3 py-2 rounded shadow';
t.textContent = 'Scan deleted';
document.body.appendChild(t);
setTimeout(()=>{ t.remove(); }, 2000);
setTimeout(()=>{ location.href = '/app/'; }, 800);
} catch (e) {
const t = document.createElement('div');
t.className = 'fixed top-4 right-4 bg-red-900 text-red-100 border border-red-700 px-3 py-2 rounded shadow max-w-sm';
t.textContent = 'Failed to delete';
document.body.appendChild(t);
setTimeout(()=>{ t.remove(); }, 3000);
}
});
load();
// Footer year
document.getElementById('year').textContent = new Date().getFullYear();
// Matomo config + tracking
(async () => {
try {
await import('/config.js');
const cfg = window.__APP_CONFIG__ || {};
if (cfg.MATOMO_URL && cfg.MATOMO_SITE_ID) {
var _paq = (window._paq = window._paq || []);
_paq.push(['trackPageView']);
_paq.push(['enableLinkTracking']);
(function() {
var u = cfg.MATOMO_URL.endsWith('/') ? cfg.MATOMO_URL : cfg.MATOMO_URL + '/';
_paq.push(['setTrackerUrl', u + 'matomo.php']);
_paq.push(['setSiteId', cfg.MATOMO_SITE_ID]);
var d = document, g = d.createElement('script'), s = d.getElementsByTagName('script')[0];
g.async = true; g.src = u + 'matomo.js'; s.parentNode.insertBefore(g, s);
})();
}
} catch (e) { /* ignore */ }
})();