HEX
Server: LiteSpeed
System: Linux houston.panomity.com 6.8.0-100-generic #100-Ubuntu SMP PREEMPT_DYNAMIC Tue Jan 13 16:40:06 UTC 2026 x86_64
User: nudepix (1011)
PHP: 7.4.33
Disabled: pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,pcntl_unshare,
Upload Files
File: //proc/self/root/tmp/ezdns_detail.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');

      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) {
        try {
          await fetch('/v1/leads', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({
              source: 'report',
              intent,
              plan,
              domain: currentReportDomain(),
              page: window.location.pathname + window.location.search
            })
          });
        } catch (e) { /* lead tracking must never block sales */ }
      }

      const ORDER_URLS = {
        quick_fix: 'https://support.panomity.com/?cmd=cart&action=add&id=867',
        cleanup: 'https://support.panomity.com/?cmd=cart&action=add&id=868',
        managed_monitoring: 'https://support.panomity.com/?cmd=cart&action=add&id=856'
      };

      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] || '';
      }

      async function requestService(plan, intent = 'report_fix') {
        trackEvent('Revenue', intent, plan);
        await recordLead(intent, plan);
        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 renderTopFixes(findingsList) {
        const host = document.getElementById('topFixes');
        if (!host) return;
        host.innerHTML = '';

        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));
        });

        const top = all.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 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>
              </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 kritische DNS-, DNSSEC- oder Mail-Findings: 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="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', (e)=>{
        const serviceBtn = e.target.closest('[data-service-lead]');
        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 (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 || []);
      }

      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 */ }
      })();