☰

🌍 Server Map Tracker

Affiche une carte interactive de tous les serveurs/ressources chargΓ©s par la page avec leurs IPs et localisations

이 슀크립트λ₯Ό μ„€μΉ˜ν•˜λ €λ©΄ Tampermonkey, Greasemonkey λ˜λŠ” Violentmonkey와 같은 ν™•μž₯ ν”„λ‘œκ·Έλž¨μ΄ ν•„μš”ν•©λ‹ˆλ‹€.

이 슀크립트λ₯Ό μ„€μΉ˜ν•˜λ €λ©΄ Tampermonkey와 같은 ν™•μž₯ ν”„λ‘œκ·Έλž¨μ„ μ„€μΉ˜ν•΄μ•Ό ν•©λ‹ˆλ‹€.

이 슀크립트λ₯Ό μ„€μΉ˜ν•˜λ €λ©΄ Tampermonkey λ˜λŠ” Violentmonkey와 같은 ν™•μž₯ ν”„λ‘œκ·Έλž¨μ΄ ν•„μš”ν•©λ‹ˆλ‹€.

이 슀크립트λ₯Ό μ„€μΉ˜ν•˜λ €λ©΄ Tampermonkey λ˜λŠ” Userscripts와 같은 ν™•μž₯ ν”„λ‘œκ·Έλž¨μ΄ ν•„μš”ν•©λ‹ˆλ‹€.

이 슀크립트λ₯Ό μ„€μΉ˜ν•˜λ €λ©΄ Tampermonkey와 같은 ν™•μž₯ ν”„λ‘œκ·Έλž¨μ΄ ν•„μš”ν•©λ‹ˆλ‹€.

이 슀크립트λ₯Ό μ„€μΉ˜ν•˜λ €λ©΄ μœ μ € 슀크립트 κ΄€λ¦¬μž ν™•μž₯ ν”„λ‘œκ·Έλž¨μ΄ ν•„μš”ν•©λ‹ˆλ‹€.

(이미 μœ μ € 슀크립트 κ΄€λ¦¬μžκ°€ μ„€μΉ˜λ˜μ–΄ μžˆμŠ΅λ‹ˆλ‹€. μ„€μΉ˜λ₯Ό μ§„ν–‰ν•©λ‹ˆλ‹€!)

이 μŠ€νƒ€μΌμ„ μ„€μΉ˜ν•˜λ €λ©΄ Stylus와 같은 ν™•μž₯ ν”„λ‘œκ·Έλž¨μ΄ ν•„μš”ν•©λ‹ˆλ‹€.

이 μŠ€νƒ€μΌμ„ μ„€μΉ˜ν•˜λ €λ©΄ Stylus와 같은 ν™•μž₯ ν”„λ‘œκ·Έλž¨μ΄ ν•„μš”ν•©λ‹ˆλ‹€.

이 μŠ€νƒ€μΌμ„ μ„€μΉ˜ν•˜λ €λ©΄ Stylus와 같은 ν™•μž₯ ν”„λ‘œκ·Έλž¨μ΄ ν•„μš”ν•©λ‹ˆλ‹€.

이 μŠ€νƒ€μΌμ„ μ„€μΉ˜ν•˜λ €λ©΄ μœ μ € μŠ€νƒ€μΌ κ΄€λ¦¬μž ν™•μž₯ ν”„λ‘œκ·Έλž¨μ΄ ν•„μš”ν•©λ‹ˆλ‹€.

이 μŠ€νƒ€μΌμ„ μ„€μΉ˜ν•˜λ €λ©΄ μœ μ € μŠ€νƒ€μΌ κ΄€λ¦¬μž ν™•μž₯ ν”„λ‘œκ·Έλž¨μ΄ ν•„μš”ν•©λ‹ˆλ‹€.

이 μŠ€νƒ€μΌμ„ μ„€μΉ˜ν•˜λ €λ©΄ μœ μ € μŠ€νƒ€μΌ κ΄€λ¦¬μž ν™•μž₯ ν”„λ‘œκ·Έλž¨μ΄ ν•„μš”ν•©λ‹ˆλ‹€.

