Proxy Dashboard (Safe) — Multi-Source + Cache + Auto-Refresh + SwitchyOmega Export (UA)

Стабильный виджет: сбор прокси из нескольких источников, кэш между сессиями, автообновление, TXT-экспорт, SwitchyOmega backup (PAC-профили Random/US/Mixed), тема (светлая/тёмная), таймер. Сделано в Украине 💙💛

// ==UserScript==
// @name         Proxy Dashboard (Safe) — Multi-Source + Cache + Auto-Refresh + SwitchyOmega Export (UA)
// @namespace    proxy-helper
// @version      3.1
// @description  Стабильный виджет: сбор прокси из нескольких источников, кэш между сессиями, автообновление, TXT-экспорт, SwitchyOmega backup (PAC-профили Random/US/Mixed), тема (светлая/тёмная), таймер. Сделано в Украине 💙💛
// @author       Gemini
// @match        *://*/*
// @exclude      *://chrome.google.com/*
// @run-at       document-idle
// @grant        GM_xmlhttpRequest
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addStyle
// @connect      hidxxy.name
// @connect      free-proxy.cz
// @connect      free.geonix.com
// @connect      smallseotools.com
// @connect      geonode.com
// @connect      fineproxy.org
// @connect      iproyal.com
// @connect      proxyscrape.com
// @connect      free-proxy-list.net
// @connect      proxy-list.download
// @connect      api.ipify.org
// @connect      *
// @noframes
// @license      MIT
// ==/UserScript==

