brother-torn / BroMerc Companion

// ==UserScript==
// @name         BroMerc Companion
// @namespace    tampermonkey
// @version      1.1
// @description  BroMerc Companion: Your Ultimate tool for Pre-War hits or monitoring multiple targets at once!
// @author       Brother [2590792]
// @match        https://www.torn.com/*
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_deleteValue
// @grant        GM_xmlhttpRequest
// @connect      api.torn.com
// @license      AGPL-3.0-only
// @updateURL    https://openuserjs.org/meta/brother-torn/BroMerc_Companion.meta.js
// ==/UserScript==

(function () {
  "use strict";

  function saveRaw(key, value) {
    try { GM_setValue(key, JSON.stringify(value)); } catch (e) { console.error("saveRaw", e); }
  }
  function loadRaw(key, def = null) {
    try {
      const v = GM_getValue(key);
      return v ? JSON.parse(v) : def;
    } catch (e) { return def; }
  }
  function delRaw(key) {
    try { GM_deleteValue(key); } catch (e) { console.error("delRaw", e); }
  }

  const KEY_API = "mc_apiKey_v1";
  const KEY_CACHE = "mc_cache_v1";
  const KEY_FILTER = "mc_filter_v1";
  const KEY_UI = "mc_ui_v1";
  const KEY_SHOW_ATTACKABLE = "mc_show_attackable_v1";
  const KEY_LASTFILTER = "mc_lastfilter_v1";
  const KEY_LASTMINUTES = "mc_lastminutes_v1";
  const CACHE_TTL = 5 * 60 * 1000;

  let apiKey = loadRaw(KEY_API, "") || "";
  let cache = loadRaw(KEY_CACHE, { timestamp: 0, input: "", data: [] });
  let filterMode = loadRaw(KEY_FILTER, "all");
  let uiState = loadRaw(KEY_UI, { minimized: false, x: null, y: null, iconX: null, iconY: null });
  let showOnlyAttackable = loadRaw(KEY_SHOW_ATTACKABLE, false);
  let lastFilterMode = loadRaw(KEY_LASTFILTER, "all");
  let lastMinutesThreshold = loadRaw(KEY_LASTMINUTES, 0);

  function escapeHtml(s) {
    return String(s).replace(/[&<>"'`=\/]/g, ch => ({
      '&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;','/':'&#x2F;','`':'&#x60;','=':'&#x3D;'
    }[ch]));
  }
  function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }

  function gmFetchJson(url) {
    return new Promise(resolve => {
      try {
        GM_xmlhttpRequest({
          method: "GET",
          url,
          responseType: "text",
          onload(res) {
            try {
              const json = JSON.parse(res.responseText);
              resolve({ ok: true, json });
            } catch (err) {
              resolve({ ok: false, error: "parse_error" });
            }
          },
          onerror() { resolve({ ok: false, error: "network" }); },
          ontimeout() { resolve({ ok: false, error: "timeout" }); }
        });
      } catch (err) {
        resolve({ ok: false, error: "gm_error" });
      }
    });
  }

  function buildApiUrl(id) {
    return `https://api.torn.com/user/${id}?selections=profile&key=${encodeURIComponent(apiKey)}&comment=TornStakeout&timestamp=${Date.now()}`;
  }

  if (!document.getElementById("mc_panel_full_v1")) {
    createUI();
  } else {
    applyUiStateIfPresent();
  }

  function createUI() {
    const panel = document.createElement("div");
    panel.id = "mc_panel_full_v1";
    panel.style.cssText = `
      position:fixed; top:80px; right:18px; width:560px; z-index:999999;
      background:#0f0f10; color:#e7e7e7; border:1px solid #222; border-radius:10px;
      padding:12px; font-family:Arial, sans-serif; font-size:13px;
      max-height:86vh; overflow:auto; box-shadow:0 8px 30px rgba(0,0,0,0.6);
    `;

    panel.innerHTML = `
      <div id="mc_header_v1" style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;cursor:move;">
        <div>
          <div style="font-weight:700;color:#7adfff">⚔️ Merc Companion</div>
          <div style="font-size:12px;color:#9aa2a6">Paste IDs, links (XID=...), or lists.</div>
        </div>
        <div style="display:flex;gap:8px;align-items:center">
          <label style="color:#9aa2a6;font-size:12px;display:flex;align-items:center">
            <input id="mc_show_attackable_v1" type="checkbox" style="margin-right:6px" />
            Only Attackable
          </label>
          <button id="mc_minbtn_v1" title="Minimize" style="background:#121212;border:1px solid #273238;color:#7adfff;padding:6px;border-radius:6px;cursor:pointer">_</button>
          <button id="mc_setKey_v1" title="Set API Key" style="background:#121212;border:1px solid #273238;color:#7adfff;padding:6px;border-radius:6px;cursor:pointer">Set API Key</button>
        </div>
      </div>

      <div id="mc_body_v1">
        <textarea id="mc_input_v1" placeholder="Paste names + IDs or Profile URLs here" style="width:100%;height:140px;background:#0b0b0b;color:#e7e7e7;border:1px solid #222;padding:8px;border-radius:6px;resize:vertical"></textarea>

        <div style="display:flex;gap:10px;margin-top:8px;align-items:center;flex-wrap:wrap;">
          <label style="color:#9aa2a6">Patch size
            <input id="mc_patch_v1" type="number" value="10" min="1" style="width:72px;margin-left:6px;background:#111;color:#e7e7e7;border:1px solid #222;padding:6px;border-radius:6px" />
          </label>
          <label style="color:#9aa2a6">Delay (s)
            <input id="mc_delay_v1" type="number" value="5" min="0" style="width:72px;margin-left:6px;background:#111;color:#e7e7e7;border:1px solid #222;padding:6px;border-radius:6px" />
          </label>
          <label style="color:#9aa2a6">Auto recheck (s)
            <input id="mc_auto_v1" type="number" value="0" min="0" style="width:90px;margin-left:6px;background:#111;color:#e7e7e7;border:1px solid #222;padding:6px;border-radius:6px" />
          </label>

          <label style="color:#9aa2a6;display:flex;align-items:center">Show which members?
            <select id="mc_lastfilter_v1" style="margin-left:6px;background:#111;color:#e7e7e7;border:1px solid #222;padding:6px;border-radius:6px">
              <option value="all">All</option>
              <option value="offline_idle">Offline + Idle</option>
              <option value="online">Online</option>
            </select>
          </label>

          <label style="color:#9aa2a6;display:flex;align-items:center">Hide if last action &lt;
            <input id="mc_lastminutes_v1" type="number" min="0" value="0" style="width:70px;margin-left:6px;background:#111;color:#e7e7e7;border:1px solid #222;padding:6px;border-radius:6px" />
            minutes
          </label>
        </div>

        <div style="display:flex;gap:8px;margin-top:10px">
          <button id="mc_run_v1" style="flex:1;background:#06232b;color:#cfffff;border:1px solid #154a56;padding:8px;border-radius:6px;cursor:pointer">▶ Run Check</button>
          <button id="mc_clear_v1" style="background:#2b1414;color:#ffb3b3;border:1px solid #4b2b2b;padding:8px;border-radius:6px;cursor:pointer">🗑 Clear Cache</button>
          <button id="mc_toggle_v1" style="background:#121212;color:#7adfff;border:1px solid #273238;padding:8px;border-radius:6px;cursor:pointer">Filter: ${filterMode === "all" ? "All" : "Non-Rev Only"}</button>
        </div>

        <div style="display:flex;justify-content:space-between;align-items:center;margin-top:8px">
          <div id="mc_info_v1" style="font-size:12px;color:#9aa2a6">Cached: ${cache.timestamp ? (new Date(cache.timestamp)).toLocaleTimeString() : "none"}</div>
          <div id="mc_status_v1" style="font-size:13px;color:lime"></div>
        </div>

        <div id="mc_results_v1" style="margin-top:8px;background:#060607;border:1px solid #121213;border-radius:6px;padding:10px;max-height:360px;overflow:auto;font-family:monospace;font-size:13px;color:#e7e7e7">
          <div style="color:#9aa2a6">No results yet.</div>
        </div>

        <textarea id="mc_readyBox_v1" readonly style="width:100%;height:36px;margin-top:8px;background:#0b0b0b;color:#00ff7f;border:1px solid #222;padding:6px;border-radius:6px;resize:none"></textarea>
      </div>
    `;

    document.body.appendChild(panel);

    const iconContainer = document.createElement("div");
    iconContainer.id = "mc_icon_wrap_v1";
    iconContainer.style.cssText = "position:fixed; top:100px; right:20px; z-index:999999; display:none;";
    iconContainer.innerHTML = `<div id="mc_icon_v1" style="width:30px;height:30px;display:flex;align-items:center;justify-content:center;background:#1a1a1a;color:#7adfff;border:1px solid #333;border-radius:6px;font-size:16px;cursor:pointer">⚔️</div>`;
    document.body.appendChild(iconContainer);

    const inputEl = document.getElementById("mc_input_v1");
    const runBtn = document.getElementById("mc_run_v1");
    const clearBtn = document.getElementById("mc_clear_v1");
    const toggleBtn = document.getElementById("mc_toggle_v1");
    const setKeyBtn = document.getElementById("mc_setKey_v1");
    const minBtn = document.getElementById("mc_minbtn_v1");
    const resultsEl = document.getElementById("mc_results_v1");
    const statusEl = document.getElementById("mc_status_v1");
    const infoEl = document.getElementById("mc_info_v1");
    const readyBox = document.getElementById("mc_readyBox_v1");
    const header = document.getElementById("mc_header_v1");
    const panelEl = document.getElementById("mc_panel_full_v1");
    const iconEl = document.getElementById("mc_icon_v1");
    const showAttackableEl = document.getElementById("mc_show_attackable_v1");
    const lastFilterEl = document.getElementById("mc_lastfilter_v1");
    const lastMinutesEl = document.getElementById("mc_lastminutes_v1");

    showAttackableEl.checked = !!showOnlyAttackable;
    lastFilterEl.value = lastFilterMode || "all";
    lastMinutesEl.value = lastMinutesThreshold || 0;

    function applyUiState() {
      if (uiState.minimized) {
        panelEl.style.display = "none";
        iconContainer.style.display = "block";
        if (typeof uiState.iconX === "number" && typeof uiState.iconY === "number") {
          iconContainer.style.left = uiState.iconX + "px";
          iconContainer.style.top = uiState.iconY + "px";
          iconContainer.style.right = "auto";
        }
      } else {
        panelEl.style.display = "block";
        iconContainer.style.display = "none";
        if (typeof uiState.x === "number" && typeof uiState.y === "number") {
          panelEl.style.left = uiState.x + "px";
          panelEl.style.top = uiState.y + "px";
          panelEl.style.right = "auto";
        }
      }
    }
    applyUiState();

    function makeDraggableWithClickDetection(handleEl, moveEl, onClickIfNotDragged) {
      let isDown = false;
      let startX = 0, startY = 0;
      let moved = false;
      function down(e) {
        if (e.target.tagName === "BUTTON" || e.target.tagName === "INPUT" || e.target.tagName === "TEXTAREA" || e.target.closest("a")) return;
        isDown = true; moved = false;
        startX = e.clientX; startY = e.clientY;
        const rect = moveEl.getBoundingClientRect();
        moveEl.dataset._drag_offsetX = e.clientX - rect.left;
        moveEl.dataset._drag_offsetY = e.clientY - rect.top;
        document.addEventListener("mousemove", move);
        document.addEventListener("mouseup", up);
        document.body.style.userSelect = "none";
      }
      function move(e) {
        if (!isDown) return;
        const dx = e.clientX - startX;
        const dy = e.clientY - startY;
        if (Math.hypot(dx, dy) > 6) moved = true;
        const offX = parseFloat(moveEl.dataset._drag_offsetX || 0);
        const offY = parseFloat(moveEl.dataset._drag_offsetY || 0);
        moveEl.style.left = (e.clientX - offX) + "px";
        moveEl.style.top = (e.clientY - offY) + "px";
        moveEl.style.right = "auto";
      }
      function up(e) {
        if (!isDown) return;
        isDown = false;
        document.removeEventListener("mousemove", move);
        document.removeEventListener("mouseup", up);
        document.body.style.userSelect = "";
        const rect = moveEl.getBoundingClientRect();
        if (moveEl === panelEl) {
          uiState.x = rect.left; uiState.y = rect.top;
        } else if (moveEl === iconContainer) {
          uiState.iconX = rect.left; uiState.iconY = rect.top;
        }
        saveRaw(KEY_UI, uiState);
        if (!moved && typeof onClickIfNotDragged === "function") {
          onClickIfNotDragged();
        }
      }
      handleEl.addEventListener("mousedown", down);
    }

    makeDraggableWithClickDetection(header, panelEl, null);
    makeDraggableWithClickDetection(iconContainer, iconContainer, () => {
      uiState.minimized = false;
      if (typeof uiState.iconX === "number" && typeof uiState.iconY === "number") {
        panelEl.style.left = uiState.iconX + "px";
        panelEl.style.top = uiState.iconY + "px";
        panelEl.style.right = "auto";
        uiState.x = uiState.iconX; uiState.y = uiState.iconY;
      }
      saveRaw(KEY_UI, uiState);
      applyUiState();
    });

    minBtn.addEventListener("click", () => {
      const r = panelEl.getBoundingClientRect();
      uiState.iconX = r.left; uiState.iconY = r.top;
      uiState.minimized = true;
      saveRaw(KEY_UI, uiState);
      applyUiState();
    });

    function extractIdsFromText(text) {
      const ids = [];
      if (!text) return ids;
      let m;
      // 1. URL Parameter (XID=)
      const urlRe = /XID=(\d+)/gi;
      while ((m = urlRe.exec(text)) !== null) ids.push(m[1]);

      // 2. Brackets
      const bracketRe = /[\[\(\{<]\s*(\d{3,})\s*[\]\)\}>]/g;
      while ((m = bracketRe.exec(text)) !== null) ids.push(m[1]);

      // 3. Fallback: Bare numbers (if none found yet or to catch strays)
      // If we found some via URL/brackets, we usually don't want every loose number,
      // but to be safe and "find ID straight ahead", we only check bare if ids are empty
      // OR we can just add them. Let's check bare only if list is empty to avoid "10" from settings.
      if (ids.length === 0) {
        const bareRe = /\b(\d{5,})\b/g;
        while ((m = bareRe.exec(text)) !== null) ids.push(m[1]);
      }
      return [...new Set(ids)];
    }

    function isAttackableStrict(d) {
      if (!d || !d.raw) return false;
      const state = (d.status || "").toLowerCase().trim();
      if (state !== "okay") return false;
      const statusObj = d.raw.status || {};
      const details = (statusObj.details || statusObj.description || "").toLowerCase();
      if (!details) return true;
      if (details.includes("hospital") || details.includes("hospitalized")) return false;
      return true;
    }

    function lastActionMatchesFilter(raw, filter) {
      if (!raw || !raw.last_action || !raw.last_action.status) return filter === "all";
      const st = String(raw.last_action.status).toLowerCase();
      if (filter === "all") return true;
      if (filter === "online") return st.includes("online");
      if (filter === "offline_idle") return st.includes("offline") || st.includes("idle") || st.includes("away") || st.includes("afk");
      return true;
    }

    function renderResults(objects) {
      let filtered = objects;
      if (showOnlyAttackable) filtered = filtered.filter(d => isAttackableStrict(d));

      filtered = filtered.filter(d => lastActionMatchesFilter(d.raw, lastFilterMode));

      const threshold = Math.max(0, Number(lastMinutesThreshold) || 0);
      if (threshold > 0) {
        const nowSec = Date.now() / 1000;
        filtered = filtered.filter(d => {
          const la = d.raw && d.raw.last_action;
          if (!la || !la.timestamp) return true;
          const mins = (nowSec - Number(la.timestamp)) / 60;
          return mins >= threshold;
        });
      }

      if (filterMode === "nonrevivable") filtered = filtered.filter(d => Number(d.revivable) === 0);

      const total = objects.length; const shown = filtered.length;
      const counts = { ok:0, hospital:0, travel:0, other:0, error:0 };

      const lines = filtered.map(d => {
        if (d.isError) { counts.error++; return `<div style="color:#ff8b8b">${escapeHtml(d.id)}: Error fetching</div>`; }
        const s = (d.status || "").toLowerCase();
        const attackable = isAttackableStrict(d);

        if (attackable) {
          counts.ok++;
          let lastActionText = "";
          if (d.raw && d.raw.last_action) {
            const la = d.raw.last_action;
            const laStatus = (la.status || "").toLowerCase();
            const rel = la.relative || "";
            if (laStatus.includes("online")) lastActionText = `Online`;
            else if (laStatus.includes("idle") || laStatus.includes("away") || laStatus.includes("afk")) lastActionText = rel ? `Idle (${escapeHtml(rel)})` : `Idle`;
            else if (laStatus.includes("offline")) lastActionText = rel ? `Offline (${escapeHtml(rel)})` : `Offline`;
            else lastActionText = rel ? `${escapeHtml(la.status)} (${escapeHtml(rel)})` : escapeHtml(la.status || "");
          }
          const attack = `<a href="https://www.torn.com/loader.php?sid=attack&user2ID=${d.player_id}" target="_blank" style="color:#7adfff">Attack</a>`;
          const laHtml = lastActionText ? ` <span style="color:#bfc9cc;margin-left:8px;font-size:12px">— ${escapeHtml(lastActionText)}</span>` : "";
          return `<div style="color:#b8f5b8">✅ ${escapeHtml(d.name)} [${d.player_id}] → ${attack}${laHtml}</div>`;
        }

        if (s.includes("hospital") || s.includes("hospitalized")) { counts.hospital++; return `<div style="color:orange;text-decoration:line-through">Hospital${Number(d.revivable) === 1 ? " – Revivable" : ""} — ${escapeHtml(d.name)} [${d.player_id}]</div>`; }
        if (s.includes("travel") || s.includes("travelling") || s.includes("traveling")) { counts.travel++; return `<div style="color:gray;text-decoration:line-through">Traveling — ${escapeHtml(d.name)} [${d.player_id}]</div>`; }
        if (s.includes("jail") || s.includes("jailed") || s.includes("prison")) { counts.other++; return `<div style="color:#ff9f9f;text-decoration:line-through">Jailed — ${escapeHtml(d.name)} [${d.player_id}]</div>`; }

        counts.other++; return `<div style="color:#ff9f9f">${escapeHtml(d.status || "Unknown")} — ${escapeHtml(d.name)} [${d.player_id}]</div>`;
      });

      const summary = `<div style="color:#9aa2a6;margin-bottom:6px">Showing ${shown} of ${total} — OK:${counts.ok} Hospital:${counts.hospital} Travel:${counts.travel} Other:${counts.other} Errors:${counts.error}</div>`;
      resultsEl.innerHTML = summary + lines.join("");
      return counts;
    }

    function saveCache(obj) {
      saveRaw(KEY_CACHE, obj);
      cache = obj;
      infoEl.textContent = `Cached: ${new Date(obj.timestamp).toLocaleTimeString()}`;
    }

    async function fetchProfile(id) {
      const url = buildApiUrl(id);
      const res = await gmFetchJson(url);
      if (!res.ok) return { id, player_id: id, name: `#${id}`, isError: true, raw: null };
      const data = res.json;
      if (data && !data.error) {
        return {
          id,
          player_id: data.player_id || id,
          name: data.name || `#${id}`,
          status: (data.status && data.status.state) ? data.status.state : (data.status ? data.status : "Unknown"),
          revivable: typeof data.revivable !== "undefined" ? Number(data.revivable) : 0,
          isError: false,
          raw: data
        };
      } else {
        return { id, player_id: id, name: `#${id}`, isError: true, raw: data || null };
      }
    }

    let currentObjects = [];
    async function runCheck(force = false) {
      if (!apiKey) {
        const k = prompt("Enter your Torn API key (saved locally):");
        if (!k) return;
        apiKey = k.trim();
        saveRaw(KEY_API, apiKey);
      }

      lastFilterMode = lastFilterEl.value || "all";
      lastMinutesThreshold = Number(lastMinutesEl.value || 0);
      saveRaw(KEY_LASTFILTER, lastFilterMode);
      saveRaw(KEY_LASTMINUTES, lastMinutesThreshold);

      const rawText = (inputEl.value && inputEl.value.trim()) ? inputEl.value.trim() : (cache.input || "");
      if (!rawText) { alert("Paste a list of targets with IDs first."); return; }

      const ids = extractIdsFromText(rawText);
      if (!ids || ids.length === 0) { alert("No valid IDs found in input."); return; }

      const patchSize = Math.max(1, parseInt(document.getElementById("mc_patch_v1").value || 10, 10));
      const delaySec = Math.max(0, parseInt(document.getElementById("mc_delay_v1").value || 5, 10));
      const autoSec = Math.max(0, parseInt(document.getElementById("mc_auto_v1").value || 0, 10));

      runBtn.disabled = true; clearBtn.disabled = true; toggleBtn.disabled = true; setKeyBtn.disabled = true;
      statusEl.style.color = "#7adfff"; statusEl.textContent = `Checking ${ids.length} targets...`;
      readyBox.value = "";

      currentObjects = [];

      for (let i = 0; i < ids.length; i += patchSize) {
        const chunk = ids.slice(i, i + patchSize);
        const promises = chunk.map(id => fetchProfile(id));
        const chunkResults = await Promise.all(promises);
        currentObjects.push(...chunkResults);

        renderResults(currentObjects);

        if (i + patchSize < ids.length && delaySec > 0) {
          statusEl.style.color = "#ffb86b";
          statusEl.textContent = `Waiting ${delaySec}s before next patch...`;
          await sleep(delaySec * 1000);
          statusEl.style.color = "#7adfff";
          statusEl.textContent = `Checking ${ids.length} targets...`;
        }
      }

      const counts = renderResults(currentObjects);
      const nowTs = Date.now();
      saveCache({ timestamp: nowTs, input: rawText, data: currentObjects });
      readyBox.value = `✅ List ready — OK:${counts.ok} Hospital:${counts.hospital} Travel:${counts.travel} Errors:${counts.error}`;
      statusEl.style.color = "lime";
      statusEl.textContent = "Done";

      runBtn.disabled = false; clearBtn.disabled = false; toggleBtn.disabled = false; setKeyBtn.disabled = false;

      if (autoSec > 0) setTimeout(() => runCheck(true), autoSec * 1000);
    }

    runBtn.addEventListener("click", () => runCheck(false));

    clearBtn.addEventListener("click", () => {
      delRaw(KEY_CACHE);
      cache = { timestamp: 0, input: "", data: [] };
      resultsEl.innerHTML = `<div style="color:#9aa2a6">Cache cleared.</div>`;
      readyBox.value = "";
      infoEl.textContent = `Cached: none`;
      statusEl.textContent = "";
    });

    toggleBtn.addEventListener("click", () => {
      filterMode = (filterMode === "all") ? "nonrevivable" : "all";
      saveRaw(KEY_FILTER, filterMode);
      toggleBtn.textContent = `Filter: ${filterMode === "all" ? "All" : "Non-Rev Only"}`;
      const src = (currentObjects && currentObjects.length) ? currentObjects : (cache && cache.data && cache.data.length ? cache.data : []);
      if (src && src.length) renderResults(src);
    });

    showAttackableEl.addEventListener("change", () => {
      showOnlyAttackable = !!showAttackableEl.checked;
      saveRaw(KEY_SHOW_ATTACKABLE, showOnlyAttackable);
      const src = (currentObjects && currentObjects.length) ? currentObjects : (cache && cache.data && cache.data.length ? cache.data : []);
      if (src && src.length) renderResults(src);
    });

    lastFilterEl.addEventListener("change", () => {
      lastFilterMode = lastFilterEl.value || "all";
      saveRaw(KEY_LASTFILTER, lastFilterMode);
      const src = (currentObjects && currentObjects.length) ? currentObjects : (cache && cache.data && cache.data.length ? cache.data : []);
      if (src && src.length) renderResults(src);
    });

    lastMinutesEl.addEventListener("change", () => {
      lastMinutesThreshold = Number(lastMinutesEl.value || 0);
      saveRaw(KEY_LASTMINUTES, lastMinutesThreshold);
      const src = (currentObjects && currentObjects.length) ? currentObjects : (cache && cache.data && cache.data.length ? cache.data : []);
      if (src && src.length) renderResults(src);
    });

    setKeyBtn.addEventListener("click", () => {
      const k = prompt("Enter your Torn API key (saved locally). Leave blank to cancel.");
      if (!k) return;
      apiKey = k.trim();
      saveRaw(KEY_API, apiKey);
      alert("API key saved.");
    });

    if (cache && cache.input) inputEl.value = cache.input;
    if (cache && cache.timestamp && (Date.now() - cache.timestamp) < CACHE_TTL && Array.isArray(cache.data) && cache.data.length) {
      const counts = renderResults(cache.data);
      readyBox.value = `✅ List ready (cached) — OK:${counts.ok} Hospital:${counts.hospital} Travel:${counts.travel} Errors:${counts.error}`;
      statusEl.textContent = "Cached";
      infoEl.textContent = `Cached: ${new Date(cache.timestamp).toLocaleTimeString()}`;
    }

    const observer = new MutationObserver(() => {
      if (!document.getElementById("mc_panel_full_v1")) {
        try { createUI(); } catch (e) { console.error("recreate UI failed", e); }
      }
    });
    observer.observe(document.body, { childList: true, subtree: true });

  }

  function applyUiStateIfPresent() {
    const panelEl = document.getElementById("mc_panel_full_v1");
    const iconContainer = document.getElementById("mc_icon_wrap_v1");
    const showAttackableEl = document.getElementById("mc_show_attackable_v1");
    const lastFilterEl = document.getElementById("mc_lastfilter_v1");
    const lastMinutesEl = document.getElementById("mc_lastminutes_v1");
    if (!panelEl || !iconContainer) return;
    if (uiState.minimized) {
      panelEl.style.display = "none";
      iconContainer.style.display = "block";
      if (typeof uiState.iconX === "number" && typeof uiState.iconY === "number") {
        iconContainer.style.left = uiState.iconX + "px";
        iconContainer.style.top = uiState.iconY + "px";
        iconContainer.style.right = "auto";
      }
    } else {
      panelEl.style.display = "block";
      iconContainer.style.display = "none";
      if (typeof uiState.x === "number" && typeof uiState.y === "number") {
        panelEl.style.left = uiState.x + "px";
        panelEl.style.top = uiState.y + "px";
        panelEl.style.right = "auto";
      }
    }
    if (showAttackableEl) showAttackableEl.checked = !!showOnlyAttackable;
    if (lastFilterEl) lastFilterEl.value = lastFilterMode || "all";
    if (lastMinutesEl) lastMinutesEl.value = lastMinutesThreshold || 0;

    const cacheLocal = loadRaw(KEY_CACHE, null);
    if (cacheLocal && cacheLocal.timestamp && (Date.now() - cacheLocal.timestamp) < CACHE_TTL && Array.isArray(cacheLocal.data) && cacheLocal.data.length) {
      try {
        const btn = document.getElementById("mc_toggle_v1");
        if (btn) { btn.click(); btn.click(); }
      } catch (e) {}
    }
  }

})();