LidelEra.user.js

ZvQjM5qv

RawDownload
Sprache
JavaScript
Größe
59953 Bytes
Erstellt am
01.11.2026, 16:06
Läuft ab am
01.11.2027, 16:06
JavaScript·18 Aufrufe
// ==UserScript==
// @name         LidelEra 1.0
// @namespace    warera-lite
// @version      1.0.0
// @description  LidelEra 1.0 – Performance-Optimierung, Blockaden-Analyse und Benchmark.
// @match        https://app.warera.io/*
// @run-at       document-start
// @noframes
// @grant        GM_getValue
// @grant        GM_setValue
// ==/UserScript==

(() => {
    'use strict';

    if (window.__WARERA_LITE__) return;
    window.__WARERA_LITE__ = true;

    const VERSION = '1.0.0';
    // Gleicher Prefix wie 8.4, damit Einstellungen und Installer-ID erhalten bleiben.
    const PREFIX = 'warera_lite_84_';
    const DE = (navigator.language || '').toLowerCase().startsWith('de');
    const L = (de, en) => (DE ? de : en);

    const MAX_TRACE = 200;
    const PHASE_MS = 15000;                 // Länge einer Benchmark-Phase
    const SETTLE_MS = 1000;                 // Einschwingzeit nach jedem Umschalten (wird nicht gemessen)
    const BENCH_PHASES = ['A', 'B', 'A', 'B']; // ABAB gleicht Drift (Cache, Spielzustand) aus
    const AUTO_CYCLE = 20000;               // AUTO misst nur 4 s alle 20 s -> kaum Eigenlast
    const AUTO_WINDOW = 4000;
    const LOAF_WINDOW = 300000;             // Blockaden-Analyse: letzte 5 Minuten
    const PROFILES = ['balanced', 'max', 'ultra'];
    const RANK = { balanced: 0, max: 1, ultra: 2 };

    const read = (key, fallback) => {
        try { return GM_getValue(PREFIX + key, fallback); }
        catch (_) { return fallback; }
    };
    const save = (key, value) => {
        try { GM_setValue(PREFIX + key, value); }
        catch (_) {}
    };

    const installerId = read('installerId', 'USER-' + Math.random().toString(36).slice(2, 8).toUpperCase());
    save('installerId', installerId);

    const storedProfile = read('profile', 'auto');
    const storedLauncher = read('launcherPosition', null);
    const CONFIG = {
        enabled: read('enabled', true) !== false,
        profile: storedProfile === 'auto' || PROFILES.includes(storedProfile) ? storedProfile : 'auto',
        launcherPosition: storedLauncher && Number.isFinite(storedLauncher.x) && Number.isFinite(storedLauncher.y)
            ? storedLauncher : { x: 16, y: 100 },
        dashboardPosition: read('dashboardPosition', null)
    };

    let effectiveProfile = CONFIG.profile === 'auto' ? 'max' : CONFIG.profile;
    let styleElement = null;
    let appliedKey = '';
    let launcher = null;
    let dashboard = null;
    let dashboardOpen = false;

    let frameHandle = 0;
    let lastFrame = 0;
    let sampledAuto = false;
    let frameSamples = [];
    let longTaskSamples = [];
    let interactionSamples = [];
    let loafSamples = [];
    let traceEvents = [];

    let longTaskObserver = null;
    let eventObserver = null;
    let loafObserver = null;
    let dashboardTimer = null;
    let benchmarkTimer = null;
    let benchmark = null;

    let lastAutoDecision = 0;
    let candidateProfile = null;
    let candidateSince = 0;

    const stats = {
        fps: 0, p50: 0, p95: 0, jank: 0, drops: 0,
        longTasks: 0, longTaskDuration: 0, longTaskAverage: 0, longTaskMax: 0, totalLongTasks: 0,
        task50: 0, task100: 0, task200: 0,
        interactions: 0, slowInteractions: 0, maxInteraction: 0,
        slowInput: 0, slowProc: 0, slowPres: 0,
        heap: 0, heapLimit: 0, resources: 0, slowResources: 0
    };

    const now = () => performance.now();
    const clamp = (v, min, max) => Math.max(min, Math.min(max, v));
    const avg = values => (values.length ? values.reduce((a, b) => a + b, 0) / values.length : 0);
    const esc = s => String(s).replace(/[&<>"]/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c]));

    // Schreibt nur, wenn sich der Inhalt geändert hat -> weniger DOM-Arbeit im Dashboard.
    function setHtml(el, html) {
        if (el && el._h !== html) {
            el._h = html;
            el.innerHTML = html;
        }
    }

    function percentile(values, p) {
        if (!values.length) return 0;
        const sorted = [...values].sort((a, b) => a - b);
        return sorted[Math.min(sorted.length - 1, Math.ceil(sorted.length * p) - 1)];
    }

    function addTrace(type, data = {}) {
        traceEvents.push({ time: now(), type, ...data });
        if (traceEvents.length > MAX_TRACE) traceEvents.splice(0, traceEvents.length - MAX_TRACE);
    }

    function activeProfile() {
        return CONFIG.profile === 'auto' ? effectiveProfile : CONFIG.profile;
    }

    // ------------------------------------------------------------
    // Optimierung (CSS)
    // Stufen sind abgestuft. Statt "animation/transition: none" werden Dauern
    // auf ~0 gesetzt, damit animationend/transitionend-Events des Spiels
    // weiterhin feuern (sonst können Dialoge/Menüs hängen bleiben).
    // ------------------------------------------------------------

    function buildCss(profile) {
        const rank = RANK[profile] ?? 1;

        // BALANCED: teuerster GPU-Effekt (Hintergrund-Blur) + Smooth-Scrolling
        let css = `
html { scroll-behavior: auto !important; }
*, *::before, *::after {
    backdrop-filter: none !important;
    -webkit-backdrop-filter: none !important;
}`;

        // MAX: + Textschatten, Übergangsanimationen praktisch sofort
        if (rank >= 1) css += `
*, *::before, *::after {
    text-shadow: none !important;
    transition-duration: 0.001ms !important;
    transition-delay: 0s !important;
}`;

        // ULTRA: + Schatten, Filter, Dauer-Animationen, will-change, fixe Hintergründe
        if (rank >= 2) css += `
*, *::before, *::after {
    box-shadow: none !important;
    filter: none !important;
    animation-duration: 0.001ms !important;
    animation-delay: 0s !important;
    animation-iteration-count: 1 !important;
    animation-fill-mode: forwards !important;
    will-change: auto !important;
    background-attachment: scroll !important;
}
:focus-visible { outline: 2px solid #6aa9ff !important; outline-offset: 1px; }`;

        return css;
    }

    function applyOptimization() {
        const root = document.documentElement;
        if (!root) return;

        const key = CONFIG.enabled ? activeProfile() : 'off';
        if (key === appliedKey && styleElement && styleElement.isConnected) return;

        if (!styleElement) {
            styleElement = document.createElement('style');
            styleElement.id = 'warera-lite-style';
        }
        if (!styleElement.isConnected) (document.head || root).appendChild(styleElement);

        styleElement.textContent = CONFIG.enabled ? buildCss(activeProfile()) : '';
        root.dataset.wareraLite = key;
        appliedKey = key;
    }

    // ------------------------------------------------------------
    // Long Tasks
    // ------------------------------------------------------------

    function recordLongTask(entry) {
        const start = Number(entry.startTime);
        const duration = Number(entry.duration);
        if (!Number.isFinite(start) || !Number.isFinite(duration) || duration < 50) return;

        const end = start + duration;

        // Duplikate nur unter den letzten Einträgen suchen (Einträge kommen sortiert).
        for (let i = longTaskSamples.length - 1, n = 0; i >= 0 && n < 5; i--, n++) {
            if (longTaskSamples[i].start === start && longTaskSamples[i].end === end) return;
        }

        longTaskSamples.push({ start, end, duration });
        stats.totalLongTasks++;

        if (longTaskSamples.length > 1000) {
            const cutoff = now() - 120000;
            longTaskSamples = longTaskSamples.filter(item => item.end >= cutoff);
        }

        addTrace('LONG_TASK', { start: Math.round(start), duration: Math.round(duration) });

        if (CONFIG.profile === 'auto' && !benchmark?.running) evaluateAutomaticProfile();
    }

    function setupLongTaskObserver() {
        if (!('PerformanceObserver' in window)) return;
        const callback = list => { for (const entry of list.getEntries()) recordLongTask(entry); };

        try {
            longTaskObserver = new PerformanceObserver(callback);
            longTaskObserver.observe({ type: 'longtask', buffered: true });
        } catch (_) {
            try {
                longTaskObserver = new PerformanceObserver(callback);
                longTaskObserver.observe({ entryTypes: ['longtask'] });
            } catch (_) {
                longTaskObserver = null;
            }
        }
    }

    function unionDuration(intervals, windowStart, windowEnd) {
        const clipped = [];
        for (const item of intervals) {
            const s = Math.max(item.start, windowStart);
            const e = Math.min(item.end, windowEnd);
            if (e > s) clipped.push([s, e]);
        }
        if (!clipped.length) return 0;

        clipped.sort((a, b) => a[0] - b[0]);

        let total = 0;
        let s = clipped[0][0];
        let e = clipped[0][1];
        for (let i = 1; i < clipped.length; i++) {
            const next = clipped[i];
            if (next[0] <= e) e = Math.max(e, next[1]);
            else { total += e - s; s = next[0]; e = next[1]; }
        }
        return total + e - s;
    }

    function getLongTaskMetrics(start, end) {
        const selected = longTaskSamples.filter(item => item.start < end && item.end > start);

        let task50 = 0, task100 = 0, task200 = 0, max = 0;
        for (const item of selected) {
            if (item.duration >= 200) task200++;
            else if (item.duration >= 100) task100++;
            else task50++;
            if (item.duration > max) max = item.duration;
        }

        return {
            count: selected.length,
            duration: Math.round(unionDuration(selected, start, end)),
            max: Math.round(max),
            task50, task100, task200
        };
    }

    function updateLongTaskStats() {
        const t = now();
        const result = getLongTaskMetrics(t - 30000, t);
        stats.longTasks = result.count;
        stats.longTaskDuration = result.duration;
        stats.longTaskAverage = result.count ? result.duration / result.count : 0;
        stats.longTaskMax = result.max;
        stats.task50 = result.task50;
        stats.task100 = result.task100;
        stats.task200 = result.task200;
    }

    // ------------------------------------------------------------
    // Long Animation Frames (Chrome 123+)
    // Zeigt, WELCHES Skript einen langsamen Frame verursacht hat und ob die Zeit
    // in Skripten oder im Rendering (Style/Layout/Paint) verloren geht.
    // ------------------------------------------------------------

    function classifySource(url) {
        if (!url) return { origin: L('inline/unbekannt', 'inline/unknown'), file: '' };
        try {
            const u = new URL(url, location.href);
            const file = (u.pathname.split('/').pop() || u.pathname).slice(0, 40);
            let origin;
            if (u.protocol === 'chrome-extension:' || u.protocol === 'moz-extension:') origin = L('Erweiterung', 'extension');
            else if (u.origin === location.origin) origin = L('Spiel', 'game');
            else origin = u.hostname;
            return { origin, file };
        } catch (_) {
            return { origin: '?', file: String(url).slice(0, 40) };
        }
    }

    function recordLoaf(entry) {
        const start = Number(entry.startTime);
        const duration = Number(entry.duration);
        if (!Number.isFinite(start) || !(duration >= 50)) return;

        const renderStart = Number(entry.renderStart) > 0 ? Number(entry.renderStart) : start + duration;
        let scriptTotal = 0;
        let forced = 0;
        const scripts = [];

        for (const s of entry.scripts || []) {
            const dur = Number(s.duration) || 0;
            scriptTotal += dur;
            forced += Number(s.forcedStyleAndLayoutDuration) || 0;

            if (dur >= 5) {   // nur relevante Skripte merken
                const c = classifySource(s.sourceURL);
                const fn = String(s.sourceFunctionName || '').slice(0, 40);
                const invoker = String(s.invoker || '').slice(0, 50);
                scripts.push({
                    key: `${c.origin}|${c.file}|${fn || invoker}`,
                    origin: c.origin, file: c.file, fn, invoker, duration: dur
                });
            }
        }

        loafSamples.push({
            start,
            end: start + duration,
            blocking: Number(entry.blockingDuration) || 0,
            script: scriptTotal,
            render: Math.max(0, start + duration - renderStart),
            forced,
            scripts
        });

        if (loafSamples.length > 300) loafSamples.splice(0, 100);
    }

    function setupLoafObserver() {
        if (!('PerformanceObserver' in window)) return;

        try {
            const supported = PerformanceObserver.supportedEntryTypes;
            if (supported && !supported.includes('long-animation-frame')) return;

            loafObserver = new PerformanceObserver(list => {
                for (const entry of list.getEntries()) recordLoaf(entry);
            });
            loafObserver.observe({ type: 'long-animation-frame', buffered: true });
        } catch (_) {
            loafObserver = null;
        }
    }

    function getLoafSummary(from, to) {
        const items = loafSamples.filter(s => s.end > from && s.start < to);
        const sum = { count: items.length, blocking: 0, script: 0, render: 0, forced: 0, sources: [] };
        const map = new Map();

        for (const s of items) {
            sum.blocking += s.blocking;
            sum.script += s.script;
            sum.render += s.render;
            sum.forced += s.forced;

            for (const sc of s.scripts) {
                let e = map.get(sc.key);
                if (!e) {
                    e = { origin: sc.origin, file: sc.file, fn: sc.fn, invoker: sc.invoker, total: 0, count: 0, max: 0 };
                    map.set(sc.key, e);
                }
                e.total += sc.duration;
                e.count++;
                if (sc.duration > e.max) e.max = sc.duration;
            }
        }

        sum.sources = [...map.values()].sort((a, b) => b.total - a.total).slice(0, 8);
        return sum;
    }

    function loafHtml(loaf) {
        if (!loafObserver) {
            return `<div style="color:#9ba9bf">${L('Von diesem Browser nicht unterstützt (Chrome 123+ nötig).', 'Not supported by this browser (needs Chrome 123+).')}</div>`;
        }
        if (!loaf.count) {
            return `<div>${L('Keine langsamen Frames in den letzten 5 Minuten.', 'No slow frames in the last 5 minutes.')}</div>`;
        }

        const total = loaf.script + loaf.render;
        const pct = v => (total > 0 ? Math.round(100 * v / total) : 0);

        let hint = '';
        if (pct(loaf.script) >= 60) {
            hint = L('→ Skripte sind der Engpass. CSS-Optimierung hilft hier kaum.', '→ Scripts are the bottleneck. CSS optimization barely helps.');
        } else if (pct(loaf.render) >= 40) {
            hint = L('→ Rendering ist ein relevanter Anteil. Höhere Profile können helfen.', '→ Rendering is a significant share. Higher profiles may help.');
        }

        let html = `
            <div>${L('Langsame Frames (>50 ms)', 'Slow frames (>50 ms)')}: <b>${loaf.count}</b> | ${L('blockierend', 'blocking')}: <b>${Math.round(loaf.blocking)} ms</b></div>
            <div>${L('Skript-Ausführung', 'Script execution')}: <b>${Math.round(loaf.script)} ms</b> (${pct(loaf.script)} %)</div>
            <div>${L('Rendering (Style/Layout/Paint)', 'Rendering (style/layout/paint)')}: <b>${Math.round(loaf.render)} ms</b> (${pct(loaf.render)} %)</div>
            <div>${L('davon erzwungenes Layout', 'of which forced layout')}: <b>${Math.round(loaf.forced)} ms</b></div>
            ${hint ? `<div style="color:#f2c96b;margin-top:4px">${hint}</div>` : ''}
            <div style="margin-top:6px;color:#9ba9bf">${L('Top-Verursacher (Skriptzeit):', 'Top culprits (script time):')}</div>`;

        for (const s of loaf.sources) {
            const name = s.fn || s.invoker || L('(anonym)', '(anonymous)');
            html += `<div style="font-size:11px;word-break:break-all;margin-top:2px">• <b>${esc(s.origin)}</b>/${esc(s.file)} – ${esc(name)}<br>&nbsp;&nbsp;${Math.round(s.total)} ms, ${s.count}×, max ${Math.round(s.max)} ms</div>`;
        }

        return html;
    }

    // ------------------------------------------------------------
    // Frame-Messung
    // Dauerhaftes requestAnimationFrame kostet selbst CPU. Im AUTO-Modus
    // läuft die Messung deshalb nur in kurzen Fenstern (4 s alle 20 s).
    // Dauerhaft nur bei offenem Dashboard oder laufendem Benchmark.
    // ------------------------------------------------------------

    function continuousTracking() {
        return dashboardOpen || Boolean(benchmark?.running);
    }

    function wantFrames() {
        if (document.hidden) return false;
        if (continuousTracking()) return true;
        return CONFIG.enabled && CONFIG.profile === 'auto' && (now() % AUTO_CYCLE) < AUTO_WINDOW;
    }

    function startFrameTracking() {
        if (!frameHandle && wantFrames()) {
            lastFrame = 0;
            frameHandle = requestAnimationFrame(frameLoop);
        }
    }

    function stopFrameTracking() {
        if (frameHandle) cancelAnimationFrame(frameHandle);
        frameHandle = 0;
        lastFrame = 0;
    }

    function frameLoop(timestamp) {
        frameHandle = 0;

        if (!wantFrames()) {
            lastFrame = 0;
            if (sampledAuto) {           // AUTO-Messfenster zu Ende -> auswerten
                sampledAuto = false;
                evaluateAutomaticProfile();
            }
            return;
        }

        if (lastFrame) {
            const delta = timestamp - lastFrame;

            if (delta >= 4 && delta <= 1000) {
                const sample = { time: timestamp, delta };
                frameSamples.push(sample);
                if (frameSamples.length > 900) frameSamples.splice(0, 300);

                if (!continuousTracking()) sampledAuto = true;

                if (benchmark?.running && timestamp >= benchmark.phaseStarted + SETTLE_MS) {
                    benchmark.acc[benchmark.cond].frames.push(sample);
                }

                if (delta > 50) addTrace('FRAME_DROP', { duration: Math.round(delta) });
            }
        }

        lastFrame = timestamp;
        frameHandle = requestAnimationFrame(frameLoop);
    }

    function getFrameMetrics(samples) {
        if (!samples.length) return { fps: 0, p50: 0, p95: 0, jank: 0, drops: 0 };

        const values = samples.map(item => item.delta);
        const average = values.reduce((a, b) => a + b, 0) / values.length;

        return {
            fps: Math.round(1000 / average),
            p50: Math.round(percentile(values, 0.50)),
            p95: Math.round(percentile(values, 0.95)),
            jank: values.filter(v => v > 50).length,
            drops: values.filter(v => v > 33.3).length
        };
    }

    function calculateFrameStats() {
        // Nur frische Samples (letzte 30 s) – verhindert veraltete Werte.
        const cutoff = now() - 30000;
        Object.assign(stats, getFrameMetrics(frameSamples.filter(s => s.time >= cutoff)));
    }

    // ------------------------------------------------------------
    // Interaktionen (mit Aufschlüsselung), Speicher, Ressourcen
    // ------------------------------------------------------------

    function describeTarget(node) {
        try {
            if (!node || !node.tagName) return '';
            let s = node.tagName.toLowerCase();
            if (node.id) s += '#' + String(node.id).slice(0, 30);
            else if (typeof node.className === 'string' && node.className.trim()) {
                s += '.' + node.className.trim().split(/\s+/)[0].slice(0, 30);
            }
            return s;
        } catch (_) {
            return '';
        }
    }

    function setupInteractionObserver() {
        if (!('PerformanceObserver' in window)) return;

        try {
            eventObserver = new PerformanceObserver(list => {
                for (const entry of list.getEntries()) {
                    if (!entry.interactionId) continue;

                    const start = entry.startTime;
                    const procStart = entry.processingStart || start;
                    const procEnd = entry.processingEnd || procStart;

                    const sample = {
                        time: start,
                        duration: entry.duration,
                        name: entry.name,
                        input: Math.max(0, procStart - start),                  // Wartezeit: Hauptthread war beschäftigt
                        proc: Math.max(0, procEnd - procStart),                 // Handler-Laufzeit
                        pres: Math.max(0, start + entry.duration - procEnd)     // bis das Ergebnis sichtbar ist
                    };

                    interactionSamples.push(sample);
                    if (interactionSamples.length > 300) interactionSamples.splice(0, 100);

                    if (entry.duration > 100) {
                        addTrace('SLOW_INTERACTION', {
                            duration: Math.round(entry.duration),
                            name: entry.name,
                            target: describeTarget(entry.target),
                            input: Math.round(sample.input),
                            proc: Math.round(sample.proc),
                            pres: Math.round(sample.pres)
                        });
                    }
                }
            });
            eventObserver.observe({ type: 'event', buffered: true, durationThreshold: 40 });
        } catch (_) {
            eventObserver = null;
        }
    }

    function calculateInteractionStats() {
        const cutoff = now() - 60000;
        const recent = interactionSamples.filter(item => item.time >= cutoff);
        const slow = recent.filter(item => item.duration > 100);

        stats.interactions = recent.length;
        stats.slowInteractions = slow.length;
        stats.maxInteraction = recent.reduce((max, item) => Math.max(max, item.duration), 0);
        stats.slowInput = avg(slow.map(i => i.input));
        stats.slowProc = avg(slow.map(i => i.proc));
        stats.slowPres = avg(slow.map(i => i.pres));
    }

    function collectMemory() {
        if (!performance.memory) return;
        stats.heap = Math.round(performance.memory.usedJSHeapSize / 1048576);
        stats.heapLimit = Math.round(performance.memory.jsHeapSizeLimit / 1048576);
    }

    function collectResources() {
        const cutoff = now() - 60000;
        const entries = performance.getEntriesByType('resource');
        let count = 0, slow = 0;

        for (let i = entries.length - 1; i >= 0; i--) {
            const entry = entries[i];
            if (entry.startTime < cutoff) break;
            count++;
            if (entry.duration > 1000 && entry.responseEnd > 0) slow++;
        }

        stats.resources = count;
        stats.slowResources = slow;
    }

    function calculateAll() {
        calculateFrameStats();
        updateLongTaskStats();
        calculateInteractionStats();
        collectMemory();
        collectResources();
    }

    // ------------------------------------------------------------
    // AUTO-Profil mit Hysterese
    // - ULTRA nur, wenn das RENDERN sichtbar leidet (P95/FPS oder hoher
    //   Rendering-Anteil laut LoAF). Reine JavaScript-Blockaden (Long Tasks)
    //   bessert CSS kaum, dafür würde ULTRA nur Optik opfern -> höchstens MAX.
    // - Hochschalten schnell (5 s), Herunterschalten Stufe für Stufe, nur bei
    //   deutlich besseren Werten und nach 45 s Stabilität.
    // ------------------------------------------------------------

    function chooseProfile() {
        const n = stats.longTasks;
        const d = stats.longTaskDuration;
        const p95 = stats.p95;
        const fps = stats.fps;
        const hasFrames = fps > 0;

        const t = now();
        const loaf = getLoafSummary(t - 30000, t);
        const renderBound = loaf.count >= 3 && loaf.render >= 400 &&
            loaf.render >= 0.4 * (loaf.script + loaf.render);

        const severe = p95 >= 45 || (hasFrames && fps < 25) || renderBound;
        const moderate = p95 >= 28 || (hasFrames && fps < 45) || n >= 4 || d >= 500;

        const desired = severe ? 'ultra' : moderate ? 'max' : 'balanced';

        if (RANK[desired] < RANK[effectiveProfile]) {
            const lim = effectiveProfile === 'ultra'
                ? { n: 7, d: 1100, p95: 34, fps: 33, render: 250 }
                : { n: 3, d: 375, p95: 21, fps: 50, render: 400 };

            const calm = n < lim.n && d < lim.d && p95 < lim.p95 &&
                (!hasFrames || fps >= lim.fps) && loaf.render < lim.render;
            if (!calm) return effectiveProfile;

            return PROFILES[Math.max(RANK[desired], RANK[effectiveProfile] - 1)];
        }

        return desired;
    }

    function evaluateAutomaticProfile() {
        if (!CONFIG.enabled || CONFIG.profile !== 'auto' || document.hidden || benchmark?.running) return;

        const t = now();
        if (t - lastAutoDecision < 5000) return;
        lastAutoDecision = t;

        calculateFrameStats();
        updateLongTaskStats();

        const desired = chooseProfile();

        if (desired === effectiveProfile) {
            candidateProfile = null;
            candidateSince = 0;
            return;
        }

        if (candidateProfile !== desired) {
            candidateProfile = desired;
            candidateSince = t;
            return;
        }

        const worsening = RANK[desired] > RANK[effectiveProfile];
        if (t - candidateSince < (worsening ? 5000 : 45000)) return;

        effectiveProfile = desired;
        candidateProfile = null;
        candidateSince = 0;

        addTrace('PROFILE', { name: desired });
        applyOptimization();
        if (dashboardOpen) updateDashboard();
    }

    function setProfile(profile) {
        if (benchmark?.running) return;
        if (profile !== 'auto' && !PROFILES.includes(profile)) return;

        CONFIG.profile = profile;
        save('profile', profile);

        candidateProfile = null;
        candidateSince = 0;

        if (profile !== 'auto') effectiveProfile = profile;

        applyOptimization();
        startFrameTracking();
        if (dashboardOpen) updateDashboard();
    }

    function setEnabled(value) {
        if (benchmark?.running) return;
        CONFIG.enabled = Boolean(value);
        save('enabled', CONFIG.enabled);
        applyOptimization();
        startFrameTracking();
        if (dashboardOpen) updateDashboard();
    }

    // ------------------------------------------------------------
    // Benchmark: ABAB (4 x 15 s), erste Sekunde jeder Phase wird verworfen
    // ------------------------------------------------------------

    const newAcc = () => ({
        frames: [], ms: 0, count: 0, duration: 0, max: 0, t50: 0, t100: 0, t200: 0,
        loafCount: 0, loafBlocking: 0, loafScript: 0, loafRender: 0
    });

    function beginBenchmarkPhase(index) {
        benchmark.index = index;
        benchmark.cond = BENCH_PHASES[index];
        benchmark.phaseStarted = now();
        benchmark.lastSecond = -1;

        CONFIG.enabled = benchmark.cond === 'B';   // wird bewusst nicht gespeichert
        applyOptimization();
        lastFrame = 0;

        addTrace('BENCHMARK_PHASE', { phase: benchmark.cond, index: index + 1 });
        renderBenchmarkLabel();
    }

    function finishBenchmarkPhase() {
        const end = now();
        const from = benchmark.phaseStarted + SETTLE_MS;
        const acc = benchmark.acc[benchmark.cond];
        const m = getLongTaskMetrics(from, end);
        const lf = getLoafSummary(from, end);

        acc.ms += Math.max(0, end - from);
        acc.count += m.count;
        acc.duration += m.duration;
        acc.max = Math.max(acc.max, m.max);
        acc.t50 += m.task50;
        acc.t100 += m.task100;
        acc.t200 += m.task200;
        acc.loafCount += lf.count;
        acc.loafBlocking += lf.blocking;
        acc.loafScript += lf.script;
        acc.loafRender += lf.render;
    }

    function summarize(acc) {
        const f = getFrameMetrics(acc.frames);
        const share = v => (acc.ms ? Math.round(1000 * v / acc.ms) / 10 : 0);
        return {
            ...f,
            frames: acc.frames.length,
            seconds: Math.round(acc.ms / 100) / 10,
            longTasks: acc.count,
            longTaskDuration: Math.round(acc.duration),
            longTaskShare: share(acc.duration),
            longTaskMax: Math.round(acc.max),
            loafCount: acc.loafCount,
            loafBlocking: Math.round(acc.loafBlocking),
            loafBlockingShare: share(acc.loafBlocking),
            loafScript: Math.round(acc.loafScript),
            loafRender: Math.round(acc.loafRender)
        };
    }

    function renderBenchmarkLabel() {
        const el = dashboard?.querySelector('#wl-benchmark');
        if (!el || !benchmark?.running) return;

        const elapsed = Math.min(PHASE_MS / 1000, Math.floor((now() - benchmark.phaseStarted) / 1000));
        const label = benchmark.cond === 'A'
            ? L('A – Optimierung AUS', 'A – optimization OFF')
            : L('B – Optimierung EIN', 'B – optimization ON');

        el.textContent = L(
            `Benchmark läuft (${benchmark.index + 1}/${BENCH_PHASES.length})\n${label} – ${elapsed}/${PHASE_MS / 1000} s\nBitte währenddessen immer dasselbe tun (z. B. dieselbe Seite offen lassen).`,
            `Benchmark running (${benchmark.index + 1}/${BENCH_PHASES.length})\n${label} – ${elapsed}/${PHASE_MS / 1000} s\nPlease keep doing the same thing meanwhile.`
        );
    }

    function setControlsDisabled(disabled) {
        if (!dashboard) return;
        dashboard.querySelectorAll('#wl-profile, #wl-power, #wl-benchmark-start')
            .forEach(el => { el.disabled = disabled; });
    }

    function restoreAfterBenchmark() {
        CONFIG.enabled = benchmark.original.enabled;
        CONFIG.profile = benchmark.original.profile;
        effectiveProfile = benchmark.original.effective;

        save('enabled', CONFIG.enabled);
        save('profile', CONFIG.profile);

        applyOptimization();
        setControlsDisabled(false);
    }

    function startBenchmark() {
        if (benchmark?.running || !dashboard) return;

        benchmark = {
            running: true,
            index: 0,
            cond: 'A',
            phaseStarted: 0,
            lastSecond: -1,
            testProfile: activeProfile(),
            acc: { A: newAcc(), B: newAcc() },
            original: { enabled: CONFIG.enabled, profile: CONFIG.profile, effective: effectiveProfile }
        };

        // AUTO ist während des Tests eingefroren (evaluateAutomaticProfile bricht ab).
        // Getestet wird das aktuell aktive Profil.
        setControlsDisabled(true);
        beginBenchmarkPhase(0);
        startFrameTracking();

        if (benchmarkTimer) clearInterval(benchmarkTimer);
        benchmarkTimer = setInterval(() => {
            if (!benchmark?.running) return;

            const elapsed = now() - benchmark.phaseStarted;

            if (elapsed >= PHASE_MS) {
                finishBenchmarkPhase();
                if (benchmark.index + 1 < BENCH_PHASES.length) beginBenchmarkPhase(benchmark.index + 1);
                else finishBenchmark();
            } else {
                const sec = Math.floor(elapsed / 1000);
                if (sec !== benchmark.lastSecond) {   // Label nur 1x pro Sekunde, kein Voll-Rerender
                    benchmark.lastSecond = sec;
                    renderBenchmarkLabel();
                }
            }
        }, 250);
    }

    function fmtResult(label, r) {
        const lines = [
            label,
            `FPS: ${r.fps} | P50: ${r.p50} ms | P95: ${r.p95} ms`,
            `Ruckler (>50 ms): ${r.jank} | Frame Drops (>33 ms): ${r.drops}`,
            `Long Tasks: ${r.longTasks} | Dauer: ${r.longTaskDuration} ms (${r.longTaskShare} % der Zeit)`,
            `Längste Task: ${r.longTaskMax} ms`
        ];

        if (loafObserver) {
            lines.push(`Blockierend (LoAF): ${r.loafBlocking} ms (${r.loafBlockingShare} %) | Skript ${r.loafScript} ms | Rendering ${r.loafRender} ms`);
        }

        lines.push(`Messzeit: ${r.seconds} s | Frames: ${r.frames}`);
        return lines.join('\n');
    }

    function buildVerdict(a, b) {
        const rel = (before, after) => (before > 0 ? (before - after) / before : 0);

        const better = [];
        const worse = [];

        const checks = [
            ['P95', rel(a.p95, b.p95), 0.10],
            ['FPS', a.fps > 0 ? (b.fps - a.fps) / a.fps : 0, 0.10],
            ['Long-Task-Anteil', a.longTaskShare >= 1 ? rel(a.longTaskShare, b.longTaskShare) : 0, 0.20]
        ];

        if (loafObserver) {
            checks.push(['Blockierzeit (LoAF)', a.loafBlockingShare >= 1 ? rel(a.loafBlockingShare, b.loafBlockingShare) : 0, 0.20]);
            checks.push(['Rendering-Zeit', a.loafRender >= 200 ? rel(a.loafRender, b.loafRender) : 0, 0.20]);
        }

        for (const [name, change, threshold] of checks) {
            if (change >= threshold) better.push(name);
            else if (change <= -threshold) worse.push(name);
        }

        if (better.length && !worse.length) return `Ergebnis: B (EIN) ist messbar besser (${better.join(', ')}).`;
        if (worse.length && !better.length) return `Ergebnis: B (EIN) ist schlechter (${worse.join(', ')}). Ein leichteres Profil testen.`;
        if (better.length && worse.length) return `Ergebnis: gemischt – besser: ${better.join(', ')}; schlechter: ${worse.join(', ')}.`;
        return 'Ergebnis: kein klarer Unterschied (liegt im Messrauschen). Dann ist eher JavaScript/Netzwerk der Engpass, nicht die Grafik.';
    }

    function finishBenchmark() {
        if (!benchmark?.running) return;

        benchmark.running = false;
        if (benchmarkTimer) clearInterval(benchmarkTimer);
        benchmarkTimer = null;

        restoreAfterBenchmark();
        startFrameTracking();

        const a = summarize(benchmark.acc.A);
        const b = summarize(benchmark.acc.B);
        benchmark.resultA = a;
        benchmark.resultB = b;

        const lines = [
            'A/B-Benchmark abgeschlossen (ABAB)',
            `Getestetes Profil: ${benchmark.testProfile.toUpperCase()}`,
            '',
            fmtResult('A – Optimierung AUS', a),
            '',
            fmtResult('B – Optimierung EIN', b),
            ''
        ];

        if (a.frames < a.seconds * 20 || b.frames < b.seconds * 20) {
            lines.push('Warnung: auffallend wenige Frames – war der Tab im Hintergrund? Test wiederholen.', '');
        }

        lines.push(buildVerdict(a, b), '', 'Ursprüngliche Einstellungen wiederhergestellt.');

        benchmark.report = lines.join('\n');
        benchmark.finishedAt = new Date().toISOString();

        addTrace('BENCHMARK_COMPLETE', { fpsA: a.fps, fpsB: b.fps, p95A: a.p95, p95B: b.p95 });

        if (dashboard) {
            updateDashboard();
            const el = dashboard.querySelector('#wl-benchmark');
            if (el) el.textContent = benchmark.report;
        }
    }

    function abortBenchmark() {
        if (!benchmark?.running) return;

        benchmark.running = false;
        if (benchmarkTimer) clearInterval(benchmarkTimer);
        benchmarkTimer = null;

        restoreAfterBenchmark();
        benchmark.report = null;
        benchmark.resultA = null;
        benchmark.resultB = null;
    }

    // ------------------------------------------------------------
    // Launcher und Dashboard
    // ------------------------------------------------------------

    function clampPosition(x, y, element) {
        return {
            x: clamp(x, 0, Math.max(0, innerWidth - element.offsetWidth)),
            y: clamp(y, 0, Math.max(0, innerHeight - element.offsetHeight))
        };
    }

    function createLauncher() {
        if (!document.body || launcher) return;

        launcher = document.createElement('button');
        launcher.id = 'warera-lite-launcher';
        launcher.type = 'button';
        launcher.textContent = '⚡';
        launcher.title = 'LidelEra 1.0';

        Object.assign(launcher.style, {
            position: 'fixed',
            left: CONFIG.launcherPosition.x + 'px',
            top: CONFIG.launcherPosition.y + 'px',
            zIndex: '2147483647',
            width: '38px',
            height: '34px',
            borderRadius: '11px',
            border: '1px solid #3b82a8',
            background: 'linear-gradient(145deg,#172b40,#101923)',
            color: '#d9f3ff',
            fontSize: '17px',
            cursor: 'grab',
            touchAction: 'none',
            userSelect: 'none'
        });

        let drag = null;

        launcher.addEventListener('pointerdown', event => {
            if (event.pointerType === 'mouse' && event.button !== 0) return;
            const rect = launcher.getBoundingClientRect();
            drag = { x: event.clientX, y: event.clientY, left: rect.left, top: rect.top, moved: false };
            try { launcher.setPointerCapture(event.pointerId); } catch (_) {}
        });

        launcher.addEventListener('pointermove', event => {
            if (!drag) return;
            const dx = event.clientX - drag.x;
            const dy = event.clientY - drag.y;
            if (Math.abs(dx) + Math.abs(dy) > 6) drag.moved = true;
            if (!drag.moved) return;

            const pos = clampPosition(drag.left + dx, drag.top + dy, launcher);
            launcher.style.left = pos.x + 'px';
            launcher.style.top = pos.y + 'px';
        });

        launcher.addEventListener('pointerup', () => {
            if (!drag) return;

            if (drag.moved) {
                const rect = launcher.getBoundingClientRect();
                CONFIG.launcherPosition = { x: Math.round(rect.left), y: Math.round(rect.top) };
                save('launcherPosition', CONFIG.launcherPosition);
            } else {
                dashboardOpen ? closeDashboard() : openDashboard();
            }
            drag = null;
        });

        launcher.addEventListener('pointercancel', () => { drag = null; });

        document.body.appendChild(launcher);

        const pos = clampPosition(CONFIG.launcherPosition.x, CONFIG.launcherPosition.y, launcher);
        launcher.style.left = pos.x + 'px';
        launcher.style.top = pos.y + 'px';
    }

    function buttonStyle() {
        return 'padding:6px 8px;border:1px solid #35536d;border-radius:8px;background:#1b3044;color:#eaf5ff;cursor:pointer;font:600 11px/1.25 system-ui,sans-serif;min-height:30px;';
    }

    function makeDraggable(element, handle) {
        let drag = null;

        handle.addEventListener('pointerdown', event => {
            if (event.target.id === 'wl-close') return;
            if (event.pointerType === 'mouse' && event.button !== 0) return;

            const rect = element.getBoundingClientRect();
            drag = { x: event.clientX - rect.left, y: event.clientY - rect.top };
            try { handle.setPointerCapture(event.pointerId); } catch (_) {}
        });

        handle.addEventListener('pointermove', event => {
            if (!drag) return;
            const pos = clampPosition(event.clientX - drag.x, event.clientY - drag.y, element);
            element.style.left = pos.x + 'px';
            element.style.top = pos.y + 'px';
        });

        const finish = () => {
            if (!drag) return;
            const rect = element.getBoundingClientRect();
            CONFIG.dashboardPosition = { x: Math.round(rect.left), y: Math.round(rect.top) };
            save('dashboardPosition', CONFIG.dashboardPosition);
            drag = null;
        };

        handle.addEventListener('pointerup', finish);
        handle.addEventListener('pointercancel', () => { drag = null; });
    }

    function card(title, bodyHtml, extra = '') {
        return `
            <div style="padding:9px 10px;background:rgba(30,48,67,.72);border:1px solid rgba(93,133,164,.18);border-radius:10px;margin-top:8px">
                <b>${title}</b>${bodyHtml}${extra}
            </div>`;
    }

    function openDashboard() {
        if (dashboard || !document.body) return;

        dashboardOpen = true;
        dashboard = document.createElement('section');
        dashboard.id = 'warera-lite-dashboard';

        Object.assign(dashboard.style, {
            position: 'fixed',
            left: '62px',
            top: '100px',
            width: '350px',
            maxWidth: 'calc(100vw - 16px)',
            maxHeight: '85vh',
            overflowY: 'auto',
            zIndex: '2147483647',
            background: 'linear-gradient(160deg,#111d2b 0%,#0b121d 100%)',
            color: '#e8f1fb',
            border: '1px solid #31516b',
            borderRadius: '14px',
            padding: '12px',
            boxSizing: 'border-box',
            font: '12px/1.45 system-ui,sans-serif'
        });

        dashboard.innerHTML = `
            <style>
                #warera-lite-dashboard { color-scheme: dark; }
                #warera-lite-dashboard #wl-drag {
                    padding: 2px 1px 9px; border-bottom: 1px solid rgba(117,164,196,.22);
                    letter-spacing: .2px;
                }
                #warera-lite-dashboard button, #warera-lite-dashboard select {
                    box-sizing: border-box; transition: background .15s ease, border-color .15s ease, transform .15s ease;
                }
                #warera-lite-dashboard button:hover:not(:disabled) {
                    border-color: #65b8df !important; filter: brightness(1.12);
                }
                #warera-lite-dashboard button:active:not(:disabled) { transform: translateY(1px); }
                #warera-lite-dashboard button:disabled { opacity: .5; cursor: not-allowed; }
                #warera-lite-dashboard select:focus-visible,
                #warera-lite-dashboard button:focus-visible {
                    outline: 2px solid #63b8e5; outline-offset: 2px;
                }
                #warera-lite-dashboard b { color: #f1f7ff; }
                #warera-lite-dashboard #wl-events {
                    padding: 7px; background: rgba(7,14,23,.45); border-radius: 7px;
                }
            </style>
            <header id="wl-drag" style="font-weight:700;font-size:16px;cursor:move;margin-bottom:12px;touch-action:none;user-select:none;display:flex;justify-content:space-between;align-items:center">
                <span>⚡ LidelEra <span style="color:#9ba9bf;font-size:10px">${VERSION}</span></span>
                <span id="wl-close" style="cursor:pointer;padding:0 4px">✕</span>
            </header>

            <div style="font-size:11px;color:#9ba9bf;margin-bottom:8px">
                ${L('Installer-ID', 'Installer ID')}: <span style="color:#e7edf7">${installerId}</span>
            </div>

            <div style="margin-bottom:10px">
                <div style="margin-bottom:5px">${L('Leistungssteuerung', 'Performance control')}</div>
                <select id="wl-profile" style="width:100%;padding:7px 9px;background:#17283a;color:#edf6ff;border:1px solid #35536d;border-radius:8px">
                    <option value="auto">${L('AUTO – automatisch', 'AUTO – automatic')}</option>
                    <option value="balanced">${L('BALANCED – Ausgewogen', 'BALANCED')}</option>
                    <option value="max">${L('MAX – Stark optimiert', 'MAX – highly optimized')}</option>
                    <option value="ultra">${L('ULTRA – Maximale Leistung', 'ULTRA – maximum performance')}</option>
                </select>
            </div>

            <button id="wl-power" style="width:100%;padding:7px 9px;margin-bottom:4px;border:1px solid #2d9b78;border-radius:8px;background:linear-gradient(135deg,#167c55,#116246);color:white;font:600 11px/1.3 system-ui,sans-serif;cursor:pointer"></button>

            ${card(L('Performance', 'Performance'), '<div id="wl-performance"></div>')}
            ${card(L('Blockaden-Analyse (letzte 5 Min.)', 'Blocking analysis (last 5 min)'), '<div id="wl-loaf"></div>')}
            ${card(L('Long Tasks – letzte 30 Sekunden', 'Long tasks – last 30 seconds'), '<div id="wl-trace-stats"></div>')}
            ${card(L('Interaktionen', 'Interactions'), '<div id="wl-interactions"></div>')}
            ${card(L('System & Ressourcen', 'System & resources'), '<div id="wl-system"></div>')}
            ${card(L('Letzte Ereignisse', 'Recent events'),
                '<div id="wl-events" style="max-height:120px;overflow:auto;white-space:pre-wrap;font-size:11px;margin-top:5px"></div>')}
            ${card(L('A/B-Benchmark', 'A/B benchmark'),
                `<div id="wl-benchmark" style="white-space:pre-line;margin-top:5px;font-size:12px">${L('Noch kein Benchmark gestartet.', 'No benchmark started yet.')}</div>`,
                `<button id="wl-benchmark-start" style="${buttonStyle()}width:100%;margin-top:7px">${L('60-Sekunden-ABAB-Test starten', 'Start 60-second ABAB test')}</button>`)}

            <div style="display:grid;grid-template-columns:1fr 1fr;gap:7px;margin-top:10px">
                <button id="wl-copy" style="${buttonStyle()}">${L('Diagnose kopieren', 'Copy diagnostics')}</button>
                <button id="wl-reset" style="${buttonStyle()}">${L('Zurücksetzen', 'Reset')}</button>
                <button id="wl-refresh" style="${buttonStyle()}">${L('Aktualisieren', 'Refresh')}</button>
                <button id="wl-close2" style="${buttonStyle()}">${L('Schließen', 'Close')}</button>
            </div>

            <div style="color:#93a0b4;font-size:10px;margin-top:10px;line-height:1.4">
                ${L('Long Tasks sind blockierende Hauptthread-Aufgaben ab 50 ms. Die Blockaden-Analyse (Long Animation Frames) zeigt, welches Skript sie verursacht.',
                    'Long tasks are blocking main-thread tasks >= 50 ms. The blocking analysis (long animation frames) shows which script caused them.')}
            </div>
        `;

        document.body.appendChild(dashboard);

        const $ = sel => dashboard.querySelector(sel);

        $('#wl-profile').value = CONFIG.profile;
        $('#wl-profile').addEventListener('change', event => setProfile(event.target.value));
        $('#wl-power').addEventListener('click', () => setEnabled(!CONFIG.enabled));
        $('#wl-close').addEventListener('click', closeDashboard);
        $('#wl-close2').addEventListener('click', closeDashboard);
        $('#wl-copy').addEventListener('click', event => copyDiagnostics(event.currentTarget));
        $('#wl-refresh').addEventListener('click', updateDashboard);
        $('#wl-benchmark-start').addEventListener('click', startBenchmark);

        $('#wl-reset').addEventListener('click', () => {
            frameSamples = [];
            longTaskSamples = [];
            interactionSamples = [];
            loafSamples = [];
            traceEvents = [];
            stats.totalLongTasks = 0;
            updateDashboard();
        });

        if (benchmark?.report && !benchmark.running) $('#wl-benchmark').textContent = benchmark.report;

        makeDraggable(dashboard, $('#wl-drag'));

        if (CONFIG.dashboardPosition) {
            dashboard.style.left = CONFIG.dashboardPosition.x + 'px';
            dashboard.style.top = CONFIG.dashboardPosition.y + 'px';
        } else if (launcher) {
            const rect = launcher.getBoundingClientRect();
            dashboard.style.left = (rect.left + 52) + 'px';
            dashboard.style.top = rect.top + 'px';
        }

        const rect = dashboard.getBoundingClientRect();
        const pos = clampPosition(rect.left, rect.top, dashboard);
        dashboard.style.left = pos.x + 'px';
        dashboard.style.top = pos.y + 'px';

        startFrameTracking();
        updateDashboard();

        dashboardTimer = setInterval(() => {
            if (dashboardOpen && !document.hidden) updateDashboard();
        }, 2000);
    }

    function closeDashboard() {
        // Ein abgebrochener Test wird nicht als gültiges Ergebnis ausgegeben.
        abortBenchmark();

        dashboardOpen = false;
        if (dashboardTimer) clearInterval(dashboardTimer);
        dashboardTimer = null;

        dashboard?.remove();
        dashboard = null;

        if (!wantFrames()) stopFrameTracking();
    }

    function updateDashboard() {
        if (!dashboard) return;

        // Während des Benchmarks nichts Schweres rendern, um die Messung nicht zu stören.
        if (benchmark?.running) {
            renderBenchmarkLabel();
            return;
        }

        calculateAll();

        const $ = sel => dashboard.querySelector(sel);

        const power = $('#wl-power');
        power.textContent = CONFIG.enabled ? L('Optimierung aktiv', 'Optimization active') : L('Optimierung deaktiviert', 'Optimization disabled');
        power.style.background = CONFIG.enabled ? '#167c55' : '#8b3434';

        const na = L('n.v.', 'n/a');

        setHtml($('#wl-performance'), `
            <div>${L('Steuerung', 'Control')}: <b>${CONFIG.profile.toUpperCase()}</b></div>
            <div>${L('Aktives Profil', 'Active profile')}: <b>${CONFIG.enabled ? activeProfile().toUpperCase() : 'OFF'}</b></div>
            <div>FPS: <b>${stats.fps || '—'}</b></div>
            <div>${L('Framezeit', 'Frame time')} P50: <b>${stats.p50} ms</b></div>
            <div>${L('Framezeit', 'Frame time')} P95: <b>${stats.p95} ms</b></div>
            <div>${L('Ruckler (>50 ms)', 'Jank (>50 ms)')}: <b>${stats.jank}</b></div>
            <div>Frame Drops (>33 ms): <b>${stats.drops}</b></div>
        `);

        const t = now();
        setHtml($('#wl-loaf'), loafHtml(getLoafSummary(t - LOAF_WINDOW, t)));

        setHtml($('#wl-trace-stats'), `
            <div>Long Tasks (30 s): <b>${stats.longTasks}</b></div>
            <div>${L('Überlappungsbereinigte Dauer', 'Overlap-free duration')}: <b>${stats.longTaskDuration} ms</b></div>
            <div>${L('Durchschnitt pro Eintrag', 'Average per entry')}: <b>${Math.round(stats.longTaskAverage)} ms</b></div>
            <div>${L('Längste Aufgabe', 'Longest task')}: <b>${stats.longTaskMax} ms</b></div>
            <div>50–99 ms: <b>${stats.task50}</b> | 100–199 ms: <b>${stats.task100}</b> | ≥200 ms: <b>${stats.task200}</b></div>
            <div>${L('Long Tasks insgesamt', 'Long tasks total')}: <b>${stats.totalLongTasks}</b></div>
        `);

        const breakdown = stats.slowInteractions
            ? `<div style="margin-top:4px">${L('Ø bei langsamen Klicks', 'Avg. of slow interactions')}:<br>
                 ${L('Wartezeit', 'Input delay')} <b>${Math.round(stats.slowInput)} ms</b> |
                 ${L('Handler', 'Handler')} <b>${Math.round(stats.slowProc)} ms</b> |
                 ${L('Darstellung', 'Presentation')} <b>${Math.round(stats.slowPres)} ms</b></div>`
            : '';

        setHtml($('#wl-interactions'), `
            <div>${L('Gemessen (60 s)', 'Measured (60 s)')}: <b>${stats.interactions}</b></div>
            <div>${L('Langsam (>100 ms)', 'Slow (>100 ms)')}: <b>${stats.slowInteractions}</b></div>
            <div>${L('Maximal', 'Maximum')}: <b>${Math.round(stats.maxInteraction)} ms</b></div>
            ${breakdown}
        `);

        setHtml($('#wl-system'), `
            <div>${L('CPU-Kerne', 'CPU cores')}: <b>${navigator.hardwareConcurrency || na}</b></div>
            <div>${L('RAM-Schätzung', 'RAM estimate')}: <b>${navigator.deviceMemory ? navigator.deviceMemory + ' GB' : na}</b></div>
            <div>JS-Heap: <b>${stats.heap ? stats.heap + ' MB' : na}</b> / ${stats.heapLimit ? stats.heapLimit + ' MB' : na}</div>
            <div>${L('Ressourcen (>1 s)', 'Resources (>1 s)')}: <b>${stats.slowResources} / ${stats.resources}</b></div>
        `);

        const recent = traceEvents.slice(-10).reverse();
        const eventsEl = $('#wl-events');
        const eventsText = recent.length
            ? recent.map(item => {
                const what = item.duration != null ? `${item.duration} ms` : (item.name ?? '—');
                const extra = item.type === 'SLOW_INTERACTION'
                    ? ` ${item.name}${item.target ? ' ' + item.target : ''} (${item.input}/${item.proc}/${item.pres})`
                    : '';
                return `${(item.time / 1000).toFixed(1)} s | ${item.type} | ${what}${extra}`;
            }).join('\n')
            : L('Keine auffälligen Ereignisse.', 'No notable events.');

        if (eventsEl._t !== eventsText) {
            eventsEl._t = eventsText;
            eventsEl.textContent = eventsText;
        }
    }

    // ------------------------------------------------------------
    // Diagnose exportieren
    // ------------------------------------------------------------

    function getDiagnostics() {
        calculateAll();

        const t = now();
        const loaf = getLoafSummary(t - LOAF_WINDOW, t);

        const lines = [
            `LIDELERA ${VERSION} DIAGNOSE`,
            `Installer ID: ${installerId}`,
            `Zeit: ${new Date().toISOString()}`,
            `Browser: ${navigator.userAgent}`,
            `CPU-Kerne: ${navigator.hardwareConcurrency || '?'}`,
            `RAM-Schätzung: ${navigator.deviceMemory || '?'} GB`,
            `Steuerung: ${CONFIG.profile}`,
            `Aktives Profil: ${activeProfile()}`,
            `Optimierung: ${CONFIG.enabled ? 'aktiv' : 'aus'}`,
            `FPS: ${stats.fps}`,
            `Framezeit P50: ${stats.p50} ms`,
            `Framezeit P95: ${stats.p95} ms`,
            `Ruckler >50 ms: ${stats.jank}`,
            `Frame Drops >33 ms: ${stats.drops}`,
            `Long Tasks letzte 30 s: ${stats.longTasks}`,
            `Long Task Dauer (überlappungsbereinigt): ${stats.longTaskDuration} ms`,
            `Längste Task: ${stats.longTaskMax} ms`,
            `Tasks 50–99 ms: ${stats.task50}`,
            `Tasks 100–199 ms: ${stats.task100}`,
            `Tasks >=200 ms: ${stats.task200}`,
            `Long Tasks insgesamt: ${stats.totalLongTasks}`,
            `Interaktionen letzte 60 s: ${stats.interactions}`,
            `Interaktionen >100 ms: ${stats.slowInteractions}`,
            `Maximale Interaktionsdauer: ${Math.round(stats.maxInteraction)} ms`,
            `Langsame Interaktionen Ø: Wartezeit ${Math.round(stats.slowInput)} ms | Handler ${Math.round(stats.slowProc)} ms | Darstellung ${Math.round(stats.slowPres)} ms`,
            `JS-Speicher: ${stats.heap} MB`,
            `JS-Heap-Limit: ${stats.heapLimit} MB`,
            `Long Task API: ${Boolean(longTaskObserver)}`,
            `Event Timing API: ${Boolean(eventObserver)}`,
            `Long Animation Frames API: ${Boolean(loafObserver)}`
        ];

        if (loafObserver) {
            const total = loaf.script + loaf.render;
            const pct = v => (total > 0 ? Math.round(100 * v / total) : 0);

            lines.push(
                '',
                'BLOCKADEN-ANALYSE (Long Animation Frames, letzte 5 Min.)',
                `Langsame Frames >50 ms: ${loaf.count}`,
                `Blockierend gesamt: ${Math.round(loaf.blocking)} ms`,
                `Skript-Ausführung: ${Math.round(loaf.script)} ms (${pct(loaf.script)} %)`,
                `Rendering (Style/Layout/Paint): ${Math.round(loaf.render)} ms (${pct(loaf.render)} %)`,
                `Davon erzwungenes Layout: ${Math.round(loaf.forced)} ms`,
                'Top-Verursacher:'
            );

            for (const s of loaf.sources) {
                lines.push(`  ${s.origin}/${s.file} | ${s.fn || s.invoker || '(anonym)'} | ${Math.round(s.total)} ms, ${s.count}x, max ${Math.round(s.max)} ms`);
            }
        }

        if (benchmark?.report) lines.push('', benchmark.report);
        else if (benchmark?.running) lines.push('', `A/B BENCHMARK läuft: Phase ${benchmark.index + 1}/${BENCH_PHASES.length}`);

        lines.push('', 'LETZTE TRACE-EREIGNISSE');
        for (const item of traceEvents.slice(-30)) {
            lines.push(`${item.time.toFixed(1)} ms | ${item.type} | ${JSON.stringify(item)}`);
        }

        return lines.join('\n');
    }

    async function copyDiagnostics(button) {
        const text = getDiagnostics();
        let ok = false;

        try {
            await navigator.clipboard.writeText(text);
            ok = true;
        } catch (_) {
            const area = document.createElement('textarea');
            area.value = text;
            area.style.cssText = 'position:fixed;left:-9999px;';
            document.body.appendChild(area);
            area.select();
            try { ok = document.execCommand('copy'); } catch (_) {}
            area.remove();
        }

        // Kein alert(): der blockiert den Hauptthread und friert das Spiel kurz ein.
        if (button) {
            const original = button.textContent;
            button.textContent = ok ? L('Kopiert ✓', 'Copied ✓') : L('Fehlgeschlagen', 'Failed');
            setTimeout(() => { if (button.isConnected) button.textContent = original; }, 1500);
        }
    }

    // ------------------------------------------------------------
    // Fenstergröße und Initialisierung
    // ------------------------------------------------------------

    function onResize() {
        if (launcher) {
            const rect = launcher.getBoundingClientRect();
            const pos = clampPosition(rect.left, rect.top, launcher);
            launcher.style.left = pos.x + 'px';
            launcher.style.top = pos.y + 'px';
            CONFIG.launcherPosition = pos;
            save('launcherPosition', pos);
        }

        if (dashboard) {
            const rect = dashboard.getBoundingClientRect();
            const pos = clampPosition(rect.left, rect.top, dashboard);
            dashboard.style.left = pos.x + 'px';
            dashboard.style.top = pos.y + 'px';
            CONFIG.dashboardPosition = pos;
            save('dashboardPosition', pos);
        }
    }

    // Stellt UI/Style wieder her, falls die SPA sie aus dem DOM entfernt hat.
    function ensureUi() {
        if (styleElement && !styleElement.isConnected) {
            appliedKey = '';
            applyOptimization();
        }
        if (launcher && document.body && !launcher.isConnected) document.body.appendChild(launcher);
    }

    function init() {
        if (document.documentElement) applyOptimization();
        else document.addEventListener('DOMContentLoaded', applyOptimization, { once: true });

        setupLongTaskObserver();
        setupLoafObserver();
        setupInteractionObserver();

        if (document.body) createLauncher();
        else document.addEventListener('DOMContentLoaded', createLauncher, { once: true });

        document.addEventListener('visibilitychange', () => {
            if (document.hidden) stopFrameTracking();
            else startFrameTracking();
        });

        window.addEventListener('resize', onResize, { passive: true });

        // Ein einziger, billiger Takt: AUTO-Messfenster starten, Profil prüfen, UI absichern.
        setInterval(() => {
            if (document.hidden) return;
            ensureUi();
            if (wantFrames()) startFrameTracking();
            if (CONFIG.profile === 'auto' && !benchmark?.running) evaluateAutomaticProfile();
        }, 1000);
    }

    init();
})();