LeJoufflu / OGame Espionnage Tracker (multi-univers/joueurs, menu fiable)

// ==UserScript==
// @name         OGame Espionnage Tracker (multi-univers/joueurs, menu fiable)
// @namespace    http://tampermonkey.net/
// @version      23.1
// @description  Menu espionnage avec stockage séparé par univers et joueur, filtres dépendants, export/import CSV, graphe horaire (global ou joueur) basé sur les résultats filtrés, parsing OGLight + regex, tri par date/heure.
// @author       Carim
// @license      MIT
// @match        https://*.ogame.gameforge.com/game/index.php?page=ingame&component=messages*
// @grant        GM_addStyle
// ==/UserScript==

(function() {
    'use strict';

    // =========================
    // Clés de stockage par univers et joueur
    // =========================
    function getUniverseKey() {
        const host = window.location.host; // ex: s123-fr.ogame.gameforge.com
        return host.split(".")[0];         // ex: "s123-fr"
    }
    function getPlayerName() {
        // Ajuste le sélecteur si besoin (selon thème/extension)
        const el = document.querySelector("#playerName, .playerName, #bar .playername");
        return el ? el.textContent.trim() : "UnknownPlayer";
    }
    function getStorageKey() {
        return "spyHistory_" + getUniverseKey() + "_" + getPlayerName();
    }

    // État
    let history = JSON.parse(localStorage.getItem(getStorageKey()) || "[]");
    function saveHistory() {
        localStorage.setItem(getStorageKey(), JSON.stringify(history));
    }

    let spyChart = null;

    // =========================
    // Librairie graphe
    // =========================
    const chartScript = document.createElement("script");
    chartScript.src = "https://cdn.jsdelivr.net/npm/chart.js";
    chartScript.async = true;
    document.head.appendChild(chartScript);

    // =========================
    // Styles
    // =========================
    GM_addStyle(`
        #spyMenu {position:fixed;top:80px;right:20px;width:300px;background:#111;color:#eee;
            border:1px solid #666;font-size:12px;z-index:999999;transition:width 0.25s;}
        #spyMenuHeader {display:flex;align-items:center;justify-content:space-between;
            padding:8px;border-bottom:1px solid #666;background:#101010;}
        #spyMenuHeaderTitle {font-weight:bold;}
        #toggleSpyMenu {background:#333;border:1px solid #666;color:#eee;font-size:14px;
            cursor:pointer;padding:4px 8px;border-radius:3px;}
        #spyMenuBody {padding:10px;}
        #spyMenu.collapsed {width:56px;}
        #spyMenu.collapsed #spyMenuHeaderTitle {display:none;}
        #spyMenu.collapsed #spyMenuBody {display:none;}
        .spy-btn {display:block;width:100%;margin:6px 0;padding:8px;background:#333;color:#eee;
            border:1px solid #666;cursor:pointer;text-align:center;}
        .spy-btn:hover {background:#3a3a3a;}
        #spyTop{margin-top:10px;border-top:1px solid #666;padding-top:10px;}
        #spyTop h4 {margin:0 0 6px 0;}
        #spyTop ul {list-style:none;padding:0;margin:0;}
        #spyTop li {margin:3px 0;}
        #spyNotification {position:fixed;bottom:20px;right:20px;background:#222;color:#eee;
            padding:10px;border:1px solid #666;border-radius:5px;z-index:1000001;opacity:0;transition:opacity 0.4s;}
        #spyHistoryPopup {position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.7);
            display:none;align-items:center;justify-content:center;z-index:1000000;}
        #spyHistoryContent {background:#111;color:#eee;padding:20px;border:1px solid #666;width:840px;
            max-height:80%;overflow-y:auto;position:relative;}
        #closeHistory {position:absolute;top:8px;right:8px;background:#333;color:#fff;border:1px solid #666;
            font-size:12px;padding:6px 10px;cursor:pointer;border-radius:4px;}
        #spyList {list-style:none;padding:0;margin:10px 0;}
        #spyFilters {margin-bottom:10px;display:grid;grid-template-columns:1fr 1fr;gap:8px;}
        #spyFilters label {display:flex;align-items:center;gap:6px;font-size:12px;}
        #spyFilters .actions {grid-column:1 / -1;display:flex;gap:8px;}
        #spyFilters select, #spyFilters input {width:100%;background:#222;color:#eee;border:1px solid #666;padding:4px;}
        #spyStats {margin:8px 0 0 0;font-size:12px;color:#aaa;}
        #spyGraphSection {position:sticky;top:0;margin:10px 0;background:#222;padding:10px;z-index:10;display:none;}
        #spyGraphSection h4 {margin:0 0 8px 0;}
    `);

    // =========================
    // Menu + Popup
    // =========================
    function createMenu() {
        if (document.getElementById("spyMenu")) return;
        const menu = document.createElement("div");
        menu.id = "spyMenu";
        menu.innerHTML = `
            <div id="spyMenuHeader">
                <span id="spyMenuHeaderTitle">Espionnages reçus</span>
                <button id="toggleSpyMenu" title="Réduire/Ouvrir">⮞</button>
            </div>
            <div id="spyMenuBody">
                <button id="openSpyPopup" class="spy-btn">Consulter les espionnages</button>

                <div id="spyTop">
                    <h4>Top 20 Espions</h4>
                    <ul id="spyRanking"></ul>
                </div>

                <button id="refreshSpy" class="spy-btn">Rafraîchir</button>
                <button id="clearSpy" class="spy-btn">Effacer historique</button>
                <button id="exportSpy" class="spy-btn">Exporter CSV</button>
                <button id="importSpy" class="spy-btn">Importer CSV</button>
                <input type="file" id="importFile" accept=".csv" style="display:none">
                <button id="verifySpy" class="spy-btn">Vérifier cohérence</button>

                <div id="spyHistoryPopup">
                    <div id="spyHistoryContent">
                        <button id="closeHistory">Fermer ✖</button>
                        <h3>Historique des espionnages</h3>
                        <div id="spyStats"></div>

                        <div id="spyFilters">
                            <label>De: <input type="datetime-local" id="filterStart"></label>
                            <label>À: <input type="datetime-local" id="filterEnd"></label>
                            <label>Heure début: <input type="time" id="filterStartHour"></label>
                            <label>Heure fin: <input type="time" id="filterEndHour"></label>
                            <label>Espion: <select id="filterPlayer"><option value="">Tous</option></select></label>
                            <label>Planète espion: <select id="filterPlanetEspion"><option value="">Toutes</option></select></label>
                            <label>Planète espionnée: <select id="filterPlanetEspionnee"><option value="">Toutes</option></select></label>
                            <div class="actions">
                                <button id="applyFilters" class="spy-btn">Appliquer</button>
                                <button id="resetFilters" class="spy-btn">Réinitialiser</button>
                            </div>
                        </div>

                        <div id="spyGraphSection">
                            <h4 id="spyGraphTitle"></h4>
                            <canvas id="spyGraphCanvas" width="780" height="250"></canvas>
                        </div>

                        <ul id="spyList"></ul>
                    </div>
                </div>
            </div>
        `;
        document.body.appendChild(menu);

        const collapsed = localStorage.getItem("spyMenuCollapsed") === "true";
        if (collapsed) {
            menu.classList.add("collapsed");
            document.getElementById("toggleSpyMenu").textContent = "⮜";
        }

        // Events
        document.getElementById("toggleSpyMenu").addEventListener("click", () => {
            menu.classList.toggle("collapsed");
            const isCollapsed = menu.classList.contains("collapsed");
            document.getElementById("toggleSpyMenu").textContent = isCollapsed ? "⮜" : "⮞";
            localStorage.setItem("spyMenuCollapsed", String(isCollapsed));
        });
        document.getElementById("openSpyPopup").addEventListener("click", () => {
            document.getElementById("spyHistoryPopup").style.display = "flex";
            renderHistory();
        });
        document.getElementById("closeHistory").addEventListener("click", () => {
            document.getElementById("spyHistoryPopup").style.display = "none";
        });

        document.getElementById("refreshSpy").addEventListener("click", () => {
            parseMessages();
            showNotification("Espionnages mis à jour ✅");
        });
        document.getElementById("clearSpy").addEventListener("click", clearHistory);
        document.getElementById("exportSpy").addEventListener("click", exportCSV);
        document.getElementById("importSpy").addEventListener("click", () => {
            document.getElementById("importFile").click();
        });
        document.getElementById("importFile").addEventListener("change", (e) => {
            const file = e.target.files[0];
            if (file) importCSV(file);
        });
        document.getElementById("verifySpy").addEventListener("click", verifyConsistency);

        document.getElementById("applyFilters").addEventListener("click", applyFilters);
        document.getElementById("resetFilters").addEventListener("click", resetFilters);
        document.getElementById("filterPlayer").addEventListener("change", populateFilters);
    }

    // =========================
    // Notifications
    // =========================
    function showNotification(msg) {
        let notif = document.getElementById("spyNotification");
        if (!notif) {
            notif = document.createElement("div");
            notif.id = "spyNotification";
            document.body.appendChild(notif);
        }
        notif.textContent = msg;
        notif.style.opacity = "1";
        setTimeout(() => { notif.style.opacity = "0"; }, 2000);
    }

    // =========================
    // Export / Import CSV (par univers/joueur)
    // =========================
    function exportCSV() {
        let csv = "Date;Heure;Joueur;Planète Espion;Planète Espionnée\n";
        history.forEach(e => {
            csv += `${e.date};${e.hour};${e.player};${e.planetEspion};${e.planetEspionnee}\n`;
        });

        const now = new Date();
        const yyyy = now.getFullYear();
        const mm   = String(now.getMonth() + 1).padStart(2, '0');
        const dd   = String(now.getDate()).padStart(2, '0');
        const HH   = String(now.getHours()).padStart(2, '0');
        const MM   = String(now.getMinutes()).padStart(2, '0');
        const filename = `spiesExport-${getUniverseKey()}-${getPlayerName()}-${yyyy}${mm}${dd}-${HH}${MM}.csv`;

        const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" });
        const url = URL.createObjectURL(blob);
        const a = document.createElement("a");
        a.href = url; a.download = filename; a.click();
        URL.revokeObjectURL(url);

        showNotification(`Export CSV créé (${filename})`);
    }

    function importCSV(file) {
        const reader = new FileReader();
        reader.onload = function(e) {
            const text = e.target.result;
            const lines = text.trim().split("\n");

            const header = lines[0].replace(/^\uFEFF/, "");
            const cols = header.split(";");
            const expected = ["Date","Heure","Joueur","Planète Espion","Planète Espionnée"];
            const isValid = expected.every((c, i) => (cols[i] || "").trim() === c);
            if (!isValid) {
                showNotification("Format CSV invalide ⚠️");
                return;
            }

            const before = history.length;
            lines.slice(1).forEach(line => {
                const parts = line.split(";");
                if (parts.length < 5) return;
                const [date, hour, player, planetEspion, planetEspionnee] = parts.map(x => (x || "").trim());
                if (!date || !hour || !player) return;

                const exists = history.some(e =>
                    e.date === date &&
                    e.hour === hour &&
                    e.player === player &&
                    e.planetEspion === planetEspion &&
                    e.planetEspionnee === planetEspionnee
                );
                if (!exists) history.push({ date, hour, player, planetEspion, planetEspionnee });
            });

            saveHistory();
            renderHistory();
            const added = history.length - before;
            showNotification(added > 0 ? `Import CSV terminé ✅ (+${added})` : "Import CSV terminé ✅ (aucun nouvel enregistrement)");
        };
        reader.readAsText(file, "UTF-8");
    }

    // =========================
    // Historique: effacer / cohérence
    // =========================
    function clearHistory() {
        const confirmation = confirm("Vous allez effacer l’historique pour cet univers et ce joueur. Continuer ?");
        if (!confirmation) return;
        history = [];
        saveHistory();
        renderHistory();
        const section = document.getElementById("spyGraphSection");
        if (section) section.style.display = "none";
        if (spyChart) { spyChart.destroy(); spyChart = null; }
        showNotification("Historique effacé 🗑️");
    }

    // Uniformisation des noms par planète espion
    function verifyConsistency() {
        const planetGroups = {};
        history.forEach(entry => {
            const key = entry.planetEspion || "??";
            if (!planetGroups[key]) planetGroups[key] = [];
            planetGroups[key].push(entry);
        });

        let changes = 0;

        Object.values(planetGroups).forEach(entries => {
            const playersSet = new Set(entries.map(e => e.player));
            if (playersSet.size > 1) {
                const latest = entries.reduce((a, b) => {
                    const da = toAbsoluteDate(a.date, a.hour);
                    const db = toAbsoluteDate(b.date, b.hour);
                    return (da && db && da > db) ? a : b;
                });
                const latestName = latest.player;
                history.forEach(e => {
                    if (entries.some(x => x.player === e.player) && e.player !== latestName) {
                        e.player = latestName;
                        changes++;
                    }
                });
            }
        });

        saveHistory();
        renderHistory();
        showNotification(changes > 0
            ? `Vérification terminée ✅ (${changes} corrections appliquées)`
            : "Vérification terminée ✅ (aucune correction)");
    }

    // =========================
    // Rendu + Top20 + Filtres
    // =========================
    function renderHistory() {
        const spyList = document.getElementById("spyList");
        const spyRanking = document.getElementById("spyRanking");
        const stats = document.getElementById("spyStats");
        if (!spyList) return;

        const sorted = history.slice().sort((a,b) => {
            const da = toAbsoluteDate(a.date, a.hour);
            const db = toAbsoluteDate(b.date, b.hour);
            return db - da;
        });

        if (stats) stats.textContent = `Univers: ${getUniverseKey()} | Joueur: ${getPlayerName()} | Total enregistrements: ${history.length}`;

        spyList.innerHTML = "";
        sorted.forEach(entry => {
            const li = document.createElement("li");

            const playerLink = document.createElement("a");
            playerLink.href = "#";
            playerLink.textContent = entry.player;
            playerLink.style.color = "#4FC3F7";
            playerLink.addEventListener("click", (e) => {
                e.preventDefault();
                showGraph(entry.player, sorted); // graphe par joueur sur la liste affichée
            });

            li.textContent = `${entry.date} ${entry.hour} - `;
            li.appendChild(playerLink);
            li.appendChild(document.createTextNode(` (de ${entry.planetEspion} → ${entry.planetEspionnee})`));
            spyList.appendChild(li);
        });

        if (spyRanking) {
            spyRanking.innerHTML = "";
            const counts = {};
            history.forEach(e => { counts[e.player] = (counts[e.player] || 0) + 1; });
            Object.entries(counts)
                .sort((a,b) => b[1] - a[1])
                .slice(0,20)
                .forEach(([player,count]) => {
                    const li = document.createElement("li");
                    const link = document.createElement("a");
                    link.href = "#";
                    link.textContent = `${player} (${count})`;
                    link.style.color = "#FFD54F";
                    link.addEventListener("click", (e) => {
                        e.preventDefault();
                        document.getElementById("spyHistoryPopup").style.display = "flex";
                        populateFilters();
                        document.getElementById("filterPlayer").value = player;
                        applyFilters(); // graphe filtré (spécifique si joueur, sinon global)
                    });
                    li.appendChild(link);
                    spyRanking.appendChild(li);
                });
        }

        populateFilters();
    }

    function populateFilters() {
        const playerSelect = document.getElementById("filterPlayer");
        const planetEspionSelect = document.getElementById("filterPlanetEspion");
        const planetEspionneeSelect = document.getElementById("filterPlanetEspionnee");
        if (!playerSelect || !planetEspionSelect || !planetEspionneeSelect) return;

        const currentPlayer = playerSelect.value;

        playerSelect.innerHTML = "<option value=''>Tous</option>";
        planetEspionSelect.innerHTML = "<option value=''>Toutes</option>";
        planetEspionneeSelect.innerHTML = "<option value=''>Toutes</option>";

        const players = [...new Set(history.map(e => e.player))].sort();
        players.forEach(p => {
            const opt = document.createElement("option");
            opt.value = p; opt.textContent = p;
            playerSelect.appendChild(opt);
        });

        // Planètes espion dépendantes du joueur sélectionné
        const planetsEspion = [...new Set(
            (currentPlayer ? history.filter(e => e.player === currentPlayer) : history)
            .map(e => e.planetEspion)
        )].sort();

        planetsEspion.forEach(p => {
            const opt = document.createElement("option");
            opt.value = p; opt.textContent = p;
            planetEspionSelect.appendChild(opt);
        });

        // Planètes espionnées: globales (peut être rendu dépendant si souhaité)
        const planetsEspionnee = [...new Set(history.map(e => e.planetEspionnee))].sort();
        planetsEspionnee.forEach(p => {
            const opt = document.createElement("option");
            opt.value = p; opt.textContent = p;
            planetEspionneeSelect.appendChild(opt);
        });

        if (currentPlayer && players.includes(currentPlayer)) {
            playerSelect.value = currentPlayer;
        }
    }

    // =========================
    // Application des filtres + graphe
    // =========================
    function applyFilters() {
        const startDateVal = document.getElementById("filterStart").value;
        const endDateVal   = document.getElementById("filterEnd").value;
        const startHourVal = document.getElementById("filterStartHour").value;
        const endHourVal   = document.getElementById("filterEndHour").value;
        const player       = document.getElementById("filterPlayer").value;
        const planetEspion = document.getElementById("filterPlanetEspion").value;
        const planetEspionnee = document.getElementById("filterPlanetEspionnee").value;

        let filtered = history.slice();

        if (startDateVal) {
            const start = new Date(startDateVal);
            filtered = filtered.filter(e => {
                const abs = toAbsoluteDate(e.date, e.hour);
                return abs && abs >= start;
            });
        }
        if (endDateVal) {
            const end = new Date(endDateVal);
            filtered = filtered.filter(e => {
                const abs = toAbsoluteDate(e.date, e.hour);
                return abs && abs <= end;
            });
        }

        if (startHourVal || endHourVal) {
            const s = startHourVal ? normalizeHour(startHourVal) : null;
            const t = endHourVal ? normalizeHour(endHourVal) : null;
            filtered = filtered.filter(e => {
                const h = normalizeHour(e.hour.slice(0,5)); // HH:MM
                if (s && t) {
                    return s <= t ? (h >= s && h <= t) : (h >= s || h <= t);
                } else if (s) {
                    return h >= s;
                } else if (t) {
                    return h <= t;
                } else {
                    return true;
                }
            });
        }

        if (player)          filtered = filtered.filter(e => e.player === player);
        if (planetEspion)    filtered = filtered.filter(e => e.planetEspion === planetEspion);
        if (planetEspionnee) filtered = filtered.filter(e => e.planetEspionnee === planetEspionnee);

        renderFilteredHistory(filtered);

        const stats = document.getElementById("spyStats");
        if (stats) stats.textContent = `Univers: ${getUniverseKey()} | Joueur: ${getPlayerName()} | Total: ${history.length} | Après filtre: ${filtered.length}`;

        // Graphe toujours affiché (spécifique si joueur sélectionné, sinon global sur la liste filtrée)
        showGraph(player || null, filtered);
    }

    function resetFilters() {
        document.getElementById("filterStart").value = "";
        document.getElementById("filterEnd").value = "";
        document.getElementById("filterStartHour").value = "";
        document.getElementById("filterEndHour").value = "";
        document.getElementById("filterPlayer").value = "";
        document.getElementById("filterPlanetEspion").value = "";
        document.getElementById("filterPlanetEspionnee").value = "";
        renderHistory();
        const section = document.getElementById("spyGraphSection");
        if (section) section.style.display = "none";
        if (spyChart) { spyChart.destroy(); spyChart = null; }
        const stats = document.getElementById("spyStats");
        if (stats) stats.textContent = `Univers: ${getUniverseKey()} | Joueur: ${getPlayerName()} | Total enregistrements: ${history.length}`;
    }

    function renderFilteredHistory(list) {
        const spyList = document.getElementById("spyList");
        if (!spyList) return;
        spyList.innerHTML = "";

        const sorted = list.slice().sort((a,b) => {
            const da = toAbsoluteDate(a.date, a.hour);
            const db = toAbsoluteDate(b.date, b.hour);
            return db - da;
        });

        sorted.forEach(entry => {
            const li = document.createElement("li");

            const playerLink = document.createElement("a");
            playerLink.href = "#";
            playerLink.textContent = entry.player;
            playerLink.style.color = "#4FC3F7";
            playerLink.addEventListener("click", (e) => {
                e.preventDefault();
                showGraph(entry.player, list); // graphe basé sur la liste filtrée
            });

            li.textContent = `${entry.date} ${entry.hour} - `;
            li.appendChild(playerLink);
            li.appendChild(document.createTextNode(` (de ${entry.planetEspion} → ${entry.planetEspionnee})`));
            spyList.appendChild(li);
        });
    }

    // =========================
    // Graphe horaire (global ou joueur)
    // =========================
    function showGraph(player, list) {
        const section = document.getElementById("spyGraphSection");
        const title = document.getElementById("spyGraphTitle");
        const canvas = document.getElementById("spyGraphCanvas");
        if (!section || !title || !canvas) return;

        const hours = Array(24).fill(0);
        const dataset = player ? list.filter(e => e.player === player) : list;

        dataset.forEach(e => {
            const m = e.hour.match(/(\d{2}):(\d{2}):(\d{2})/);
            if (m) {
                const hour = parseInt(m[1], 10);
                if (!isNaN(hour)) hours[hour]++;
            }
        });

        section.style.display = "block";
        title.textContent = player
            ? `Espionnages de ${player} (après filtre)`
            : `Espionnages (tous joueurs, après filtre)`;

        if (spyChart) spyChart.destroy();
        // eslint-disable-next-line no-undef
        spyChart = new Chart(canvas, {
            type: 'bar',
            data: {
                labels: [...Array(24).keys()].map(h => h + "h"),
                datasets: [{ label: 'Nombre d’espionnages', data: hours, backgroundColor: '#FF9800' }]
            },
            options: { scales: { y: { beginAtZero: true } }, plugins: { legend: { display: false } } }
        });
    }

    // =========================
    // Parsing des messages (OGLight + regex fallback)
    // =========================
    function parseMessages() {
        const rows = document.querySelectorAll(".messageContentWrapper");
        rows.forEach(row => {
            const titleEl  = row.querySelector(".msgTitle");
            const dateEl   = row.querySelector(".msgDate");
            const playerEl = row.querySelector(".msgContent .player, .tooltipHTML.player");

            const titleText = titleEl ? titleEl.textContent : "";
            if (!titleText || !/espionnage/i.test(titleText)) return;

            const player = playerEl ? playerEl.textContent.trim() : "Inconnu";
            const full   = dateEl ? dateEl.textContent.trim() : "";
            const { date, hour } = splitOgameDate(full);

            let planetEspion = "??";
            let planetEspionnee = "??";

            // OGLight
            const oglCoords = Array.from(row.querySelectorAll(".msgContent a span"))
                .map(s => (s.textContent || "").trim())
                .filter(t => /\[\d+:\d+:\d+\]/.test(t));

            if (oglCoords.length >= 2) {
                planetEspion    = oglCoords[0];
                planetEspionnee = oglCoords[1];
            } else if (oglCoords.length === 1) {
                planetEspion = oglCoords[0];
            } else {
                // Fallback regex
                const text = row.textContent || "";
                const regexCoords = (text.match(/\[\d+:\d+:\d+\]/g) || []).map(s => s.trim());
                if (regexCoords.length >= 2) {
                    planetEspion    = regexCoords[0];
                    planetEspionnee = regexCoords[1];
                } else if (regexCoords.length === 1) {
                    planetEspion = regexCoords[0];
                }
                if (planetEspion === planetEspionnee && regexCoords.length >= 2) {
                    planetEspionnee = regexCoords[1];
                }
            }

            const exists = history.some(e =>
                e.date === date &&
                e.hour === hour &&
                e.player === player &&
                e.planetEspion === planetEspion &&
                e.planetEspionnee === planetEspionnee
            );
            if (!exists) {
                history.push({ date, hour, player, planetEspion, planetEspionnee });
            }
        });

        saveHistory();
        renderHistory();
    }

    // =========================
    // Utilitaires
    // =========================
    function normalizeHour(hhmm) {
        const m = hhmm.match(/(\d{1,2}):(\d{2})/);
        if (!m) return hhmm;
        const HH = String(m[1]).padStart(2, "0");
        const MM = m[2];
        return `${HH}:${MM}`;
    }
    function toAbsoluteDate(dateStr, timeStr) {
        const dm = dateStr.match(/(\d{2})\.(\d{2})\.(\d{4})/);
        const tm = timeStr.match(/(\d{2}):(\d{2}):(\d{2})/);
        if (!dm || !tm) return null;
        const [ , dd, mm, yyyy ] = dm;
        const [ , HH, MM, SS ]   = tm;
        return new Date(Number(yyyy), Number(mm)-1, Number(dd), Number(HH), Number(MM), Number(SS));
    }
    function splitOgameDate(str) {
        const m = str.match(/(\d{2}\.\d{2}\.\d{4})\s+(\d{2}:\d{2}:\d{2})/);
        if (!m) return { date: "??", hour: "??" };
        return { date: m[1], hour: m[2] };
    }

    // =========================
    // Chargement (version stable)
    // =========================
    window.addEventListener("load", () => {
        createMenu();
        renderHistory();
        parseMessages();
        showNotification("Espionnages analysés au chargement ✅");
    });
})();