Accurate Level

Show an estimated fractional level progress on the Torn sidebar.

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

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

Tendrás que instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Tendrás que instalar una extensión como Tampermonkey antes de poder instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name         Accurate Level
// @namespace    http://tampermonkey.net/
// @version      2.1.1
// @description  Show an estimated fractional level progress on the Torn sidebar.
// @author       Maximate
// @icon         https://editor.torn.com/33b77f1c-dcd9-4d96-867f-5b578631d137-3441977.png
// @match        https://www.torn.com/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM.setValue
// @grant        GM.getValue
// @grant        GM_xmlhttpRequest
// @grant        GM.xmlHttpRequest
// @grant        GM_registerMenuCommand
// @connect      api.torn.com
// @license      MIT
// ==/UserScript==

(function () {
  "use strict";

  const STORAGE = {
    apiKey: "torn_api_key",
    cache: "level_progress_cache_v2",
    rateWindowStart: "level_progress_rate_window_start_v2",
    rateCount: "level_progress_rate_count_v2",
    lastCallAt: "level_progress_last_call_at_v2",
    needsRecalc: "level_progress_needs_recalc_v2",
    firstRunDone: "level_progress_first_run_done_v2",
  };

  const PAGE_SIZE = 100;
  const CACHE_DURATION_MS = 12 * 60 * 60 * 1000;
  const INACTIVE_THRESHOLD_SECONDS = 365 * 24 * 60 * 60;
  const RATE_LIMIT = 60;
  const RATE_WINDOW_MS = 60 * 1000;
  const MIN_INTERVAL_MS = 1100;
  const MAX_SCAN_PAGES = 30;
  const PDA_APIKEY_PLACEHOLDER = "###PDA-APIKEY###";
  const PDA_RETRY_INTERVAL_MS = 2500;

  let inflightLevelPromise = null;
  let applyTimer = null;
  let hasUpdatedThisSession = false;
  let runtimePlatform = "desktop";
  let periodicRefreshTimer = null;
  const storageState = Object.create(null);

  function safeLocalStorageGet(key) {
    try {
      return localStorage.getItem(key);
    } catch {
      return null;
    }
  }

  function safeLocalStorageSet(key, value) {
    try {
      localStorage.setItem(key, value);
    } catch {}
  }

  function getRequestFunction() {
    if (typeof GM_xmlhttpRequest === "function") {
      return GM_xmlhttpRequest;
    }
    if (typeof GM !== "undefined" && typeof GM.xmlHttpRequest === "function") {
      return GM.xmlHttpRequest.bind(GM);
    }
    return null;
  }

  function detectPlatform() {
    const hasPdaApiKey = Boolean(safeLocalStorageGet("APIKey"));
    const injectedKeyPresent = PDA_APIKEY_PLACEHOLDER.charAt(0) !== "#";
    const userAgent = String(navigator.userAgent || "");
    const looksLikePda =
      /torn\s*pda|cordova|capacitor|android.+wv|iphone.+mobile/i.test(
        userAgent,
      );
    return hasPdaApiKey || injectedKeyPresent || looksLikePda
      ? "pda"
      : "desktop";
  }

  function decodeLocalValue(raw, fallback) {
    if (raw === null || raw === undefined || raw === "") {
      return fallback;
    }

    try {
      return JSON.parse(raw);
    } catch {
      return raw;
    }
  }

  async function readPersistentValue(key, fallback) {
    if (typeof GM !== "undefined" && typeof GM.getValue === "function") {
      const value = await GM.getValue(key, fallback);
      return value === undefined ? fallback : value;
    }

    if (typeof GM_getValue === "function") {
      const value = GM_getValue(key, fallback);
      return value === undefined ? fallback : value;
    }

    return decodeLocalValue(safeLocalStorageGet(key), fallback);
  }

  function writePersistentValue(key, value) {
    if (typeof GM !== "undefined" && typeof GM.setValue === "function") {
      return GM.setValue(key, value);
    }

    if (typeof GM_setValue === "function") {
      return Promise.resolve(GM_setValue(key, value));
    }

    safeLocalStorageSet(key, JSON.stringify(value));
    return Promise.resolve();
  }

  function readPdaApiKey() {
    const injectedKey =
      PDA_APIKEY_PLACEHOLDER.charAt(0) !== "#" ? PDA_APIKEY_PLACEHOLDER : "";
    const savedKey = safeLocalStorageGet("APIKey") || "";
    return String(savedKey || injectedKey || "").trim();
  }

  async function initStorage() {
    runtimePlatform = detectPlatform();

    for (const key of Object.values(STORAGE)) {
      storageState[key] = await readPersistentValue(key, "");
    }

    const savedApiKey = String(storageState[STORAGE.apiKey] || "").trim();
    if (!savedApiKey) {
      const pdaApiKey = readPdaApiKey();
      if (pdaApiKey) {
        storageState[STORAGE.apiKey] = pdaApiKey;
        await writePersistentValue(STORAGE.apiKey, pdaApiKey);
      }
    }
  }

  function getStored(key, fallback) {
    const value =
      Object.prototype.hasOwnProperty.call(storageState, key) &&
      storageState[key] !== ""
        ? storageState[key]
        : fallback;
    return value === undefined ? fallback : value;
  }

  function setStored(key, value) {
    storageState[key] = value;
    void writePersistentValue(key, value);
  }

  function getApiKey() {
    const storedKey = String(getStored(STORAGE.apiKey, "") || "").trim();
    if (storedKey) {
      return storedKey;
    }

    const pdaApiKey = readPdaApiKey();
    if (pdaApiKey) {
      storageState[STORAGE.apiKey] = pdaApiKey;
      void writePersistentValue(STORAGE.apiKey, pdaApiKey);
      return pdaApiKey;
    }

    return "";
  }

  function getCache() {
    const raw = getStored(STORAGE.cache, "");
    if (!raw) {
      return null;
    }

    try {
      const parsed = JSON.parse(raw);
      return parsed && typeof parsed === "object" ? parsed : null;
    } catch (error) {
      console.warn("[Level Progress] Failed to parse cache.", error);
      return null;
    }
  }

  function setCache(value) {
    if (!value) {
      setStored(STORAGE.cache, "");
      return;
    }
    setStored(STORAGE.cache, JSON.stringify(value));
  }

  function clearCache() {
    setCache(null);
    setStored(STORAGE.needsRecalc, false);
  }

  function sleep(ms) {
    return new Promise((resolve) => setTimeout(resolve, ms));
  }

  async function waitForApiSlot() {
    while (true) {
      const now = Date.now();
      let windowStart = Number(getStored(STORAGE.rateWindowStart, 0)) || 0;
      let callCount = Number(getStored(STORAGE.rateCount, 0)) || 0;
      let lastCallAt = Number(getStored(STORAGE.lastCallAt, 0)) || 0;

      if (!windowStart || now - windowStart >= RATE_WINDOW_MS) {
        windowStart = now;
        callCount = 0;
        setStored(STORAGE.rateWindowStart, windowStart);
        setStored(STORAGE.rateCount, callCount);
      }

      const waitForInterval = Math.max(0, MIN_INTERVAL_MS - (now - lastCallAt));
      const waitForWindow =
        callCount >= RATE_LIMIT
          ? Math.max(0, RATE_WINDOW_MS - (now - windowStart))
          : 0;
      const waitMs = Math.max(waitForInterval, waitForWindow);

      if (waitMs <= 0) {
        setStored(STORAGE.lastCallAt, now);
        setStored(STORAGE.rateCount, callCount + 1);
        return;
      }

      await sleep(waitMs + 25);
    }
  }

  function requestJson(url) {
    return new Promise(async (resolve, reject) => {
      try {
        await waitForApiSlot();
      } catch (error) {
        reject(error);
        return;
      }

      const requester = getRequestFunction();
      if (requester) {
        requester({
          method: "GET",
          url,
          onload(response) {
            if (response.status !== 200) {
              reject(
                new Error(
                  `HTTP ${response.status}: ${response.statusText || "Request failed"}`,
                ),
              );
              return;
            }

            try {
              const data = JSON.parse(response.responseText);
              if (data && data.error) {
                reject(
                  new Error(
                    data.error.error || data.error.message || "API error",
                  ),
                );
                return;
              }
              resolve(data);
            } catch (error) {
              reject(new Error("Invalid JSON response"));
            }
          },
          onerror() {
            reject(new Error("Network error"));
          },
        });
        return;
      }

      fetch(url, { method: "GET", credentials: "omit" })
        .then(async (response) => {
          if (!response.ok) {
            throw new Error(
              `HTTP ${response.status}: ${response.statusText || "Request failed"}`,
            );
          }
          const data = await response.json();
          if (data && data.error) {
            throw new Error(
              data.error.error || data.error.message || "API error",
            );
          }
          resolve(data);
        })
        .catch((error) => {
          reject(error instanceof Error ? error : new Error("Network error"));
        });
    });
  }

  async function getUserLevelData(apiKey) {
    const url = `https://api.torn.com/v2/user/hof?key=${encodeURIComponent(apiKey)}`;
    const data = await requestJson(url);
    const levelNode = data && data.hof && data.hof.level;

    if (
      !levelNode ||
      typeof levelNode.value !== "number" ||
      typeof levelNode.rank !== "number"
    ) {
      throw new Error("Unexpected user HOF response");
    }

    return {
      level: levelNode.value,
      rank: levelNode.rank,
    };
  }

  async function getHofPage(apiKey, offset) {
    const url = `https://api.torn.com/v2/torn/hof?limit=${PAGE_SIZE}&offset=${offset}&cat=level&key=${encodeURIComponent(apiKey)}`;
    const data = await requestJson(url);
    return Array.isArray(data && data.hof) ? data.hof : [];
  }

  function isInactivePlayer(player) {
    if (!player || typeof player.last_action !== "number") {
      return false;
    }
    const nowSeconds = Math.floor(Date.now() / 1000);
    return nowSeconds - player.last_action >= INACTIVE_THRESHOLD_SECONDS;
  }

  function findNearestCurrentAnchor(players, userLevel, userRank) {
    let best = null;

    for (const player of players) {
      if (!player || player.level !== userLevel || !isInactivePlayer(player)) {
        continue;
      }
      if (typeof player.position !== "number" || player.position >= userRank) {
        continue;
      }
      if (!best || player.position > best.position) {
        best = player;
      }
    }

    return best;
  }

  function findNearestLowerAnchor(players, lowerLevel, userRank) {
    let best = null;

    for (const player of players) {
      if (!player || player.level !== lowerLevel || !isInactivePlayer(player)) {
        continue;
      }
      if (typeof player.position !== "number" || player.position <= userRank) {
        continue;
      }
      if (!best || player.position < best.position) {
        best = player;
      }
    }

    return best;
  }

  async function findInactiveAnchors(apiKey, userLevel, userRank) {
    if (userLevel <= 1) {
      return {
        currentAnchor: null,
        lowerAnchor: null,
      };
    }

    const pageCache = new Map();
    const startOffset = Math.max(
      0,
      Math.floor((Math.max(1, userRank) - 1) / PAGE_SIZE) * PAGE_SIZE,
    );
    const lowerLevel = userLevel - 1;

    let currentAnchor = null;
    let lowerAnchor = null;
    let currentBandSeen = false;
    let lowerBandSeen = false;
    let currentSearchDone = false;
    let lowerSearchDone = false;

    async function fetchPage(offset) {
      if (offset < 0) {
        return [];
      }
      if (pageCache.has(offset)) {
        return pageCache.get(offset);
      }
      const page = await getHofPage(apiKey, offset);
      pageCache.set(offset, page);
      return page;
    }

    for (let distance = 0; distance < MAX_SCAN_PAGES; distance += 1) {
      const upOffset = startOffset - distance * PAGE_SIZE;
      const downOffset = startOffset + distance * PAGE_SIZE;

      if (!currentSearchDone && upOffset >= 0) {
        const page = await fetchPage(upOffset);
        const containsCurrentLevel = page.some(
          (player) => player && player.level === userLevel,
        );
        if (containsCurrentLevel) {
          currentBandSeen = true;
        }

        const candidate = findNearestCurrentAnchor(page, userLevel, userRank);
        if (candidate) {
          currentAnchor = candidate;
          currentSearchDone = true;
        } else if (distance > 0 && currentBandSeen && !containsCurrentLevel) {
          currentSearchDone = true;
        }
      }

      if (!lowerSearchDone) {
        const page = await fetchPage(downOffset);
        const containsLowerLevel = page.some(
          (player) => player && player.level === lowerLevel,
        );
        if (containsLowerLevel) {
          lowerBandSeen = true;
        }

        const candidate = findNearestLowerAnchor(page, lowerLevel, userRank);
        if (candidate) {
          lowerAnchor = candidate;
          lowerSearchDone = true;
        } else if (distance > 0 && lowerBandSeen && !containsLowerLevel) {
          lowerSearchDone = true;
        }
      }

      if (
        (currentAnchor || currentSearchDone) &&
        (lowerAnchor || lowerSearchDone)
      ) {
        break;
      }
    }

    return {
      currentAnchor,
      lowerAnchor,
    };
  }

  function calculateFinalLevel(
    userLevel,
    userRank,
    currentAnchor,
    lowerAnchor,
  ) {
    if (userLevel >= 100) {
      return 100;
    }

    if (userLevel <= 1 || !currentAnchor || !lowerAnchor) {
      return userLevel;
    }

    const currentPos = Number(currentAnchor.position);
    const lowerPos = Number(lowerAnchor.position);

    if (
      !Number.isFinite(currentPos) ||
      !Number.isFinite(lowerPos) ||
      lowerPos <= currentPos
    ) {
      return userLevel;
    }

    const relative = (lowerPos - userRank) / (lowerPos - currentPos);
    const clamped = Math.max(0, Math.min(0.99, relative));
    return Number((userLevel + clamped).toFixed(2));
  }

  function buildCacheRecord(userData, anchors, finalLevel, mode) {
    return {
      level: userData.level,
      rank: userData.rank,
      finalLevel,
      currentAnchor: anchors.currentAnchor
        ? {
            id: anchors.currentAnchor.id,
            position: anchors.currentAnchor.position,
            lastAction: anchors.currentAnchor.last_action,
          }
        : null,
      lowerAnchor: anchors.lowerAnchor
        ? {
            id: anchors.lowerAnchor.id,
            position: anchors.lowerAnchor.position,
            lastAction: anchors.lowerAnchor.last_action,
          }
        : null,
      mode,
      updatedAt: Date.now(),
    };
  }

  async function computeDisplayData(forceRecalc = false) {
    const apiKey = getApiKey();
    if (!apiKey) {
      return null;
    }

    const userData = await getUserLevelData(apiKey);

    if (userData.level >= 100) {
      const record = buildCacheRecord(
        userData,
        { currentAnchor: null, lowerAnchor: null },
        100,
        "capped",
      );
      setCache(record);
      return record;
    }

    if (!forceRecalc) {
      const cached = getCache();
      if (
        cached &&
        cached.level === userData.level &&
        typeof cached.updatedAt === "number" &&
        Date.now() - cached.updatedAt < CACHE_DURATION_MS
      ) {
        if (cached.currentAnchor && cached.lowerAnchor) {
          const finalLevel = calculateFinalLevel(
            userData.level,
            userData.rank,
            cached.currentAnchor,
            cached.lowerAnchor,
          );

          const refreshed = {
            ...cached,
            rank: userData.rank,
            finalLevel,
            updatedAt: Date.now(),
            mode: "cached",
          };
          setCache(refreshed);
          return refreshed;
        }

        const fallback = {
          ...cached,
          rank: userData.rank,
          finalLevel: userData.level,
          updatedAt: Date.now(),
          mode: "cached",
        };
        setCache(fallback);
        return fallback;
      }
    }

    const anchors = await findInactiveAnchors(
      apiKey,
      userData.level,
      userData.rank,
    );
    const hasBothAnchors = Boolean(
      anchors.currentAnchor && anchors.lowerAnchor,
    );
    const finalLevel = calculateFinalLevel(
      userData.level,
      userData.rank,
      anchors.currentAnchor,
      anchors.lowerAnchor,
    );
    const record = buildCacheRecord(
      userData,
      anchors,
      finalLevel,
      hasBothAnchors ? "live" : "fallback",
    );
    setCache(record);
    return record;
  }

  async function getDisplayData() {
    if (!getApiKey()) {
      return null;
    }

    if (inflightLevelPromise) {
      return inflightLevelPromise;
    }

    const forceRecalc = Boolean(getStored(STORAGE.needsRecalc, false));
    inflightLevelPromise = computeDisplayData(forceRecalc)
      .catch((error) => {
        console.error(
          "[Level Progress] Failed to compute display data.",
          error,
        );
        return null;
      })
      .finally(() => {
        setStored(STORAGE.needsRecalc, false);
        inflightLevelPromise = null;
      });

    return inflightLevelPromise;
  }

  function wrapClassicLevelTextNode(row, textNode) {
    const text = String(textNode.textContent || "");
    const match = text.match(/^(\s*Level:?\s*)(\d+(?:\.\d+)?)(.*)$/i);

    if (!match) {
      return null;
    }

    const fragment = document.createDocumentFragment();
    if (match[1]) {
      fragment.appendChild(document.createTextNode(match[1]));
    }

    const valueSpan = document.createElement("span");
    valueSpan.dataset.accurateLevelValue = "1";
    valueSpan.textContent = match[2];
    fragment.appendChild(valueSpan);

    if (match[3]) {
      fragment.appendChild(document.createTextNode(match[3]));
    }

    row.replaceChild(fragment, textNode);
    return valueSpan;
  }

  function findClassicLevelValueElement() {
    const rows = document.querySelectorAll("div, li, p, td, tr");

    for (const row of rows) {
      const rowText = (row.textContent || "").replace(/\s+/g, " ").trim();
      if (!/^Level:?\s*\d+(?:\.\d+)?/i.test(rowText)) {
        continue;
      }

      const injected = row.querySelector("[data-accurate-level-value='1']");
      if (injected) {
        return injected;
      }

      const cells = Array.from(row.children);
      if (cells.length >= 2) {
        const labelText = (cells[0].textContent || "")
          .replace(/\s+/g, " ")
          .trim();
        const valueText = (cells[1].textContent || "").trim();
        if (/^Level:?$/i.test(labelText) && /^\d+(?:\.\d+)?$/.test(valueText)) {
          cells[1].dataset.accurateLevelValue = "1";
          return cells[1];
        }
      }

      const directChildren = Array.from(row.children);
      for (const child of directChildren) {
        const childText = (child.textContent || "").trim();
        if (/^\d+(?:\.\d+)?$/.test(childText)) {
          child.dataset.accurateLevelValue = "1";
          return child;
        }
      }

      for (const node of Array.from(row.childNodes)) {
        if (node.nodeType !== Node.TEXT_NODE) {
          continue;
        }

        const wrapped = wrapClassicLevelTextNode(row, node);
        if (wrapped) {
          return wrapped;
        }
      }
    }

    return null;
  }

  function findLevelValueElement() {
    const blocks = document.querySelectorAll("[class*='point-block']");

    for (const block of blocks) {
      const label = block.querySelector("[class*='name']");
      const value = block.querySelector("[class*='value']");

      if (!label || !value) {
        continue;
      }

      const labelText = (label.textContent || "").trim().replace(/\s+/g, " ");
      if (/^level:?$/i.test(labelText)) {
        return value;
      }
    }

    return findClassicLevelValueElement();
  }

  function extractVisibleLevel(element) {
    if (!element) {
      return null;
    }

    const match = (element.textContent || "").trim().match(/^(\d+)/);
    return match ? Number(match[1]) : null;
  }

  function setLevelTooltip(element, data) {
    if (!element) {
      return;
    }

    if (!data) {
      element.title =
        runtimePlatform === "pda"
          ? "Tap this level value to configure Accurate Level for PDA."
          : "Set your Torn API key to enable fractional level progress.";
      return;
    }

    const modeText =
      data.mode === "cached"
        ? "Updated from cached anchors."
        : data.mode === "live"
          ? "Calculated from nearby inactive HOF anchors."
          : data.mode === "fallback"
            ? "Showing integer level because suitable anchors were not found."
            : "Level progress calculated.";

    const anchorBits = [];
    if (data.currentAnchor && typeof data.currentAnchor.position === "number") {
      anchorBits.push(
        `Current-level anchor: #${data.currentAnchor.position} (ID ${data.currentAnchor.id})`,
      );
    }
    if (data.lowerAnchor && typeof data.lowerAnchor.position === "number") {
      anchorBits.push(
        `Lower-level anchor: #${data.lowerAnchor.position} (ID ${data.lowerAnchor.id})`,
      );
    }

    const anchorText = anchorBits.length ? ` ${anchorBits.join(". ")}.` : "";
    element.title = `${modeText} Rank: ${data.rank}.${anchorText}`;
  }

  function bindConfigTrigger(element) {
    if (!element || element.dataset.accurateLevelConfigBound === "1") {
      return;
    }

    element.dataset.accurateLevelConfigBound = "1";
    element.addEventListener("click", () => {
      if (runtimePlatform === "pda" || !getApiKey()) {
        showConfig();
      }
    });
  }

  async function applyLevelDisplay() {
    const valueElement = findLevelValueElement();
    if (!valueElement) {
      return;
    }

    bindConfigTrigger(valueElement);

    const visibleLevel = extractVisibleLevel(valueElement);
    const cached = getCache();

    if (
      visibleLevel &&
      cached &&
      cached.level &&
      visibleLevel !== cached.level
    ) {
      clearCache();
      setStored(STORAGE.needsRecalc, true);
      hasUpdatedThisSession = false;
    }

    if (hasUpdatedThisSession && !getStored(STORAGE.needsRecalc, false)) {
      if (cached && typeof cached.finalLevel === "number") {
        valueElement.textContent = cached.finalLevel.toFixed(2);
      }
      setLevelTooltip(valueElement, cached);
      return;
    }

    const data = await getDisplayData();
    setLevelTooltip(valueElement, data);

    if (!data || typeof data.finalLevel !== "number") {
      return;
    }

    valueElement.textContent = data.finalLevel.toFixed(2);
    hasUpdatedThisSession = true;
  }

  function scheduleApply() {
    if (applyTimer) {
      clearTimeout(applyTimer);
    }

    applyTimer = setTimeout(() => {
      applyLevelDisplay().catch((error) => {
        console.error("[Level Progress] Failed to apply sidebar level.", error);
      });
    }, 150);
  }

  function startPeriodicRefresh() {
    if (periodicRefreshTimer || runtimePlatform !== "pda") {
      return;
    }

    periodicRefreshTimer = setInterval(() => {
      const levelElement = findLevelValueElement();
      const text = (levelElement?.textContent || "").trim();
      const needsAnotherPass =
        !/\d+\.\d{2}/.test(text) ||
        Boolean(getStored(STORAGE.needsRecalc, false));

      if (needsAnotherPass) {
        scheduleApply();
      }
    }, PDA_RETRY_INTERVAL_MS);
  }

  async function validateKey(key) {
    try {
      const url = `https://api.torn.com/v2/key/info?key=${encodeURIComponent(key)}`;
      const data = await requestJson(url);

      if (
        data &&
        data.info &&
        data.info.access &&
        Number(data.info.access.level) >= 1
      ) {
        return { valid: true };
      }

      return { valid: false, error: "Key does not have the required access." };
    } catch (error) {
      return { valid: false, error: error.message || "Validation failed." };
    }
  }

  function showConfig() {
    const overlay = document.createElement("div");
    overlay.style.cssText = [
      "position:fixed",
      "inset:0",
      "background:rgba(0,0,0,0.55)",
      "z-index:99999",
    ].join(";");

    const modal = document.createElement("div");
    modal.style.cssText = [
      "position:fixed",
      "top:50%",
      "left:50%",
      "transform:translate(-50%,-50%)",
      "width:min(92vw,420px)",
      "background:#fff",
      "color:#111",
      "padding:20px",
      "border-radius:10px",
      "box-shadow:0 20px 60px rgba(0,0,0,0.25)",
      "z-index:100000",
      "font:14px/1.4 Arial,sans-serif",
    ].join(";");

    modal.innerHTML = `
            <h3 style="margin:0 0 8px;font-size:18px;">Torn API Configuration</h3>
            <p style="margin:0 0 12px;">Enter your Torn API key to estimate fractional level progress.${runtimePlatform === "pda" ? " PDA can also use the app API key automatically if it is already saved." : ""}</p>
            <input
                id="levelProgressKeyInput"
                type="text"
                value="${escapeHtml(getApiKey())}"
                placeholder="Paste API key"
                style="width:100%;padding:10px;border:1px solid #c9ced6;border-radius:6px;box-sizing:border-box;"
            />
            <div id="levelProgressStatus" style="min-height:20px;margin:10px 0 6px;color:#a33;"></div>
            <div style="display:flex;gap:8px;flex-wrap:wrap;">
                <button id="levelProgressSave" style="${buttonStyle("#0b6efd")}">Validate & Save</button>
                <button id="levelProgressClear" style="${buttonStyle("#6c757d")}">Clear Cache</button>
                <button id="levelProgressCancel" style="${buttonStyle("#495057")}">Close</button>
            </div>
        `;

    overlay.appendChild(modal);
    document.body.appendChild(overlay);

    const input = modal.querySelector("#levelProgressKeyInput");
    const status = modal.querySelector("#levelProgressStatus");
    const saveButton = modal.querySelector("#levelProgressSave");
    const clearButton = modal.querySelector("#levelProgressClear");
    const cancelButton = modal.querySelector("#levelProgressCancel");

    function close() {
      overlay.remove();
    }

    saveButton.addEventListener("click", async () => {
      const newKey = String(input.value || "").trim();

      if (!newKey) {
        status.textContent = "Please enter an API key.";
        status.style.color = "#b42318";
        return;
      }

      saveButton.disabled = true;
      saveButton.textContent = "Validating...";
      status.textContent = "Checking key...";
      status.style.color = "#0b6efd";

      const result = await validateKey(newKey);

      if (!result.valid) {
        status.textContent = result.error || "Invalid API key.";
        status.style.color = "#b42318";
        saveButton.disabled = false;
        saveButton.textContent = "Validate & Save";
        return;
      }

      setStored(STORAGE.apiKey, newKey);
      clearCache();
      setStored(STORAGE.needsRecalc, true);
      hasUpdatedThisSession = false;

      status.textContent = "API key saved.";
      status.style.color = "#16794a";
      saveButton.textContent = "Saved";

      setTimeout(() => {
        close();
        scheduleApply();
      }, 500);
    });

    clearButton.addEventListener("click", () => {
      clearCache();
      hasUpdatedThisSession = false;
      status.textContent = "Cache cleared.";
      status.style.color = "#16794a";
      scheduleApply();
    });

    cancelButton.addEventListener("click", close);
    overlay.addEventListener("click", (event) => {
      if (event.target === overlay) {
        close();
      }
    });
  }

  function buttonStyle(background) {
    return [
      "appearance:none",
      "border:none",
      "border-radius:6px",
      "padding:9px 14px",
      `background:${background}`,
      "color:#fff",
      "cursor:pointer",
      "font-weight:600",
    ].join(";");
  }

  function escapeHtml(value) {
    return String(value || "")
      .replace(/&/g, "&amp;")
      .replace(/</g, "&lt;")
      .replace(/>/g, "&gt;")
      .replace(/"/g, "&quot;");
  }

  async function bootstrap() {
    await initStorage();

    document.addEventListener("keydown", (event) => {
      if (event.ctrlKey && event.shiftKey && event.key.toLowerCase() === "t") {
        showConfig();
      }
    });

    if (typeof GM_registerMenuCommand === "function") {
      GM_registerMenuCommand("Configure API Key", showConfig);
    }

    if (!getStored(STORAGE.firstRunDone, false) && getApiKey()) {
      setStored(STORAGE.firstRunDone, true);
      setStored(STORAGE.needsRecalc, true);
    }

    scheduleApply();
    startPeriodicRefresh();

    if (document.readyState === "loading") {
      document.addEventListener("DOMContentLoaded", scheduleApply, {
        once: true,
      });
    }

    const observer = new MutationObserver((mutations) => {
      for (const mutation of mutations) {
        if (mutation.type !== "childList" || !mutation.addedNodes.length) {
          continue;
        }

        for (const node of mutation.addedNodes) {
          if (
            node.nodeType === 1 &&
            (node.matches?.("[class*='point-block']") ||
              node.querySelector?.("[class*='point-block']") ||
              /\bLevel:\b/i.test(node.textContent || ""))
          ) {
            scheduleApply();
            return;
          }
        }
      }
    });

    function startObserver() {
      if (!document.body) {
        return;
      }

      observer.observe(document.body, {
        childList: true,
        subtree: true,
      });
    }

    if (document.body) {
      startObserver();
    } else {
      document.addEventListener("DOMContentLoaded", startObserver, {
        once: true,
      });
    }
  }

  bootstrap().catch((error) => {
    console.error("[Level Progress] Bootstrap failed.", error);
  });
})();