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

})();