Horlonche

Planificateur de posts, sniper de topics et historique pour Onche.org

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Horlonche
// @namespace    http://tampermonkey.net/
// @version      11.0
// @description  Planificateur de posts, sniper de topics et historique pour Onche.org
// @author       Musclor1000
// @match        https://onche.org/topic/*
// @match        https://onche.org/forum/*
// @match        https://onche.org/
// @grant        none
// ==/UserScript==

(function () {
    'use strict';

    var estForum = window.location.pathname.indexOf('/forum/') === 0;

    // File d'attente : liste de { id, mode, titre, message, tsEcheance, timer, label }
    var fileAttente = [];
    var idCounter = 0;

    // =========================================================
    //  Sauvegarde / restauration de la file via sessionStorage
    // =========================================================

    function sauvegarderFile() {
        var data = fileAttente.map(function(item) {
            return {
                id: item.id,
                mode: item.mode,
                titre: item.titre,
                message: item.message,
                tsEcheance: item.tsEcheance,
                label: item.label,
                cibleInfo: item.cibleInfo || null
            };
        }).filter(function(item) {
            return item.tsEcheance > Date.now();
        });
        sessionStorage.setItem('horlonche_file', JSON.stringify(data));
    }

    function restaurerFile() {
        var raw = sessionStorage.getItem('horlonche_file');
        if (!raw) return;
        try {
            var data = JSON.parse(raw);
            for (var i = 0; i < data.length; i++) {
                var item = data[i];
                if (item.tsEcheance > Date.now()) {
                    if (item.id >= idCounter) idCounter = item.id;
                    ajouterFile(item);
                }
            }
        } catch(e) {}
    }

    var vueActive = 'liste';
    var modeChoisi = null;

    // =========================================================
    //  Panel principal
    // =========================================================

    var panel = document.createElement('div');
    panel.style.cssText = 'position:fixed;bottom:20px;right:20px;background:#1e2a3a;border:1px solid #4a90d9;border-radius:8px;padding:14px;z-index:99999;width:270px;font-family:sans-serif;font-size:13px;color:#eee;box-shadow:0 4px 15px rgba(0,0,0,0.5)';

    panel.innerHTML = ''
        + '<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:12px">'
        + '<span style="font-weight:bold;font-size:14px;color:#4a90d9">Horlonche</span>'
        + '<div style="display:flex;gap:4px">'
        + '<button id="os-btn-sniper" title="Sniper de topic" style="background:none;border:1px solid #e74c3c;color:#e74c3c;border-radius:4px;width:24px;height:24px;cursor:pointer;font-size:13px;line-height:1;padding:0">🎯</button>'
        + '<button id="os-btn-plus" title="Ajouter un post" style="background:none;border:1px solid #4a90d9;color:#4a90d9;border-radius:4px;width:24px;height:24px;cursor:pointer;font-size:16px;line-height:1;padding:0">+</button>'
        + '</div>'
        + '</div>'
        + '<div id="os-vue-liste">'
        + '<div id="os-file-vide" style="color:#aaa;font-size:12px;text-align:center;padding:10px 0">Aucun post programmé.<br>Clique sur <b>+</b> pour en ajouter un.</div>'
        + '<div id="os-file-liste"></div>'
        + '</div>'
        + '<div id="os-vue-form" style="display:none">'
        + '<div id="os-etape1">'
        + '<div style="margin-bottom:8px;color:#aaa;font-size:12px">Que veux-tu faire ?</div>'
        + (estForum
            ? '<button class="os-mode-btn" data-mode="nouveau" style="width:100%;padding:7px;margin-bottom:6px;background:#2c3e50;color:#eee;border:1px solid #4a90d9;border-radius:4px;cursor:pointer;text-align:left">📝 Créer un nouveau topic</button>'
            : '<button class="os-mode-btn" data-mode="topic" style="width:100%;padding:7px;margin-bottom:6px;background:#2c3e50;color:#eee;border:1px solid #4a90d9;border-radius:4px;cursor:pointer;text-align:left">💬 Répondre dans le topic</button>'
              + '<button class="os-mode-btn" data-mode="citation" style="width:100%;padding:7px;margin-bottom:6px;background:#2c3e50;color:#eee;border:1px solid #4a90d9;border-radius:4px;cursor:pointer;text-align:left">↩ Citer / répondre à un message</button>'
          )
        + '<button id="os-btn-annuler-form" style="width:100%;padding:5px;margin-top:4px;background:#2c3e50;color:#aaa;border:1px solid #555;border-radius:4px;cursor:pointer;font-size:12px">← Retour</button>'
        + '</div>'
        + '<div id="os-etape2" style="display:none">'
        + '<div id="os-label-mode" style="margin-bottom:10px;padding:5px 8px;background:#0d1b2a;border-radius:4px;font-size:12px;color:#4a90d9"></div>'
        + '<div id="os-instruction-citation" style="display:none;margin-bottom:10px;padding:6px;background:#2c1a00;border:1px solid #f39c12;border-radius:4px;font-size:11px;color:#f39c12">⚠ Clique sur la flèche du message AVANT de cliquer "Ajouter"</div>'
        + '<div id="os-champ-titre" style="display:none">'
        + '<label style="display:block;margin-bottom:4px">Titre du topic</label>'
        + '<input id="os-titre" type="text" placeholder="Titre..." style="width:100%;padding:5px;background:#0d1b2a;color:#eee;border:1px solid #4a90d9;border-radius:4px;margin-bottom:10px;box-sizing:border-box">'
        + '</div>'
        + '<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:4px">'
        + '<label>Heure (HH:MM:SS)</label>'
        + '<label style="font-size:11px;color:#aaa;cursor:pointer;display:flex;align-items:center;gap:4px"><input id="os-chk-jour" type="checkbox" style="cursor:pointer"> Jour</label>'
        + '</div>'
        + '<input id="os-heure" type="time" step="1" style="width:100%;padding:5px;background:#0d1b2a;color:#eee;border:1px solid #4a90d9;border-radius:4px;margin-bottom:6px;box-sizing:border-box">'
        + '<div id="os-champ-jour" style="display:none;margin-bottom:6px"><input id="os-jour" type="date" style="width:100%;padding:5px;background:#0d1b2a;color:#eee;border:1px solid #4a90d9;border-radius:4px;box-sizing:border-box"></div>'
        + '<label style="display:block;margin-bottom:4px">Message</label>'
        + '<textarea id="os-message" rows="3" style="width:100%;padding:5px;background:#0d1b2a;color:#eee;border:1px solid #4a90d9;border-radius:4px;margin-bottom:6px;box-sizing:border-box;resize:vertical"></textarea>'
        + '<button id="os-btn-preview" style="width:100%;padding:5px;background:#2c3e50;color:#aaa;border:1px solid #4a90d9;border-radius:4px;cursor:pointer;font-size:12px;margin-bottom:8px">👁 Prévisualiser</button>'
        + '<button id="os-btn-start" style="width:100%;padding:7px;background:#4a90d9;color:#fff;border:none;border-radius:4px;cursor:pointer;font-size:13px;margin-bottom:6px">Ajouter à la file</button>'
        + '<button id="os-btn-back" style="width:100%;padding:5px;background:#2c3e50;color:#aaa;border:1px solid #4a90d9;border-radius:4px;cursor:pointer;font-size:12px">← Retour</button>'
        + '</div>'
        + '</div>'
        + '<div id="os-vue-sniper" style="display:none">'
        + '<div style="margin-bottom:8px;color:#e74c3c;font-weight:bold;font-size:13px">🎯 Sniper de topic</div>'
        + '<label style="display:block;margin-bottom:4px;font-size:12px">Forums à scanner</label>'
        + '<div id="os-sniper-forums-liste" style="margin-bottom:4px"></div>'
        + '<div style="display:flex;gap:4px;margin-bottom:8px">'
        + '<input id="os-sniper-url-input" type="text" placeholder="https://onche.org/forum/..." style="flex:1;padding:5px;background:#0d1b2a;color:#eee;border:1px solid #e74c3c;border-radius:4px;font-size:11px;box-sizing:border-box">'
        + '<button id="os-sniper-url-add" style="padding:5px 8px;background:#e74c3c;color:#fff;border:none;border-radius:4px;cursor:pointer;font-size:13px">+</button>'
        + '</div>'
        + '<label style="display:block;margin-bottom:4px;font-size:12px">Mots clés (séparés par des virgules)</label>'
        + '<input id="os-sniper-mot" type="text" placeholder="ex: post, crypto, bonjour..." style="width:100%;padding:5px;background:#0d1b2a;color:#eee;border:1px solid #e74c3c;border-radius:4px;margin-bottom:8px;box-sizing:border-box">'
        + '<label style="display:block;margin-bottom:4px;font-size:12px">Message à poster</label>'
        + '<textarea id="os-sniper-message" rows="3" style="width:100%;padding:5px;background:#0d1b2a;color:#eee;border:1px solid #e74c3c;border-radius:4px;margin-bottom:8px;box-sizing:border-box;resize:vertical"></textarea>'
        + '<label style="display:block;margin-bottom:4px;font-size:12px">Intervalle de scan (secondes)</label>'
        + '<input id="os-sniper-intervalle" type="number" value="10" min="5" max="60" style="width:100%;padding:5px;background:#0d1b2a;color:#eee;border:1px solid #e74c3c;border-radius:4px;margin-bottom:8px;box-sizing:border-box">'
        + '<button id="os-sniper-start" style="width:100%;padding:7px;background:#e74c3c;color:#fff;border:none;border-radius:4px;cursor:pointer;font-size:13px;margin-bottom:6px">Activer le sniper</button>'
        + '<button id="os-sniper-stop" style="display:none;width:100%;padding:7px;background:#c0392b;color:#fff;border:none;border-radius:4px;cursor:pointer;font-size:13px;margin-bottom:6px">Désactiver</button>'
        + '<div style="display:flex;gap:4px">'
        + '<button id="os-sniper-retour" style="flex:1;padding:5px;background:#2c3e50;color:#aaa;border:1px solid #555;border-radius:4px;cursor:pointer;font-size:12px">← Retour</button>'
        + '<button id="os-sniper-historique-btn" style="padding:5px 8px;background:#2c3e50;color:#f39c12;border:1px solid #f39c12;border-radius:4px;cursor:pointer;font-size:12px">📋</button>'
        + '</div>'
        + '<div id="os-sniper-status" style="margin-top:8px;font-size:11px;color:#aaa;text-align:center"></div>'
        + '<div id="os-sniper-log" style="margin-top:6px;max-height:80px;overflow-y:auto;font-size:10px;color:#666"></div>'
        + '</div>'
        + '<div id="os-vue-historique" style="display:none">'
        + '<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:8px">'
        + '<span style="font-weight:bold;font-size:13px;color:#f39c12">📋 Historique</span>'
        + '<button id="os-historique-clear" style="padding:3px 8px;background:none;border:1px solid #c0392b;color:#c0392b;border-radius:3px;cursor:pointer;font-size:11px">Vider</button>'
        + '</div>'
        + '<div style="display:flex;gap:4px;margin-bottom:8px">'
        + '<button id="os-hist-tab-tracking" style="flex:1;padding:5px;background:#4a90d9;color:#fff;border:none;border-radius:4px;cursor:pointer;font-size:11px">Tracking (0)</button>'
        + '<button id="os-hist-tab-posting" style="flex:1;padding:5px;background:#2c3e50;color:#aaa;border:1px solid #2ecc71;border-radius:4px;cursor:pointer;font-size:11px">Posting (0)</button>'
        + '</div>'
        + '<div id="os-historique-liste" style="max-height:260px;overflow-y:auto"></div>'
        + '<button id="os-historique-retour" style="width:100%;padding:5px;margin-top:8px;background:#2c3e50;color:#aaa;border:1px solid #555;border-radius:4px;cursor:pointer;font-size:12px">← Retour</button>'
        + '</div>'
        + '<div id="os-vue-aspirateur" style="display:none">'
        + '<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:10px">'
        + '<span style="font-weight:bold;font-size:13px;color:#e67e22">🔍 Aspirateur de profil</span>'
        + '</div>'
        + '<div style="display:flex;gap:4px;margin-bottom:10px">'
        + '<input id="os-asp-pseudo" type="text" placeholder="Pseudo cible..." style="flex:1;padding:6px;background:#0d1b2a;color:#eee;border:1px solid #e67e22;border-radius:4px;font-size:13px;box-sizing:border-box">'
        + '<button id="os-asp-go" style="padding:6px 10px;background:#e67e22;color:#fff;border:none;border-radius:4px;cursor:pointer;font-size:13px">Go</button>'
        + '</div>'
        + '<div id="os-asp-status" style="font-size:11px;color:#aaa;text-align:center;margin-bottom:6px"></div>'
        + '<button id="os-asp-retour" style="width:100%;padding:5px;background:#2c3e50;color:#aaa;border:1px solid #555;border-radius:4px;cursor:pointer;font-size:12px">← Retour</button>'
        + '</div>'
        + '<div id="os-vue-aspirateur-fiche" style="display:none">'
        + '<div id="os-asp-fiche-contenu"></div>'
        + '<button id="os-asp-fiche-retour" style="width:100%;padding:5px;margin-top:8px;background:#2c3e50;color:#aaa;border:1px solid #555;border-radius:4px;cursor:pointer;font-size:12px">← Retour</button>'
        + '</div>'
        + '</div>';

    document.body.appendChild(panel);
    restaurerFile();

    // =========================================================
    //  Sniper de topic
    // =========================================================

    var sniperTimer     = null;
    var sniperActif     = false;
    var sniperTopicsVus = {};

    // Charge les topics deja vus depuis sessionStorage
    try {
        var raw = sessionStorage.getItem('horlonche_sniper_vus');
        if (raw) sniperTopicsVus = JSON.parse(raw);
    } catch(e) {}

    function sauvegarderTopicsVus() {
        try { sessionStorage.setItem('horlonche_sniper_vus', JSON.stringify(sniperTopicsVus)); } catch(e) {}
    }

    // =========================================================
    //  Liste des forums a scanner
    // =========================================================

    var sniperForums = ['https://onche.org/forum/1/blabla-general'];

    function afficherForums() {
        var liste = document.getElementById('os-sniper-forums-liste');
        if (!liste) return;
        liste.innerHTML = '';
        sniperForums.forEach(function(url, idx) {
            var ligne = document.createElement('div');
            ligne.style.cssText = 'display:flex;align-items:center;gap:4px;margin-bottom:4px;background:#0d1b2a;border:1px solid #3a2000;border-radius:4px;padding:3px 6px';
            var nom = url.replace('https://onche.org/forum/', '').replace(/\/.*/, '');
            nom = ({1:'Blabla General', 4:'Goulag', 7:'Finance', 8:'Jeux Video', 9:'Autonomie', 13:'Videoclub'})[nom] || url.split('/').pop();
            ligne.innerHTML = '<span style="flex:1;color:#eee;font-size:11px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">' + nom + '</span>'
                + '<button data-idx="' + idx + '" style="background:none;border:none;color:#c0392b;cursor:pointer;font-size:13px;padding:0 2px">✕</button>';
            ligne.querySelector('button').addEventListener('click', function() {
                sniperForums.splice(parseInt(this.getAttribute('data-idx')), 1);
                afficherForums();
            });
            liste.appendChild(ligne);
        });
    }

    // Initialiser avec Blabla General et afficher
    setTimeout(function() {
        afficherForums();

        document.getElementById('os-sniper-url-add').addEventListener('click', function() {
            var input = document.getElementById('os-sniper-url-input');
            var url   = input.value.trim();
            if (!url) return;
            if (url.indexOf('onche.org/forum/') === -1) { alert('URL invalide - doit etre un forum Onche'); return; }
            if (sniperForums.indexOf(url) !== -1) { alert('Forum deja dans la liste'); return; }
            sniperForums.push(url);
            input.value = '';
            afficherForums();
        });

        // Aussi ajouter en appuyant Entree
        document.getElementById('os-sniper-url-input').addEventListener('keydown', function(e) {
            if (e.key === 'Enter') document.getElementById('os-sniper-url-add').click();
        });
    }, 400);

    // =========================================================
    //  Historique du sniper (localStorage = survit a la fermeture)
    // =========================================================

    function ajouterHistorique(type, data) {
        var today = new Date().toLocaleDateString();
        var raw   = localStorage.getItem('horlonche_historique_' + today);
        var liste = [];
        try { if (raw) liste = JSON.parse(raw); } catch(e) {}
        liste.unshift(Object.assign({ heure: new Date().toLocaleTimeString(), type: type }, data));
        if (liste.length > 500) liste = liste.slice(0, 500);
        localStorage.setItem('horlonche_historique_' + today, JSON.stringify(liste));
    }

    var ongletHistActif = 'tracking';

    function chargerEntrees() {
        var entrees = [];
        for (var d = 0; d < 7; d++) {
            var date = new Date();
            date.setDate(date.getDate() - d);
            var key = date.toLocaleDateString();
            var raw = localStorage.getItem('horlonche_historique_' + key);
            try {
                if (raw) {
                    var items = JSON.parse(raw);
                    items.forEach(function(item) { item.date = key; });
                    entrees = entrees.concat(items);
                }
            } catch(e) {}
        }
        return entrees;
    }

    function afficherHistorique() {
        var liste = document.getElementById('os-historique-liste');
        if (!liste) return;
        liste.innerHTML = '';

        var entrees = chargerEntrees();
        var filtrees = entrees.filter(function(e) {
            return ongletHistActif === 'tracking' ? e.type === 'trouve' : (e.type === 'poste' || e.type === 'erreur');
        });

        // Mettre a jour les compteurs
        var nbTracking = entrees.filter(function(e) { return e.type === 'trouve'; }).length;
        var nbPosting  = entrees.filter(function(e) { return e.type === 'poste' || e.type === 'erreur'; }).length;
        var btnT = document.getElementById('os-hist-tab-tracking');
        var btnP = document.getElementById('os-hist-tab-posting');
        if (btnT) btnT.textContent = 'Tracking (' + nbTracking + ')';
        if (btnP) btnP.textContent = 'Posting (' + nbPosting + ')';

        if (filtrees.length === 0) {
            liste.innerHTML = '<div style="color:#aaa;text-align:center;padding:20px;font-size:12px">Aucune entree</div>';
            return;
        }

        filtrees.forEach(function(item) {
            var ligne = document.createElement('div');
            ligne.style.cssText = 'padding:7px 8px;margin-bottom:5px;background:#0d1b2a;border-radius:4px;font-size:11px;' +
                (item.type === 'trouve' ? 'border-left:3px solid #4a90d9' :
                 item.type === 'poste'  ? 'border-left:3px solid #2ecc71' :
                                          'border-left:3px solid #e74c3c');

            var html = '<div style="display:flex;justify-content:space-between;margin-bottom:4px">'
                     + '<span style="color:#888">' + item.date + ' ' + item.heure + '</span>'
                     + (item.type === 'erreur' ? '<span style="color:#e74c3c;font-size:10px">ERREUR</span>' : '')
                     + '</div>';

            if (item.type === 'trouve') {
                // Tracking
                html += '<div style="color:#4a90d9;font-weight:bold;margin-bottom:3px">' + (item.titre || '(sans titre)') + '</div>';
                if (item.auteur)  html += '<div style="color:#aaa">Auteur : <span style="color:#eee">' + item.auteur + '</span></div>';
                if (item.forum)   html += '<div style="color:#aaa">Forum : <span style="color:#eee">' + item.forum + '</span></div>';
                if (item.topicUrl) html += '<a href="' + item.topicUrl + '" target="_blank" style="color:#4a90d9;text-decoration:none;font-size:10px;display:inline-block;margin-top:3px">Voir le topic →</a>';
            } else {
                // Posting
                html += '<div style="color:#aaa;margin-bottom:3px">Topic : <a href="' + (item.topicUrl || '#') + '" target="_blank" style="color:#2ecc71;text-decoration:none">' + (item.topicTitre || item.topicUrl || '?') + '</a></div>';
                if (item.message) html += '<div style="color:#eee;background:#111;padding:4px 6px;border-radius:3px;margin-top:3px;word-break:break-word">' + item.message.substring(0, 100) + (item.message.length > 100 ? '...' : '') + '</div>';
                if (item.type === 'erreur' && item.erreur) html += '<div style="color:#e74c3c;margin-top:3px">' + item.erreur + '</div>';
            }

            ligne.innerHTML = html;
            liste.appendChild(ligne);
        });
    }

    // Bouton historique
    document.getElementById('os-sniper-historique-btn').addEventListener('click', function() {
        document.getElementById('os-vue-sniper').style.display    = 'none';
        document.getElementById('os-vue-historique').style.display = 'block';
        afficherHistorique();
    });

    document.getElementById('os-historique-retour').addEventListener('click', function() {
        document.getElementById('os-vue-historique').style.display = 'none';
        document.getElementById('os-vue-sniper').style.display     = 'block';
    });

    document.getElementById('os-hist-tab-tracking').addEventListener('click', function() {
        ongletHistActif = 'tracking';
        this.style.background = '#4a90d9'; this.style.color = '#fff'; this.style.border = 'none';
        var p = document.getElementById('os-hist-tab-posting');
        p.style.background = '#2c3e50'; p.style.color = '#aaa'; p.style.border = '1px solid #2ecc71';
        afficherHistorique();
    });

    document.getElementById('os-hist-tab-posting').addEventListener('click', function() {
        ongletHistActif = 'posting';
        this.style.background = '#2ecc71'; this.style.color = '#fff'; this.style.border = 'none';
        var t = document.getElementById('os-hist-tab-tracking');
        t.style.background = '#2c3e50'; t.style.color = '#aaa'; t.style.border = '1px solid #4a90d9';
        afficherHistorique();
    });

    document.getElementById('os-historique-clear').addEventListener('click', function() {
        if (!confirm("Vider tout l'historique ?")) return;
        for (var d = 0; d < 7; d++) {
            var date = new Date();
            date.setDate(date.getDate() - d);
            localStorage.removeItem('horlonche_historique_' + date.toLocaleDateString());
        }
        afficherHistorique();
    });

    // Restaurer le sniper apres refresh
    function restaurerSniper() {
        var raw = sessionStorage.getItem('horlonche_sniper_config');
        if (!raw) return;
        try {
            var config = JSON.parse(raw);
            if (!config.actif) return;
            // Remplir les champs
            document.getElementById('os-sniper-mot').value        = config.mot;
            document.getElementById('os-sniper-message').value    = config.message;
            document.getElementById('os-sniper-intervalle').value = config.intervalle;
            if (config.forums && Array.isArray(config.forums)) { sniperForums = config.forums; afficherForums(); }
            // Relancer le sniper automatiquement
            document.getElementById('os-sniper-start').click();
            ajouterLogSniper('Sniper relancé après refresh.', 'info');
        } catch(e) {}
    }

    // Lancer apres que le DOM est pret
    setTimeout(restaurerSniper, 300);

    document.getElementById('os-btn-sniper').addEventListener('click', function() {
        document.getElementById('os-vue-liste').style.display  = 'none';
        document.getElementById('os-vue-form').style.display   = 'none';
        document.getElementById('os-vue-sniper').style.display = 'block';
    });

    document.getElementById('os-sniper-retour').addEventListener('click', function() {
        document.getElementById('os-vue-sniper').style.display = 'none';
        document.getElementById('os-vue-liste').style.display  = 'block';
    });

    document.getElementById('os-sniper-start').addEventListener('click', function() {
        var mot        = document.getElementById('os-sniper-mot').value.trim().toLowerCase().replace(/\s*,\s*/g, ',');
        var message    = document.getElementById('os-sniper-message').value.trim();
        var intervalle = parseInt(document.getElementById('os-sniper-intervalle').value) || 10;
        var scanUrl    = sniperForums.length > 0 ? sniperForums : ['https://onche.org/forum/1/blabla-general'];

        if (!mot)     { alert('Ecris un mot clé !'); return; }
        if (!message) { alert('Ecris un message !'); return; }

        sniperActif = true;
        document.getElementById('os-sniper-start').style.display = 'none';
        document.getElementById('os-sniper-stop').style.display  = 'block';
        setSniperStatus('Actif — scan toutes les ' + intervalle + 's', '#2ecc71');
        ajouterLogSniper('Sniper activé : "' + mot + '"', 'info');

        // Sauvegarder l'état pour survie au refresh
        sessionStorage.setItem('horlonche_sniper_config', JSON.stringify({
            actif: true, mot: mot, message: message, intervalle: intervalle, scanUrl: scanUrl, forums: sniperForums
        }));

        // Scan immediat puis toutes les X secondes
        var forumsAScan = Array.isArray(scanUrl) ? scanUrl : [scanUrl];
        forumsAScan.forEach(function(url) { scannerTopics(mot, message, url); });
        sniperTimer = setInterval(function() {
            forumsAScan.forEach(function(url) { scannerTopics(mot, message, url); });
        }, intervalle * 1000);
    });

    document.getElementById('os-sniper-stop').addEventListener('click', function() {
        if (sniperTimer) clearInterval(sniperTimer);
        sniperTimer = null;
        sniperActif = false;
        document.getElementById('os-sniper-start').style.display = 'block';
        document.getElementById('os-sniper-stop').style.display  = 'none';
        setSniperStatus('Inactif', '#aaa');
        ajouterLogSniper('Sniper désactivé.', 'info');
        sessionStorage.removeItem('horlonche_sniper_config');
    });

    function scannerTopics(mot, message, scanUrl) {
        var forumUrl = scanUrl || 'https://onche.org/forum/1/blabla-general';
        setSniperStatus('Scan en cours...', '#f39c12');

        fetch(forumUrl, { credentials: 'include' })
        .then(function(r) { return r.text(); })
        .then(function(html) {
            // Extrait tous les liens de topics avec leur titre
            var parser = new DOMParser();
            var doc    = parser.parseFromString(html, 'text/html');
            var liens  = doc.querySelectorAll('a.topic-subject, a[href*="/topic/"]');
            var trouves = 0;

            liens.forEach(function(lien) {
                var titre  = lien.textContent.trim().toLowerCase();
                var href   = lien.getAttribute('href');
                if (!href || href.indexOf('/topic/') === -1) return;

                // Extrait l'ID du topic depuis l'URL
                var matchId = href.match(/\/topic\/(\d+)\//);
                if (!matchId) return;
                var topicId = matchId[1];

                // Deja traite ?
                if (sniperTopicsVus[topicId]) return;

                // Contient au moins un des mots cles ?
                var mots = mot.split(',').map(function(m) { return m.trim(); }).filter(function(m) { return m.length > 0; });
                var correspondance = mots.some(function(m) { return titre.indexOf(m) !== -1; });
                if (!correspondance) return;

                // Nouveau topic correspondant !
                trouves++;
                sniperTopicsVus[topicId] = true;
                sauvegarderTopicsVus();

                ajouterLogSniper('🎯 ' + lien.textContent.trim().substring(0, 40), 'trouve');
                ajouterHistorique('trouve', {
                    titre:    lien.textContent.trim(),
                    auteur:   (lien.querySelector('.topic-username') || {textContent:''}).textContent.trim(),
                    forum:    forumUrl,
                    topicUrl: 'https://onche.org' + href
                });

                // Poster dans ce topic
                posterDansTopicSniper(href, message, topicId);
            });

            setSniperStatus('Actif — dernier scan : ' + new Date().toLocaleTimeString(), '#2ecc71');
        })
        .catch(function() {
            setSniperStatus('Erreur de scan', '#e74c3c');
        });
    }

    function posterDansTopicSniper(topicHref, message, topicId) {
        // Construit l'URL complete
        var topicUrl = 'https://onche.org' + topicHref.replace(/\/\d+$/, '/1');
        if (topicHref.indexOf('https://') === 0) topicUrl = topicHref;

        // Recupere un token frais depuis la page du topic
        fetch(topicUrl, { credentials: 'include' })
        .then(function(r) { return r.text(); })
        .then(function(html) {
            var match = html.match(/name="token"[^>]*value="([^"]+)"/) || html.match(/value="([^"]+)"[^>]*name="token"/);
            var token = match ? match[1] : '';
            if (!token) { ajouterLogSniper('❌ Token introuvable pour topic #' + topicId, 'erreur'); return; }

            var actionUrl = topicUrl.split('#')[0] + '#last';
            var body = new URLSearchParams();
            body.append('message',       message);
            body.append('poll_question', '');
            body.append('poll[]',        '');
            body.append('token',         token);

            return fetch(actionUrl, {
                method: 'POST',
                headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
                body: body.toString(),
                credentials: 'include'
            });
        })
        .then(function(r) {
            if (r && (r.ok || r.redirected)) {
                ajouterLogSniper('✅ Posté dans topic #' + topicId, 'poste');
            ajouterHistorique('poste', {
                topicUrl:   topicUrl,
                topicTitre: 'Topic #' + topicId,
                message:    message
            });
            } else if (r) {
                ajouterLogSniper('❌ Erreur ' + r.status + ' pour topic #' + topicId, 'erreur');
            ajouterHistorique('erreur', { topicUrl: topicUrl, topicTitre: 'Topic #' + topicId, message: message, erreur: 'HTTP ' + r.status });
            }
        })
        .catch(function() {
            ajouterLogSniper('❌ Erreur réseau pour topic #' + topicId, 'erreur');
        ajouterHistorique('erreur', { topicUrl: topicUrl, topicTitre: 'Topic #' + topicId, message: message, erreur: 'Erreur reseau' });
        });
    }

    function setSniperStatus(texte, couleur) {
        var el = document.getElementById('os-sniper-status');
        if (el) { el.textContent = texte; el.style.color = couleur || '#aaa'; }
    }

    function ajouterLogSniper(texte, type, topicUrl) {
        var log = document.getElementById('os-sniper-log');
        if (log) {
            var ligne = document.createElement('div');
            ligne.style.color = type === 'poste' ? '#2ecc71' : type === 'erreur' ? '#e74c3c' : type === 'trouve' ? '#4a90d9' : '#888';
            ligne.textContent = new Date().toLocaleTimeString() + ' — ' + texte;
            log.insertBefore(ligne, log.firstChild);
            while (log.children.length > 20) log.removeChild(log.lastChild);
        }
        // Sauvegarder dans l'historique persistant
        if (type) ajouterHistorique(type, texte, topicUrl);
    }

    // =========================================================
    //  Capture citation (data-message-quote)
    // =========================================================

    var messageCibleCapture = null;

    document.addEventListener('click', function(e) {
        var fleche = e.target.closest('[data-message-quote]');
        if (!fleche) return;
        var messageEl = fleche.closest('[data-id]');
        if (!messageEl) return;
        messageCibleCapture = {
            messageId: messageEl.getAttribute('data-id'),
            username:  messageEl.getAttribute('data-username') || ''
        };
    }, true);

    // =========================================================
    //  Preview
    // =========================================================

    var previewBox = document.createElement('div');
    previewBox.style.cssText = 'display:none;position:fixed;bottom:20px;right:310px;background:#1e2a3a;border:1px solid #4a90d9;border-radius:8px;padding:14px;z-index:99998;width:320px;max-height:280px;font-family:sans-serif;font-size:14px;color:#eee;box-shadow:0 4px 15px rgba(0,0,0,0.5);overflow-y:auto;word-break:break-word;white-space:pre-wrap;line-height:1.6';
    document.body.appendChild(previewBox);
    var previewOuvert = false;

    document.getElementById('os-btn-preview').addEventListener('click', function () {
        var msg = document.getElementById('os-message').value;
        if (!previewOuvert) {
            previewBox.textContent = msg || '(message vide)';
            previewBox.style.display = 'block';
            this.textContent = '✕ Fermer la prévisualisation';
            this.style.color = '#4a90d9';
            previewOuvert = true;
            document.getElementById('os-message').addEventListener('input', majPreview);
        } else {
            previewBox.style.display = 'none';
            this.textContent = '👁 Prévisualiser';
            this.style.color = '#aaa';
            previewOuvert = false;
            document.getElementById('os-message').removeEventListener('input', majPreview);
        }
    });
    function majPreview() { previewBox.textContent = document.getElementById('os-message').value || '(message vide)'; }

    // =========================================================
    //  Navigation
    // =========================================================

    document.getElementById('os-btn-plus').addEventListener('click', function () {
        document.getElementById('os-vue-liste').style.display = 'none';
        document.getElementById('os-vue-form').style.display = 'block';
        document.getElementById('os-etape1').style.display = 'block';
        document.getElementById('os-etape2').style.display = 'none';
        vueActive = 'formulaire';
    });

    document.getElementById('os-btn-annuler-form').addEventListener('click', retourListe);
    document.getElementById('os-btn-back').addEventListener('click', function () {
        document.getElementById('os-etape2').style.display = 'none';
        document.getElementById('os-etape1').style.display = 'block';
        modeChoisi = null;
    });

    function retourListe() {
        document.getElementById('os-vue-form').style.display = 'none';
        document.getElementById('os-vue-liste').style.display = 'block';
        vueActive = 'liste';
        modeChoisi = null;
        if (previewOuvert) document.getElementById('os-btn-preview').click();
    }

    var modeBtns = panel.querySelectorAll('.os-mode-btn');
    for (var i = 0; i < modeBtns.length; i++) {
        modeBtns[i].addEventListener('click', function () {
            modeChoisi = this.getAttribute('data-mode');
            document.getElementById('os-etape1').style.display = 'none';
            document.getElementById('os-etape2').style.display = 'block';
            var labels = { 'topic': 'Répondre dans le topic', 'citation': 'Citer un message', 'nouveau': 'Nouveau topic' };
            document.getElementById('os-label-mode').textContent = '→ ' + labels[modeChoisi];
            document.getElementById('os-instruction-citation').style.display = (modeChoisi === 'citation') ? 'block' : 'none';
            document.getElementById('os-champ-titre').style.display = (modeChoisi === 'nouveau') ? 'block' : 'none';
        });
    }

    document.getElementById('os-chk-jour').addEventListener('change', function () {
        var champ = document.getElementById('os-champ-jour');
        if (this.checked) {
            champ.style.display = 'block';
            var auj = new Date();
            document.getElementById('os-jour').value = auj.getFullYear() + '-' + pad(auj.getMonth() + 1) + '-' + pad(auj.getDate());
        } else {
            champ.style.display = 'none';
            document.getElementById('os-jour').value = '';
        }
    });

    // =========================================================
    //  Ajouter a la file
    // =========================================================

    document.getElementById('os-btn-start').addEventListener('click', function () {
        var heureVal   = document.getElementById('os-heure').value;
        var messageVal = document.getElementById('os-message').value.trim();
        var titreVal   = document.getElementById('os-titre') ? document.getElementById('os-titre').value.trim() : '';
        var jourVal    = document.getElementById('os-jour').value;

        if (!heureVal)                             { alert('Choisis une heure !'); return; }
        if (!messageVal)                           { alert('Ecris un message !'); return; }
        if (modeChoisi === 'nouveau' && !titreVal) { alert('Ecris un titre !'); return; }

        var parts = heureVal.split(':');
        var cible = new Date();
        if (jourVal) {
            var jp = jourVal.split('-');
            cible.setFullYear(parseInt(jp[0]), parseInt(jp[1]) - 1, parseInt(jp[2]));
        }
        cible.setHours(parseInt(parts[0]), parseInt(parts[1]), parseInt(parts[2] || 0), 0);
        if (!jourVal && cible.getTime() <= Date.now()) cible.setDate(cible.getDate() + 1);

        var labels    = { 'topic': '💬', 'citation': '↩', 'nouveau': '📝' };
        var labelJour = jourVal ? jourVal.split('-').reverse().join('/') + ' ' : '';
        var labelAffiche = labels[modeChoisi] + ' ' + labelJour + heureVal;
        if (modeChoisi === 'nouveau'  && titreVal)           labelAffiche += ' — ' + titreVal.substring(0, 20);
        if (modeChoisi === 'citation' && messageCibleCapture) labelAffiche += ' (@' + (messageCibleCapture.username || '?') + ')';

        var cibleInfoPourCetItem = (modeChoisi === 'citation') ? messageCibleCapture : null;
        messageCibleCapture = null;

        if (modeChoisi === 'citation' && !cibleInfoPourCetItem) {
            alert('Aucun message cible ! Clique la flèche du message avant de programmer.');
            return;
        }

        ajouterFile({
            id:         ++idCounter,
            mode:       modeChoisi,
            titre:      titreVal,
            message:    messageVal,
            tsEcheance: cible.getTime(),
            label:      labelAffiche,
            heure:      heureVal,
            pageUrl:    window.location.href.split('#')[0],
            cibleInfo:  cibleInfoPourCetItem
        });

        document.getElementById('os-heure').value = '';
        document.getElementById('os-message').value = '';
        if (document.getElementById('os-titre')) document.getElementById('os-titre').value = '';
        document.getElementById('os-chk-jour').checked = false;
        document.getElementById('os-champ-jour').style.display = 'none';
        document.getElementById('os-jour').value = '';
        retourListe();
    });

    // =========================================================
    //  File d'attente
    // =========================================================

    function ajouterFile(item) {
        fileAttente.push(item);
        afficherFile();
        sauvegarderFile();

        item.timer = setInterval(function () {
            var resteMs = item.tsEcheance - Date.now();
            if (resteMs <= 0) {
                clearInterval(item.timer);
                executerItem(item);
                return;
            }
            var el = document.getElementById('os-countdown-' + item.id);
            if (el) {
                var resteS = Math.floor(resteMs / 1000);
                el.textContent = pad(Math.floor(resteS/3600)) + ':' + pad(Math.floor((resteS%3600)/60)) + ':' + pad(resteS%60);
            }
        }, 200);
    }

    function supprimerFile(id) {
        for (var i = 0; i < fileAttente.length; i++) {
            if (fileAttente[i].id === id) {
                clearInterval(fileAttente[i].timer);
                fileAttente.splice(i, 1);
                break;
            }
        }
        afficherFile();
        sauvegarderFile();
    }

    function afficherFile() {
        var liste = document.getElementById('os-file-liste');
        var vide  = document.getElementById('os-file-vide');
        liste.style.cssText = 'max-height:220px;overflow-y:auto';
        if (fileAttente.length === 0) { vide.style.display = 'block'; liste.innerHTML = ''; return; }
        vide.style.display = 'none';
        liste.innerHTML = '';
        for (var i = 0; i < fileAttente.length; i++) {
            (function(item) {
                var apercu = item.message.length > 40 ? item.message.substring(0, 40) + '...' : item.message;
                var ligne = document.createElement('div');
                ligne.style.cssText = 'padding:6px 8px;margin-bottom:5px;background:#0d1b2a;border:1px solid #2a3f55;border-radius:4px;font-size:12px';
                ligne.innerHTML = ''
                    + '<div style="display:flex;align-items:center;justify-content:space-between">'
                    + '<span style="color:#eee;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:160px">' + item.label + '</span>'
                    + '<div style="display:flex;align-items:center;gap:6px">'
                    + '<span id="os-countdown-' + item.id + '" style="color:#4a90d9;font-size:11px;font-weight:bold">--:--:--</span>'
                    + '<button data-id="' + item.id + '" style="background:none;border:1px solid #c0392b;color:#c0392b;border-radius:3px;cursor:pointer;font-size:11px;padding:2px 5px;flex-shrink:0">✕</button>'
                    + '</div>'
                    + '</div>'
                    + '<div style="color:#8aa8c8;font-size:11px;margin-top:3px;font-style:italic;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">' + apercu + '</div>';
                ligne.querySelector('button').addEventListener('click', function () {
                    supprimerFile(parseInt(this.getAttribute('data-id')));
                });
                liste.appendChild(ligne);
            })(fileAttente[i]);
        }
    }

    // =========================================================
    //  Execution
    // =========================================================

    function executerItem(item) {
        var el = document.getElementById('os-countdown-' + item.id);
        if (el) { el.textContent = 'Envoi...'; el.style.color = '#f39c12'; }
        if (item.mode === 'citation') {
            posterAvecFetch(item, true);
        } else if (item.mode === 'nouveau') {
            posterNouveauTopic(item);
        } else {
            posterAvecFetch(item, false);
        }
    }

    function terminerItem(id, succes, msg) {
        var el = document.getElementById('os-countdown-' + id);
        if (el) { el.textContent = msg; el.style.color = succes ? '#2ecc71' : '#e74c3c'; }
        setTimeout(function () {
            for (var i = 0; i < fileAttente.length; i++) {
                if (fileAttente[i].id === id) { fileAttente.splice(i, 1); break; }
            }
            afficherFile();
            sauvegarderFile();
        }, 3000);
    }

    // =========================================================
    //  Poster reponse ou citation via fetch
    // =========================================================

    function posterAvecFetch(item, estCitation) {
        // Recupere un token frais sur la page du topic
        var pageUrl = item.pageUrl || window.location.href.split('#')[0];
        var action  = pageUrl + '#last';

        fetch(pageUrl, { credentials: 'include' })
        .then(function(r) { return r.text(); })
        .then(function(html) {
            var match = html.match(/name="token"[^>]*value="([^"]+)"/) || html.match(/value="([^"]+)"[^>]*name="token"/);
            var token = match ? match[1] : '';
            if (!token) { terminerItem(item.id, false, 'Token introuvable !'); return; }

            var body = new URLSearchParams();
            if (estCitation && item.cibleInfo && item.cibleInfo.messageId) {
                body.append('answer', item.cibleInfo.messageId);
            }
            body.append('message',       item.message);
            body.append('poll_question', '');
            body.append('poll[]',        '');
            body.append('token',         token);

            return fetch(action, {
                method: 'POST',
                headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
                body: body.toString(),
                credentials: 'include'
            });
        })
        .then(function(r) {
            if (r && (r.ok || r.redirected)) { terminerItem(item.id, true, estCitation ? 'Citation postee !' : 'Poste !'); }
            else if (r) { terminerItem(item.id, false, 'Erreur ' + r.status); }
        })
        .catch(function() { terminerItem(item.id, false, 'Erreur reseau !'); });
    }

    // =========================================================
    //  Creer un nouveau topic
    // =========================================================

    function posterNouveauTopic(item) {
        var pageUrl = item.pageUrl || window.location.href.split('#')[0];
        var action  = pageUrl + '#post';

        fetch(pageUrl, { credentials: 'include' })
        .then(function(r) { return r.text(); })
        .then(function(html) {
            var match = html.match(/name="token"[^>]*value="([^"]+)"/) || html.match(/value="([^"]+)"[^>]*name="token"/);
            var token = match ? match[1] : '';
            if (!token) { terminerItem(item.id, false, 'Token introuvable !'); return; }

            var body = new URLSearchParams();
            body.append('title',   item.titre);
            body.append('message', item.message);
            body.append('poll[]',  '');
            body.append('token',   token);

            return fetch(action, {
                method: 'POST',
                headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
                body: body.toString(),
                credentials: 'include'
            });
        })
        .then(function(r) {
            if (r && (r.ok || r.redirected)) { terminerItem(item.id, true, 'Topic cree !'); }
            else if (r) { terminerItem(item.id, false, 'Erreur ' + r.status); }
        })
        .catch(function() { terminerItem(item.id, false, 'Erreur reseau !'); });
    }

    // =========================================================
    //  Utilitaires
    // =========================================================

    // =========================================================
    //  Horloge titre onglet
    // =========================================================

    function mettreAJourTitre() {
        var n = new Date();
        document.title = pad(n.getHours()) + ':' + pad(n.getMinutes()) + ':' + pad(n.getSeconds()) + ' | Horlonche';
    }
    mettreAJourTitre();
    setInterval(mettreAJourTitre, 1000);

})();