(이미 μœ μ € μŠ€νƒ€μΌ κ΄€λ¦¬μžκ°€ μ„€μΉ˜λ˜μ–΄ μžˆμŠ΅λ‹ˆλ‹€. μ„€μΉ˜λ₯Ό μ§„ν–‰ν•©λ‹ˆλ‹€!)

// ==UserScript==
// @name         🌍 Server Map Tracker
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  Affiche une carte interactive de tous les serveurs/ressources chargΓ©s par la page avec leurs IPs et localisations
// @author       ServerMapTracker
// @match        *://*/*
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @connect      ip-api.com
// @run-at       document-idle
// ==/UserScript==

(function () {
  'use strict';

  // ── Γ‰TAT ────────────────────────────────────────────────────────────────────
  const servers = new Map();   // host β†’ { ip, country, countryCode, city, lat, lon, org, type, count }
  let mapVisible = false;
  let map = null;
  let markers = [];
  let pendingQueue = [];
  let isProcessing = false;
  const MAX_ENTRIES = 80;      // reset auto au-delΓ 

  // ── STYLES ──────────────────────────────────────────────────────────────────
  GM_addStyle(`
    #smt-btn {
      position: fixed;
      bottom: 24px;
      right: 24px;
      z-index: 2147483646;
      background: #0f172a;
      border: 1.5px solid #38bdf8;
      color: #38bdf8;
      font-family: 'Courier New', monospace;
      font-size: 12px;
      padding: 8px 14px;
      border-radius: 6px;
      cursor: pointer;
      box-shadow: 0 0 12px #38bdf840;
      transition: all .2s;
      display: flex;
      align-items: center;
      gap: 6px;
    }
    #smt-btn:hover { background: #38bdf8; color: #0f172a; box-shadow: 0 0 20px #38bdf870; }
    #smt-badge {
      background: #f43f5e;
      color: #fff;
      border-radius: 10px;
      padding: 1px 6px;
      font-size: 10px;
      font-weight: bold;
    }
    #smt-panel {
      position: fixed;
      bottom: 72px;
      right: 24px;
      width: 620px;
      height: 500px;
      z-index: 2147483647;
      background: #0f172a;
      border: 1.5px solid #334155;
      border-radius: 12px;
      box-shadow: 0 8px 40px #00000080;
      display: flex;
      flex-direction: column;
      overflow: hidden;
      font-family: 'Courier New', monospace;
    }
    #smt-header {
      display: flex;
      align-items: center;
      justify-content: space-between;
      padding: 10px 16px;
      background: #1e293b;
      border-bottom: 1px solid #334155;
      flex-shrink: 0;
    }
    #smt-title { color: #38bdf8; font-size: 13px; font-weight: bold; letter-spacing: 1px; }
    #smt-controls { display: flex; gap: 8px; }
    .smt-ctrl-btn {
      background: #0f172a;
      border: 1px solid #475569;
      color: #94a3b8;
      font-size: 10px;
      padding: 4px 10px;
      border-radius: 4px;
      cursor: pointer;
      transition: all .15s;
    }
    .smt-ctrl-btn:hover { border-color: #38bdf8; color: #38bdf8; }
    .smt-ctrl-btn.active { border-color: #38bdf8; color: #38bdf8; background: #0e3a52; }
    #smt-map {
      flex: 1;
      min-height: 0;
    }
    #smt-list {
      flex: 1;
      min-height: 0;
      overflow-y: auto;
      display: none;
    }
    #smt-list::-webkit-scrollbar { width: 4px; }
    #smt-list::-webkit-scrollbar-track { background: #0f172a; }
    #smt-list::-webkit-scrollbar-thumb { background: #334155; border-radius: 2px; }
    .smt-row {
      display: grid;
      grid-template-columns: 24px 1fr 90px 80px 60px;
      gap: 8px;
      padding: 6px 14px;
      border-bottom: 1px solid #1e293b;
      align-items: center;
      font-size: 11px;
      transition: background .1s;
    }
    .smt-row:hover { background: #1e293b; }
    .smt-flag { font-size: 16px; text-align: center; }
    .smt-host { color: #e2e8f0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
    .smt-ip { color: #38bdf8; font-size: 10px; }
    .smt-org { color: #64748b; font-size: 10px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
    .smt-type { font-size: 9px; padding: 2px 5px; border-radius: 3px; text-align: center; font-weight: bold; }
    .type-js { background: #f59e0b20; color: #f59e0b; }
    .type-css { background: #8b5cf620; color: #8b5cf6; }
    .type-img { background: #10b98120; color: #10b981; }
    .type-api { background: #f43f5e20; color: #f43f5e; }
    .type-font { background: #06b6d420; color: #06b6d4; }
    .type-other { background: #47556920; color: #475569; }
    .smt-copy-ip {
      background: none;
      border: 1px solid #334155;
      color: #475569;
      font-size: 9px;
      padding: 2px 5px;
      border-radius: 3px;
      cursor: pointer;
      transition: all .15s;
      font-family: 'Courier New', monospace;
    }
    .smt-copy-ip:hover { border-color: #38bdf8; color: #38bdf8; }
    .smt-copy-ip.copied { border-color: #10b981; color: #10b981; }
    #smt-footer {
      padding: 6px 14px;
      background: #1e293b;
      border-top: 1px solid #334155;
      display: flex;
      justify-content: space-between;
      align-items: center;
      flex-shrink: 0;
    }
    #smt-stats { color: #475569; font-size: 10px; }
    #smt-reset-btn {
      background: #f43f5e20;
      border: 1px solid #f43f5e40;
      color: #f43f5e;
      font-size: 10px;
      padding: 3px 10px;
      border-radius: 4px;
      cursor: pointer;
      transition: all .15s;
    }
    #smt-reset-btn:hover { background: #f43f5e; color: #fff; }
    .leaflet-container { background: #0f172a !important; }
    .smt-marker-popup {
      font-family: 'Courier New', monospace;
      font-size: 11px;
      line-height: 1.6;
    }
    .smt-marker-popup .host { color: #38bdf8; font-weight: bold; }
    .smt-marker-popup .ip-line { display: flex; align-items: center; gap: 6px; margin-top: 4px; }
    .smt-popup-copy {
      background: #0f172a;
      border: 1px solid #38bdf8;
      color: #38bdf8;
      font-size: 9px;
      padding: 1px 6px;
      border-radius: 3px;
      cursor: pointer;
    }
  `);

  // ── DOM ──────────────────────────────────────────────────────────────────────
  const btn = document.createElement('div');
  btn.id = 'smt-btn';
  btn.innerHTML = `🌍 Serveurs <span id="smt-badge">0</span>`;
  document.body.appendChild(btn);

  const panel = document.createElement('div');
  panel.id = 'smt-panel';
  panel.style.display = 'none';
  panel.innerHTML = `
    <div id="smt-header">
      <span id="smt-title">🌍 SERVER MAP TRACKER</span>
      <div id="smt-controls">
        <button class="smt-ctrl-btn active" id="smt-tab-map">CARTE</button>
        <button class="smt-ctrl-btn" id="smt-tab-list">LISTE</button>
      </div>
    </div>
    <div id="smt-map"></div>
    <div id="smt-list"></div>
    <div id="smt-footer">
      <span id="smt-stats">0 serveurs β€’ 0 pays</span>
      <button id="smt-reset-btn">⟳ Réinitialiser</button>
    </div>
  `;
  document.body.appendChild(panel);

  // ── LEAFLET ──────────────────────────────────────────────────────────────────
  function loadLeaflet(cb) {
    if (window.L) { cb(); return; }
    const css = document.createElement('link');
    css.rel = 'stylesheet';
    css.href = 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.css';
    document.head.appendChild(css);
    const js = document.createElement('script');
    js.src = 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.js';
    js.onload = cb;
    document.head.appendChild(js);
  }

  function initMap() {
    if (map) return;
    map = window.L.map('smt-map', {
      center: [20, 0],
      zoom: 2,
      zoomControl: true,
      attributionControl: false
    });
    window.L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', {
      maxZoom: 18
    }).addTo(map);
    refreshMarkers();
  }

  function refreshMarkers() {
    if (!map) return;
    markers.forEach(m => m.remove());
    markers = [];
    servers.forEach((data, host) => {
      if (!data.lat || !data.lon) return;
      const color = typeColor(data.type);
      const icon = window.L.divIcon({
        html: `<div style="width:10px;height:10px;border-radius:50%;background:${color};border:2px solid #fff;box-shadow:0 0 6px ${color}80;"></div>`,
        className: '',
        iconSize: [10, 10]
      });
      const marker = window.L.marker([data.lat, data.lon], { icon })
        .addTo(map)
        .bindPopup(`
          <div class="smt-marker-popup">
            <div class="host">${host}</div>
            <div>${flagEmoji(data.countryCode)} ${data.city || ''} ${data.country || ''}</div>
            <div class="ip-line">
              <span>${data.ip || '?'}</span>
              <button class="smt-popup-copy" onclick="navigator.clipboard.writeText('${data.ip}');this.textContent='βœ“'">COPIER</button>
            </div>
            <div style="color:#64748b;font-size:10px;margin-top:2px;">${data.org || ''}</div>
            <div style="color:#64748b;font-size:10px;">${data.count} ressource(s)</div>
          </div>
        `);
      markers.push(marker);
    });
  }

  // ── UTILITAIRES ──────────────────────────────────────────────────────────────
  function typeColor(t) {
    return { js: '#f59e0b', css: '#8b5cf6', img: '#10b981', api: '#f43f5e', font: '#06b6d4' }[t] || '#475569';
  }

  function getType(url) {
    if (/\.(js|mjs)(\?|$)/.test(url)) return 'js';
    if (/\.css(\?|$)/.test(url)) return 'css';
    if (/\.(png|jpe?g|gif|svg|webp|ico)(\?|$)/.test(url)) return 'img';
    if (/\.(woff2?|ttf|eot)(\?|$)/.test(url)) return 'font';
    if (/api|graphql|rest|json/.test(url)) return 'api';
    return 'other';
  }

  function flagEmoji(code) {
    if (!code || code.length !== 2) return '🏳️';
    return String.fromCodePoint(...[...code.toUpperCase()].map(c => 0x1F1E6 - 65 + c.charCodeAt(0)));
  }

  function updateBadge() {
    const badge = document.getElementById('smt-badge');
    if (badge) badge.textContent = servers.size;
    const stats = document.getElementById('smt-stats');
    if (stats) {
      const countries = new Set([...servers.values()].map(s => s.countryCode).filter(Boolean));
      stats.textContent = `${servers.size} serveurs β€’ ${countries.size} pays`;
    }
  }

  function autoReset() {
    if (servers.size >= MAX_ENTRIES) {
      doReset();
    }
  }

  function doReset() {
    servers.clear();
    pendingQueue = [];
    markers.forEach(m => m.remove());
    markers = [];
    updateBadge();
    refreshList();
    if (map) map.setView([20, 0], 2);
  }

  // ── LISTE ────────────────────────────────────────────────────────────────────
  function refreshList() {
    const list = document.getElementById('smt-list');
    if (!list) return;
    list.innerHTML = '';
    [...servers.entries()]
      .sort((a, b) => b[1].count - a[1].count)
      .forEach(([host, data]) => {
        const row = document.createElement('div');
        row.className = 'smt-row';
        row.innerHTML = `
          <div class="smt-flag">${flagEmoji(data.countryCode)}</div>
          <div>
            <div class="smt-host" title="${host}">${host}</div>
            <div class="smt-org">${data.org || 'β€”'}</div>
          </div>
          <div class="smt-ip">${data.ip || '...'}</div>
          <div class="smt-type type-${data.type}">${data.type.toUpperCase()}</div>
          <button class="smt-copy-ip" data-ip="${data.ip}">COPIER IP</button>
        `;
        const copyBtn = row.querySelector('.smt-copy-ip');
        copyBtn.addEventListener('click', () => {
          navigator.clipboard.writeText(data.ip || '').then(() => {
            copyBtn.textContent = 'βœ“ COPIΓ‰';
            copyBtn.classList.add('copied');
            setTimeout(() => { copyBtn.textContent = 'COPIER IP'; copyBtn.classList.remove('copied'); }, 1500);
          });
        });
        list.appendChild(row);
      });
  }

  // ── GΓ‰OLOCALISATION IP ───────────────────────────────────────────────────────
  function processQueue() {
    if (isProcessing || pendingQueue.length === 0) return;
    isProcessing = true;
    const host = pendingQueue.shift();
    if (!host || servers.has(host)) { isProcessing = false; processQueue(); return; }

    servers.set(host, { ip: null, country: null, countryCode: null, city: null, lat: null, lon: null, org: null, type: 'other', count: 1 });
    updateBadge();

    GM_xmlhttpRequest({
      method: 'GET',
      url: `http://ip-api.com/json/${host}?fields=status,country,countryCode,city,lat,lon,org,query`,
      onload(res) {
        try {
          const d = JSON.parse(res.responseText);
          if (d.status === 'success') {
            const existing = servers.get(host) || {};
            servers.set(host, { ...existing, ip: d.query, country: d.country, countryCode: d.countryCode, city: d.city, lat: d.lat, lon: d.lon, org: d.org });
            updateBadge();
            if (mapVisible) { refreshMarkers(); refreshList(); }
          }
        } catch (e) {}
        isProcessing = false;
        setTimeout(processQueue, 120); // limite rate
      },
      onerror() { isProcessing = false; setTimeout(processQueue, 120); }
    });
  }

  function trackHost(url, type) {
    try {
      const host = new URL(url).hostname;
      if (!host || host === location.hostname) return;
      autoReset();
      if (servers.has(host)) {
        servers.get(host).count++;
        return;
      }
      servers.set(host, { ip: null, country: null, countryCode: null, city: null, lat: null, lon: null, org: null, type, count: 1 });
      pendingQueue.push(host);
      processQueue();
      updateBadge();
    } catch (e) {}
  }

  // ── INTERCEPTION DES RESSOURCES ──────────────────────────────────────────────
  // 1. PerformanceObserver (toutes ressources rΓ©seau)
  const observer = new PerformanceObserver(list => {
    list.getEntries().forEach(entry => {
      if (entry.initiatorType && entry.name.startsWith('http')) {
        trackHost(entry.name, getType(entry.name));
      }
    });
  });
  observer.observe({ type: 'resource', buffered: true });

  // 2. Fetch intercept
  const origFetch = window.fetch;
  window.fetch = function (...args) {
    const url = typeof args[0] === 'string' ? args[0] : (args[0] instanceof Request ? args[0].url : '');
    if (url) trackHost(url, 'api');
    return origFetch.apply(this, args);
  };

  // 3. XHR intercept
  const origOpen = XMLHttpRequest.prototype.open;
  XMLHttpRequest.prototype.open = function (method, url) {
    if (url) trackHost(url, 'api');
    return origOpen.apply(this, arguments);
  };

  // ── TOGGLE PANEL ─────────────────────────────────────────────────────────────
  btn.addEventListener('click', () => {
    mapVisible = !mapVisible;
    panel.style.display = mapVisible ? 'flex' : 'none';
    if (mapVisible) {
      loadLeaflet(() => {
        setTimeout(() => { initMap(); refreshList(); }, 100);
      });
    }
  });

  document.getElementById('smt-tab-map').addEventListener('click', () => {
    document.getElementById('smt-map').style.display = 'block';
    document.getElementById('smt-list').style.display = 'none';
    document.getElementById('smt-tab-map').classList.add('active');
    document.getElementById('smt-tab-list').classList.remove('active');
    if (map) setTimeout(() => map.invalidateSize(), 50);
  });

  document.getElementById('smt-tab-list').addEventListener('click', () => {
    document.getElementById('smt-map').style.display = 'none';
    document.getElementById('smt-list').style.display = 'block';
    document.getElementById('smt-tab-list').classList.add('active');
    document.getElementById('smt-tab-map').classList.remove('active');
    refreshList();
  });

  document.getElementById('smt-reset-btn').addEventListener('click', doReset);

})();