(function () {
  'use strict';

  if (window.__PX_DASH_RUNNING__) return; // защита от двойного запуска
  window.__PX_DASH_RUNNING__ = true;

  // -------- Settings / keys --------
  var STORE_KEYS = {
    proxies: 'px_all_proxies_v31',
    ts: 'px_updated_ts_v31',
    theme: 'px_theme_v31',
    collapsed: 'px_collapsed_v31',
    refreshMin: 'px_refresh_min_v31'
  };
  var DEFAULT_REFRESH_MIN = 10;
  var EXPERIMENTAL_TEST = false; // в обычном TM параметр proxy для GM_xmlhttpRequest не работает

  // -------- State --------
  var ALL_PROXIES = [];
  var TIMER = null;
  var nextRefreshAt = 0;
  var widget = null;

  // -------- Utils --------
  function get(k, d){ try{ var v=GM_getValue(k); return (v===undefined)?d:v; }catch(e){ return d; } }
  function set(k, v){ try{ GM_setValue(k, v); }catch(e){} }
  function now(){ return Date.now(); }
  function minutes(m){ return m*60*1000; }
  function uniqueBy(arr, keyFn){
    var map = Object.create(null), out=[], i, k;
    for(i=0;i<arr.length;i++){ k = keyFn(arr[i]); if(!map[k]){ map[k]=1; out.push(arr[i]); } }
    return out;
  }
  function shuffle(a){
    var arr=a.slice(), i, j, t;
    for (i=arr.length-1;i>0;i--){ j=(Math.random()*(i+1))|0; t=arr[i]; arr[i]=arr[j]; arr[j]=t; }
    return arr;
  }

  // -------- Sources --------
  var sources = [
    { url: 'https://hidxxy.name/proxy-list/', parser: parseHidemyName },
    { url: 'http://free-proxy.cz/ru/', parser: parseFreeProxyCz },
    { url: 'https://free.geonix.com/ru/', parser: parseGeonix },
    { url: 'https://smallseotools.com/ru/free-proxy-list/', parser: parseSmallSeoTools },
    { url: 'https://geonode.com/free-proxy-list', parser: parseGeonode },
    { url: 'https://fineproxy.org/free-proxies/north-america/united-states/', parser: parseFineProxy },
    { url: 'https://iproyal.com/free-proxy-list/', parser: parseIproyal },
    { url: 'https://proxyscrape.com/free-proxy-list', parser: parseProxyScrape },
    { url: 'https://free-proxy-list.net/', parser: parseFreeProxyListNet },
    { url: 'https://www.proxy-list.download/api/v1/get?type=http&anon=elite&country=US', parser: parseProxyListDownloadUS }
  ];
  var IPIFY = 'https://api.ipify.org?format=json';

  // -------- Theme --------
  function currentTheme(){ return get(STORE_KEYS.theme, 'dark'); }
  function setTheme(t){
    try{
      document.documentElement.setAttribute('data-px-theme', t);
      set(STORE_KEYS.theme, t);
    }catch(e){}
  }

  // -------- Styles --------
  var css = `
  :root { --px-bg:#0f1220; --px-fg:#e9eef3; --px-muted:#9aa4b2; --px-accent:#2dd4bf; --px-border:#263042; --px-btn:#243147; --px-red:#ef4444; --px-orange:#f59e0b; --px-green:#22c55e; --px-blue:#3b82f6; }
  html[data-px-theme="light"] { --px-bg:#ffffff; --px-fg:#111827; --px-muted:#4b5563; --px-accent:#0891b2; --px-border:#e5e7eb; --px-btn:#f3f4f6; --px-red:#dc2626; --px-orange:#d97706; --px-green:#16a34a; --px-blue:#2563eb; }
  #px-root{position:fixed;top:84px;right:24px;width:360px;background:var(--px-bg);color:var(--px-fg);border:1px solid var(--px-border);border-radius:14px;box-shadow:0 12px 32px rgba(0,0,0,.35);font:14px/1.45 system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,'Helvetica Neue',Arial,sans-serif;z-index:2147483647;overflow:hidden}
  #px-header{display:flex;align-items:center;justify-content:space-between;padding:10px 12px;background:linear-gradient(135deg,#0ea5e9,#22c55e)}
  #px-title{font-weight:800;font-size:15px;display:flex;align-items:center;gap:8px}
  #px-ua{display:flex;align-items:center;gap:6px;font-weight:800;background:#ffe15a;color:#0057b8;padding:3px 8px;border-radius:999px;border:1px solid #ffd24a;box-shadow:0 0 0 2px rgba(0,0,0,.05)}
  #px-body{padding:10px;display:block;max-height:480px;overflow:auto;background:var(--px-bg)}
  .px-row{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-top:8px}
  .px-input, .px-select{width:100%;background:transparent;border:1px solid var(--px-border);color:var(--px-fg);border-radius:10px;padding:8px 10px;outline:none}
  .px-btn{background:var(--px-btn);color:var(--px-fg);border:1px solid var(--px-border);border-radius:10px;padding:9px 10px;font-weight:700;cursor:pointer}
  .px-btn.primary{background:var(--px-blue);border-color:transparent;color:#fff}
  .px-btn.good{background:var(--px-green);border-color:transparent;color:#fff}
  .px-btn.warn{background:var(--px-orange);border-color:transparent;color:#111}
  .px-note{font-size:12px;color:var(--px-muted)}
  .px-badge{display:inline-flex;align-items:center;gap:6px;background:transparent;border:1px solid var(--px-border);padding:4px 8px;border-radius:8px;font-size:12px}
  .px-proxy-list{font:12px/1.5 ui-monospace,Menlo,Consolas,monospace;background:rgba(0,0,0,.2);border:1px solid var(--px-border);border-radius:10px;padding:8px;color:var(--px-fg);white-space:pre-wrap;word-break:break-all;max-height:180px;overflow:auto}
  .px-right{display:flex;gap:6px;align-items:center}
  .px-toggle{appearance:none;width:42px;height:24px;background:var(--px-border);border-radius:999px;position:relative;outline:none;cursor:pointer;border:none}
  .px-toggle:after{content:"";position:absolute;top:3px;left:3px;width:18px;height:18px;border-radius:50%;background:#fff;transition:all .2s;box-shadow:0 1px 3px rgba(0,0,0,.3)}
  .px-toggle:checked{background:var(--px-blue)}
  .px-toggle:checked:after{left:21px}
  #px-min{background:rgba(0,0,0,.15);border:1px solid var(--px-border);color:#fff}
  .px-progress{height:6px;background:rgba(0,0,0,.15);border-radius:999px;overflow:hidden;margin:8px 0}
  .px-progress > div{height:100%;background:var(--px-blue);width:0%}
  `;

  try { GM_addStyle(css); } catch(e){
    var st = document.createElement('style'); st.textContent = css; (document.head||document.documentElement).appendChild(st);
  }
  setTheme(currentTheme());

  // -------- UI --------
  function buildUI(){
    widget = document.createElement('div');
    widget.id = 'px-root';
    widget.innerHTML =
      '<div id="px-header">' +
        '<div id="px-title">🧭 Proxy Dashboard <span class="px-badge"><span id="px-count">0</span> proxies</span></div>' +
        '<div class="px-right">' +
          '<label class="px-note">Light</label><input id="px-theme" type="checkbox" class="px-toggle">' +
          '<button id="px-min" class="px-btn">—</button>' +
        '</div>' +
      '</div>' +
      '<div id="px-body">' +
        '<div class="px-note" style="display:flex;align-items:center;gap:8px;justify-content:space-between">' +
          '<span id="px-madeinua"><span id="px-ua" title="Сделано в Украине">🇺🇦 <span>Made in Ukraine</span></span></span>' +
          '<span class="px-badge">Кэш: <span id="px-updated">—</span></span>' +
        '</div>' +
        '<div class="px-row" style="margin-top:8px">' +
          '<div><label class="px-note">Интервал автообновления (мин)</label><input id="px-refresh" type="number" min="1" step="1" class="px-input" value="' + esc(get(STORE_KEYS.refreshMin, DEFAULT_REFRESH_MIN)) + '"></div>' +
          '<div><label class="px-note">Профиль/фильтр</label><select id="px-filter" class="px-select">' +
              '<option value="random">Random</option>' +
              '<option value="us">US only</option>' +
              '<option value="mixed">Mixed</option>' +
            '</select></div>' +
        '</div>' +
        '<div class="px-row">' +
          '<button id="px-refresh-now" class="px-btn primary">🔄 Обновить сейчас</button>' +
          '<button id="px-copy" class="px-btn">📋 Скопировать всё</button>' +
        '</div>' +
        '<div class="px-row">' +
          '<button id="px-export-txt" class="px-btn">⬇️ Экспорт TXT</button>' +
          '<button id="px-export-omega" class="px-btn good">🧩 Export SwitchyOmega</button>' +
        '</div>' +
        '<div class="px-row">' +
          '<button id="px-test-one" class="px-btn warn">🧪 Тест (эксп.)</button>' +
          '<span class="px-badge">До обновления: <span id="px-eta">—</span></span>' +
        '</div>' +
        '<div class="px-progress"><div id="px-progress-bar"></div></div>' +
        '<div id="px-list" class="px-proxy-list">—</div>' +
        '<div class="px-note">Источники: ' + sources.length + ' (парсеры защищены — если разметка изменилась, источник пропускается).</div>' +
      '</div>' +
      '<div id="px-footer"><span class="px-note">IP-проверка: ' + (EXPERIMENTAL_TEST?'экспериментальная':'отключена') + '</span></div>';

    (document.body||document.documentElement).appendChild(widget);

    // Theme init & toggle
    var themeToggle = document.getElementById('px-theme');
    themeToggle.checked = (currentTheme()==='light');
    themeToggle.addEventListener('change', function(){
      setTheme(this.checked ? 'light' : 'dark');
    });

    // Collapse
    var body = document.getElementById('px-body');
    var collapsed = !!get(STORE_KEYS.collapsed, false);
    if (collapsed) body.style.display = 'none';
    var minBtn = document.getElementById('px-min');
    minBtn.textContent = collapsed ? '+' : '—';
    minBtn.addEventListener('click', function(){
      var hidden = body.style.display === 'none';
      body.style.display = hidden ? 'block' : 'none';
      minBtn.textContent = hidden ? '—' : '+';
      set(STORE_KEYS.collapsed, !hidden);
    });

    // Drag header
    var header = document.getElementById('px-header');
    var dragging=false,sx=0,sy=0,ox=0,oy=0;
    header.addEventListener('mousedown', function(e){
      if (e.target && e.target.id === 'px-theme') return;
      dragging = true; sx=e.clientX; sy=e.clientY;
      var r = widget.getBoundingClientRect(); ox=r.left; oy=r.top;
      document.body.style.userSelect='none';
    });
    document.addEventListener('mousemove', function(e){
      if(!dragging) return;
      widget.style.left = (ox + (e.clientX - sx)) + 'px';
      widget.style.top  = (oy + (e.clientY - sy)) + 'px';
      widget.style.right = 'auto';
    });
    document.addEventListener('mouseup', function(){
      if(!dragging) return; dragging=false; document.body.style.userSelect='';
    });

    // Buttons
    id('px-refresh-now').addEventListener('click', function(){ refreshAll(true); });
    id('px-copy').addEventListener('click', copyAll);
    id('px-export-txt').addEventListener('click', exportTXT);
    id('px-export-omega').addEventListener('click', exportSwitchyOmega);
    id('px-test-one').addEventListener('click', testOneProxy);

    // Refresh input
    id('px-refresh').addEventListener('change', function(){
      var v = parseInt(this.value,10); if (!v || v<1) v = DEFAULT_REFRESH_MIN;
      set(STORE_KEYS.refreshMin, v); scheduleNext();
    });

    // Load from cache
    var cached = get(STORE_KEYS.proxies, []);
    var ts = get(STORE_KEYS.ts, 0);
    if (cached && cached.length){
      ALL_PROXIES = cached;
      renderList();
      updateUpdated(ts);
    }

    scheduleNext();
    var refreshMin = get(STORE_KEYS.refreshMin, DEFAULT_REFRESH_MIN);
    var stale = !ts || (now()-ts > minutes(refreshMin));
    if (stale) refreshAll(true);
  }

  // -------- DOM helpers --------
  function id(s){ return document.getElementById(s); }
  function esc(s){ return String(s===undefined||s===null?'':s); }

  function updateProgress(pct){
    var bar = id('px-progress-bar'); if (bar) bar.style.width = (Math.max(0, Math.min(100, pct))+'%');
  }
  function updateUpdated(ts){
    var el = id('px-updated'); if (!el) return;
    el.textContent = ts ? new Date(ts).toLocaleString() : '—';
  }
  function renderList(){
    var list = id('px-list'), count = id('px-count');
    if (count) count.textContent = String(ALL_PROXIES.length||0);
    var lines = [];
    for (var i=0;i<ALL_PROXIES.length;i++){
      var p = ALL_PROXIES[i];
      lines.push(p.ip + ':' + p.port + (p.country ? (' # ' + p.country) : ''));
    }
    list.textContent = lines.length ? lines.join('\n') : '—';
  }
  function scheduleNext(){
    var mins = get(STORE_KEYS.refreshMin, DEFAULT_REFRESH_MIN);
    nextRefreshAt = now() + minutes(mins);
    if (TIMER) clearInterval(TIMER);
    TIMER = setInterval(function(){
      var etaEl = id('px-eta'); if (!etaEl) return;
      var d = nextRefreshAt - now();
      if (d<=0){ etaEl.textContent='0s'; clearInterval(TIMER); refreshAll(true); return; }
      var s = Math.ceil(d/1000), mm = Math.floor(s/60), ss=s%60;
      etaEl.textContent = mm + ':' + (ss<10?'0':'') + ss;
    }, 1000);
  }

  // -------- Refresh / parsers --------
  function refreshAll(withProgress){
    var total = sources.length, done = 0;
    updateProgress(0);
    var collected = [];
    var i;

    function onDone(){ done++; if (withProgress) updateProgress(100*done/total); if (done>=total) finalize(); }

    for (i=0;i<sources.length;i++){
      (function(src){
        GM_xmlhttpRequest({
          method: 'GET',
          url: src.url,
          timeout: 20000,
          onload: function(res){
            try{
              var list = src.parser(res.responseText || '');
              for (var j=0;j<list.length;j++){
                var raw = list[j];
                if (typeof raw === 'string'){
                  var parts = raw.split(':'); if (parts.length>=2) collected.push({ip:parts[0].trim(), port:parts[1].trim(), source:src.url});
                } else if (raw && raw.ip && raw.port){
                  collected.push({ip:raw.ip, port:raw.port, country:raw.country, source:src.url});
                }
              }
            }catch(e){ /* ignore */ }
            onDone();
          },
          onerror: onDone,
          ontimeout: onDone
        });
      })(sources[i]);
    }

    function finalize(){
      ALL_PROXIES = uniqueBy(collected, function(o){ return o.ip+':'+o.port; });
      set(STORE_KEYS.proxies, ALL_PROXIES);
      var ts = now(); set(STORE_KEYS.ts, ts); updateUpdated(ts);
      renderList(); scheduleNext();
    }
  }

  // ---- Parsers (максимально совместимые) ----
  function parseHidemyName(html){
    var d = new DOMParser().parseFromString(html,'text/html'), out=[], rows=d.querySelectorAll('tbody tr'), i;
    for (i=0;i<rows.length;i++){
      var tds = rows[i].querySelectorAll('td');
      if (tds.length>=2){
        var ip = (tds[0].textContent||'').trim();
        var port = (tds[1].textContent||'').trim();
        if (ip && port) out.push({ip:ip,port:port});
      }
    }
    return out;
  }
  function parseFreeProxyCz(html){
    var d = new DOMParser().parseFromString(html,'text/html'), out=[], rows=d.querySelectorAll('.fwp-list > table tr'), i;
    for (i=0;i<rows.length;i++){
      var ipCell = rows[i].querySelector('td:nth-of-type(1)');
      var portCell = rows[i].querySelector('td:nth-of-type(2)');
      if (ipCell && portCell){
        try{
          var sc = ipCell.querySelector('script');
          var enc = sc ? ((sc.textContent||'').match(/"(.*?)"/)||[])[1] : null;
          var ip = enc ? atob(enc) : (ipCell.textContent||'').trim();
          var port = (portCell.textContent||'').trim();
          if (ip && port) out.push({ip:ip,port:port});
        }catch(e){}
      }
    }
    return out;
  }
  function parseGeonix(html){
    var d = new DOMParser().parseFromString(html,'text/html'), out=[], rows=d.querySelectorAll('.proxy-list tbody tr'), i;
    for (i=0;i<rows.length;i++){
      var ip = (rows[i].querySelector('td:nth-of-type(1)')||{}).textContent;
      var port = (rows[i].querySelector('td:nth-of-type(2)')||{}).textContent;
      if (ip && port){ ip=ip.trim(); port=port.trim(); if(ip&&port) out.push({ip:ip,port:port}); }
    }
    return out;
  }
  function parseSmallSeoTools(html){
    var d = new DOMParser().parseFromString(html,'text/html'), out=[], rows=d.querySelectorAll('#content-section table tbody tr'), i;
    for (i=0;i<rows.length;i++){
      var ip = (rows[i].querySelector('td:nth-of-type(1)')||{}).textContent;
      var port = (rows[i].querySelector('td:nth-of-type(2)')||{}).textContent;
      if (ip && port){ ip=ip.trim(); port=port.trim(); if(ip&&port) out.push({ip:ip,port:port}); }
    }
    return out;
  }
  function parseGeonode(html){
    var d = new DOMParser().parseFromString(html,'text/html'), out=[], rows=d.querySelectorAll('.list-container .list-row'), i;
    for (i=0;i<rows.length;i++){
      var ip = (rows[i].querySelector('p:nth-of-type(1)')||{}).textContent;
      var port = (rows[i].querySelector('p:nth-of-type(2)')||{}).textContent;
      if (ip && port){ ip=ip.trim(); port=port.trim(); if(ip&&port) out.push({ip:ip,port:port}); }
    }
    return out;
  }
  function parseFineProxy(html){
    var d = new DOMParser().parseFromString(html,'text/html'), out=[], rows=d.querySelectorAll('#proxytable tbody tr'), i;
    for (i=0;i<rows.length;i++){
      var tds = rows[i].querySelectorAll('td');
      if (tds.length>=2){
        var ip = (tds[0].textContent||'').trim();
        var port = (tds[1].textContent||'').trim();
        var country = (tds[3] && tds[3].textContent) ? tds[3].textContent.trim() : '';
        if (ip && port) out.push({ip:ip,port:port,country:country});
      }
    }
    return out;
  }
  function parseIproyal(html){
    var d = new DOMParser().parseFromString(html,'text/html'), out=[], ips=d.querySelectorAll('td.ip'), ports=d.querySelectorAll('td.port');
    var n = Math.min(ips.length, ports.length), i;
    for (i=0;i<n;i++){
      var ip=(ips[i].textContent||'').trim(); var port=(ports[i].textContent||'').trim();
      if (ip && port) out.push({ip:ip,port:port});
    }
    return out;
  }
  function parseProxyScrape(html){
    var d = new DOMParser().parseFromString(html,'text/html'), out=[], rows=d.querySelectorAll('table.w-full.text-left.text-sm tbody tr'), i;
    for (i=0;i<rows.length;i++){
      var ip = (rows[i].querySelector('td:nth-of-type(1)')||{}).textContent;
      var port = (rows[i].querySelector('td:nth-of-type(2)')||{}).textContent;
      if (ip && port){ ip=ip.trim(); port=port.trim(); if(ip&&port) out.push({ip:ip,port:port}); }
    }
    return out;
  }
  function parseFreeProxyListNet(html){
    var d = new DOMParser().parseFromString(html,'text/html'), out=[], rows=d.querySelectorAll('#proxylisttable tbody tr'), i;
    for (i=0;i<rows.length;i++){
      var ip = (rows[i].querySelector('td:nth-of-type(1)')||{}).textContent;
      var port = (rows[i].querySelector('td:nth-of-type(2)')||{}).textContent;
      var country = (rows[i].querySelector('td:nth-of-type(4)')||{}).textContent;
      if (ip && port){ ip=ip.trim(); port=port.trim(); if(ip&&port) out.push({ip:ip,port:port,country:country?country.trim():''}); }
    }
    return out;
  }
  function parseProxyListDownloadUS(text){
    var lines = String(text||'').split('\n'), out=[], i;
    for (i=0;i<lines.length;i++){
      var s = lines[i].trim(); if (!s) continue;
      var parts = s.split(':'); if (parts.length>=2){
        out.push({ip:parts[0].trim(), port:parts[1].trim(), country:'US'});
      }
    }
    return out;
  }

  // -------- Filters --------
  function filteredList(mode){
    var list = ALL_PROXIES.slice();
    if (mode==='us') {
      var us = [], i, p, c;
      for (i=0;i<list.length;i++){
        p = list[i]; c = (p.country||'') + ' ' + (p.source||'');
        if (/(^|\s)(US|United\s+States)(\s|$)/i.test(c) || /country=US/i.test(c)) us.push(p);
      }
      return us;
    }
    if (mode==='mixed'){
      var allUS = filteredList('us');
      var non = [];
      for (var j=0;j<list.length;j++){
        if (allUS.indexOf(list[j])===-1) non.push(list[j]);
      }
      var size = Math.min(list.length, 50), half = Math.floor(size/2);
      return shuffle(allUS).slice(0,half).concat(shuffle(non).slice(0,size-half));
    }
    return shuffle(list);
  }

  // -------- Export / copy --------
  function copyAll(){
    var mode = id('px-filter').value;
    var arr = filteredList(mode), out=[], i;
    for (i=0;i<arr.length;i++){ out.push(arr[i].ip+':'+arr[i].port); }
    navigator.clipboard.writeText(out.join('\n')).catch(function(){ alert('Не удалось скопировать в буфер.'); });
  }
  function exportTXT(){
    var mode = id('px-filter').value;
    var arr = filteredList(mode), out=[], i;
    for (i=0;i<arr.length;i++){ out.push(arr[i].ip+':'+arr[i].port); }
    blobDownload(out.join('\n'), 'proxies_'+mode+'_'+isoStamp()+'.txt', 'text/plain');
  }
  function exportSwitchyOmega(){
    function makePAC(arr){
      var list=[], i, lim=Math.min(arr.length, 200);
      for (i=0;i<lim;i++){ list.push(arr[i].ip+':'+arr[i].port); }
      var body = 'function FindProxyForURL(url, host){\n' +
        '  var L = ' + JSON.stringify(list) + ';\n' +
        '  if (!L || L.length===0) return "DIRECT";\n' +
        '  var i = Math.floor(Math.random()*L.length);\n' +
        '  return "PROXY " + L[i] + "; DIRECT";\n' +
        '}';
      return body;
    }
    var backup = {
      version: '3.0.0',
      time: Date.now(),
      schema: 'omega-profiles',
      profiles: [
        { name:'Random', type:'pac', profilePicture:'pac', pacScript: makePAC(filteredList('random')) },
        { name:'US',     type:'pac', profilePicture:'pac', pacScript: makePAC(filteredList('us')) },
        { name:'Mixed',  type:'pac', profilePicture:'pac', pacScript: makePAC(filteredList('mixed')) }
      ]
    };
    blobDownload(JSON.stringify(backup,null,2), 'SwitchyOmega_Backup_'+isoStamp()+'.json', 'application/json');
  }
  function blobDownload(content, filename, type){
    var blob = new Blob([content], {type:type});
    var url = URL.createObjectURL(blob);
    var a = document.createElement('a'); a.href=url; a.download=filename;
    (document.body||document.documentElement).appendChild(a); a.click(); a.remove();
    setTimeout(function(){ URL.revokeObjectURL(url); }, 2000);
  }
  function isoStamp(){ return new Date().toISOString().slice(0,19).replace(/[:T]/g,'-'); }

  // -------- Test (informative only) --------
  function testOneProxy(){
    if (!EXPERIMENTAL_TEST){ alert('В стандартном Tampermonkey тест через сам прокси недоступен. Экспорт/кэш/обновления работают.'); return; }
    var mode = id('px-filter').value;
    var list = filteredList(mode);
    if (!list.length) return alert('Список пуст.');
    var pick = list[(Math.random()*list.length)|0];
    var proxyURL = 'http://' + pick.ip + ':' + pick.port;

    GM_xmlhttpRequest({
      method: 'GET',
      url: IPIFY,
      timeout: 8000,
      proxy: proxyURL, // скорее всего будет проигнорировано
      onload: function(res){
        try{ var data = JSON.parse(res.responseText||'{}'); alert('Ответ: ' + (data.ip||'—')); }catch(e){ alert('Ответ получен, но JSON не разобран.'); }
      },
      onerror: function(){ alert('Ошибка запроса (вероятно, TM проигнорировал proxy-поле).'); },
      ontimeout: function(){ alert('Таймаут/прокси медленный.'); }
    });
  }

  // -------- Boot --------
  try { buildUI(); } catch(e){ console.error('[PX] UI build error', e); }

})();