ChatGPT 服务降级监控

监控 ChatGPT 服务状态、IP 质量和 PoW 难度

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         ChatGPT Degraded
// @name:zh-CN   ChatGPT 服务降级监控
// @name:zh-TW   ChatGPT 服務降級監控
// @namespace    https://github.com/lroolle/chatgpt-degraded
// @version      0.2.8
// @description  Monitor ChatGPT service level, IP quality and PoW difficulty
// @description:zh-CN  监控 ChatGPT 服务状态、IP 质量和 PoW 难度
// @description:zh-TW  監控 ChatGPT 服務狀態、IP 質量和 PoW 難度
// @author       lroolle
// @license      AGPL-3.0
// @match        *://chat.openai.com/*
// @match        *://chatgpt.com/*
// @connect      status.openai.com
// @connect      scamalytics.com
// @grant        GM_xmlhttpRequest
// @grant        unsafeWindow
// @run-at       document-start
// @icon         data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI2NCIgaGVpZ2h0PSI2NCIgdmlld0JveD0iMCAwIDY0IDY0Ij4KICA8ZGVmcz4KICAgIDxsaW5lYXJHcmFkaWVudCBpZD0iZ3JhZGllbnQiIHgxPSIwJSIgeTE9IjAlIiB4Mj0iMTAwJSIgeTI9IjEwMCUiPgogICAgICA8c3RvcCBvZmZzZXQ9IjAlIiBzdHlsZT0ic3RvcC1jb2xvcjojMmE5ZDhmO3N0b3Atb3BhY2l0eToxIi8+CiAgICAgIDxzdG9wIG9mZnNldD0iMTAwJSIgc3R5bGU9InN0b3AtY29sb3I6IzJhOWQ4ZjtzdG9wLW9wYWNpdHk6MC44Ii8+CiAgICA8L2xpbmVhckdyYWRpZW50PgogIDwvZGVmcz4KICA8Zz4KICAgIDxjaXJjbGUgY3g9IjMyIiBjeT0iMzIiIHI9IjI4IiBmaWxsPSJ1cmwoI2dyYWRpZW50KSIgc3Ryb2tlPSIjZmZmIiBzdHJva2Utd2lkdGg9IjEiLz4KPCEtLU91dGVyIGNpcmNsZSBtb2RpZmllZCB0byBsb29rIGxpa2UgIkMiLS0+CiAgICA8Y2lyY2xlIGN4PSIzMiIgY3k9IjMyIiByPSIyMCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjZmZmIiBzdHJva2Utd2lkdGg9IjEiIHN0cm9rZS1kYXNoYXJyYXk9IjEyNSA1NSIgc3Ryb2tlLWRhc2hvZmZzZXQ9IjIwIi8+CiAgICA8Y2lyY2xlIGN4PSIzMiIgY3k9IjMyIiByPSIxMiIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjZmZmIiBzdHJva2Utd2lkdGg9IjEiLz4KICAgIDxjaXJjbGUgY3g9IjMyIiBjeT0iMzIiIHI9IjQiIGZpbGw9IiNmZmYiLz4KICA8L2c+Cjwvc3ZnPg==
// @homepageURL  https://github.com/lroolle/chatgpt-degraded
// @supportURL   https://github.com/lroolle/chatgpt-degraded/issues
// ==/UserScript==

