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

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

คุณจะต้องติดตั้งส่วนขยาย เช่น Tampermonkey, Greasemonkey หรือ Violentmonkey เพื่อติดตั้งสคริปต์นี้

คุณจะต้องติดตั้งส่วนขยาย เช่น Tampermonkey หรือ Violentmonkey เพื่อติดตั้งสคริปต์นี้

คุณจะต้องติดตั้งส่วนขยาย เช่น Tampermonkey หรือ Violentmonkey เพื่อติดตั้งสคริปต์นี้

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==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); }

})();