Загрузка данных


(() => {
  "use strict";

  if (location.hostname !== "web.max.ru") {
    alert("Откройте этот скрипт в консоли вкладки https://web.max.ru/");
    return;
  }

  const NUMBERS = `
9002691575
9002721234
9012059312
9021999192
9032508110
9034497720
9034590684
9045855213
9053976455
9064318343
9085852506
9086620972
9086756074
9087066962
9087467483
9094433720
9094448382
9094464875
9094523366
9094670013
9094680046
9103064558
9107793336
9114207029
9116809538
9120313443
9122008259
9128462550
9129399105
9136128777
9136468662
9138077229
9138557354
9140318884
9143097188
9154568522
9155103677
9180144627
9180224985
9180315266
9180340441
9180394132
9180446727
9180451160
9180576669
9180666740
9180690414
9180730849
9180731789
9180760683
9180761234
9180783324
9180883011
9180981878
9181101807
9181111502
9181127027
9181350615
9181406307
9181723168
9181813212
9181910213
9181934414
9181992215
9182062006
9182167686
9182535529
9182646644
9182840282
9182891048
9183094004
9183150421
9183172240
9183210239
9183836720
9183853148
9184558622
9184600063
9186080692
9186316296
9186396729
9186452477
9186570768
9186576612
9186777760
9186856971
9186949585
9188001880
9189003365
9189332516
9189503724
9189506718
9189798559
9189915174
9192268154
9192756070
9197420068
9207582040
9221719454
9229955607
9232184805
9242751497
9243001609
9243533773
9244670673
9247087227
9247757040
9250377765
9257033928
9280367823
9282406382
9282727412
9282820224
9282826224
9282947987
9284064464
9284202084
9284230363
9284312937
9284590429
9287373790
9288810366
9290401615
9292634317
9293431932
9297435515
9298399951
9322483316
9340066606
9380772657
9384024215
9384828910
9385019108
9501230830
9502707490
9504696005
9507947749
9508410375
9519435010
9522295845
9528169868
9528210173
9528371157
9528550527
9530736896
9530823108
9530957833
9531107212
9531161246
9533878833
9535537889
9536926142
9539231912
9588830797
9590144083
9604710006
9604711177
9615131949
9615261666
9615831634
9615902211
9617105399
9618300221
9619653199
9628587922
9630872006
9632935528
9637774927
9649020430
9649055996
9649338277
9649388883
9667402766
9667402776
9667405930
9667497529
9667771077
9669973743
9673005775
9676457595
9682626342
9778521610
9780201238
9782375532
9784497761
9784648879
9784681191
9787009174
9787834589
9788016345
9790034349
9821103391
9822871945
9824526637
9830116230
9831705952
9832898484
9832944313
9834084777
9853176393
9858710955
9874867463
9881616550
9883253498
9883636531
9883699533
9884743513
9886046933
9887047940
9887731327
9889421814
9891217989
9891218722
9892109610
9892335701
9892339766
9892463977
9892679991
9892776931
9892790969
9892877122
9894973099
9895088982
9897776096
9898050019
9898098919
9898201196
9898261639
9898261739
9898318905
9898557373
9917037672
9923391876
9933095777
9933151994
9933230918
9937707277
9951200069
9951235022
9951241105
9953789874
9956118049
9956396333
9959367754
9960502343
9962528527
9966349494
9992929656
9994263100
`;

  const phones = [...new Set(
    NUMBERS.split(/\s+/)
      .map((v) => v.replace(/\D/g, ""))
      .filter((v) => v.length >= 10)
  )];

  const PANEL_ID = "max-console-manual-outreach-panel";
  const STYLE_ID = "max-console-manual-outreach-style";

  const state = {
    index: 0,
    sent: 0,
    notFound: 0,
    skipped: 0,
    errors: 0,
    status: "idle",          // idle | searching | sending | paused | done | stopped | error
    statusText: "СИСТЕМА ГОТОВА",
    current: null,
    message: "",
    log: [],
    stopRequested: false,
    operationId: 0,
    delayBetweenMs: 1200
  };

  document.getElementById(PANEL_ID)?.remove();
  document.getElementById(STYLE_ID)?.remove();

  // ---------- утилиты ----------
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
  const text = (v) => String(v || "").replace(/\s+/g, " ").trim();

  // Для сравнения текста сообщения: сохраняем переносы строк,
  // но нормализуем пробелы/отступы и убираем эмодзи.
  const comparable = (v) => String(v || "")
    .replace(/\r\n?/g, "\n")            // CRLF/CR -> LF
    .replace(/[ \t]+/g, " ")             // горизонтальные пробелы -> один пробел
    .replace(/[ \t]*\n[ \t]*/g, "\n")    // убираем пробелы вокруг переносов
    .replace(/\n{2,}/g, "\n")            // подряд идущие переносы -> один
    .replace(/[\p{Extended_Pictographic}\uFE0F]/gu, "")
    .trim();

  const normalizePhone = (v) => {
    const d = String(v || "").replace(/\D/g, "");
    return d.length === 11 && (d.startsWith("7") || d.startsWith("8")) ? d.slice(1) : d;
  };
  const visible = (el) => {
    if (!el) return false;
    const r = el.getBoundingClientRect();
    const s = getComputedStyle(el);
    return r.width > 0 && r.height > 0 && s.display !== "none" && s.visibility !== "hidden";
  };
  const label = (el) => text([
    el?.textContent || "",
    el?.getAttribute("aria-label") || "",
    el?.getAttribute("title") || ""
  ].join(" ")).toLowerCase();
  const buttons = () => [...document.querySelectorAll("button, [role='button'], a")].filter(visible);
  const findText = (wanted) => {
    const needle = text(wanted).toLowerCase();
    return buttons().find((el) => label(el) === needle)
        || buttons().find((el) => label(el).includes(needle));
  };
  const click = (el) => {
    if (!el) return false;
    el.scrollIntoView({ block: "center", inline: "center" });
    // Один штатный клик. Серия pointer/mouse-событий может быть
    // обработана MAX как несколько нажатий.
    if (typeof el.click === "function") el.click();
    else el.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true, view: window }));
    return true;
  };
  const waitFor = async (predicate, timeout = 12000, interval = 220) => {
    const started = Date.now();
    while (Date.now() - started < timeout) {
      if (state.stopRequested) return null;
      const r = predicate();
      if (r) return r;
      await sleep(interval);
    }
    return null;
  };
  const editableValue = (el) => {
    if (!el) return "";
    return el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement
      ? el.value || ""
      : el.innerText || el.textContent || "";
  };
  const emitInput = (el, value) => {
    el.dispatchEvent(new InputEvent("input", { bubbles: true, inputType: "insertText", data: value }));
    el.dispatchEvent(new Event("change", { bubbles: true }));
  };

  // --- вставка в contenteditable / Lexical ---
  const setContentEditableValue = (el, value) => {
    el.focus();

    const range = document.createRange();
    range.selectNodeContents(el);
    const selection = window.getSelection();
    selection.removeAllRanges();
    selection.addRange(range);

    const normalized = String(value).replace(/\r\n?/g, "\n");
    const lines = normalized.split("\n");

    // Первую строку вставляем поверх выделения (Lexical/React это подхватят).
    // Пустую первую строку обрабатываем отдельным delete, чтобы не потерять её.
    if (lines[0].length) {
      try { document.execCommand("insertText", false, lines[0]); } catch (_) {}
    } else {
      try { document.execCommand("delete", false); } catch (_) {}
    }

    // Остальные строки: сначала перенос, потом текст.
    // insertLineBreak — мягкий перенос (Shift+Enter), insertParagraph — жёсткий.
    // Пробуем по очереди; если ни один не сработал — вставляем "\n".
    for (let i = 1; i < lines.length; i++) {
      let inserted = false;
      try { inserted = document.execCommand("insertLineBreak", false); } catch (_) {}
      if (!inserted) {
        try { inserted = document.execCommand("insertParagraph", false); } catch (_) {}
      }
      if (!inserted) {
        try { document.execCommand("insertText", false, "\n"); } catch (_) {}
      }
      if (lines[i].length) {
        try { document.execCommand("insertText", false, lines[i]); } catch (_) {}
      }
    }
  };

  const setValue = (el, value) => {
    if (!el) return;
    if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
      el.focus();
      const proto = Object.getPrototypeOf(el);
      const desc = Object.getOwnPropertyDescriptor(proto, "value");
      if (desc?.set) desc.set.call(el, value);
      else el.value = value;
      emitInput(el, value);
      return;
    }
    if (el.isContentEditable || el.getAttribute("contenteditable") === "true") {
      setContentEditableValue(el, value);
    }
  };

  const findComposer = () => {
    // 1. Lexical-редактор MAX
    const lexical = [...document.querySelectorAll(
      'div.contenteditable[contenteditable="true"][role="textbox"], [data-lexical-editor="true"]'
    )].filter(visible).filter((el) => !el.closest("[role='dialog'], [aria-modal='true'], [class*='modal'], [class*='dialog']"));
    if (lexical.length) return lexical.at(-1);

    // 2. contenteditable внутри .composer
    const inComposer = [...document.querySelectorAll(
      '.composer [contenteditable="true"], .composer textarea, .composer input'
    )].filter(visible);
    if (inComposer.length) return inComposer.at(-1);

    // 3. фолбэк по placeholder
    const candidates = [...document.querySelectorAll(
      "textarea, input, [role='textbox'], [contenteditable='true'], [data-placeholder*='Сообщение' i]"
    )].filter((el) => visible(el) && !el.closest("[role='dialog'], [aria-modal='true'], [class*='modal'], [class*='dialog']"));
    const fields = candidates.filter((el) => {
      const d = `${el.getAttribute("placeholder") || ""} ${el.getAttribute("aria-label") || ""} ${el.getAttribute("data-placeholder") || ""}`.toLowerCase();
      return d.includes("сообщение") || d.includes("message");
    });
    return fields.at(-1) || null;
  };

  const findSearchInput = () => {
    const candidates = [...document.querySelectorAll("input, textarea, [contenteditable='true']")].filter(visible);
    return candidates.find((el) => `${el.getAttribute("placeholder") || ""} ${el.getAttribute("aria-label") || ""}`.toLowerCase().match(/123 456|номер телефона|номер/))
        || candidates.find((el) => el.closest("[role='dialog'], [aria-modal='true'], [class*='modal'], [class*='dialog']"))
        || null;
  };

  const dialogOpen    = () => Boolean(findSearchInput() || findText("Найти в MAX"));
  const notFoundOpen  = () => Boolean(findText("Найти другой номер"));
  const maxPlus       = () => findText("Начать общение")
    || [...document.querySelectorAll("button")].filter(visible).find((b) => /создать|общение/i.test(`${b.getAttribute("aria-label") || ""} ${b.getAttribute("title") || ""}`));

  const closeNotFound = async () => {
    const closeButton = await waitFor(() => findText("Найти другой номер"), 4000);
    if (!closeButton || !click(closeButton)) return false;
    return Boolean(await waitFor(() => !notFoundOpen() && (findText("Найти в MAX") || findSearchInput()), 7000));
  };

  const openSearch = async () => {
    if (dialogOpen()) return true;
    const plus = maxPlus();
    if (!plus || !click(plus)) return false;
    return Boolean(await waitFor(() => findText("Найти по номеру"), 6000));
  };

  const searchPhone = async (phone) => {
    if (notFoundOpen() && !await closeNotFound()) return { found: false, error: "Не удалось закрыть карточку «Не нашли номер»." };
    if (!(await openSearch())) return { found: false, error: "Не удалось открыть поиск по номеру." };
    const menuItem = findText("Найти по номеру");
    if (menuItem) click(menuItem);
    const input = await waitFor(findSearchInput, 5000);
    if (!input) return { found: false, error: "Поле номера не найдено." };
    setValue(input, phone);
    const findButton = await waitFor(() => {
      const b = findText("Найти в MAX");
      return b && !b.disabled && normalizePhone(editableValue(input)) === normalizePhone(phone) ? b : null;
    }, 5000);
    if (!findButton) return { found: false, error: "MAX не принял номер." };
    click(findButton);
    const earliestFoundAt = Date.now() + 2200;
    const result = await waitFor(() => {
      if (notFoundOpen()) return { found: false };
      if (Date.now() >= earliestFoundAt && !dialogOpen() && findComposer()) return { found: true };
      return null;
    }, 15000);
    if (!result) return { found: false, error: state.stopRequested ? "Остановлено пользователем." : "MAX не дал результат вовремя." };
    if (!result.found) {
      const closed = await closeNotFound();
      return { found: false, closed, noMatch: true };
    }
    return { found: true };
  };

  const findSendButton = (composer) => {
    // 1. По aria-label
    const byAria = [...document.querySelectorAll("button[aria-label], [role='button'][aria-label]")]
      .filter(visible)
      .filter((b) => /отправить сообщение|отправить|send message|send/i.test(b.getAttribute("aria-label") || ""));
    if (byAria.length) return byAria.at(-1);

    // 2. По иконке #icon_send внутри .composer
    const inComposer = composer?.closest(".composer") || document.querySelector(".composer");
    if (inComposer) {
      const withSendIcon = [...inComposer.querySelectorAll("button, [role='button']")].filter(visible).find((b) => {
        const source = b.outerHTML.toLowerCase();
        return /icon[_-]?send|send[_-]?icon|paper[_-]?plane|отправить/.test(source);
      });
      if (withSendIcon) return withSendIcon;
    }

    // 3. Текстовый фолбэк
    const root = composer?.closest("form, [class*='composer'], [class*='message-input'], [class*='footer']") || document;
    const candidates = [...root.querySelectorAll("button, [role='button']")].filter((b) => visible(b) && !b.disabled && b.getAttribute("aria-disabled") !== "true");
    return candidates.find((b) => /отправить сообщение|отправить|send message|send/i.test(label(b)))
      || candidates.find((b) => /icon[_-]?send|send[_-]?icon|paper[_-]?plane/.test(b.outerHTML.toLowerCase()))
      || null;
  };

  const addLog = (event, phone = "", details = "") => {
    state.log.push({ time: new Date().toISOString(), event, phone, details });
    state.log = state.log.slice(-500);
    render();
  };
  const exportLog = () => {
    const payload = JSON.stringify({
      exportedAt: new Date().toISOString(),
      message: state.message,
      phones,
      state: { ...state, log: undefined },
      log: state.log
    }, null, 2);
    const url = URL.createObjectURL(new Blob([payload], { type: "application/json" }));
    const a = document.createElement("a");
    a.href = url;
    a.download = `max-outreach-${new Date().toISOString().replace(/[:.]/g, "-")}.json`;
    a.click();
    setTimeout(() => URL.revokeObjectURL(url), 1000);
  };
  const nextPhone = () => phones[state.index] || null;
  const escapeHtml = (s) => String(s).replace(/[&<>"']/g, (c) => ({
    "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;"
  }[c]));

  // ---------- стиль: Cyberpunk 2077 ----------
  const style = document.createElement("style");
  style.id = STYLE_ID;
  style.textContent = `
    #${PANEL_ID} {
      position: fixed; z-index: 2147483647; right: 20px; top: 20px;
      width: 400px; color: #d6f7ff;
      background: linear-gradient(160deg, #04121a 0%, #071a25 55%, #0a0f1c 100%);
      border: 1px solid rgba(0, 229, 255, .35);
      border-radius: 6px;
      box-shadow:
        0 0 0 1px rgba(255, 0, 128, .12),
        0 0 22px rgba(0, 229, 255, .28),
        0 0 60px rgba(255, 0, 128, .12),
        inset 0 0 40px rgba(0, 229, 255, .05);
      font: 13px/1.45 "Rajdhani", "Segoe UI", Roboto, system-ui, sans-serif;
      overflow: hidden; user-select: none;
      clip-path: polygon(0 10px, 10px 0, calc(100% - 10px) 0, 100% 10px, 100% calc(100% - 10px), calc(100% - 10px) 100%, 10px 100%, 0 calc(100% - 10px));
      letter-spacing: .3px;
    }
    #${PANEL_ID} * { box-sizing: border-box; }

    /* scanlines overlay */
    #${PANEL_ID}::after {
      content: ""; position: absolute; inset: 0; pointer-events: none; z-index: 1;
      background: repeating-linear-gradient(
        0deg,
        rgba(0, 229, 255, .035) 0px,
        rgba(0, 229, 255, .035) 1px,
        transparent 1px,
        transparent 3px
      );
      mix-blend-mode: screen;
    }

    #${PANEL_ID} .head {
      position: relative; z-index: 2;
      display: flex; justify-content: space-between; align-items: center;
      padding: 12px 14px; cursor: grab;
      background:
        linear-gradient(135deg, rgba(0, 229, 255, .15), rgba(255, 0, 128, .12)),
        #04121a;
      border-bottom: 1px solid rgba(0, 229, 255, .35);
      text-transform: uppercase;
    }
    #${PANEL_ID} .head:active { cursor: grabbing; }
    #${PANEL_ID} .head .title {
      font-family: "Orbitron", "Rajdhani", monospace;
      font-weight: 800; font-size: 13px; letter-spacing: 2px;
      color: #fcee0a;
      text-shadow: 0 0 6px rgba(252, 238, 10, .55), 0 0 14px rgba(255, 0, 128, .35);
    }
    #${PANEL_ID} .head .sub {
      font-size: 10px; letter-spacing: 2px; margin-top: 3px;
      color: #00e5ff; opacity: .75; text-transform: uppercase;
    }
    #${PANEL_ID} .head .tools { display: flex; gap: 6px; }
    #${PANEL_ID} .head button {
      background: rgba(0, 229, 255, .08); color: #00e5ff; border: 1px solid rgba(0, 229, 255, .4);
      width: 26px; height: 26px; border-radius: 3px; cursor: pointer; font-size: 14px;
      display: inline-flex; align-items: center; justify-content: center;
      transition: all .15s;
    }
    #${PANEL_ID} .head button:hover {
      background: rgba(0, 229, 255, .22); color: #fcee0a;
      box-shadow: 0 0 10px rgba(0, 229, 255, .55);
    }

    #${PANEL_ID} .body { position: relative; z-index: 2; padding: 12px 14px 14px; }

    #${PANEL_ID} .label {
      font-size: 10.5px; text-transform: uppercase; letter-spacing: 2px;
      color: #00e5ff; margin: 4px 0 6px; opacity: .85;
    }
    #${PANEL_ID} .label::before { content: "▸ "; color: #ff0080; }

    #${PANEL_ID} textarea {
      width: 100%; min-height: 92px; resize: vertical;
      background: #030a10; color: #d6f7ff;
      border: 1px solid rgba(0, 229, 255, .3);
      border-radius: 3px;
      padding: 9px 10px; font: inherit; outline: none;
      transition: all .15s;
      font-family: "JetBrains Mono", ui-monospace, monospace; font-size: 12px;
      caret-color: #ff0080;
      box-shadow: inset 0 0 20px rgba(0, 229, 255, .04);
    }
    #${PANEL_ID} textarea:focus {
      border-color: #00e5ff;
      box-shadow: inset 0 0 20px rgba(0, 229, 255, .1), 0 0 12px rgba(0, 229, 255, .35);
    }
    #${PANEL_ID} textarea::placeholder { color: rgba(0, 229, 255, .35); }

    #${PANEL_ID} .stats {
      display: grid; grid-template-columns: repeat(4, 1fr); gap: 6px; margin: 10px 0 8px;
    }
    #${PANEL_ID} .stat {
      position: relative;
      background: rgba(0, 229, 255, .04);
      border: 1px solid rgba(0, 229, 255, .25);
      border-radius: 3px; padding: 8px 6px; text-align: center;
      clip-path: polygon(0 6px, 6px 0, 100% 0, 100% calc(100% - 6px), calc(100% - 6px) 100%, 0 100%);
      transition: all .2s;
    }
    #${PANEL_ID} .stat b {
      display: block; font-family: "Orbitron", monospace;
      font-size: 17px; font-weight: 800; letter-spacing: 1px;
      color: #d6f7ff;
    }
    #${PANEL_ID} .stat span {
      font-size: 9px; color: #00e5ff; text-transform: uppercase; letter-spacing: 1.5px;
      opacity: .75;
    }
    #${PANEL_ID} .stat.sent     { border-color: rgba(0, 255, 128, .45); }
    #${PANEL_ID} .stat.sent b   { color: #00ff80; text-shadow: 0 0 8px rgba(0, 255, 128, .7); }
    #${PANEL_ID} .stat.left     { border-color: rgba(0, 229, 255, .45); }
    #${PANEL_ID} .stat.left b   { color: #00e5ff; text-shadow: 0 0 8px rgba(0, 229, 255, .7); }
    #${PANEL_ID} .stat.notfound { border-color: rgba(252, 238, 10, .45); }
    #${PANEL_ID} .stat.notfound b { color: #fcee0a; text-shadow: 0 0 8px rgba(252, 238, 10, .7); }
    #${PANEL_ID} .stat.errors   { border-color: rgba(255, 0, 128, .55); }
    #${PANEL_ID} .stat.errors b { color: #ff0080; text-shadow: 0 0 8px rgba(255, 0, 128, .8); }

    #${PANEL_ID} .progress {
      height: 8px; background: #030a10;
      border: 1px solid rgba(0, 229, 255, .3);
      border-radius: 2px; overflow: hidden; position: relative;
    }
    #${PANEL_ID} .progress > i {
      display: block; height: 100%; width: 0%;
      background: linear-gradient(90deg, #00e5ff, #ff0080, #fcee0a);
      box-shadow: 0 0 14px rgba(255, 0, 128, .7);
      transition: width .3s ease;
    }

    #${PANEL_ID} .status {
      margin-top: 10px; padding: 9px 11px;
      background: rgba(0, 229, 255, .05);
      border: 1px solid rgba(0, 229, 255, .3);
      border-left: 3px solid #00e5ff;
      border-radius: 3px;
      color: #d6f7ff; min-height: 38px; font-size: 12px;
      display: flex; align-items: center; gap: 9px;
      font-family: "JetBrains Mono", ui-monospace, monospace;
      text-transform: uppercase; letter-spacing: 1px;
    }
    #${PANEL_ID} .status .dot {
      width: 9px; height: 9px; background: #64748b; flex-shrink: 0;
      clip-path: polygon(50% 0, 100% 50%, 50% 100%, 0 50%);
    }
    #${PANEL_ID} .status[data-state="searching"] { border-left-color: #00e5ff; }
    #${PANEL_ID} .status[data-state="searching"] .dot {
      background: #00e5ff; box-shadow: 0 0 10px #00e5ff;
      animation: cbpulse 1s infinite;
    }
    #${PANEL_ID} .status[data-state="sending"] { border-left-color: #ff0080; }
    #${PANEL_ID} .status[data-state="sending"] .dot {
      background: #ff0080; box-shadow: 0 0 10px #ff0080;
      animation: cbpulse .6s infinite;
    }
    #${PANEL_ID} .status[data-state="done"] { border-left-color: #00ff80; }
    #${PANEL_ID} .status[data-state="done"] .dot { background: #00ff80; box-shadow: 0 0 10px #00ff80; }
    #${PANEL_ID} .status[data-state="error"] { border-left-color: #ff0080; }
    #${PANEL_ID} .status[data-state="error"] .dot { background: #ff0080; box-shadow: 0 0 10px #ff0080; }
    #${PANEL_ID} .status[data-state="paused"] { border-left-color: #fcee0a; }
    #${PANEL_ID} .status[data-state="paused"] .dot { background: #fcee0a; box-shadow: 0 0 10px #fcee0a; }

    @keyframes cbpulse {
      0%, 100% { opacity: 1; transform: scale(1); }
      50%      { opacity: .35; transform: scale(.75); }
    }

    #${PANEL_ID} .current {
      margin-top: 8px; font-size: 11.5px; color: #00e5ff;
      display: flex; justify-content: space-between; gap: 8px;
      text-transform: uppercase; letter-spacing: 1px;
      font-family: "JetBrains Mono", ui-monospace, monospace;
    }
    #${PANEL_ID} .current b { color: #fcee0a; font-weight: 700; text-shadow: 0 0 6px rgba(252, 238, 10, .5); }

    #${PANEL_ID} .row { display: flex; gap: 7px; margin-top: 10px; }
    #${PANEL_ID} .row button { flex: 1; }

    #${PANEL_ID} button.action {
      position: relative;
      border: 1px solid rgba(0, 229, 255, .4);
      background: rgba(0, 229, 255, .06);
      color: #00e5ff;
      border-radius: 3px; padding: 10px 10px;
      font: inherit; font-weight: 700;
      text-transform: uppercase; letter-spacing: 2px;
      font-family: "Orbitron", "Rajdhani", monospace;
      font-size: 11px;
      cursor: pointer;
      transition: all .15s;
      clip-path: polygon(0 5px, 5px 0, 100% 0, 100% calc(100% - 5px), calc(100% - 5px) 100%, 0 100%);
    }
    #${PANEL_ID} button.action:hover:not(:disabled) {
      background: rgba(0, 229, 255, .18);
      color: #fcee0a;
      border-color: #00e5ff;
      box-shadow: 0 0 14px rgba(0, 229, 255, .55), inset 0 0 20px rgba(0, 229, 255, .08);
      text-shadow: 0 0 8px currentColor;
    }
    #${PANEL_ID} button.action:active:not(:disabled) { transform: translateY(1px); }
    #${PANEL_ID} button.action:disabled { opacity: .35; cursor: not-allowed; }

    #${PANEL_ID} button.primary {
      background: linear-gradient(135deg, rgba(0, 229, 255, .22), rgba(255, 0, 128, .22));
      border-color: #00e5ff; color: #fcee0a;
      text-shadow: 0 0 6px rgba(252, 238, 10, .55);
    }
    #${PANEL_ID} button.primary:hover:not(:disabled) {
      box-shadow: 0 0 18px rgba(255, 0, 128, .65), 0 0 30px rgba(0, 229, 255, .35), inset 0 0 24px rgba(255, 0, 128, .12);
    }
    #${PANEL_ID} button.danger {
      border-color: rgba(255, 0, 128, .6); color: #ff0080;
      background: rgba(255, 0, 128, .07);
    }
    #${PANEL_ID} button.danger:hover:not(:disabled) {
      background: rgba(255, 0, 128, .18); color: #fff;
      box-shadow: 0 0 16px rgba(255, 0, 128, .75);
    }
    #${PANEL_ID} button.ghost {
      background: transparent; border-color: rgba(0, 229, 255, .25);
      color: rgba(0, 229, 255, .8); letter-spacing: 1.5px; font-size: 10px;
    }

    #${PANEL_ID} .log {
      max-height: 140px; overflow: auto; margin-top: 10px;
      background: #030a10;
      border: 1px solid rgba(0, 229, 255, .25);
      border-radius: 3px; padding: 8px 10px;
      color: #00e5ff; font-size: 11px;
      font-family: "JetBrains Mono", ui-monospace, monospace;
      box-shadow: inset 0 0 25px rgba(0, 229, 255, .06);
    }
    #${PANEL_ID} .log div { padding: 2px 0; border-bottom: 1px dashed rgba(0, 229, 255, .12); }
    #${PANEL_ID} .log div:last-child { border-bottom: 0; }
    #${PANEL_ID} .log .t  { color: rgba(0, 229, 255, .5); }
    #${PANEL_ID} .log .ev { color: #d6f7ff; text-transform: uppercase; letter-spacing: .5px; }
    #${PANEL_ID} .log .ph { color: #fcee0a; }
    #${PANEL_ID} .log .de { color: #ff0080; }
    #${PANEL_ID} .log::-webkit-scrollbar { width: 6px; }
    #${PANEL_ID} .log::-webkit-scrollbar-thumb { background: rgba(0, 229, 255, .35); border-radius: 0; }
    #${PANEL_ID} .log::-webkit-scrollbar-track { background: #030a10; }

    #${PANEL_ID}.collapsed .body { display: none; }

    /* декоративные уголки */
    #${PANEL_ID} .corner {
      position: absolute; width: 14px; height: 14px; pointer-events: none; z-index: 3;
    }
    #${PANEL_ID} .corner.tl { top: -1px; left: -1px; border-top: 2px solid #ff0080; border-left: 2px solid #ff0080; }
    #${PANEL_ID} .corner.tr { top: -1px; right: -1px; border-top: 2px solid #00e5ff; border-right: 2px solid #00e5ff; }
    #${PANEL_ID} .corner.bl { bottom: -1px; left: -1px; border-bottom: 2px solid #00e5ff; border-left: 2px solid #00e5ff; }
    #${PANEL_ID} .corner.br { bottom: -1px; right: -1px; border-bottom: 2px solid #ff0080; border-right: 2px solid #ff0080; }
  `;
  document.documentElement.appendChild(style);

  // ---------- панель ----------
  const panel = document.createElement("div");
  panel.id = PANEL_ID;
  panel.innerHTML = `
    <div class="corner tl"></div>
    <div class="corner tr"></div>
    <div class="corner bl"></div>
    <div class="corner br"></div>
    <div class="head" data-drag>
      <div>
        <div class="title">▚ MAX_AUTO 2077</div>
        <div class="sub">net_runner v2.0.77</div>
      </div>
      <div class="tools">
        <button data-action="collapse" title="Свернуть">–</button>
        <button data-action="close" title="Отключиться">×</button>
      </div>
    </div>
    <div class="body">
      <div class="label">payload :: message</div>
      <textarea data-message placeholder="Введите текст сообщения..."></textarea>

      <div class="stats">
        <div class="stat sent"><b data-sent>0</b><span>sent</span></div>
        <div class="stat left"><b data-left>${phones.length}</b><span>left</span></div>
        <div class="stat notfound"><b data-not-found>0</b><span>miss</span></div>
        <div class="stat errors"><b data-errors>0</b><span>err</span></div>
      </div>

      <div class="progress"><i data-progress></i></div>

      <div class="status" data-status data-state="idle">
        <span class="dot"></span>
        <span data-status-text>СИСТЕМА ГОТОВА</span>
      </div>

      <div class="current">
        <span>target: <b data-current>—</b></span>
        <span>progress: <b data-progress-text>0 / ${phones.length}</b></span>
      </div>

      <div class="row">
        <button class="action primary" data-action="start">▶ run</button>
        <button class="action" data-action="pause" disabled>⏸ pause</button>
        <button class="action danger" data-action="stop" disabled>■ kill</button>
      </div>

      <div class="row">
        <button class="action ghost" data-action="download">⬇ dump log</button>
        <button class="action ghost" data-action="clear">⌫ wipe log</button>
      </div>

      <div class="log" data-log>// awaiting input...</div>
    </div>`;
  document.documentElement.appendChild(panel);

  const q = (sel) => panel.querySelector(sel);

  const STATUS_MAP = {
    idle:      "idle",
    searching: "searching",
    sending:   "sending",
    paused:    "paused",
    done:      "done",
    stopped:   "error",
    error:     "error"
  };

  const render = () => {
    q("[data-sent]").textContent = state.sent;
    q("[data-left]").textContent = Math.max(0, phones.length - state.index);
    q("[data-not-found]").textContent = state.notFound;
    q("[data-errors]").textContent = state.errors;

    const done = state.index;
    const pct = phones.length ? Math.round((done / phones.length) * 100) : 0;
    q("[data-progress]").style.width = `${pct}%`;
    q("[data-progress-text]").textContent = `${done} / ${phones.length}`;

    q("[data-current]").textContent = state.current || "—";
    q("[data-status]").dataset.state = STATUS_MAP[state.status] || "idle";
    q("[data-status-text]").textContent = state.statusText;

    const isRunning = ["searching", "sending"].includes(state.status);
    const isPaused  = state.status === "paused";
    const isDone    = ["done", "stopped", "idle", "error"].includes(state.status);

    q("[data-action='start']").disabled = isRunning || isPaused;
    q("[data-action='start']").textContent =
      (state.status === "done" || state.status === "stopped") ? "▶ rerun" : "▶ run";
    q("[data-action='pause']").disabled = !(isRunning || isPaused);
    q("[data-action='pause']").textContent = isPaused ? "▶ resume" : "⏸ pause";
    q("[data-action='stop']").disabled = isDone;

    const logEl = q("[data-log]");
    if (!state.log.length) {
      logEl.textContent = "// awaiting input...";
    } else {
      logEl.innerHTML = state.log.slice(-50).map((item) => {
        const t = new Date(item.time).toLocaleTimeString("ru-RU");
        return `<div><span class="t">[${t}]</span> <span class="ev">${escapeHtml(item.event)}</span>${
          item.phone ? ` <span class="ph">${escapeHtml(item.phone)}</span>` : ""
        }${item.details ? ` <span class="de">${escapeHtml(item.details)}</span>` : ""}</div>`;
      }).join("");
      logEl.scrollTop = logEl.scrollHeight;
    }
  };

  const setStatus = (status, textMsg) => {
    state.status = status;
    if (textMsg) state.statusText = textMsg;
    render();
  };

  // ---------- основной цикл ----------
  async function processNext() {
    if (state.stopRequested) {
      setStatus("stopped", "ОСТАНОВЛЕНО ПОЛЬЗОВАТЕЛЕМ");
      addLog("стоп");
      return;
    }
    const phone = nextPhone();
    if (!phone) {
      state.current = null;
      setStatus("done", `ГОТОВО · SENT ${state.sent} · MISS ${state.notFound} · ERR ${state.errors}`);
      addLog("готово");
      return;
    }

    const op = ++state.operationId;
    state.current = phone;
    setStatus("searching", `ПОИСК ${phone}…`);
    addLog("поиск", phone);

    const result = await searchPhone(phone);
    if (state.stopRequested || op !== state.operationId) return;

    if (!result.found) {
      if (result.noMatch) {
        state.notFound += 1;
        addLog("не найдено", phone);
      } else {
        state.errors += 1;
        addLog("ошибка поиска", phone, result.error || "неизвестная ошибка");
      }
      state.index += 1;
      render();
      await sleep(400);
      return processNext();
    }

    const composer = await waitFor(findComposer, 5000);
    if (!composer) {
      state.errors += 1;
      addLog("ошибка", phone, "поле сообщения не найдено");
      state.index += 1;
      render();
      return processNext();
    }

    setValue(composer, state.message);
    // Lexical обновляется асинхронно; проверяем несколько раз, а не один раз
    // через фиксированные 320 мс.
    const accepted = await waitFor(
      () => comparable(editableValue(composer)) === comparable(state.message),
      1800,
      120
    );

    if (!accepted) {
      state.errors += 1;
      addLog("ошибка", phone, "MAX не подтвердил вставку сообщения");
      state.index += 1;
      render();
      return processNext();
    }

    setStatus("sending", `ОТПРАВКА ${phone}…`);
    const sendButton = findSendButton(composer);
    if (!sendButton) {
      state.errors += 1;
      addLog("ошибка отправки", phone, "кнопка отправки не найдена");
      state.index += 1;
      render();
      await sleep(400);
      return processNext();
    }

    click(sendButton);

    const sent = await waitFor(() => {
      const c = findComposer();
      return !c || comparable(editableValue(c)) !== comparable(state.message);
    }, 6000);

    if (sent) {
      state.sent += 1;
      addLog("отправлено", phone);
    } else {
      state.errors += 1;
      addLog("ошибка отправки", phone, "текст остался в поле");
    }

    state.index += 1;
    state.current = null;
    render();

    if (state.stopRequested) return;
    await sleep(state.delayBetweenMs);
    return processNext();
  }

  // ---------- обработчики ----------
  panel.addEventListener("click", (event) => {
    const action = event.target.closest("[data-action]")?.dataset.action;
    if (!action) return;

    if (action === "close") {
      state.stopRequested = true;
      state.operationId += 1;
      panel.remove();
      style.remove();
      return;
    }
    if (action === "collapse") {
      panel.classList.toggle("collapsed");
      event.target.textContent = panel.classList.contains("collapsed") ? "+" : "–";
      return;
    }
    if (action === "download") return exportLog();
    if (action === "clear") { state.log = []; render(); return; }

    if (action === "start") {
      if (["searching", "sending"].includes(state.status)) return;
      const message = q("[data-message]").value.trim();
      if (!message) {
        alert("Сначала введите текст сообщения.");
        return;
      }
      state.message = message;
      state.stopRequested = false;
      state.operationId += 1;
      state.index = 0;
      state.sent = 0;
      state.notFound = 0;
      state.skipped = 0;
      state.errors = 0;
      state.current = null;
      state.log = [];
      setStatus("searching", "ЗАПУСК…");
      addLog("запуск", "", `${phones.length} номеров`);
      processNext();
      return;
    }

    if (action === "pause") {
      if (state.status === "paused") {
        state.stopRequested = false;
        state.operationId += 1;
        setStatus("searching", "ПРОДОЛЖАЮ…");
        addLog("продолжение");
        processNext();
        return;
      }
      if (["searching", "sending"].includes(state.status)) {
        state.stopRequested = true;
        state.operationId += 1;
        setStatus("paused", "ПАУЗА · нажмите resume");
        addLog("пауза");
      }
      return;
    }

    if (action === "stop") {
      state.stopRequested = true;
      state.operationId += 1;
      setStatus("stopped", "ОСТАНОВЛЕНО ПОЛЬЗОВАТЕЛЕМ");
      addLog("стоп");
      return;
    }
  });

  // drag за заголовок
  (() => {
    const head = q("[data-drag]");
    let dragging = false, sx = 0, sy = 0, ox = 0, oy = 0;
    head.addEventListener("mousedown", (e) => {
      if (e.target.closest("button")) return;
      dragging = true;
      const rect = panel.getBoundingClientRect();
      ox = rect.left; oy = rect.top;
      sx = e.clientX; sy = e.clientY;
      panel.style.right = "auto";
      panel.style.left = `${ox}px`;
      panel.style.top  = `${oy}px`;
      e.preventDefault();
    });
    window.addEventListener("mousemove", (e) => {
      if (!dragging) return;
      panel.style.left = `${ox + e.clientX - sx}px`;
      panel.style.top  = `${oy + e.clientY - sy}px`;
    });
    window.addEventListener("mouseup", () => { dragging = false; });
  })();

  // горячие клавиши
  window.addEventListener("keydown", (e) => {
    if (e.altKey && e.key.toLowerCase() === "s") { e.preventDefault(); q("[data-action='start']").click(); }
    if (e.altKey && e.key.toLowerCase() === "p") { e.preventDefault(); q("[data-action='pause']").click(); }
    if (e.altKey && e.key.toLowerCase() === "x") { e.preventDefault(); q("[data-action='stop']").click(); }
  });

  // публичное API
  window.maxConsoleOutreach = {
    start:  () => q("[data-action='start']").click(),
    pause:  () => q("[data-action='pause']").click(),
    stop:   () => q("[data-action='stop']").click(),
    status: () => ({ ...state, total: phones.length, remaining: Math.max(0, phones.length - state.index) }),
    downloadLog: exportLog,
    phones
  };

  render();
  console.info(
    "%c▚ MAX_AUTO 2077 ▞%c загружено · %c" + phones.length + " целей · панель справа",
    "color:#fcee0a;background:#04121a;padding:4px 8px;font-weight:bold;border:1px solid #00e5ff;",
    "color:#00e5ff;background:#04121a;padding:4px 0;",
    "color:#ff0080;background:#04121a;padding:4px 8px;font-weight:bold;"
  );
})();