CLAUSEAGE

A compact floating widget that displays your Claude AI usage limits in real time on chat pages

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         CLAUSEAGE
// @namespace    https://github.com/Yorkian/CLAUSEAGE
// @version      1.0.0
// @description  A compact floating widget that displays your Claude AI usage limits in real time on chat pages
// @author       York
// @match        https://claude.ai/*
// @icon         https://claude.ai/favicon.ico
// @grant        GM_addStyle
// @license      MIT
// @homepageURL  https://github.com/Yorkian/CLAUSEAGE
// @supportURL   https://github.com/Yorkian/CLAUSEAGE/issues
// @run-at       document-idle
// ==/UserScript==

(function () {
  'use strict';

  // ══════════════════════════════════════════
  // Styles
  // ══════════════════════════════════════════
  GM_addStyle(`
/* ── Claude Usage Widget ── */
#claude-usage-widget {
  position: fixed;
  right: 16px;
  bottom: 16px;
  z-index: 2147483647;
  width: 252px;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  font-size: 12px;
  color: #1a1a1a;
  background: rgba(255, 255, 255, 0.92);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  border: 1px solid rgba(0, 0, 0, 0.08);
  border-radius: 10px;
  box-shadow: 0 4px 24px rgba(0, 0, 0, 0.10), 0 1px 4px rgba(0, 0, 0, 0.06);
  overflow: hidden;
  user-select: none;
  transition: box-shadow 0.2s, opacity 0.2s;
}
#claude-usage-widget.cuw-dragging {
  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.18);
  opacity: 0.92;
}
#claude-usage-widget.cuw-collapsed .cuw-body-wrap,
#claude-usage-widget.cuw-collapsed #cuw-body {
  display: none;
}
.cuw-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 6px 10px;
  cursor: grab;
  border-bottom: 1px solid rgba(0, 0, 0, 0.06);
  background: rgba(0, 0, 0, 0.02);
}
.cuw-header:active { cursor: grabbing; }
.cuw-title {
  font-weight: 600;
  font-size: 11px;
  letter-spacing: 0.3px;
  text-transform: uppercase;
  color: #666;
}
.cuw-plan {
  text-transform: none;
  font-weight: 500;
  opacity: 0.75;
}
.cuw-actions { display: flex; gap: 2px; }
.cuw-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  border: none;
  border-radius: 5px;
  background: transparent;
  color: #888;
  cursor: pointer;
  padding: 0;
  transition: background 0.15s, color 0.15s;
}
.cuw-btn:hover { background: rgba(0, 0, 0, 0.07); color: #333; }
#cuw-refresh:active svg { transform: rotate(-180deg); transition: transform 0.3s ease; }
#cuw-body { padding: 8px 10px 6px; }
.cuw-loading { display: flex; align-items: center; gap: 6px; color: #999; font-size: 11px; padding: 4px 0; }
.cuw-spinner {
  display: inline-block;
  width: 12px;
  height: 12px;
  border: 1.5px solid rgba(0,0,0,0.1);
  border-top-color: #6b8afd;
  border-radius: 50%;
  animation: cuw-spin 0.7s linear infinite;
}
@keyframes cuw-spin { to { transform: rotate(360deg); } }
.cuw-empty { color: #aaa; font-size: 11px; padding: 4px 0; }
.cuw-row { margin-bottom: 6px; }
.cuw-row:last-child, .cuw-row:last-of-type { margin-bottom: 2px; }
.cuw-row-top { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 3px; }
.cuw-label { font-weight: 500; font-size: 11.5px; color: #333; }
.cuw-pct { font-weight: 600; font-size: 11.5px; color: #333; font-variant-numeric: tabular-nums; }
.cuw-bar-track { height: 5px; background: rgba(0, 0, 0, 0.06); border-radius: 3px; overflow: hidden; }
.cuw-bar-fill { height: 100%; border-radius: 3px; transition: width 0.4s ease; }
.cuw-reset { font-size: 10px; color: #999; margin-top: 2px; }
.cuw-balance { color: #22c55e; }
.cuw-footer {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-top: 4px;
  padding-top: 4px;
  border-top: 1px solid rgba(0, 0, 0, 0.04);
}
.cuw-updated { font-size: 10px; color: #bbb; }
.cuw-github { display: flex; align-items: center; color: #ccc; text-decoration: none; transition: color 0.15s; line-height: 1; }
.cuw-github:hover { color: #888; }

/* ── Dark mode ── */
@media (prefers-color-scheme: dark) {
  #claude-usage-widget { background: rgba(32, 33, 36, 0.92); border-color: rgba(255, 255, 255, 0.08); color: #e0e0e0; box-shadow: 0 4px 24px rgba(0, 0, 0, 0.3), 0 1px 4px rgba(0, 0, 0, 0.2); }
  .cuw-header { border-bottom-color: rgba(255, 255, 255, 0.06); background: rgba(255, 255, 255, 0.02); }
  .cuw-title { color: #aaa; }
  .cuw-btn { color: #888; }
  .cuw-btn:hover { background: rgba(255, 255, 255, 0.08); color: #ddd; }
  .cuw-label, .cuw-pct { color: #ddd; }
  .cuw-bar-track { background: rgba(255, 255, 255, 0.08); }
  .cuw-reset { color: #777; }
  .cuw-balance { color: #4ade80; }
  .cuw-footer { border-top-color: rgba(255, 255, 255, 0.06); }
  .cuw-updated { color: #666; }
  .cuw-github { color: #555; }
  .cuw-github:hover { color: #aaa; }
  .cuw-loading { color: #777; }
  .cuw-empty { color: #666; }
}
html[data-theme="dark"] #claude-usage-widget, body.dark #claude-usage-widget, [data-mode="dark"] #claude-usage-widget { background: rgba(32, 33, 36, 0.92); border-color: rgba(255, 255, 255, 0.08); color: #e0e0e0; box-shadow: 0 4px 24px rgba(0, 0, 0, 0.3), 0 1px 4px rgba(0, 0, 0, 0.2); }
html[data-theme="dark"] .cuw-header, body.dark .cuw-header, [data-mode="dark"] .cuw-header { border-bottom-color: rgba(255, 255, 255, 0.06); background: rgba(255, 255, 255, 0.02); }
html[data-theme="dark"] .cuw-title, body.dark .cuw-title, [data-mode="dark"] .cuw-title { color: #aaa; }
html[data-theme="dark"] .cuw-btn, body.dark .cuw-btn, [data-mode="dark"] .cuw-btn { color: #888; }
html[data-theme="dark"] .cuw-label, body.dark .cuw-label, [data-mode="dark"] .cuw-label, html[data-theme="dark"] .cuw-pct, body.dark .cuw-pct, [data-mode="dark"] .cuw-pct { color: #ddd; }
html[data-theme="dark"] .cuw-bar-track, body.dark .cuw-bar-track, [data-mode="dark"] .cuw-bar-track { background: rgba(255, 255, 255, 0.08); }
html[data-theme="dark"] .cuw-reset, body.dark .cuw-reset, [data-mode="dark"] .cuw-reset { color: #777; }
html[data-theme="dark"] .cuw-balance, body.dark .cuw-balance, [data-mode="dark"] .cuw-balance { color: #4ade80; }
html[data-theme="dark"] .cuw-footer, body.dark .cuw-footer, [data-mode="dark"] .cuw-footer { border-top-color: rgba(255, 255, 255, 0.06); }
html[data-theme="dark"] .cuw-updated, body.dark .cuw-updated, [data-mode="dark"] .cuw-updated { color: #666; }
html[data-theme="dark"] .cuw-github, body.dark .cuw-github, [data-mode="dark"] .cuw-github { color: #555; }
html[data-theme="dark"] .cuw-github:hover, body.dark .cuw-github:hover, [data-mode="dark"] .cuw-github:hover { color: #aaa; }
  `);

  // ══════════════════════════════════════════
  // Script
  // ══════════════════════════════════════════

  const WIDGET_ID = 'claude-usage-widget';
  const IFRAME_ID = 'claude-usage-iframe';
  const STORAGE_KEY = 'claude-usage-widget-pos';
  const MARGIN_RIGHT = 16;
  const MARGIN_BOTTOM = 16;
  const POLL_INTERVAL = 500;
  const MAX_POLLS = 40;
  const AUTO_REFRESH_MS = 5 * 60 * 1000;

  let autoRefreshTimer = null;
  let lastRefreshTime = null;
  let updatedTickerTimer = null;
  let wasDragged = false;
  let savedTop = null;

  const GITHUB_SVG = '<svg viewBox="0 0 24 24" width="14" height="14"><path fill="currentColor" d="M12 2A10 10 0 0 0 2 12c0 4.42 2.87 8.17 6.84 9.5c.5.08.66-.23.66-.5v-1.69c-2.77.6-3.36-1.34-3.36-1.34c-.46-1.16-1.11-1.47-1.11-1.47c-.91-.62.07-.6.07-.6c1 .07 1.53 1.03 1.53 1.03c.87 1.52 2.34 1.07 2.91.83c.09-.65.35-1.09.63-1.34c-2.22-.25-4.55-1.11-4.55-4.92c0-1.11.38-2 1.03-2.71c-.1-.25-.45-1.29.1-2.64c0 0 .84-.27 2.75 1.02c.79-.22 1.65-.33 2.5-.33c.85 0 1.71.11 2.5.33c1.91-1.29 2.75-1.02 2.75-1.02c.55 1.35.2 2.39.1 2.64c.65.71 1.03 1.6 1.03 2.71c0 3.82-2.34 4.66-4.57 4.91c.36.31.69.92.69 1.85V21c0 .27.16.59.67.5C19.14 20.16 22 16.42 22 12A10 10 0 0 0 12 2Z"/></svg>';

  // ── SPA Navigation ──
  function isOnChatPage() {
    return location.pathname.startsWith('/chat/');
  }

  function showWidget() {
    var w = document.getElementById(WIDGET_ID);
    if (w) w.style.display = '';
  }

  function hideWidget() {
    var w = document.getElementById(WIDGET_ID);
    if (w) w.style.display = 'none';
  }

  function monitorNavigation() {
    var lastPath = location.pathname;
    var check = function () {
      var curPath = location.pathname;
      if (curPath === lastPath) return;
      lastPath = curPath;
      onRouteChange();
    };
    var origPush = history.pushState;
    history.pushState = function () {
      origPush.apply(this, arguments);
      setTimeout(check, 0);
    };
    var origReplace = history.replaceState;
    history.replaceState = function () {
      origReplace.apply(this, arguments);
      setTimeout(check, 0);
    };
    window.addEventListener('popstate', check);
    setInterval(check, 1000);
  }

  function onRouteChange() {
    if (isOnChatPage()) {
      ensureWidget();
      showWidget();
    } else {
      hideWidget();
    }
  }

  // ── Widget creation ──
  function ensureWidget() {
    if (document.getElementById(WIDGET_ID)) return;
    createWidget();
    fetchUsageData();
    scheduleAutoRefresh();
  }

  function createWidget() {
    var wrap = document.createElement('div');
    wrap.id = WIDGET_ID;
    wrap.innerHTML =
      '<div class="cuw-header" id="cuw-drag-handle">' +
        '<span class="cuw-title">CLAUSEAGE</span>' +
        '<div class="cuw-actions">' +
          '<button class="cuw-btn" id="cuw-refresh" title="Refresh">' +
            '<svg viewBox="0 0 16 16" width="13" height="13" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">' +
              '<path d="M1.5 2v4.5H6"/>' +
              '<path d="M2.2 10.2a6 6 0 1 0 .6-6.7L1.5 6.5"/>' +
            '</svg>' +
          '</button>' +
          '<button class="cuw-btn" id="cuw-minimize" title="Minimize">' +
            '<svg viewBox="0 0 16 16" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">' +
              '<line x1="3" y1="8" x2="13" y2="8"/>' +
            '</svg>' +
          '</button>' +
        '</div>' +
      '</div>' +
      '<div class="cuw-body" id="cuw-body">' +
        '<div class="cuw-loading">Loading…</div>' +
      '</div>';
    document.body.appendChild(wrap);

    try {
      var saved = JSON.parse(localStorage.getItem(STORAGE_KEY));
      if (saved && saved.wasDragged) {
        wasDragged = true;
        savedTop = saved.top;
      }
    } catch (_) {}
    applyPosition(wrap);

    initDrag(wrap, document.getElementById('cuw-drag-handle'));

    window.addEventListener('resize', function () {
      var w = document.getElementById(WIDGET_ID);
      if (w) applyPosition(w);
    });

    var minBtn = document.getElementById('cuw-minimize');
    minBtn.addEventListener('click', function (e) {
      e.stopPropagation();
      var collapsed = wrap.classList.toggle('cuw-collapsed');
      minBtn.title = collapsed ? 'Expand' : 'Minimize';
      minBtn.innerHTML = collapsed
        ? '<svg viewBox="0 0 16 16" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="8" y1="3" x2="8" y2="13"/><line x1="3" y1="8" x2="13" y2="8"/></svg>'
        : '<svg viewBox="0 0 16 16" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="3" y1="8" x2="13" y2="8"/></svg>';
    });

    document.getElementById('cuw-refresh').addEventListener('click', function (e) {
      e.stopPropagation();
      fetchUsageData();
      scheduleAutoRefresh();
    });

    return wrap;
  }

  // ── Position logic ──
  function applyPosition(el) {
    if (!wasDragged) {
      el.style.right = MARGIN_RIGHT + 'px';
      el.style.bottom = MARGIN_BOTTOM + 'px';
      el.style.left = 'auto';
      el.style.top = 'auto';
      return;
    }
    var rect = el.getBoundingClientRect();
    var left = window.innerWidth - rect.width - MARGIN_RIGHT;
    el.style.right = 'auto';
    el.style.bottom = 'auto';
    el.style.left = left + 'px';
    var t = savedTop;
    if (t < 0) t = 0;
    if (t + rect.height > window.innerHeight) t = window.innerHeight - rect.height;
    el.style.top = t + 'px';
  }

  // ── Drag logic ──
  function initDrag(el, handle) {
    var startX, startY, startLeft, startTop, dragging = false;

    handle.addEventListener('mousedown', function (e) {
      if (e.target.closest('.cuw-btn')) return;
      e.preventDefault();
      dragging = true;
      var rect = el.getBoundingClientRect();
      startX = e.clientX;
      startY = e.clientY;
      startLeft = rect.left;
      startTop = rect.top;
      el.style.right = 'auto';
      el.style.bottom = 'auto';
      el.style.left = rect.left + 'px';
      el.style.top = rect.top + 'px';
      el.classList.add('cuw-dragging');
    });

    document.addEventListener('mousemove', function (e) {
      if (!dragging) return;
      el.style.left = (startLeft + (e.clientX - startX)) + 'px';
      el.style.top = (startTop + (e.clientY - startY)) + 'px';
    });

    document.addEventListener('mouseup', function () {
      if (!dragging) return;
      dragging = false;
      el.classList.remove('cuw-dragging');
      clampToViewport(el);
      var rect = el.getBoundingClientRect();
      wasDragged = true;
      savedTop = rect.top;
      localStorage.setItem(STORAGE_KEY, JSON.stringify({ top: savedTop, wasDragged: true }));
      applyPosition(el);
    });
  }

  function clampToViewport(el) {
    var rect = el.getBoundingClientRect();
    var vw = window.innerWidth, vh = window.innerHeight;
    var l = rect.left, t = rect.top;
    if (l < 0) l = 0;
    if (t < 0) t = 0;
    if (l + rect.width > vw) l = vw - rect.width;
    if (t + rect.height > vh) t = vh - rect.height;
    el.style.left = l + 'px';
    el.style.top = t + 'px';
  }

  // ── Parse usage data ──
  function parseUsage(doc) {
    var data = { sections: [], plan: '' };
    var allText = doc.body.innerText;
    if (!allText || allText.includes('Sign in') || allText.includes('Log in')) return null;

    var planWalker = doc.createTreeWalker(doc.body, NodeFilter.SHOW_TEXT, null);
    while (planWalker.nextNode()) {
      var ptxt = planWalker.currentNode.textContent.trim();
      if (/^(Free|Pro|Team|Enterprise|Max)$/i.test(ptxt)) {
        data.plan = ptxt;
        break;
      }
    }

    var currentBalance = null;
    var balWalker = doc.createTreeWalker(doc.body, NodeFilter.SHOW_TEXT, null);
    while (balWalker.nextNode()) {
      if (/current balance/i.test(balWalker.currentNode.textContent.trim())) {
        var balEl = balWalker.currentNode.parentElement;
        for (var d = 0; d < 5 && balEl; d++) {
          var balText = balEl.innerText || '';
          var balMatch = balText.match(/\$\s*([\d,]+(?:\.\d{1,2})?)/);
          if (balMatch) {
            currentBalance = parseFloat(balMatch[1].replace(/,/g, ''));
            break;
          }
          balEl = balEl.parentElement;
        }
        break;
      }
    }

    var hideExtraUsage = (currentBalance === null || currentBalance === 0);

    var walker = doc.createTreeWalker(doc.body, NodeFilter.SHOW_TEXT, null);
    var usageTexts = [];
    while (walker.nextNode()) {
      if (walker.currentNode.textContent.trim().includes('% used')) usageTexts.push(walker.currentNode);
    }

    usageTexts.forEach(function (node) {
      var pctMatch = node.textContent.match(/(\d+)%\s*used/);
      if (!pctMatch) return;
      var pct = parseInt(pctMatch[1], 10);
      var container = findAncestorBlock(node);
      if (!container) return;
      var blockText = container.innerText || '';
      var lines = blockText.split('\n').map(function (l) { return l.trim(); }).filter(Boolean);

      var label = '', resetInfo = '';
      lines.forEach(function (line) {
        if (/current session|all models/i.test(line)) label = line;
        if (/resets?\s/i.test(line)) resetInfo = line;
      });
      if (!label) {
        label = lines.find(function (l) { return !l.includes('% used') && !l.includes('Resets') && !l.includes('Learn more'); }) || 'Unknown';
      }

      if (/\$/.test(label) && hideExtraUsage) return;

      data.sections.push({ type: 'pct', label: label, pct: pct, resetInfo: resetInfo });
    });

    return data.sections.length > 0 ? data : null;
  }

  function findAncestorBlock(node) {
    var el = node.parentElement;
    var depth = 0;
    while (el && depth < 15) {
      var text = el.innerText || '';
      if ((text.includes('Resets') || text.includes('Reset')) && text.includes('% used')) return el;
      el = el.parentElement;
      depth++;
    }
    return node.parentElement && node.parentElement.parentElement && node.parentElement.parentElement.parentElement || null;
  }

  // ── Relative time + ticker ──
  function getRelativeTimeText() {
    if (!lastRefreshTime) return '';
    var elapsed = Math.floor((Date.now() - lastRefreshTime) / 1000);
    if (elapsed < 30)  return 'Updated: just now';
    if (elapsed < 60)  return 'Updated: half a minute ago';
    if (elapsed < 120) return 'Updated: a minute ago';
    if (elapsed < 180) return 'Updated: 2 minutes ago';
    if (elapsed < 240) return 'Updated: 3 minutes ago';
    if (elapsed < 290) return 'Updated: 4 minutes ago';
    return 'Refreshing soon';
  }

  function stopUpdatedTicker() {
    if (updatedTickerTimer) { clearInterval(updatedTickerTimer); updatedTickerTimer = null; }
  }

  function startUpdatedTicker() {
    stopUpdatedTicker();
    updatedTickerTimer = setInterval(function () {
      var el = document.getElementById('cuw-updated-line');
      if (el) el.textContent = getRelativeTimeText();
    }, 1000);
  }

  // ── Render data ──
  function renderData(data) {
    var body = document.getElementById('cuw-body');
    if (!data) {
      body.innerHTML = '<div class="cuw-empty">Unable to load</div>';
      return;
    }

    lastRefreshTime = Date.now();

    var titleEl = document.querySelector('#' + WIDGET_ID + ' .cuw-title');
    if (titleEl) {
      if (data.plan) {
        titleEl.innerHTML = 'CLAUSEAGE <span class="cuw-plan">| ' + escapeHtml(data.plan) + '</span>';
      } else {
        titleEl.textContent = 'CLAUSEAGE';
      }
    }

    var html = '';
    data.sections.forEach(function (s) {
      var barColor = s.pct >= 90 ? '#ef4444' : s.pct >= 70 ? '#f59e0b' : '#6b8afd';
      html +=
        '<div class="cuw-row">' +
          '<div class="cuw-row-top">' +
            '<span class="cuw-label">' + escapeHtml(s.label) + '</span>' +
            '<span class="cuw-pct">' + s.pct + '%</span>' +
          '</div>' +
          '<div class="cuw-bar-track">' +
            '<div class="cuw-bar-fill" style="width:' + s.pct + '%;background:' + barColor + '"></div>' +
          '</div>' +
          (s.resetInfo ? '<div class="cuw-reset">' + escapeHtml(s.resetInfo) + '</div>' : '') +
        '</div>';
    });

    html +=
      '<div class="cuw-footer">' +
        '<span class="cuw-updated" id="cuw-updated-line">' + getRelativeTimeText() + '</span>' +
        '<a class="cuw-github" href="https://github.com/Yorkian/CLAUSEAGE" target="_blank" title="GitHub">' + GITHUB_SVG + '</a>' +
      '</div>';

    body.innerHTML = html;
    startUpdatedTicker();
  }

  function escapeHtml(s) {
    var d = document.createElement('div');
    d.textContent = s;
    return d.innerHTML;
  }

  // ── Fetch data via hidden iframe ──
  function fetchUsageData() {
    stopUpdatedTicker();

    var body = document.getElementById('cuw-body');
    if (body) body.innerHTML = '<div class="cuw-loading"><span class="cuw-spinner"></span> Loading…</div>';

    var old = document.getElementById(IFRAME_ID);
    if (old) old.remove();

    var iframe = document.createElement('iframe');
    iframe.id = IFRAME_ID;
    iframe.src = 'https://claude.ai/settings/usage';
    iframe.style.cssText = 'position:fixed;width:0;height:0;border:none;opacity:0;pointer-events:none;top:-9999px;left:-9999px;';
    document.body.appendChild(iframe);

    var polls = 0;
    var timer = setInterval(function () {
      polls++;
      try {
        var doc = iframe.contentDocument || (iframe.contentWindow && iframe.contentWindow.document);
        if (!doc || !doc.body) {
          if (polls >= MAX_POLLS) { clearInterval(timer); iframe.remove(); renderData(null); }
          return;
        }

        var currentUrl = (iframe.contentWindow && iframe.contentWindow.location && iframe.contentWindow.location.href) || '';
        if (currentUrl.includes('/login') || currentUrl.includes('/signin')) {
          clearInterval(timer); iframe.remove(); hideWidget(); return;
        }

        var text = doc.body.innerText || '';

        if (text.includes('% used')) {
          clearInterval(timer);
          var data = parseUsage(doc);
          renderData(data);
          iframe.remove();
          return;
        }

        if (text.includes('Sign in') || text.includes('Log in') || text.includes('Continue with')) {
          clearInterval(timer); iframe.remove(); hideWidget(); return;
        }

      } catch (e) {
        if (e.name === 'SecurityError' || (e.message && e.message.includes('cross-origin'))) {
          clearInterval(timer); iframe.remove(); hideWidget(); return;
        }
      }

      if (polls >= MAX_POLLS) { clearInterval(timer); iframe.remove(); renderData(null); }
    }, POLL_INTERVAL);
  }

  // ── Auto-refresh ──
  function scheduleAutoRefresh() {
    if (autoRefreshTimer) clearInterval(autoRefreshTimer);
    autoRefreshTimer = setInterval(function () {
      fetchUsageData();
    }, AUTO_REFRESH_MS);
  }

  // ── Init ──
  monitorNavigation();
  if (isOnChatPage()) {
    ensureWidget();
  }

})();