(function () {
  "use strict";

  let displayBox, collapsedIndicator;

  const i18n = {
    en: {
      service: "Service",
      ip: "IP",
      pow: "PoW",
      status: "Status",
      unknown: "Unknown",
      copyHistory: "Click to copy history",
      historyCopied: "History copied!",
      copyFailed: "Copy failed",
      riskLevels: {
        veryEasy: "Very Easy",
        easy: "Easy",
        medium: "Medium",
        hard: "Hard",
        critical: "Critical",
      },
      tooltips: {
        powDifficulty: "PoW Difficulty: Lower (green) means faster responses.",
        ipHistory: "IP History (recent 10):",
        warpPlus: "Protected by Cloudflare WARP+",
        warp: "Protected by Cloudflare WARP",
        clickToCopy: "Click to copy full history",
      },
    },
    "zh-CN": {
      service: "服务",
      ip: "IP",
      pow: "算力",
      status: "状态",
      unknown: "未知",
      copyHistory: "点击复制历史",
      historyCopied: "已复制历史!",
      copyFailed: "复制失败",
      riskLevels: {
        veryEasy: "非常容易",
        easy: "容易",
        medium: "中等",
        hard: "困难",
        critical: "严重",
      },
      tooltips: {
        powDifficulty: "PoW 难度:越低(绿色)响应越快",
        ipHistory: "IP 历史(最近10条):",
        warpPlus: "已启用 Cloudflare WARP+",
        warp: "已启用 Cloudflare WARP",
        clickToCopy: "点击复制完整历史",
      },
    },
    "zh-TW": {
      service: "服務",
      ip: "IP",
      pow: "算力",
      status: "狀態",
      unknown: "未知",
      copyHistory: "點擊複製歷史",
      historyCopied: "已複製歷史!",
      copyFailed: "複製失敗",
      riskLevels: {
        veryEasy: "非常容易",
        easy: "容易",
        medium: "中等",
        hard: "困難",
        critical: "嚴重",
      },
      tooltips: {
        powDifficulty: "PoW 難度:越低(綠色)回應越快",
        ipHistory: "IP 歷史(最近10筆):",
        warpPlus: "已啟用 Cloudflare WARP+",
        warp: "已啟用 Cloudflare WARP",
        clickToCopy: "點擊複製完整歷史",
      },
    },
  };

  // Get user language
  const userLang = (navigator.language || "en").toLowerCase();
  const lang = i18n[userLang]
    ? userLang
    : userLang.startsWith("zh-tw")
      ? "zh-TW"
      : userLang.startsWith("zh")
        ? "zh-CN"
        : "en";
  const t = (key) => {
    const keys = key.split(".");
    return (
      keys.reduce((obj, k) => obj?.[k], i18n[lang]) ||
      i18n.en[keys[keys.length - 1]]
    );
  };

  function updateUserType(type) {
    const userTypeElement = document.getElementById("user-type");
    if (!userTypeElement) return;
    const isPaid =
      type &&
      (type === "plus" ||
        type === "chatgpt-paid" ||
        type.includes("paid") ||
        type.includes("premium") ||
        type.includes("pro"));
    userTypeElement.textContent = isPaid ? "Paid" : "Free";
    userTypeElement.dataset.tooltip = `ChatGPT Account Type: ${isPaid ? "Paid" : "Free"}`;
    userTypeElement.style.color = isPaid
      ? "var(--success-color, #10a37f)"
      : "var(--text-primary, #374151)";
  }

  function getRiskColorAndLevel(difficulty) {
    if (!difficulty || difficulty === "N/A") {
      return { color: "#e63946", level: "Unknown", percentage: 0 };
    }
    const cleanDifficulty = difficulty.replace(/^0x/, "").replace(/^0+/, "");
    const hexLength = cleanDifficulty.length;
    if (hexLength <= 2) {
      return { color: "#e63946", level: "Critical", percentage: 100 };
    } else if (hexLength <= 3) {
      return { color: "#FAB12F", level: "Hard", percentage: 75 };
    } else if (hexLength <= 4) {
      return { color: "#859F3D", level: "Medium", percentage: 50 };
    } else if (hexLength <= 5) {
      return { color: "#2a9d8f", level: "Easy", percentage: 25 };
    } else {
      return { color: "#4CAF50", level: "Very Easy", percentage: 0 };
    }
  }

  function setProgressBar(bar, label, percentage, text, gradient, title) {
    bar.style.width = "100%";
    bar.style.background = gradient;
    bar.dataset.tooltip = title;
    label.innerText = text;
  }

  function updateProgressBars(difficulty) {
    const powBar = document.getElementById("pow-bar");
    const powLevel = document.getElementById("pow-level");
    const difficultyElement = document.getElementById("difficulty");
    if (!powBar || !powLevel || !difficultyElement) return;
    const { color, level, percentage } = getRiskColorAndLevel(difficulty);
    const gradient = `linear-gradient(90deg, ${color} ${percentage}%, rgba(255, 255, 255, 0.1) ${percentage}%)`;
    setProgressBar(
      powBar,
      powLevel,
      percentage,
      level,
      gradient,
      "PoW Difficulty: Lower (green) means faster responses.",
    );
    difficultyElement.style.color = color;
    powLevel.style.color = color;

    // Update icon animation based on difficulty level
    if (collapsedIndicator) {
      const outerRingAnim =
        collapsedIndicator.querySelector("#outer-ring-anim");
      const middleRingAnim =
        collapsedIndicator.querySelector("#middle-ring-anim");
      const centerDotAnim =
        collapsedIndicator.querySelector("#center-dot-anim");
      const gradientStops = collapsedIndicator.querySelector("#gradient");

      // Adjust animation speed based on difficulty level
      const animationSpeed = percentage < 25 ? 0.5 : percentage / 25; // Make it more still when easy
      if (outerRingAnim)
        outerRingAnim.setAttribute("dur", `${8 / animationSpeed}s`);
      if (middleRingAnim)
        middleRingAnim.setAttribute("dur", `${4 / animationSpeed}s`);
      if (centerDotAnim) {
        centerDotAnim.setAttribute("dur", `${2 / animationSpeed}s`);
        // Smaller pulse for easy difficulty
        centerDotAnim.setAttribute(
          "values",
          percentage < 25 ? "4;4.5;4" : "4;5;4",
        );
      }

      // Update color
      if (gradientStops) {
        gradientStops.innerHTML = `
          <stop offset="0%" style="stop-color:${color};stop-opacity:1" />
          <stop offset="100%" style="stop-color:${color};stop-opacity:0.8" />
        `;
      }
    }
  }

  const originalFetch = unsafeWindow.fetch;
  unsafeWindow.fetch = async function (resource, options) {
    const response = await originalFetch(resource, options);
    const url = typeof resource === "string" ? resource : resource?.url;
    // console.log("Fetch URL:", url);
    // console.log("Fetch options:", options);

    const isChatRequirements =
      url &&
      (url.includes("/backend-api/sentinel/chat-requirements") ||
        url.includes("/backend-anon/sentinel/chat-requirements") ||
        url.includes("/api/sentinel/chat-requirements"));

    // const method = options?.method?.toUpperCase() || "GET";
    // console.log("Method:", method, "isChatRequirements URL match:", isChatRequirements);

    // Check if this is a chat requirements request (regardless of method for now)
    if (isChatRequirements) {
      // console.log("Processing chat requirements request with method:", method);
      try {
        const clonedResponse = response.clone();
        const data = await clonedResponse.json();
        // console.log("Chat requirements response data:", data);
        const difficulty = data?.proofofwork?.difficulty;
        const userType = data?.persona || data?.user_type || data?.account_type;
        const difficultyElement = document.getElementById("difficulty");
        if (difficultyElement) {
          if (difficulty) {
            difficultyElement.innerText = difficulty;
            difficultyElement.dataset.tooltip = `Raw Difficulty Value: ${difficulty}`;
            // Update IP log with new PoW difficulty
            const ipElement = document.getElementById("ip-address");
            if (ipElement) {
              const fullIP = ipElement.dataset.fullIp;
              const ipQualityElement = document.getElementById("ip-quality");
              const score = ipQualityElement
                ? parseInt(ipQualityElement.dataset.score)
                : null;
              if (fullIP) {
                const logs = addIPLog(fullIP, score, difficulty);
                const formattedLogs = formatIPLogs(logs);
                const ipContainerTooltip = [
                  "IP History (recent 10):",
                  formattedLogs,
                  "\n---",
                  "Click to copy history",
                ].join("\n");
                ipElement.dataset.tooltip = ipContainerTooltip;
              }
            }
          } else {
            difficultyElement.innerText = "N/A";
            difficultyElement.dataset.tooltip = "No difficulty value found";
          }
        }
        updateUserType(userType || "free");
        updateProgressBars(difficulty || "N/A");
      } catch (error) {
        console.error("Error processing chat requirements:", error);
        const difficultyElement = document.getElementById("difficulty");
        if (difficultyElement) {
          difficultyElement.innerText = "N/A";
          difficultyElement.dataset.tooltip = `Error: ${error.message}`;
        }
        updateUserType("free");
        updateProgressBars("N/A");
      }
    }
    return response;
  };

  function initUI() {
    displayBox = document.createElement("div");
    displayBox.style.position = "fixed";
    displayBox.style.bottom = "10px";
    displayBox.style.right = "55px";
    displayBox.style.width = "360px";
    displayBox.style.padding = "24px";
    displayBox.style.backgroundColor =
      "var(--surface-primary, rgb(255, 255, 255))";
    displayBox.style.color = "var(--text-primary, #374151)";
    displayBox.style.fontSize = "14px";
    displayBox.style.borderRadius = "16px";
    displayBox.style.boxShadow = "0 4px 24px rgba(0, 0, 0, 0.08)";
    displayBox.style.zIndex = "10000";
    displayBox.style.transition = "opacity 0.15s ease, transform 0.15s ease";
    displayBox.style.display = "none";
    displayBox.style.opacity = "0";
    displayBox.style.transform = "translateX(10px)";
    displayBox.style.border =
      "1px solid var(--border-light, rgba(0, 0, 0, 0.05))";

    displayBox.innerHTML = `
      <div id="content">
        <div class="monitor-item">
          <div class="monitor-row">
            <span class="label">${t("service")}</span>
            <span id="user-type" class="value" data-tooltip="ChatGPT Account Type"></span>
          </div>
        </div>

        <!-- Proof of Work Difficulty -->
        <div class="monitor-item">
          <div class="monitor-row">
            <span class="label">${t("pow")}</span>
            <div class="pow-container">
              <span id="difficulty" class="value monospace" data-tooltip="PoW Difficulty Value"></span>
              <span id="pow-level" class="value-tag" data-tooltip="Difficulty Level"></span>
            </div>
          </div>
          <div class="progress-wrapper" data-tooltip="${t("tooltips.powDifficulty")}">
            <div class="progress-container">
              <div id="pow-bar" class="progress-bar"></div>
            </div>
            <div class="progress-background"></div>
          </div>
        </div>

        <!-- IP + IP Quality -->
        <div class="monitor-item">
          <div class="monitor-row">
            <span class="label">${t("ip")}</span>
            <div class="ip-container">
              <span id="ip-address" class="value monospace" data-tooltip="Click to copy IP address"></span>
              <span id="warp-badge" class="warp-badge"></span>
              <span id="ip-quality" class="value-tag" data-tooltip="IP Risk Info (Scamlytics)"></span>
            </div>
          </div>
        </div>

        <!-- OpenAI System Status -->
        <div class="monitor-item">
          <div class="monitor-row">
            <span class="label">${t("status")}</span>
            <a id="status-description"
               href="https://status.openai.com"
               target="_blank"
               class="value"
               data-tooltip="Click to open status.openai.com">
               ${t("unknown")}
            </a>
          </div>
        </div>
      </div>

      <style>
        :root {
          --warning-color: #FAB12F;
          --warning-background: rgba(251, 177, 47, 0.1);
          --error-color: #e63946;
          --error-background: rgba(230, 57, 70, 0.1);
        }
        .monitor-item {
          margin-bottom: 16px;
        }
        .monitor-item:last-child {
          margin-bottom: 0;
        }
        .monitor-row {
          display: flex;
          align-items: center;
          gap: 6px;
          margin-bottom: 6px;
        }
        .monitor-row:last-child {
          margin-bottom: 4px;
        }
        .label {
          font-size: 14px;
          color: var(--text-secondary, #6B7280);
          flex-shrink: 0;
          min-width: 40px;
        }
        .value {
          font-size: 14px;
          color: var(--text-primary, #374151);
          flex: 1;
        }
        .monospace {
          font-family: 'Menlo', 'Monaco', 'Courier New', monospace;
          font-size: 14px;
        }
        .value-tag {
          font-size: 14px;
          color: var(--success-color, #10a37f);
          white-space: nowrap;
          font-weight: 500;
          transition: opacity 0.15s ease;
          cursor: pointer;
          display: inline-block;
        }
        .value-tag:hover {
          opacity: 0.8;
        }
        .progress-wrapper {
          position: relative;
          margin-left: 40px;
          margin-top: 4px;
        }
        .progress-container {
          position: relative;
          height: 4px;
          background: transparent;
          border-radius: 2px;
          overflow: hidden;
          z-index: 1;
        }
        .progress-background {
          position: absolute;
          top: 0;
          left: 0;
          right: 0;
          height: 4px;
          background: var(--surface-secondary, rgba(0, 0, 0, 0.08));
          border-radius: 2px;
        }
        .progress-bar {
          height: 100%;
          width: 0%;
          transition: all 0.3s ease;
          background: var(--success-color, #10a37f);
        }
        #status-description {
          text-decoration: none;
          color: inherit;
        }
        #status-description:hover {
          text-decoration: underline;
        }
        #ip-address {
          cursor: pointer;
        }
        #ip-address:hover {
          opacity: 0.7;
        }
        #user-type {
          font-weight: 500;
        }
        .ip-container,
        .pow-container {
          display: flex;
          align-items: center;
          gap: 6px;
          flex: 1;
        }
        /* Ensure IP risk level (ip-quality) is right-aligned, just like pow-level */
        #ip-quality {
          margin-left: auto;
        }
        .warp-badge {
          font-size: 12px;
          color: var(--success-color, #10a37f);
          background-color: var(--surface-secondary, rgba(16, 163, 127, 0.1));
          padding: 2px 4px;
          border-radius: 4px;
          font-weight: 500;
          cursor: help;
          display: none;
          transition: background-color 0.2s ease, color 0.2s ease;
        }
        .warp-badge:hover {
          opacity: 0.8;
        }
        .ip-container .value-tag {
          padding-right: 0;
          position: relative;
        }
        /* Special handling for IP Risk tooltip */
        .ip-container .value-tag[data-tooltip]::after {
          left: auto;
          right: 0;
          transform: translateY(4px);
        }
        .ip-container .value-tag[data-tooltip]:hover::after {
          transform: translateY(0);
          left: auto;
          right: 0;
        }
        /* General tooltip styles */
        [data-tooltip] {
          position: relative;
          cursor: help;
        }
        [data-tooltip]::after {
          content: attr(data-tooltip);
          position: absolute;
          bottom: 100%;
          left: 50%;
          transform: translateX(-50%) translateY(4px);
          background: var(--surface-primary, rgba(0, 0, 0, 0.8));
          color: #fff;
          padding: 12px 16px;
          border-radius: 6px;
          font-size: 12px;
          white-space: pre-line;
          width: max-content;
          max-width: 600px;
          min-width: 450px;
          box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
          z-index: 1000;
          pointer-events: none;
          margin-bottom: 8px;
          opacity: 0;
          transition: opacity 0.15s ease, transform 0.15s ease;
          font-family: 'Menlo', 'Monaco', 'Courier New', monospace;
        }
        [data-tooltip]:hover::after {
          opacity: 1;
          transform: translateX(-50%) translateY(0);
        }
        /* Arrow styles */
        [data-tooltip]::before {
          content: '';
          position: absolute;
          bottom: 100%;
          left: 50%;
          transform: translateX(-50%) translateY(4px);
          border: 6px solid transparent;
          border-top-color: var(--surface-primary, rgba(0, 0, 0, 0.8));
          margin-bottom: -4px;
          pointer-events: none;
          opacity: 0;
          transition: opacity 0.15s ease, transform 0.15s ease;
        }
        [data-tooltip]:hover::before {
          opacity: 1;
          transform: translateY(0);
        }
        /* Special handling for IP Risk tooltip arrow */
        .ip-container .value-tag[data-tooltip]::before {
          left: auto;
          right: 12px;
          transform: translateY(4px);
        }
        .ip-container .value-tag[data-tooltip]:hover::before {
          transform: translateY(0);
          left: auto;
          right: 12px;
        }
        /* Ensure tooltips don't get cut off at viewport edges */
        @media screen and (max-width: 768px) {
          [data-tooltip]::after {
            min-width: 300px;
            max-width: calc(100vw - 48px);
          }
        }
      </style>
    `;
    document.body.appendChild(displayBox);

    collapsedIndicator = document.createElement("div");
    collapsedIndicator.style.position = "fixed";
    collapsedIndicator.style.bottom = "10px";
    collapsedIndicator.style.right = "15px";
    collapsedIndicator.style.width = "24px";
    collapsedIndicator.style.height = "24px";
    collapsedIndicator.style.backgroundColor = "transparent";
    collapsedIndicator.style.border =
      "1px solid var(--token-border-light, rgba(0, 0, 0, 0.1))";
    collapsedIndicator.style.borderRadius = "50%";
    collapsedIndicator.style.cursor = "pointer";
    collapsedIndicator.style.zIndex = "10000";
    collapsedIndicator.style.display = "flex";
    collapsedIndicator.style.alignItems = "center";
    collapsedIndicator.style.justifyContent = "center";
    collapsedIndicator.style.transition = "all 0.3s ease";

    collapsedIndicator.innerHTML = `
      <svg width="24" height="24" viewBox="0 0 64 64">
        <defs>
          <linearGradient id="gradient" x1="0%" y1="0%" x2="100%" y2="100%">
            <stop offset="0%" style="stop-color:#666;stop-opacity:1" />
            <stop offset="100%" style="stop-color:#666;stop-opacity:0.8" />
          </linearGradient>
          <filter id="glow">
            <feGaussianBlur stdDeviation="1" result="coloredBlur"/>
            <feMerge>
              <feMergeNode in="coloredBlur"/>
              <feMergeNode in="SourceGraphic"/>
            </feMerge>
          </filter>
        </defs>
        <g id="icon-group" filter="url(#glow)" transform="rotate(165, 32, 32)">
          <circle cx="32" cy="32" r="28" fill="url(#gradient)" stroke="#fff" stroke-width="1"/>
          <circle cx="32" cy="32" r="20" fill="none" stroke="#fff" stroke-width="1"
                  stroke-dasharray="80 40" transform="rotate(-90, 32, 32)">
            <animate attributeName="stroke-dashoffset"
                     dur="4s"
                     values="0;120"
                     repeatCount="indefinite"
                     id="outer-ring-anim"/>
          </circle>
          <circle cx="32" cy="32" r="12" fill="none" stroke="#fff" stroke-width="1">
            <animate attributeName="r"
                     dur="2s"
                     values="12;14;12"
                     repeatCount="indefinite"
                     id="middle-ring-anim"/>
          </circle>
          <circle id="center-dot" cx="32" cy="32" r="4" fill="#fff">
            <animate attributeName="r"
                     dur="1s"
                     values="4;5;4"
                     repeatCount="indefinite"
                     id="center-dot-anim"/>
          </circle>
        </g>
      </svg>
    `;
    document.body.appendChild(collapsedIndicator);

    collapsedIndicator.addEventListener("mouseenter", () => {
      displayBox.style.display = "block";
      requestAnimationFrame(() => {
        displayBox.style.opacity = "1";
        displayBox.style.transform = "translateX(0)";
      });
    });

    displayBox.addEventListener("mouseleave", () => {
      displayBox.style.opacity = "0";
      displayBox.style.transform = "translateX(10px)";
      setTimeout(() => {
        displayBox.style.display = "none";
      }, 150);
    });

    const observer = new MutationObserver(updateTheme);
    observer.observe(document.documentElement, {
      attributes: true,
      attributeFilter: ["class"],
    });

    fetchIPInfo();
    fetchChatGPTStatus();
    updateTheme();
    const statusCheckInterval = 60 * 60 * 1000;
    let statusCheckTimer = setInterval(fetchChatGPTStatus, statusCheckInterval);

    document.addEventListener("visibilitychange", () => {
      if (document.visibilityState === "visible") {
        clearInterval(statusCheckTimer);
        fetchChatGPTStatus();
        statusCheckTimer = setInterval(fetchChatGPTStatus, statusCheckInterval);
      }
    });
  }

  if (document.readyState !== "loading") {
    initUI();
  } else {
    document.addEventListener("DOMContentLoaded", initUI);
  }

  function maskIP(ip) {
    if (!ip || ip === "Unknown") return ip;
    if (ip.includes(".")) {
      const parts = ip.split(".");
      if (parts.length === 4) {
        return `${parts[0]}.*.*.${parts[3]}`;
      }
    }
    if (ip.includes(":")) {
      const parts = ip.split(":");
      // Shorten IPv6 to just show first and last part
      if (parts.length > 2) {
        return `${parts[0]}:*:${parts[parts.length - 1]}`;
      }
    }
    return ip;
  }

  async function fetchIPQuality(ip) {
    try {
      const response = await new Promise((resolve, reject) => {
        GM_xmlhttpRequest({
          method: "GET",
          url: `https://scamalytics.com/ip/${ip}`,
          timeout: 3000,
          onload: (r) =>
            r.status === 200
              ? resolve(r.responseText)
              : reject(new Error(`HTTP ${r.status}`)),
          onerror: reject,
          ontimeout: () => reject(new Error("Request timed out")),
        });
      });
      console.log("fetchIPQuality.response", response);
      const parser = new DOMParser();
      const doc = parser.parseFromString(response, "text/html");
      const scoreElement = doc.querySelector(".score_bar .score");
      const scoreMatch =
        scoreElement?.textContent.match(/Fraud Score:\s*(\d+)/i);
      if (!scoreMatch) {
        return {
          label: "Unknown",
          color: "#aaa",
          tooltip: "Could not determine IP quality",
          score: null,
        };
      }
      const score = parseInt(scoreMatch[1], 10);
      const riskElement = doc.querySelector(".panel_title");
      const riskText = riskElement?.textContent.trim() || "Unknown Risk";
      const panelColor = riskElement?.style.backgroundColor || "#aaa";
      const descriptionElement = doc.querySelector(".panel_body");
      const description = descriptionElement?.textContent.trim() || "";
      const trimmedDescription =
        description.length > 150
          ? `${description.substring(0, 147)}...`
          : description;

      function extractTableValue(header) {
        const row = Array.from(doc.querySelectorAll("th")).find(
          (th) => th.textContent.trim() === header,
        )?.parentElement;
        return row?.querySelector("td")?.textContent.trim() || null;
      }
      function isRiskYes(header) {
        const row = Array.from(doc.querySelectorAll("th")).find(
          (th) => th.textContent.trim() === header,
        )?.parentElement;
        return row?.querySelector(".risk.yes") !== null;
      }
      const details = {
        location: extractTableValue("City") || "Unknown",
        state: extractTableValue("State / Province"),
        country: extractTableValue("Country Name"),
        isp: extractTableValue("ISP Name") || "Unknown",
        organization: extractTableValue("Organization Name"),
        isVPN: isRiskYes("Anonymizing VPN"),
        isTor: isRiskYes("Tor Exit Node"),
        isServer: isRiskYes("Server"),
        isProxy:
          isRiskYes("Public Proxy") ||
          isRiskYes("Web Proxy") ||
          isRiskYes("Proxy"),
      };
      let label, color;
      if (riskText && riskText !== "Unknown Risk") {
        label = riskText;
        color = panelColor !== "#aaa" ? panelColor : getColorForScore(score);
      } else {
        ({ label, color } = getLabelAndColorForScore(score));
      }
      const warnings = [];
      if (details.isVPN) warnings.push("VPN");
      if (details.isTor) warnings.push("Tor");
      if (details.isServer) warnings.push("Server");
      if (details.isProxy) warnings.push("Proxy");
      const location = [details.location, details.state, details.country]
        .filter(Boolean)
        .join(", ");
      const tooltip = [
        "IP Risk Info (Scamlytics):",
        label !== "Unknown" ? `Risk: ${label} (${score}/100)` : "",
        `Location: ${location}`,
        `ISP: ${details.isp}${details.organization ? ` (${details.organization})` : ""}`,
        warnings.length ? `Warnings: ${warnings.join(", ")}` : "",
        trimmedDescription ? `\n${trimmedDescription}` : "",
        "\nClick to view full analysis",
      ]
        .filter(Boolean)
        .join("\n");
      return { label, color, tooltip, score };
    } catch (error) {
      return {
        label: "Unknown",
        color: "#aaa",
        tooltip: "Could not check IP quality",
        score: null,
      };
    }
  }

  function getColorForScore(score) {
    if (score < 25) return "#4CAF50";
    if (score < 50) return "#859F3D";
    if (score < 75) return "#FAB12F";
    return "#e63946";
  }

  function getLabelAndColorForScore(score) {
    if (score < 25)
      return { label: t("riskLevels.veryEasy"), color: "#4CAF50" };
    if (score < 50) return { label: t("riskLevels.easy"), color: "#859F3D" };
    if (score < 75) return { label: t("riskLevels.medium"), color: "#FAB12F" };
    return { label: t("riskLevels.critical"), color: "#e63946" };
  }

  function getIPLogs() {
    try {
      const logs = localStorage.getItem("chatgpt_ip_logs");
      return logs ? JSON.parse(logs) : [];
    } catch (error) {
      console.error("Error reading IP logs:", error);
      return [];
    }
  }

  function addIPLog(ip, score, difficulty) {
    try {
      const logs = getIPLogs();
      const timestamp = new Date().toISOString();
      const newLog = { timestamp, ip, score, difficulty };
      if (logs.length > 0 && logs[0].ip === ip) {
        logs[0] = newLog;
      } else {
        logs.unshift(newLog);
      }
      const trimmedLogs = logs.slice(0, 10);
      localStorage.setItem("chatgpt_ip_logs", JSON.stringify(trimmedLogs));
      return trimmedLogs;
    } catch (error) {
      console.error("Error adding IP log:", error);
      return [];
    }
  }

  function formatIPLogs(logs) {
    return logs
      .map((log) => {
        const date = new Date(log.timestamp);
        const formattedDate = date
          .toLocaleString("en-US", {
            year: "numeric",
            month: "2-digit",
            day: "2-digit",
            hour: "2-digit",
            minute: "2-digit",
            hour12: false,
          })
          .replace(/(\d+)\/(\d+)\/(\d+),\s(\d+):(\d+)/, "[$3-$1-$2 $4:$5]");
        const { color: powColor, level: powLevel } = getRiskColorAndLevel(
          log.difficulty,
        );
        const scoreDisplay =
          log.score !== null && log.score !== undefined ? log.score : "N/A";
        return `${formattedDate} ${log.ip}(${scoreDisplay}), ${log.difficulty || "N/A"}(${powLevel})`;
      })
      .join("\n");
  }

  async function fetchIPInfo() {
    const fallbackServices = [
      {
        url: "https://chatgpt.com/cdn-cgi/trace",
        parser: (text) => {
          const data = text.split("\n").reduce((obj, line) => {
            const [key, value] = line.split("=");
            if (key && value) obj[key.trim()] = value.trim();
            return obj;
          }, {});
          return {
            ip: data.ip,
            warp: data.warp || "off",
            location: data.loc,
            colo: data.colo,
          };
        },
      },
      {
        url: "https://www.cloudflare.com/cdn-cgi/trace",
        parser: (text) => {
          const data = text.split("\n").reduce((obj, line) => {
            const [key, value] = line.split("=");
            if (key && value) obj[key.trim()] = value.trim();
            return obj;
          }, {});
          return {
            ip: data.ip,
            warp: data.warp || "off",
            location: data.loc,
            colo: data.colo,
          };
        },
      },
      {
        url: "https://ipinfo.io/json",
        parser: (text) => {
          const data = JSON.parse(text);
          return {
            ip: data.ip,
            warp: "off", // ipinfo.io doesn't provide WARP status
            location: data.loc,
            city: data.city,
            country: data.country,
          };
        },
      },
    ];

    let lastError = null;

    for (let i = 0; i < fallbackServices.length; i++) {
      const service = fallbackServices[i];
      try {
        console.log(
          `Attempting to fetch IP info from service ${i + 1}:`,
          service.url,
        );

        const response = await new Promise((resolve, reject) => {
          GM_xmlhttpRequest({
            method: "GET",
            url: service.url,
            timeout: 5000,
            onload: (r) => {
              if (r.status === 200) {
                resolve(r.responseText);
              } else {
                reject(new Error(`HTTP ${r.status}: ${r.statusText}`));
              }
            },
            onerror: (err) =>
              reject(
                new Error(`Network error: ${err.message || "Unknown error"}`),
              ),
            ontimeout: () => reject(new Error("Request timed out")),
          });
        });

        console.log(`Service ${i + 1} response:`, response);
        const data = service.parser(response);
        console.log(`Parsed data from service ${i + 1}:`, data);

        if (!data.ip) {
          throw new Error("No IP address found in response");
        }

        await updateIPDisplay(data);
        return; // Success, exit function
      } catch (error) {
        console.error(`Service ${i + 1} failed:`, error.message);
        lastError = error;

        // If not the last service, wait a bit before trying next
        if (i < fallbackServices.length - 1) {
          await new Promise((resolve) => setTimeout(resolve, 1000));
        }
      }
    }

    // All services failed
    console.error("All IP services failed. Last error:", lastError);
    await handleIPFetchFailure(lastError);
  }

  async function updateIPDisplay(data) {
    const ipElement = document.getElementById("ip-address");
    const warpBadge = document.getElementById("warp-badge");
    const ipQualityElement = document.getElementById("ip-quality");

    if (!ipElement || !warpBadge || !ipQualityElement) {
      throw new Error("IP display elements not found");
    }

    const maskedIP = maskIP(data.ip);
    const fullIP = data.ip;
    const warpStatus = data.warp || "off";

    ipElement.innerText = maskedIP;
    ipElement.dataset.fullIp = fullIP;

    // Handle WARP display and warnings
    if (warpStatus === "on" || warpStatus === "plus") {
      warpBadge.style.display = "inline-flex";
      warpBadge.innerText = warpStatus === "plus" ? "warp+" : "warp";
      warpBadge.dataset.tooltip = t(
        `tooltips.${warpStatus === "plus" ? "warpPlus" : "warp"}`,
      );
      warpBadge.style.backgroundColor =
        "var(--success-color, rgba(16, 163, 127, 0.1))";
      warpBadge.style.color = "var(--success-color, #10a37f)";
    } else {
      // Show warning when WARP is not enabled
      warpBadge.style.display = "inline-flex";
      warpBadge.innerText = "no warp";
      warpBadge.dataset.tooltip =
        "⚠️ WARP not enabled - Consider enabling Cloudflare WARP for better privacy and potentially improved IP quality";
      warpBadge.style.backgroundColor =
        "var(--warning-background, rgba(251, 177, 47, 0.1))";
      warpBadge.style.color = "var(--warning-color, #FAB12F)";
    }

    // Fetch IP quality
    try {
      const { label, color, tooltip, score } = await fetchIPQuality(fullIP);
      // console.log("IP Quality result:", { label, color, tooltip, score });

      ipElement.style.color = color;
      ipQualityElement.innerText =
        score !== null ? `${label} (${score})` : label;
      ipQualityElement.style.color = color;
      ipQualityElement.dataset.score = score;
      ipQualityElement.dataset.tooltip = tooltip;

      // Update IP logs
      const difficultyElement = document.getElementById("difficulty");
      const currentDifficulty = difficultyElement?.innerText || "N/A";
      const logs = addIPLog(fullIP, score, currentDifficulty);
      const formattedLogs = formatIPLogs(logs);
      const ipContainerTooltip = [
        t("tooltips.ipHistory"),
        formattedLogs,
        "\n---",
        t("tooltips.clickToCopy"),
      ].join("\n");
      ipElement.dataset.tooltip = ipContainerTooltip;

      // Add click handlers
      ipQualityElement.onclick = () =>
        window.open(`https://scamalytics.com/ip/${fullIP}`, "_blank");

      const copyHandler = async () => {
        try {
          const logs = getIPLogs();
          const formattedHistory = formatIPLogs(logs);
          await navigator.clipboard.writeText(formattedHistory);
          const originalText = ipElement.innerText;
          ipElement.innerText = t("historyCopied");
          setTimeout(() => {
            ipElement.innerText = originalText;
          }, 1000);
        } catch (err) {
          console.error("Copy failed:", err);
          const originalText = ipElement.innerText;
          ipElement.innerText = t("copyFailed");
          setTimeout(() => {
            ipElement.innerText = originalText;
          }, 1000);
        }
      };

      ipElement.removeEventListener("click", copyHandler);
      ipElement.addEventListener("click", copyHandler);
    } catch (qualityError) {
      console.error("Failed to fetch IP quality:", qualityError);
      ipQualityElement.innerText = "Quality check failed";
      ipQualityElement.style.color = "#aaa";
      ipQualityElement.dataset.tooltip = `Could not check IP quality: ${qualityError.message}`;
    }
  }

  async function handleIPFetchFailure(error) {
    console.error("All IP fetch attempts failed:", error);

    const ipElement = document.getElementById("ip-address");
    const warpBadge = document.getElementById("warp-badge");
    const ipQualityElement = document.getElementById("ip-quality");

    if (ipElement) {
      ipElement.innerText = "Failed to fetch";
      ipElement.style.color = "#e63946";
      ipElement.dataset.tooltip = `IP fetch failed: ${error?.message || "Unknown error"}\nTry refreshing the page`;
    }

    if (warpBadge) {
      warpBadge.style.display = "inline-flex";
      warpBadge.innerText = "error";
      warpBadge.dataset.tooltip =
        "Could not determine WARP status due to network error";
      warpBadge.style.backgroundColor =
        "var(--error-background, rgba(230, 57, 70, 0.1))";
      warpBadge.style.color = "var(--error-color, #e63946)";
    }

    if (ipQualityElement) {
      ipQualityElement.innerText = "Network Error";
      ipQualityElement.style.color = "#e63946";
      ipQualityElement.dataset.tooltip =
        "Could not check IP quality due to network error";
    }
  }

  async function fetchChatGPTStatus() {
    try {
      if (typeof GM_xmlhttpRequest === "undefined") {
        throw new Error("GM_xmlhttpRequest not supported");
      }
      return new Promise((resolve, reject) => {
        GM_xmlhttpRequest({
          method: "GET",
          url: "https://status.openai.com/api/v2/status.json",
          timeout: 3000,
          ontimeout: () => reject(new Error("Status check timed out")),
          onload: (response) => {
            if (response.status === 200) {
              try {
                const data = JSON.parse(response.responseText);
                const status = data.status;
                const statusDescription =
                  document.getElementById("status-description");
                const statusMonitorItem =
                  statusDescription?.closest(".monitor-item");
                if (!statusDescription || !statusMonitorItem) {
                  reject(new Error("Status UI elements not found"));
                  return;
                }
                statusMonitorItem.style.display = "block";
                if (status) {
                  const indicator = (status.indicator || "").toLowerCase();
                  const description =
                    status.description || "All Systems Operational";
                  const indicatorColors = {
                    none: "var(--success-color, #10a37f)",
                    minor: "#FAB12F",
                    major: "#FFA500",
                    critical: "#e63946",
                  };
                  if (description === "All Systems Operational") {
                    statusDescription.style.color =
                      "var(--success-color, #10a37f)";
                  } else {
                    statusDescription.style.color =
                      indicatorColors[indicator] || "#aaa";
                  }
                  statusDescription.textContent = description;
                }
                resolve();
              } catch (err) {
                reject(err);
              }
            } else {
              reject(new Error(`HTTP error: ${response.status}`));
            }
          },
          onerror: (err) => reject(err),
        });
      });
    } catch (error) {
      const statusDescription = document.getElementById("status-description");
      const statusMonitorItem = statusDescription?.closest(".monitor-item");
      if (statusMonitorItem) statusMonitorItem.style.display = "none";
    }
  }

  function updateTheme() {
    const isDark =
      document.documentElement.classList.contains("dark") ||
      localStorage.getItem("theme") === "dark" ||
      document.documentElement.dataset.theme === "dark";
    displayBox.style.backgroundColor = isDark
      ? "var(--surface-primary, rgba(0, 0, 0, 0.8))"
      : "var(--surface-primary, rgba(255, 255, 255, 0.9))";
    displayBox.style.color = isDark
      ? "var(--text-primary, #fff)"
      : "var(--text-primary, #000)";
    displayBox.querySelectorAll(".label").forEach((label) => {
      label.style.color = isDark
        ? "var(--text-secondary, #aaa)"
        : "var(--text-secondary, #666)";
    });
  }
})();