BR Admin Panel Pro v2.0

Панель администратора Black Russia — шаблоны, автовставка, префиксы, AI Groq

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

Advertisement:

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

Advertisement:

// ==UserScript==
// @name         BR Admin Panel Pro v2.0
// @namespace    https://vk.ru/club237051164
// @version      2.0.0
// @description  Панель администратора Black Russia — шаблоны, автовставка, префиксы, AI Groq
// @author       Akzholch1k | Dev: https://vk.ru/brunoverona
// @match        https://forum.blackrussia.online/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_xmlhttpRequest
// @connect      api.groq.com
// @connect      api.telegram.org
// @license      Connect
// ==/UserScript==

(function () {
  'use strict';

  // ══════════════════════════════════════
  //  КОНФИГ
  // ══════════════════════════════════════
  const TG_TOKEN   = '8572813058:AAEdG21L9oRpvP_nbuiT8l8xDMufJt2s-bo';
  const DEV_PASS   = 'devpanel';
  const GROQ_KEY_DEFAULT = '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';
  const GROQ_MODEL = 'llama3-70b-8192';

  // ══════════════════════════════════════
  //  ПРЕФИКСЫ
  // ══════════════════════════════════════
  const PFX = {
    NO_PREFIX:      0,
    PIN_PREFIX:     2,
    UNACCEPT:       4,
    PENDING:        5,
    DECIDED:        6,
    CLOSE:          7,
    ACCEPT:         8,
    WATCHED:        9,
    COMMAND:       10,
    GA:            12,
    TEX:           13,
    WAIT:          14,
  };

  const PFX_LABEL = {
    0:  '— Без префикса',
    2:  '📌 Закрепить',
    4:  '❌ Отказано',
    5:  '🕐 На рассмотрении',
    6:  '✅ Решено',
    7:  '🔒 Закрыто',
    8:  '✅ Одобрено',
    9:  '👁 Рассмотрено',
    10: '👥 Команде проекта',
    12: '🔰 GA',
    13: '🔧 Тех. специалисту',
    14: '⏳ Ожидание',
  };

  // ══════════════════════════════════════
  //  ВСТРОЕННЫЕ ШАБЛОНЫ
  //  {{ greeting }}  → время суток
  //  {{ user.name }} → ник автора темы
  // ══════════════════════════════════════
  const FLOWER_IMG =
    '[CENTER][url=https://postimages.org/][img]https://i.postimg.cc/cCG97p5p/Pics-Art-07-12-03-23-18-1.png[/img][/url][/CENTER]';

  const BUILTIN = [
    {
      id: 'b_pending',
      name: '🕐 На рассмотрении',
      prefix: PFX.PENDING,
      autoPin: true,
      borderColor: 'rgb(236,124,38,0.5)',
      content:
        '[COLOR=#FF00FF][FONT=times new roman][SIZE=4][I][CENTER][ICODE]{{ greeting }}, уважаемый(ая) игрок {{ user.name }}[/ICODE][/CENTER][/I][/SIZE][/FONT][/COLOR]\n' +
        FLOWER_IMG + '\n' +
        '[B][CENTER][FONT=times new roman][COLOR=#ffffff][ICODE]Ваша заявка принята и находится на рассмотрении.\nМы свяжемся с вами в ближайшее время.[/ICODE][/COLOR][/CENTER][/FONT][/B]\n' +
        FLOWER_IMG + '\n' +
        '[CENTER][COLOR=#00FF00][ICODE]🕐 На рассмотрении.[/ICODE][/COLOR][/CENTER]',
    },
    {
      id: 'b_accept',
      name: '✅ Одобрено',
      prefix: PFX.ACCEPT,
      autoPin: false,
      borderColor: 'rgb(152,251,152,0.5)',
      content:
        '[COLOR=#FF00FF][FONT=times new roman][SIZE=4][I][CENTER][ICODE]{{ greeting }}, уважаемый(ая) игрок {{ user.name }}[/ICODE][/CENTER][/I][/SIZE][/FONT][/COLOR]\n' +
        FLOWER_IMG + '\n' +
        '[B][CENTER][FONT=times new roman][COLOR=#ffffff][ICODE]Ваша заявка была рассмотрена и одобрена!\nПоздравляем, следите за дальнейшими инструкциями.[/ICODE][/COLOR][/CENTER][/FONT][/B]\n' +
        FLOWER_IMG + '\n' +
        '[CENTER][COLOR=#00FF00][ICODE]✅ Одобрено.[/ICODE][/COLOR][/CENTER]',
    },
    {
      id: 'b_unaccept',
      name: '❌ Отказано',
      prefix: PFX.UNACCEPT,
      autoPin: false,
      borderColor: 'rgb(255,80,80,0.5)',
      content:
        '[COLOR=#FF00FF][FONT=times new roman][SIZE=4][I][CENTER][ICODE]{{ greeting }}, уважаемый(ая) игрок {{ user.name }}[/ICODE][/CENTER][/I][/SIZE][/FONT][/COLOR]\n' +
        FLOWER_IMG + '\n' +
        '[B][CENTER][FONT=times new roman][COLOR=#ffffff][ICODE]К сожалению, ваша заявка была отклонена.\nПричина: несоответствие требованиям.[/ICODE][/COLOR][/CENTER][/FONT][/B]\n' +
        FLOWER_IMG + '\n' +
        '[CENTER][COLOR=#FF0000][ICODE]❌ Отказано.[/ICODE][/COLOR][/CENTER]',
    },
    {
      id: 'b_close',
      name: '🔒 Закрыто',
      prefix: PFX.CLOSE,
      autoPin: false,
      borderColor: 'rgb(150,150,150,0.5)',
      content:
        '[COLOR=#FF00FF][FONT=times new roman][SIZE=4][I][CENTER][ICODE]{{ greeting }}, уважаемый(ая) игрок {{ user.name }}[/ICODE][/CENTER][/I][/SIZE][/FONT][/COLOR]\n' +
        FLOWER_IMG + '\n' +
        '[B][CENTER][FONT=times new roman][COLOR=#ffffff][ICODE]Тема закрыта.\nПо вопросам обращайтесь к администрации.[/ICODE][/COLOR][/CENTER][/FONT][/B]\n' +
        FLOWER_IMG + '\n' +
        '[CENTER][COLOR=#888888][ICODE]🔒 Закрыто.[/ICODE][/COLOR][/CENTER]',
    },
    {
      id: 'b_decided',
      name: '✅ Решено',
      prefix: PFX.DECIDED,
      autoPin: false,
      borderColor: 'rgb(46,212,122,0.5)',
      content:
        '[COLOR=#FF00FF][FONT=times new roman][SIZE=4][I][CENTER][ICODE]{{ greeting }}, уважаемый(ая) игрок {{ user.name }}[/ICODE][/CENTER][/I][/SIZE][/FONT][/COLOR]\n' +
        FLOWER_IMG + '\n' +
        '[B][CENTER][FONT=times new roman][COLOR=#ffffff][ICODE]Ваш вопрос рассмотрен и решён.[/ICODE][/COLOR][/CENTER][/FONT][/B]\n' +
        FLOWER_IMG + '\n' +
        '[CENTER][COLOR=#00FF00][ICODE]✅ Решено.[/ICODE][/COLOR][/CENTER]',
    },
    {
      id: 'b_watched',
      name: '👁 Рассмотрено',
      prefix: PFX.WATCHED,
      autoPin: false,
      borderColor: 'rgb(100,180,255,0.5)',
      content:
        '[COLOR=#FF00FF][FONT=times new roman][SIZE=4][I][CENTER][ICODE]{{ greeting }}, уважаемый(ая) игрок {{ user.name }}[/ICODE][/CENTER][/I][/SIZE][/FONT][/COLOR]\n' +
        FLOWER_IMG + '\n' +
        '[B][CENTER][FONT=times new roman][COLOR=#ffffff][ICODE]Ваше обращение рассмотрено.[/ICODE][/COLOR][/CENTER][/FONT][/B]\n' +
        FLOWER_IMG + '\n' +
        '[CENTER][COLOR=#64b4ff][ICODE]👁 Рассмотрено.[/ICODE][/COLOR][/CENTER]',
    },
    {
      id: 'b_wait',
      name: '⏳ Ожидание',
      prefix: PFX.WAIT,
      autoPin: false,
      borderColor: 'rgb(250,204,21,0.5)',
      content:
        '[COLOR=#FF00FF][FONT=times new roman][SIZE=4][I][CENTER][ICODE]{{ greeting }}, уважаемый(ая) игрок {{ user.name }}[/ICODE][/CENTER][/I][/SIZE][/FONT][/COLOR]\n' +
        FLOWER_IMG + '\n' +
        '[B][CENTER][FONT=times new roman][COLOR=#ffffff][ICODE]Ваша заявка ожидает дополнительных действий с вашей стороны.[/ICODE][/COLOR][/CENTER][/FONT][/B]\n' +
        FLOWER_IMG + '\n' +
        '[CENTER][COLOR=#facc15][ICODE]⏳ Ожидание.[/ICODE][/COLOR][/CENTER]',
    },
    // ── Биографии ──
    {
      id: 'b_bio_pending',
      name: '📖 Биография — На рассмотрении',
      prefix: PFX.PENDING,
      autoPin: true,
      cat: 'bio',
      borderColor: 'rgb(236,124,38,0.5)',
      content:
        '[COLOR=#FF00FF][FONT=times new roman][SIZE=4][I][CENTER][ICODE]{{ greeting }}, уважаемый(ая) игрок {{ user.name }}[/ICODE][/CENTER][/I][/SIZE][/FONT][/COLOR]\n' +
        FLOWER_IMG + '\n' +
        '[B][CENTER][FONT=times new roman][COLOR=#ffffff][ICODE]Ваша биография принята на рассмотрение.\nОжидайте ответа куратора.[/ICODE][/COLOR][/CENTER][/FONT][/B]\n' +
        FLOWER_IMG + '\n' +
        '[CENTER][COLOR=#00FF00][ICODE]🕐 На рассмотрении.[/ICODE][/COLOR][/CENTER]',
    },
    {
      id: 'b_bio_accept',
      name: '📖 Биография — Одобрено',
      prefix: PFX.ACCEPT,
      autoPin: false,
      cat: 'bio',
      borderColor: 'rgb(152,251,152,0.5)',
      content:
        '[COLOR=#FF00FF][FONT=times new roman][SIZE=4][I][CENTER][ICODE]{{ greeting }}, уважаемый(ая) игрок {{ user.name }}[/ICODE][/CENTER][/I][/SIZE][/FONT][/COLOR]\n' +
        FLOWER_IMG + '\n' +
        '[B][CENTER][FONT=times new roman][COLOR=#ffffff][ICODE]Ваша биография проверена и одобрена!\nПоздравляем, она теперь активна.[/ICODE][/COLOR][/CENTER][/FONT][/B]\n' +
        FLOWER_IMG + '\n' +
        '[CENTER][COLOR=#00FF00][ICODE]✅ Одобрено.[/ICODE][/COLOR][/CENTER]',
    },
    {
      id: 'b_bio_deny',
      name: '📖 Биография — Отказано',
      prefix: PFX.UNACCEPT,
      autoPin: false,
      cat: 'bio',
      borderColor: 'rgb(255,80,80,0.5)',
      content:
        '[COLOR=#FF00FF][FONT=times new roman][SIZE=4][I][CENTER][ICODE]{{ greeting }}, уважаемый(ая) игрок {{ user.name }}[/ICODE][/CENTER][/I][/SIZE][/FONT][/COLOR]\n' +
        FLOWER_IMG + '\n' +
        '[B][CENTER][FONT=times new roman][COLOR=#ffffff][ICODE]Ваша биография отклонена.\nПричина: несоответствие требованиям биографии.[/ICODE][/COLOR][/CENTER][/FONT][/B]\n' +
        FLOWER_IMG + '\n' +
        '[CENTER][COLOR=#FF0000][ICODE]❌ Отказано.[/ICODE][/COLOR][/CENTER]',
    },
  ];

  // ══════════════════════════════════════
  //  ХРАНИЛИЩЕ
  // ══════════════════════════════════════
  const S = {
    get: (k, d) => { try { return JSON.parse(GM_getValue(k, JSON.stringify(d))); } catch { return d; } },
    set: (k, v) => GM_setValue(k, JSON.stringify(v)),
  };

  let nickname   = S.get('nickname', null);
  let myTpls     = S.get('my_tpls', []);
  let logs       = S.get('logs', []);
  let devOpen    = false;

  const saveMyTpls = () => S.set('my_tpls', myTpls);
  const saveLogs   = () => S.set('logs', logs.slice(-300));

  function log(action, detail) {
    logs.push({ t: new Date().toLocaleString('ru-RU'), u: nickname || '?', action, detail });
    saveLogs();
    tg(`📋 ${action}\n👤 ${nickname}\n📝 ${detail}`);
  }

  // ══════════════════════════════════════
  //  TELEGRAM
  // ══════════════════════════════════════
  function tg(text) {
    const cid = S.get('tg_cid', '');
    if (!cid) return;
    GM_xmlhttpRequest({
      method: 'POST', url: `https://api.telegram.org/bot${TG_TOKEN}/sendMessage`,
      headers: { 'Content-Type': 'application/json' },
      data: JSON.stringify({ chat_id: cid, text, parse_mode: 'HTML' }),
    });
  }

  // ══════════════════════════════════════
  //  GROQ AI
  // ══════════════════════════════════════
  function groqAI(userMsg, sysMsg) {
    return new Promise(resolve => {
      const key = S.get('groq_key', GROQ_KEY_DEFAULT);
      GM_xmlhttpRequest({
        method: 'POST',
        url: 'https://api.groq.com/openai/v1/chat/completions',
        headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${key}` },
        data: JSON.stringify({
          model: GROQ_MODEL,
          max_tokens: 800,
          messages: [
            { role: 'system', content: sysMsg || 'Ты помощник модератора форума Black Russia. Улучши сообщение: сделай вежливым и профессиональным. Верни ТОЛЬКО текст, без пояснений.' },
            { role: 'user', content: userMsg },
          ],
        }),
        onload: r => { try { resolve(JSON.parse(r.responseText).choices[0].message.content); } catch { resolve(null); } },
        onerror: () => resolve(null),
      });
    });
  }

  // ══════════════════════════════════════
  //  ВСПОМОГАТЕЛЬНЫЕ
  // ══════════════════════════════════════
  function greeting() {
    const h = new Date().getHours();
    if (h >= 6  && h < 12) return 'Доброе утро';
    if (h >= 12 && h < 17) return 'Добрый день';
    if (h >= 17 && h < 22) return 'Добрый вечер';
    return 'Доброй ночи';
  }

  function threadAuthor() {
    const el = document.querySelector(
      '.message--post:first-child .username, .message-userDetails .username, h1.p-title-value'
    );
    return el ? el.textContent.trim() : 'игрок';
  }

  function applyVars(text) {
    return text
      .replace(/\{\{\s*greeting\s*\}\}/g, greeting())
      .replace(/\{\{\s*user\.name\s*\}\}/g, threadAuthor());
  }

  // ══════════════════════════════════════
  //  ВСТАВКА В ПОЛЕ ОТВЕТА + ОТПРАВКА
  // ══════════════════════════════════════
  function insertAndSend(text, prefixId, doSend) {
    // 1. Установить префикс
    if (prefixId) {
      const sel = document.querySelector('select[name="prefix_id"], #ctrl_prefix_id');
      if (sel) { sel.value = prefixId; sel.dispatchEvent(new Event('change', { bubbles: true })); }
    }

    // 2. Найти поле ввода XenForo
    const editors = [
      document.querySelector('textarea[name="message"]'),
      document.querySelector('.fr-element[contenteditable="true"]'),
      document.querySelector('[data-xf-init="editor"] .fr-element'),
      document.querySelector('.redactor-in'),
    ].filter(Boolean);

    let inserted = false;
    for (const ed of editors) {
      if (ed.tagName === 'TEXTAREA') {
        ed.value = text;
        ed.dispatchEvent(new Event('input', { bubbles: true }));
        ed.focus();
        inserted = true;
      } else {
        ed.focus();
        document.execCommand('selectAll', false, null);
        document.execCommand('insertText', false, text);
        ed.dispatchEvent(new Event('input', { bubbles: true }));
        inserted = true;
      }
      break;
    }

    if (!inserted) {
      navigator.clipboard.writeText(text).catch(() => {});
      toast('📋 Скопировано — вставьте вручную', '#e8a020');
      return;
    }

    toast('✅ Шаблон вставлен!');

    // 3. Автоотправка
    if (doSend) {
      setTimeout(() => {
        const btn = document.querySelector(
          'button[data-xf-click="submit"], .js-submitButton, button[type="submit"].button--primary'
        );
        if (btn) btn.click();
      }, 300);
    }
  }

  // ══════════════════════════════════════
  //  ТОСТ
  // ══════════════════════════════════════
  function toast(msg, bg) {
    const el = document.createElement('div');
    el.textContent = msg;
    Object.assign(el.style, {
      position: 'fixed', bottom: '90px', left: '50%', transform: 'translateX(-50%)',
      background: bg || '#2ed47a', color: bg ? '#fff' : '#000',
      fontWeight: '700', fontSize: '13px', padding: '10px 22px',
      borderRadius: '20px', zIndex: '9999999',
      boxShadow: '0 4px 20px rgba(0,0,0,0.4)',
      fontFamily: 'Nunito, sans-serif',
      animation: 'none', opacity: '0', transition: 'opacity .2s',
    });
    document.body.appendChild(el);
    requestAnimationFrame(() => { el.style.opacity = '1'; });
    setTimeout(() => { el.style.opacity = '0'; setTimeout(() => el.remove(), 300); }, 2200);
  }

  // ══════════════════════════════════════
  //  CSS
  // ══════════════════════════════════════
  document.head.insertAdjacentHTML('beforeend', `
  <style>
  @import url('https://fonts.googleapis.com/css2?family=Rajdhani:wght@600;700&family=Nunito:wght@400;600;700&display=swap');
  :root{--bg:#0a0c12;--panel:#12151f;--bd:#1e2535;--acc:#e8a020;--red:#ff4f4f;--txt:#d4daf0;--muted:#5a6080;--grn:#2ed47a;--pur:#a855f7;--r:12px;--sh:0 8px 40px rgba(0,0,0,.6)}
  #br-ov{position:fixed;inset:0;z-index:999998;background:rgba(0,0,0,.88);backdrop-filter:blur(6px);display:flex;align-items:center;justify-content:center}
  #br-box{background:var(--panel);border:1px solid var(--bd);border-radius:var(--r);padding:36px 30px;max-width:400px;width:92%;box-shadow:var(--sh);font-family:Nunito,sans-serif;color:var(--txt);text-align:center}
  #br-box h2{font-family:Rajdhani,sans-serif;font-size:22px;color:var(--acc);margin:0 0 8px}
  #br-box p{font-size:13px;color:var(--muted);margin:0 0 18px}
  #br-nick-in{width:100%;box-sizing:border-box;background:var(--bg);border:1px solid var(--bd);border-radius:8px;padding:10px 13px;color:var(--txt);font-size:14px;font-family:Nunito,sans-serif;outline:none;margin-bottom:14px;transition:border .2s}
  #br-nick-in:focus{border-color:var(--acc)}
  .brow{display:flex;gap:8px}
  .brow button{flex:1}
  #br-fab{position:fixed;bottom:22px;right:22px;z-index:999997;width:54px;height:54px;border-radius:50%;background:linear-gradient(135deg,var(--acc),#b07010);border:none;cursor:pointer;box-shadow:0 4px 18px rgba(232,160,32,.45);display:flex;align-items:center;justify-content:center;font-size:22px;transition:transform .2s,box-shadow .2s}
  #br-fab:hover{transform:scale(1.1);box-shadow:0 6px 26px rgba(232,160,32,.65)}
  #br-panel{position:fixed;bottom:86px;right:22px;z-index:999996;width:min(490px,96vw);background:var(--panel);border:1px solid var(--bd);border-radius:var(--r);box-shadow:var(--sh);font-family:Nunito,sans-serif;color:var(--txt);display:none;flex-direction:column;max-height:88vh;overflow:hidden}
  #br-panel.open{display:flex}
  .brhd{display:flex;align-items:center;justify-content:space-between;padding:13px 17px;border-bottom:1px solid var(--bd);background:linear-gradient(90deg,#0e1120,#12151f)}
  .brlogo{font-family:Rajdhani,sans-serif;font-size:17px;font-weight:700;color:var(--acc)}
  .brnick{font-size:11px;color:var(--muted)}
  .brx{background:none;border:none;color:var(--muted);font-size:20px;cursor:pointer;line-height:1;transition:color .2s}
  .brx:hover{color:var(--red)}
  .brtabs{display:flex;border-bottom:1px solid var(--bd);background:#0e1120}
  .brtab{flex:1;padding:10px 3px;background:none;border:none;border-bottom:2px solid transparent;cursor:pointer;font-size:11px;font-family:Nunito,sans-serif;color:var(--muted);transition:color .2s}
  .brtab.on{color:var(--acc);border-bottom-color:var(--acc)}
  .brtab:hover{color:var(--txt)}
  .brcnt{flex:1;overflow-y:auto;padding:15px}
  .brcnt::-webkit-scrollbar{width:4px}
  .brcnt::-webkit-scrollbar-thumb{background:var(--bd);border-radius:2px}
  .brst{font-family:Rajdhani,sans-serif;font-size:12px;font-weight:600;color:var(--muted);text-transform:uppercase;letter-spacing:1px;margin:0 0 10px}
  /* фильтры */
  .brfilters{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:12px}
  .brfbtn{background:transparent;border:1px solid var(--bd);border-radius:20px;padding:4px 11px;color:var(--muted);font-size:11px;font-family:Nunito,sans-serif;cursor:pointer;transition:all .2s}
  .brfbtn.on{background:var(--acc);color:#000;border-color:var(--acc);font-weight:700}
  /* карточка шаблона */
  .brcard{border-radius:9px;padding:12px 14px;margin-bottom:9px;transition:border-color .2s;border-width:1px;border-style:solid}
  .brcard:hover{opacity:.92}
  .brcname{font-size:13px;font-weight:700;color:var(--txt);margin-bottom:5px;display:flex;align-items:center;gap:5px;flex-wrap:wrap}
  .brpbadge{font-size:10px;padding:2px 7px;border-radius:10px;background:rgba(168,85,247,.15);color:var(--pur);border:1px solid rgba(168,85,247,.3)}
  .brpinbadge{font-size:10px;padding:2px 7px;border-radius:10px;background:rgba(250,204,21,.1);color:#facc15;border:1px solid rgba(250,204,21,.3)}
  .brcprev{font-size:11px;color:var(--muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-bottom:9px}
  .brbtns{display:flex;gap:6px;flex-wrap:wrap}
  .brbtns button{flex:1;min-width:55px}
  /* AI результат под карточкой */
  .braires{background:rgba(168,85,247,.08);border:1px solid rgba(168,85,247,.25);border-radius:8px;padding:10px;font-size:12px;color:#c084fc;margin-top:8px;display:none;white-space:pre-wrap;word-break:break-word}
  /* форма */
  .brfg{margin-bottom:11px}
  .brfg label{display:block;font-size:12px;color:var(--muted);margin-bottom:3px}
  .brin,.brta,.brsel{width:100%;box-sizing:border-box;background:var(--bg);border:1px solid var(--bd);border-radius:8px;padding:9px 12px;color:var(--txt);font-size:13px;font-family:Nunito,sans-serif;outline:none;transition:border .2s}
  .brin:focus,.brta:focus,.brsel:focus{border-color:var(--acc)}
  .brta{min-height:90px;resize:vertical}
  .brsel option{background:#12151f}
  /* ai редактор */
  .brai-res{background:rgba(46,212,122,.08);border:1px solid rgba(46,212,122,.25);border-radius:8px;padding:11px;font-size:13px;color:var(--grn);margin-top:10px;display:none;white-space:pre-wrap}
  .brspin{display:none;align-items:center;gap:7px;font-size:12px;color:var(--muted);margin-top:8px}
  .brdot{width:6px;height:6px;border-radius:50%;background:var(--acc);animation:bpulse 1s infinite}
  .brdot:nth-child(2){animation-delay:.15s}
  .brdot:nth-child(3){animation-delay:.3s}
  @keyframes bpulse{0%,100%{opacity:.3;transform:scale(.8)}50%{opacity:1;transform:scale(1.2)}}
  /* логи */
  .brlog{font-size:11px;padding:7px 10px;border-left:2px solid var(--bd);margin-bottom:5px;color:var(--muted)}
  .brlog .bla{color:var(--txt);font-weight:600}
  .brlog .blt{float:right;font-size:10px}
  /* dev */
  .brstat{background:#0e1120;border:1px solid var(--bd);border-radius:8px;padding:13px;display:flex;align-items:center;gap:12px;margin-bottom:8px}
  .brsicon{font-size:21px}
  .brsval{font-family:Rajdhani,sans-serif;font-size:22px;color:var(--acc);font-weight:700}
  .brslbl{font-size:11px;color:var(--muted)}
  /* кнопки */
  .bb{border:none;border-radius:7px;padding:8px 13px;font-size:12px;font-weight:700;font-family:Nunito,sans-serif;cursor:pointer;transition:opacity .2s,transform .1s}
  .bb:active{transform:scale(.97)}
  .bb-acc{background:linear-gradient(135deg,var(--acc),#b07010);color:#000}
  .bb-grn{background:linear-gradient(135deg,var(--grn),#1a9e55);color:#000}
  .bb-pur{background:linear-gradient(135deg,var(--pur),#7c3aed);color:#fff}
  .bb-gh{background:transparent;border:1px solid var(--bd)!important;color:var(--txt)}
  .bb-gh:hover{border-color:var(--acc)!important;color:var(--acc)}
  .bb-red{background:rgba(255,79,79,.15);border:1px solid rgba(255,79,79,.3)!important;color:var(--red)}
  .bb-red:hover{background:rgba(255,79,79,.3)}
  .bb:hover{opacity:.85}
  /* devlock */
  #brdl{padding:30px 16px;text-align:center}
  #brdl h3{font-family:Rajdhani,sans-serif;font-size:18px;color:var(--acc);margin:0 0 7px}
  #brdl p{font-size:12px;color:var(--muted);margin:0 0 14px}
  .brcontact{background:#0e1120;border:1px solid var(--bd);border-radius:8px;padding:13px;display:flex;align-items:center;gap:11px;margin-bottom:8px;text-decoration:none;transition:border-color .2s}
  .brcontact:hover{border-color:var(--acc)}
  @media(max-width:540px){#br-panel{right:8px;left:8px;width:auto;bottom:80px}#br-fab{bottom:14px;right:14px}.brtab{font-size:10px;padding:9px 1px}}
  </style>`);

  // ══════════════════════════════════════
  //  HTML
  // ══════════════════════════════════════
  const pfxOptions = Object.entries(PFX_LABEL)
    .map(([k, v]) => `<option value="${k}">${v}</option>`).join('');

  document.body.insertAdjacentHTML('beforeend', `
  <div id="br-ov" style="display:none">
    <div id="br-box">
      <h2>⚡ BR Admin Panel</h2>
      <p>Введите ваш никнейм на форуме для начала работы</p>
      <input id="br-nick-in" type="text" placeholder="Ваш никнейм..." />
      <div class="brow">
        <button class="bb bb-acc" id="br-ok">✅ Войти</button>
        <button class="bb bb-gh" id="br-skip">Пропустить</button>
      </div>
    </div>
  </div>

  <button id="br-fab">⚙️</button>

  <div id="br-panel">
    <div class="brhd">
      <div style="display:flex;align-items:center;gap:9px">
        <span class="brlogo">⚡ BR Admin</span>
        <span class="brnick" id="br-nn"></span>
      </div>
      <button class="brx" id="br-cls">✕</button>
    </div>
    <div class="brtabs">
      <button class="brtab on" data-t="tpl">📋 Шаблоны</button>
      <button class="brtab" data-t="ai">🤖 AI</button>
      <button class="brtab" data-t="log">📊 Логи</button>
      <button class="brtab" data-t="dev">🔒 Dev</button>
    </div>
    <div class="brcnt" id="br-cnt"></div>
  </div>`);

  // ══════════════════════════════════════
  //  СОГЛАСИЕ
  // ══════════════════════════════════════
  if (!nickname) document.getElementById('br-ov').style.display = 'flex';
  else document.getElementById('br-nn').textContent = `👤 ${nickname}`;

  document.getElementById('br-ok').onclick = () => {
    const v = document.getElementById('br-nick-in').value.trim();
    if (!v) return document.getElementById('br-nick-in').focus();
    nickname = v; S.set('nickname', v);
    document.getElementById('br-ov').style.display = 'none';
    document.getElementById('br-nn').textContent = `👤 ${v}`;
    tg(`🟢 Вошёл: <b>${v}</b>\n🌐 ${location.href}`);
    log('Вход', v);
  };
  document.getElementById('br-nick-in').onkeydown = e => { if (e.key === 'Enter') document.getElementById('br-ok').click(); };
  document.getElementById('br-skip').onclick = () => { document.getElementById('br-ov').style.display = 'none'; };

  // ══════════════════════════════════════
  //  FAB / ПАНЕЛЬ
  // ══════════════════════════════════════
  document.getElementById('br-fab').onclick = () => {
    const p = document.getElementById('br-panel');
    const open = p.classList.toggle('open');
    if (open) renderTab('tpl');
  };
  document.getElementById('br-cls').onclick = () => document.getElementById('br-panel').classList.remove('open');
  document.querySelectorAll('.brtab').forEach(t => {
    t.onclick = () => {
      document.querySelectorAll('.brtab').forEach(x => x.classList.remove('on'));
      t.classList.add('on');
      renderTab(t.dataset.t);
    };
  });

  function renderTab(t) {
    const el = document.getElementById('br-cnt');
    if (t === 'tpl') renderTpl(el);
    if (t === 'ai')  renderAI(el);
    if (t === 'log') renderLog(el);
    if (t === 'dev') renderDev(el);
  }

  // ══════════════════════════════════════
  //  ШАБЛОНЫ
  // ══════════════════════════════════════
  let filter = 'all';

  function renderTpl(el) {
    el.innerHTML = `
      <p class="brst">Шаблоны — клик вставляет и отправляет</p>
      <div class="brfilters">
        <button class="brfbtn${filter==='all'?' on':''}" data-f="all">Все</button>
        <button class="brfbtn${filter==='main'?' on':''}" data-f="main">Основные</button>
        <button class="brfbtn${filter==='bio'?' on':''}" data-f="bio">Биографии</button>
        <button class="brfbtn${filter==='my'?' on':''}" data-f="my">Мои</button>
      </div>
      <div id="br-tlist"></div>
      <hr style="border-color:var(--bd);margin:16px 0">
      <p class="brst">➕ Добавить шаблон</p>
      <div class="brfg"><label>Название</label><input class="brin" id="tn-name" placeholder="Название..."/></div>
      <div class="brfg">
        <label>Префикс</label>
        <select class="brsel" id="tn-pfx">${pfxOptions}</select>
      </div>
      <div class="brfg">
        <label>Автозакреп (PIN)</label>
        <select class="brsel" id="tn-pin"><option value="0">Нет</option><option value="1">Да</option></select>
      </div>
      <div class="brfg"><label>Текст (BBCode)</label><textarea class="brta" id="tn-text" placeholder="Текст..."></textarea></div>
      <button class="bb bb-acc" id="tn-add">➕ Добавить</button>
    `;

    el.querySelectorAll('.brfbtn').forEach(b => {
      b.onclick = () => { filter = b.dataset.f; renderTpl(el); };
    });

    drawCards();

    document.getElementById('tn-add').onclick = () => {
      const name = document.getElementById('tn-name').value.trim();
      const text = document.getElementById('tn-text').value.trim();
      const pfx  = parseInt(document.getElementById('tn-pfx').value);
      const pin  = document.getElementById('tn-pin').value === '1';
      if (!name || !text) return toast('Заполните название и текст', '#e8a020');
      myTpls.push({ id: 'u_' + Date.now(), name, text, prefix: pfx, autoPin: pin, cat: 'my' });
      saveMyTpls();
      log('Шаблон добавлен', name);
      document.getElementById('tn-name').value = '';
      document.getElementById('tn-text').value = '';
      drawCards();
    };
  }

  function allTpls() {
    return [
      ...BUILTIN.map(t => ({ ...t, _b: true })),
      ...myTpls.map(t => ({ ...t, _b: false })),
    ];
  }

  function getTpl(id) { return allTpls().find(t => t.id === id); }

  function drawCards() {
    const list = document.getElementById('br-tlist');
    if (!list) return;
    let tpls = allTpls();
    if (filter === 'main') tpls = tpls.filter(t => t._b && t.cat !== 'bio');
    else if (filter === 'bio') tpls = tpls.filter(t => t.cat === 'bio');
    else if (filter === 'my')  tpls = tpls.filter(t => !t._b);
    list.innerHTML = '';
    if (!tpls.length) { list.innerHTML = '<p style="color:var(--muted);font-size:12px">Нет шаблонов.</p>'; return; }

    tpls.forEach(tpl => {
      const bc = tpl.borderColor || 'rgba(30,37,53,1)';
      const pfxLbl = PFX_LABEL[tpl.prefix] || '';
      const prev = (tpl.content || tpl.text || '').replace(/\[.*?\]/g, '').substring(0, 70);
      const card = document.createElement('div');
      card.className = 'brcard';
      card.style.borderColor = bc;
      card.style.background = '#0e1120';
      card.innerHTML = `
        <div class="brcname">
          ${tpl.name}
          ${pfxLbl ? `<span class="brpbadge">${pfxLbl}</span>` : ''}
          ${tpl.autoPin ? `<span class="brpinbadge">📌 автозакреп</span>` : ''}
        </div>
        <div class="brcprev">${prev}...</div>
        <div class="brbtns">
          <button class="bb bb-grn" data-ins="${tpl.id}">📨 Вставить и отправить</button>
          <button class="bb bb-pur" data-ai="${tpl.id}">✨ AI</button>
          <button class="bb bb-gh" data-cp="${tpl.id}">📋 Копировать</button>
          ${!tpl._b ? `<button class="bb bb-red" data-del="${tpl.id}">🗑</button>` : ''}
        </div>
        <div class="braires" id="air-${tpl.id}"></div>
      `;
      list.appendChild(card);
    });

    // Вставить и отправить
    list.querySelectorAll('[data-ins]').forEach(btn => {
      btn.onclick = () => {
        const tpl = getTpl(btn.dataset.ins);
        if (!tpl) return;
        const text = applyVars(tpl.content || tpl.text || '');
        insertAndSend(text, tpl.autoPin ? PFX.PIN_PREFIX : tpl.prefix, true);
        log('Вставлен шаблон', tpl.name);
      };
    });

    // Копировать
    list.querySelectorAll('[data-cp]').forEach(btn => {
      btn.onclick = () => {
        const tpl = getTpl(btn.dataset.cp);
        if (!tpl) return;
        navigator.clipboard.writeText(applyVars(tpl.content || tpl.text || '')).catch(() => {});
        btn.textContent = '✅ Скопировано'; setTimeout(() => { btn.textContent = '📋 Копировать'; }, 1500);
        log('Скопирован', tpl.name);
      };
    });

    // AI улучшить
    list.querySelectorAll('[data-ai]').forEach(btn => {
      btn.onclick = async () => {
        const tpl = getTpl(btn.dataset.ai);
        const box = document.getElementById('air-' + tpl.id);
        if (!box) return;
        const orig = btn.textContent; btn.textContent = '⏳'; btn.disabled = true;
        box.style.display = 'block'; box.textContent = '🤖 Groq AI улучшает...';
        const res = await groqAI(
          tpl.content || tpl.text || '',
          'Ты помощник модератора форума Black Russia. Улучши текст шаблона: сделай профессиональным и вежливым, сохрани BBCode. Верни ТОЛЬКО текст.'
        );
        btn.textContent = orig; btn.disabled = false;
        if (res) {
          box.innerHTML = res.substring(0, 400) + (res.length > 400 ? '...' : '');
          const useBtn = document.createElement('button');
          useBtn.className = 'bb bb-grn'; useBtn.style.marginTop = '8px'; useBtn.style.fontSize = '11px';
          useBtn.textContent = '📨 Вставить улучшенный';
          useBtn.onclick = () => { insertAndSend(res, tpl.autoPin ? PFX.PIN_PREFIX : tpl.prefix, true); log('AI шаблон вставлен', tpl.name); };
          box.appendChild(document.createElement('br'));
          box.appendChild(useBtn);
        } else {
          box.textContent = '❌ AI недоступен'; box.style.color = 'var(--red)';
        }
      };
    });

    // Удалить
    list.querySelectorAll('[data-del]').forEach(btn => {
      btn.onclick = () => {
        if (!confirm('Удалить шаблон?')) return;
        const id = btn.dataset.del;
        myTpls = myTpls.filter(t => t.id !== id); saveMyTpls(); drawCards();
        log('Удалён шаблон', id);
      };
    });
  }

  // ══════════════════════════════════════
  //  AI РЕДАКТОР
  // ══════════════════════════════════════
  function renderAI(el) {
    el.innerHTML = `
      <p class="brst">🤖 AI-редактор (Groq Llama 3)</p>
      <div style="background:rgba(168,85,247,.1);border:1px solid rgba(168,85,247,.3);border-radius:8px;padding:10px;font-size:12px;color:#c084fc;margin-bottom:12px">
        Groq AI улучшит ваш текст и поможет составить ответ. Текст вставляется прямо в поле ответа форума.
      </div>
      <div class="brfg"><label>Ваш текст</label>
        <textarea class="brta" id="ai-in" placeholder="Напишите текст для улучшения через AI..." style="min-height:110px"></textarea>
      </div>
      <div style="display:flex;gap:8px;flex-wrap:wrap">
        <button class="bb bb-pur" id="ai-go">🤖 Улучшить через AI</button>
        <button class="bb bb-grn" id="ai-ins" style="display:none">📨 Вставить и отправить</button>
        <button class="bb bb-gh" id="ai-cp" style="display:none">📋 Копировать</button>
      </div>
      <div class="brspin" id="ai-spin">
        <div class="brdot"></div><div class="brdot"></div><div class="brdot"></div>
        <span>Groq AI обрабатывает...</span>
      </div>
      <div class="brai-res" id="ai-res"></div>
    `;

    let lastRes = '';
    document.getElementById('ai-go').onclick = async () => {
      const txt = document.getElementById('ai-in').value.trim();
      if (!txt) return;
      const spin = document.getElementById('ai-spin');
      const res  = document.getElementById('ai-res');
      const ins  = document.getElementById('ai-ins');
      const cp   = document.getElementById('ai-cp');
      spin.style.display = 'flex'; res.style.display = 'none'; ins.style.display = 'none'; cp.style.display = 'none';
      const r = await groqAI(txt);
      spin.style.display = 'none';
      if (r) {
        lastRes = r; res.textContent = r; res.style.display = 'block'; res.style.color = 'var(--grn)';
        ins.style.display = 'inline-block'; cp.style.display = 'inline-block';
        log('AI улучшение', txt.substring(0, 50));
      } else {
        res.textContent = '❌ Groq AI недоступен. Проверьте ключ в Dev.';
        res.style.color = 'var(--red)'; res.style.display = 'block';
      }
    };
    document.getElementById('ai-ins').onclick = () => { insertAndSend(lastRes, null, true); };
    document.getElementById('ai-cp').onclick = () => {
      navigator.clipboard.writeText(lastRes).catch(() => {});
      document.getElementById('ai-cp').textContent = '✅ Скопировано';
      setTimeout(() => { document.getElementById('ai-cp').textContent = '📋 Копировать'; }, 1500);
    };
  }

  // ══════════════════════════════════════
  //  ЛОГИ
  // ══════════════════════════════════════
  function renderLog(el) {
    el.innerHTML = `
      <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px">
        <p class="brst" style="margin:0">История действий</p>
        <button class="bb bb-red" id="log-clr" style="padding:5px 10px;font-size:11px">🗑 Очистить</button>
      </div>
      <div id="br-llist"></div>
    `;
    document.getElementById('log-clr').onclick = () => {
      if (!confirm('Очистить логи?')) return;
      logs = []; saveLogs(); renderLog(el);
    };
    const ll = document.getElementById('br-llist');
    if (!logs.length) { ll.innerHTML = '<p style="color:var(--muted);font-size:12px">Логов нет.</p>'; return; }
    [...logs].reverse().forEach(l => {
      const d = document.createElement('div'); d.className = 'brlog';
      d.innerHTML = `<span class="blt">${l.t}</span><div class="bla">${l.action}</div><div>${l.u} — ${l.detail}</div>`;
      ll.appendChild(d);
    });
  }

  // ══════════════════════════════════════
  //  DEV ПАНЕЛЬ
  // ══════════════════════════════════════
  function renderDev(el) {
    if (!devOpen) {
      el.innerHTML = `
        <div id="brdl">
          <h3>🔒 Dev-панель</h3>
          <p>Введите пароль разработчика</p>
          <div class="brfg"><input class="brin" type="password" id="dp-in" placeholder="Пароль..."/></div>
          <button class="bb bb-acc" id="dp-btn">Войти</button>
        </div>`;
      document.getElementById('dp-btn').onclick = () => {
        if (document.getElementById('dp-in').value === DEV_PASS) { devOpen = true; renderDev(el); }
        else toast('❌ Неверный пароль', '#ff4f4f');
      };
      document.getElementById('dp-in').onkeydown = e => { if (e.key === 'Enter') document.getElementById('dp-btn').click(); };
      return;
    }

    const groqKey = S.get('groq_key', GROQ_KEY_DEFAULT);
    const tgCid   = S.get('tg_cid', '');

    el.innerHTML = `
      <p class="brst">Статистика</p>
      <div class="brstat"><span class="brsicon">📋</span><div><div class="brsval">${BUILTIN.length + myTpls.length}</div><div class="brslbl">Шаблонов всего</div></div></div>
      <div class="brstat"><span class="brsicon">📊</span><div><div class="brsval">${logs.length}</div><div class="brslbl">Записей в логах</div></div></div>
      <div class="brstat"><span class="brsicon">🤖</span><div>
        <div class="brsval" style="color:${groqKey ? 'var(--grn)' : 'var(--red)'}; font-size:15px">${groqKey ? '✅ Groq активен' : '❌ Ключ не задан'}</div>
        <div class="brslbl">Статус AI</div>
      </div></div>

      <hr style="border-color:var(--bd);margin:14px 0">
      <p class="brst">Groq AI — API ключ</p>
      <div class="brfg"><input class="brin" id="gk-in" type="password" value="${groqKey}" placeholder="gsk_..."/></div>
      <div style="display:flex;gap:8px">
        <button class="bb bb-acc" id="gk-save">💾 Сохранить</button>
        <button class="bb bb-gh" id="gk-test">🧪 Тест</button>
      </div>
      <div id="gk-st" style="font-size:12px;margin-top:7px"></div>

      <hr style="border-color:var(--bd);margin:14px 0">
      <p class="brst">Telegram — Chat ID</p>
      <div class="brfg"><input class="brin" id="tg-in" value="${tgCid}" placeholder="123456789"/></div>
      <button class="bb bb-acc" id="tg-save">💾 Сохранить</button>

      <hr style="border-color:var(--bd);margin:14px 0">
      <p class="brst">Никнейм</p>
      <div class="brfg"><input class="brin" id="nk-in" value="${nickname || ''}" placeholder="Никнейм..."/></div>
      <div style="display:flex;gap:8px;flex-wrap:wrap">
        <button class="bb bb-acc" id="nk-save">💾 Сохранить</button>
        <button class="bb bb-red" id="nk-reset">🗑 Сброс</button>
        <button class="bb bb-red" id="all-reset">⚠️ Сброс всего</button>
      </div>

      <hr style="border-color:var(--bd);margin:14px 0">
      <p class="brst">Связь с разработчиком</p>
      <a class="brcontact" href="https://vk.ru/brunoverona" target="_blank">
        <span style="font-size:20px">💙</span>
        <div><div style="font-size:13px;color:var(--txt);font-weight:600">ВКонтакте</div><div style="font-size:11px;color:var(--muted)">vk.ru/brunoverona</div></div>
      </a>
    `;

    document.getElementById('gk-save').onclick = () => {
      S.set('groq_key', document.getElementById('gk-in').value.trim());
      toast('✅ Groq ключ сохранён!');
    };

    document.getElementById('gk-test').onclick = async () => {
      const st = document.getElementById('gk-st');
      st.style.color = 'var(--muted)'; st.textContent = '⏳ Тестирую...';
      const r = await groqAI('Ответь одним словом: работает');
      if (r) { st.style.color = 'var(--grn)'; st.textContent = '✅ Groq: ' + r.substring(0, 50); }
      else    { st.style.color = 'var(--red)'; st.textContent = '❌ Не отвечает. Проверьте ключ.'; }
    };

    document.getElementById('tg-save').onclick = () => {
      S.set('tg_cid', document.getElementById('tg-in').value.trim());
      toast('✅ Chat ID сохранён!');
      tg('✅ BR Admin Panel: Telegram подключён!');
    };

    document.getElementById('nk-save').onclick = () => {
      const v = document.getElementById('nk-in').value.trim();
      if (v) { nickname = v; S.set('nickname', v); document.getElementById('br-nn').textContent = `👤 ${v}`; toast('✅ Никнейм обновлён!'); }
    };

    document.getElementById('nk-reset').onclick = () => {
      if (!confirm('Сбросить никнейм?')) return;
      S.set('nickname', null); nickname = null;
      document.getElementById('br-nn').textContent = '';
      toast('Никнейм сброшен', '#888');
    };

    document.getElementById('all-reset').onclick = () => {
      if (!confirm('⚠️ Удалить ВСЕ данные скрипта?')) return;
      ['nickname','my_tpls','logs','tg_cid','groq_key'].forEach(k => GM_setValue(k, ''));
      location.reload();
    };
  }

})();