Cursor.com Usage Tracker

Enhanced usage statistics and analytics for Cursor.com's new frontend, providing detailed insights into usage patterns and billing cycles.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Cursor.com Usage Tracker
// @author       monnef, v2 by Sonnet 4.0, original by Sonnet 3.5 (via Perplexity and Cursor), some help from Cursor Tab and Cursor Small
// @namespace    http://monnef.eu
// @version      2.0
// @description  Enhanced usage statistics and analytics for Cursor.com's new frontend, providing detailed insights into usage patterns and billing cycles.
// @match        https://www.cursor.com/*
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_xmlhttpRequest
// @connect      unpkg.com
// @require      https://code.jquery.com/jquery-3.6.0.min.js
// @license      AGPL-3.0
// ==/UserScript==

/* CHANGES:
 * 2.0:
 * - Complete rewrite for new Cursor frontend
 * - Auto-extract usage data and reset dates
 * - New card design matching Cursor's updated UI
 * 0.4:
 * - Improved error handling for icon loads
 * 0.3:
 * - Updated to CSP changes causing icons to fail to load
 */

(function () {
  'use strict';

  const $ = jQuery.noConflict();

  const $c = (cls, parent) => $(`.${cls}`, parent);
  const $i = (id, parent) => $(`#${id}`, parent);

  $.fn.nthParent = function (n) {
    return this.parents().eq(n - 1);
  };

  const log = (...messages) => {
    console.log(`[UsageTracker v2]`, ...messages);
  };

  const error = (...messages) => {
    console.error(`[UsageTracker v2]`, ...messages);
  };

  const genCssId = name => `ut-${name}`;

  const sigCls = genCssId('sig');
  const trackerCardCls = genCssId('tracker-card');
  const donationModalCls = genCssId('donation-modal');
  const settingsModalCls = genCssId('settings-modal');
  const modalCls = genCssId('modal');
  const modalContentCls = genCssId('modal-content');
  const modalCloseCls = genCssId('modal-close');
  const copyButtonCls = genCssId('copy-button');
  const inputCls = genCssId('input');
  const inputWithButtonCls = genCssId('input-with-button');
  const buttonCls = genCssId('button');
  const buttonWhiteCls = genCssId('button-white');
  const errorMessageCls = genCssId('error-message');
  const hrCls = genCssId('hr');
  const highlightValueCls = genCssId('highlight-value');
  const progressBarContainerCls = genCssId('progress-bar-container');
  const progressBarMainCls = genCssId('progress-bar-main');
  const progressBarFillCls = genCssId('progress-bar-fill');
  const progressBarTrackCls = genCssId('progress-bar-track');
  const progressOverflowContainerCls = genCssId('progress-overflow-container');
  const progressOverflowBoxCls = genCssId('progress-overflow-box');

  const colors = Object.freeze({
    cursor: {
      bg: '#16181c',
      cardBg: '#1d1f22',
      text: '#fff', // but opacity 0.8
      textBrandGray300: '#666',
      barColor: '#81A1C1', // bar secondary color is the same as bar color, but opacity 0.1
      blue: '#3864f6',
      blueDarker: '#2e53cc',
      lightGray: '#e5e7eb',
      gray: '#a7a9ac',
      grayDark: '#333333',
      buttonBg: '#242629',
      buttonBorder: '#3a3a3a',
      buttonHover: '#2a2a2d',
    }
  });

  const styles = `
    .${modalCls} {
      display: none;
      position: fixed;
      z-index: 1000;
      left: 0;
      top: 0;
      width: 100%;
      height: 100%;
      overflow: auto;
      background-color: rgba(0, 0, 0, 0.4);
      backdrop-filter: blur(5px) contrast(0.5);
    }

    .${modalContentCls} {
      background-color: ${colors.cursor.bg};
      color: ${colors.cursor.text};
      margin: 15% auto;
      padding: 15px 20px;
      width: 600px;
      border-radius: 12px;
      position: relative;
      border: 1px solid ${colors.cursor.buttonBorder};
    }

    .${modalCloseCls} {
      color: ${colors.cursor.text};
      position: absolute;
      top: 0px;
      right: 10px;
      font-size: 25px;
      font-weight: bold;
      cursor: pointer;
      opacity: 0.7;
      transition: opacity 0.2s;
    }

    .${modalCloseCls}:hover {
      opacity: 1;
    }

    .${copyButtonCls} {
      background-color: ${colors.cursor.buttonBg};
      color: ${colors.cursor.text};
      border: 1px solid ${colors.cursor.buttonBorder};
      padding: 0;
      display: flex;
      align-items: center;
      justify-content: center;
      height: 40px;
      border-radius: 8px;
      cursor: pointer;
      font-size: 14px;
      font-weight: 500;
      transition: all 0.2s;
      margin-left: 10px;
      width: 3em;
    }

    .${copyButtonCls}:hover {
      background-color: ${colors.cursor.buttonHover};
    }

    .${buttonCls} {
      background-color: ${colors.cursor.buttonBg};
      color: ${colors.cursor.text};
      border: 1px solid ${colors.cursor.buttonBorder};
      padding: 8px 16px;
      border-radius: 8px;
      cursor: pointer;
      font-size: 14px;
      font-weight: 500;
      transition: all 0.2s;
    }

    .${buttonCls}:hover {
      background-color: ${colors.cursor.buttonHover};
    }

    .${buttonWhiteCls} {
      background-color: ${colors.cursor.buttonBg};
      color: ${colors.cursor.text};
      border: none;
      padding: 6px 10px;
      border-radius: 6px;
      cursor: pointer;
      font-size: 12px;
      font-weight: 400;
      transition: all 0.2s;
    }

    .${buttonWhiteCls}:hover {
      background-color: ${colors.cursor.buttonHover};
    }

    .${inputCls} {
      background-color: ${colors.cursor.cardBg};
      color: ${colors.cursor.text};
      border: 1px solid ${colors.cursor.buttonBorder};
      padding: 8px 12px;
      width: 100%;
      border-radius: 8px;
      font-size: 14px;
      height: 40px;
    }

    .${inputWithButtonCls} {
      width: calc(100% - 2em - 10px);
    }

    .${errorMessageCls} {
      color: #ff4d4f;
      font-size: 14px;
      margin-top: 5px;
    }

    .${hrCls} {
      border: 0;
      height: 1px;
      background-color: ${colors.cursor.buttonBorder};
      margin: 10px 0;
    }

    .${sigCls} {
      opacity: 0.3;
      transition: opacity 0.2s ease;
      cursor: pointer;
      font-size: 12px;
      margin-left: 8px;
    }

    .${sigCls}:hover {
      opacity: 0.8;
    }

    .${modalContentCls} h2 {
      margin-bottom: 20px;
    }

    .${modalContentCls} p {
      margin-bottom: 15px;
    }

    .${highlightValueCls} {
      font-weight: 600;
      opacity: 0.5;
      color: ${colors.cursor.text};
    }

    /* Progress Bar Classes */
    .${progressBarContainerCls} {
      display: flex;
      align-items: center;
      gap: 1px;
      width: 100%;
      height: 4px;
      padding: 8px 0;
    }

    .${progressBarMainCls} {
      flex-grow: 1;
      display: flex;
      gap: 1px;
      height: 4px;
    }

    .${progressBarFillCls} {
      height: 4px;
      /* background-color and width set via inline styles */
    }

    .${progressBarTrackCls} {
      height: 4px;
      flex-grow: 1;
      opacity: 0.1;
      /* background-color set via inline styles */
    }

    .${progressOverflowContainerCls} {
      position: relative;
      width: 0;
      height: 4px;
    }

    .${progressOverflowBoxCls} {
      position: absolute;
      height: 4px;
      margin-right: 2px;
      /* background-color, width, and right position set via inline styles */
    }
  `;

  const bitcoinAddress = 'bc1qr7crhydmp68qpa0gumuf2h6jcvdtta4wju49r7';
  const bitcoinPaymentLink = `bitcoin:${bitcoinAddress}`;

  const iconCache = {};

  const createLucideIcon = ({ iconName, size = '16px', invert = false }) => {
    const src = `https://unpkg.com/lucide-static@latest/icons/${iconName}.svg`;
    const img = $('<img>')
      .css({
        width: size,
        height: size,
        display: 'inline-block',
        verticalAlign: 'text-bottom',
        filter: invert ? 'invert(1)' : 'none'
      });

    const cleanupFailedIcon = (reason) => {
      img.remove();
      error(`Failed to load icon: ${iconName}. Reason: ${reason}`);
    };

    if (iconCache[iconName]) {
      img.attr('src', iconCache[iconName]);
      return img;
    }

    GM_xmlhttpRequest({
      method: 'GET',
      url: src,
      onload: function (response) {
        if (response.status >= 200 && response.status < 300) {
          const svg = response.responseText;
          const dataUrl = 'data:image/svg+xml;base64,' + btoa(svg);
          img.attr('src', dataUrl);
          iconCache[iconName] = dataUrl;
        } else {
          cleanupFailedIcon(`HTTP status ${response.status}`);
        }
      },
      onerror: function (error) {
        cleanupFailedIcon(`Network error: ${error.message}`);
      },
      ontimeout: function () {
        cleanupFailedIcon('Request timed out');
      }
    });

    return img;
  };

  const getUsageCard = () => {
    return $('div:contains("Included Requests")').closest('.rounded-xl').first();
  };

  const extractUsageData = () => {
    const usageCard = getUsageCard();
    if (!usageCard.length) {
      log('Usage card not found');
      return null;
    }

    // Extract current usage (e.g., "15")
    const usageSpan = usageCard.find('span').filter(function () {
      return /^\d+$/.test($(this).text().trim());
    }).first();

    // Extract total usage (e.g., "/ 500")
    const totalSpan = usageCard.find('span').filter(function () {
      return /^\/\s*\d+$/.test($(this).text().trim());
    }).first();

    // Extract reset date
    const resetText = usageCard.find('p:contains("Last reset on:")').text();
    const resetMatch = resetText.match(/Last reset on:\s*(.+)/);

    const used = usageSpan.length ? parseInt(usageSpan.text().trim()) : 0;
    const total = totalSpan.length ? parseInt(totalSpan.text().replace(/[^\d]/g, '')) : 500;
    const resetDate = resetMatch ? resetMatch[1].trim() : null;

    log(`Extracted usage data: ${used}/${total}, Reset: ${resetDate}`);
    return { used, total, resetDate };
  };

  const getManualBillingDay = () => {
    return GM_getValue('paymentDay', '');
  };

  const setManualBillingDay = (day) => {
    GM_setValue('paymentDay', day);
  };

  const calculateDaysSinceReset = (resetDateStr) => {
    // Check for manual override first - handle non-string values safely
    const manualDay = getManualBillingDay();
    let resetDate;

    // Safely check if manualDay is a valid string with content
    const manualDayStr = typeof manualDay === 'string' ? manualDay.trim() : String(manualDay || '').trim();

    if (manualDayStr && manualDayStr !== '' && !isNaN(manualDayStr)) {
      // Use manual billing day
      const today = new Date();
      const billingDay = parseInt(manualDayStr, 10);

      if (billingDay >= 1 && billingDay <= 31) {
        resetDate = new Date(today.getFullYear(), today.getMonth(), billingDay);

        // If billing day hasn't occurred this month, use last month
        if (resetDate > today) {
          resetDate.setMonth(resetDate.getMonth() - 1);
        }

        log(`Using manual billing day: ${billingDay}, calculated reset date: ${resetDate}`);
      } else {
        log(`Invalid manual billing day: ${billingDay}, falling back to extracted date`);
      }
    }

    // If no valid manual date or manual date failed, use page-extracted reset date
    if (!resetDate && resetDateStr) {
      try {
        resetDate = new Date(resetDateStr);
        log(`Using extracted reset date: ${resetDate}`);
      } catch (e) {
        error('Failed to parse reset date:', e);
        return null;
      }
    }

    if (!resetDate) {
      log('No reset date available (manual or extracted)');
      return null;
    }

    const today = new Date();
    const diffTime = today - resetDate;
    const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24));

    // Estimate next reset (assuming monthly billing)
    const nextReset = new Date(resetDate);
    nextReset.setMonth(nextReset.getMonth() + 1);
    const totalDays = Math.floor((nextReset - resetDate) / (1000 * 60 * 60 * 24));
    const remainingDays = Math.max(0, totalDays - diffDays);

    return { daysPassed: diffDays, totalDays, remainingDays, resetDate, nextReset };
  };

  const createProgressBar = (percentage, color = colors.cursor.barColor) => {
    const actualPercentage = Math.min(percentage, 100); // Cap main bar at 100%
    
    const container = $('<div>').addClass(progressBarContainerCls);

    // Main progress bar (always stays within its bounds)
    const mainBarContainer = $('<div>')
      .addClass(progressBarMainCls)
      .append(
        $('<div>')
          .addClass(progressBarFillCls)
          .css({
            'background-color': color,
            'width': `${actualPercentage}%`
          })
      )
      .append(
        $('<div>')
          .addClass(progressBarTrackCls)
          .css('background-color', color)
      );

    container.append(mainBarContainer);

    // Add overflow boxes if over 100%
    if (percentage > 100) {
      const overflowContainer = $('<div>').addClass(progressOverflowContainerCls);

      // overflow boxes using Array.from to avoid for loop
      Array.from({ length: 2 }, (_, i) =>
        $('<div>')
          .addClass(progressOverflowBoxCls)
          .css({
            'background-color': color,
            'right': `-${(i + 1) * 8 + 4}px`,
            'width': `${8 / (i + 1)}px`
          })
      ).forEach(box => overflowContainer.append(box));

      container.append(overflowContainer);
    }

    return container;
  };

  // Helper function for highlighting values in text
  const createStyledText = (parts) => {
    const container = $('<span>');

    parts.forEach(part => {
      if (typeof part === 'string') {
        container.append(document.createTextNode(part));
      } else if (part.value !== undefined) {
        const valueSpan = $('<span>')
          .addClass(highlightValueCls)
          .text(part.value);
        container.append(valueSpan);
      }
    });

    return container;
  };

  // Helper function for proper pluralization
  const pluralize = (count, singular, plural = null) => {
    const num = parseFloat(count);
    if (num === 1) {
      return singular;
    }
    return plural || singular + 's';
  };

  const createStatsColumn = ({
    mainValue,
    secondaryValue = null,
    progressPercent,
    progressColor,
    title,
    description
  }) => {
    const mainNumberDiv = $('<div>')
      .addClass('flex items-baseline gap-1.5 w-full min-w-0 overflow-hidden')
      .append(
        $('<span>')
          .addClass('[&_b]:md:font-semibold [&_strong]:md:font-semibold text-xl sm:text-2xl leading-tight tracking-tight font-medium flex-shrink-0')
          .text(mainValue)
      );

    // Add secondary value if provided (like "/ 500" or "avg/day")
    if (secondaryValue) {
      mainNumberDiv.append(
        $('<span>')
          .addClass('[&_b]:md:font-semibold [&_strong]:md:font-semibold text-lg sm:text-xl leading-tight tracking-tight font-medium opacity-30 truncate')
          .text(secondaryValue)
      );
    }

    const descriptionElement = $('<p>')
      .addClass('[&_b]:md:font-semibold [&_strong]:md:font-semibold tracking-4 md:text-sm/[1.25rem] text-xs sm:text-sm leading-snug text-brand-gray-300');

    // Handle both string and jQuery element descriptions
    if (typeof description === 'string') {
      descriptionElement.text(description);
    } else {
      descriptionElement.append(description);
    }

    return $('<div>')
      .addClass('flex flex-col gap-4 p-2')
      .append(
        $('<div>')
          .addClass('flex flex-col gap-2 w-full min-w-0')
          .append(mainNumberDiv)
          .append(createProgressBar(progressPercent, progressColor))
          .append(
            $('<span>')
              .addClass('[&_b]:md:font-semibold [&_strong]:md:font-semibold text-sm sm:text-base leading-tight tracking-tight font-medium opacity-80')
              .text(title)
          )
      )
      .append(descriptionElement);
  };

  const createTrackerCard = (usageData, daysInfo) => {
    const { used, total, resetDate } = usageData;
    const usagePercent = (used / total * 100).toFixed(1);

    const header = $('<div>')
      .addClass('flex items-center gap-2 mb-2 ml-2')
      .append(
        $('<h3>')
          .addClass('[&_b]:md:font-semibold [&_strong]:md:font-semibold text-lg sm:text-xl leading-tight tracking-tight font-medium')
          .text('Usage Tracker')
      )
      .append(
        $('<span>')
          .addClass(sigCls)
          .attr('title', 'Enjoying this script? Consider a small donation.')
          .text('by monnef')
      )
      .append(
        $('<button>')
          .addClass(buttonWhiteCls)
          .css({
            'margin-left': 'auto',
            'padding': '6px',
            'width': '32px',
            'height': '32px',
            'display': 'flex',
            'align-items': 'center',
            'justify-content': 'center'
          })
          .attr('title', 'Settings of Usage Tracker UserScript')
          .append(createLucideIcon({ iconName: 'settings', size: '24px', invert: true }))
          .on('click', () => $(`.${settingsModalCls}`).show())
      );

    const gridContainer = $('<div>').addClass('grid grid-cols-1 gap-6 lg:grid-cols-3');

    if (daysInfo) {
      const { daysPassed, totalDays, remainingDays } = daysInfo;
      const daysPercent = (daysPassed / totalDays * 100).toFixed(1);
      const avgPerDay = daysPassed > 0 ? (used / daysPassed).toFixed(1) : '0.0';

      // Calculate theoretical daily allowance based on standard month (30 days)
      const daysInMonth = 30; // Standard month for calculation
      const theoreticalDailyAllowance = total / daysInMonth;
      const avgPerDayPercent = Math.min((avgPerDay / theoreticalDailyAllowance) * 100, 100);

      const optimalUsesPerDay = total / totalDays; // Keep this for the last column

      // Fix the logic for the last column
      const remainingUses = total - used;
      const remainingUsesPerDay = remainingDays > 0 ? remainingUses / remainingDays : 0;
      const remainingUsesPercent = (remainingUsesPerDay / optimalUsesPerDay) * 100;

      // Create Days Elapsed column with styled description
      const daysColumn = createStatsColumn({
        mainValue: daysPassed,
        secondaryValue: `/ ${totalDays}`,
        progressPercent: daysPercent,
        progressColor: '#A3BE8C',
        title: pluralize(daysPassed, 'Day', 'Days') + ' Elapsed',
        description: createStyledText([
          { value: remainingDays },
          ' ',
          pluralize(remainingDays, 'day'),
          ' remaining in current cycle.'
        ])
      });

      // Premium Requests/Day column - using totalUsesPerMonth / daysInMonth
      const usageColumn = createStatsColumn({
        mainValue: avgPerDay,
        secondaryValue: `/ ${theoreticalDailyAllowance.toFixed(1)}`,
        progressPercent: avgPerDayPercent,
        progressColor: colors.cursor.barColor,
        title: 'Premium Requests/Day',
        description: createStyledText([
          'Current average: ',
          { value: avgPerDay },
          ' requests per day since cycle started.'
        ])
      });

      // Create Remaining Uses/Day column - fixed logic
      const metricsColumn = createStatsColumn({
        mainValue: remainingUsesPerDay.toFixed(1),
        secondaryValue: `/ ${optimalUsesPerDay.toFixed(1)}`,
        progressPercent: remainingUsesPercent,
        progressColor: '#D08770',
        title: 'Remaining Uses/Day',
        description: createStyledText([
          'You have ',
          { value: remainingUsesPerDay.toFixed(1) },
          ' uses per day available for the remaining ',
          { value: remainingDays },
          ' ',
          pluralize(remainingDays, 'day'),
          '.'
        ])
      });

      gridContainer
        .append(daysColumn)
        .append(usageColumn)
        .append(metricsColumn);
    }

    const card = $('<div>')
      .addClass('rounded-xl border-brand-neutrals-100 text-brand-foreground dark:border-brand-neutrals-800 dark:shadow-none shadow-[0px_51px_20px_rgba(186,186,186,0.01),0px_29px_17px_rgba(186,186,186,0.05),0px_13px_13px_rgba(186,186,186,0.09),0px_3px_7px_rgba(186,186,186,0.1)] border-0 bg-brand-dashboard-card p-6 dark:bg-brand-dashboard-card')
      .addClass(trackerCardCls)
      .append(header)
      .append(gridContainer);

    return card;
  };

  const createSettingsModal = () => {
    const modal = $('<div>')
      .addClass(modalCls)
      .addClass(settingsModalCls);

    const modalContent = $('<div>')
      .addClass(modalContentCls);

    const closeBtn = $('<span>')
      .addClass(modalCloseCls)
      .text('×')
      .on('click', () => {
        // Clear error message when closing
        errorMessage.hide();
        modal.hide();
      });

    const title = $('<h2>')
      .css({
        'display': 'flex',
        'align-items': 'center',
        'gap': '8px',
        'margin-bottom': '20px'
      })
      .append($('<span>').text('Settings'))
      .append(createLucideIcon({ iconName: 'settings', size: '32px', invert: true }));

    const description = $('<p>').text('Enter the day of the month when you are billed (1-31):');

    const input = $('<input>')
      .addClass(inputCls)
      .attr({
        'type': 'number',
        'min': '1',
        'max': '31'
      })
      .val(getManualBillingDay() || '');

    const tip = $('<p>')
      .addClass('text-sm text-gray-500 mt-1')
      .text('You can find your billing date via the "Manage Subscription" button on the left.');

    const errorMessage = $('<div>')
      .addClass(errorMessageCls)
      .hide();

    const saveButton = $('<button>')
      .addClass(buttonCls)
      .text('Save')
      .css('margin-right', '10px')
      .on('click', () => {
        const newPaymentDay = parseInt(input.val(), 10);
        if (newPaymentDay && newPaymentDay >= 1 && newPaymentDay <= 31) {
          setManualBillingDay(newPaymentDay);
          log(`Payment day has been set to: ${newPaymentDay}`);
          modal.hide();
          setTimeout(init, 100);
        } else {
          errorMessage.text('Invalid input. Please enter a number between 1 and 31.').show();
        }
      });

    const clearButton = $('<button>')
      .addClass(buttonCls)
      .text('Clear')
      .on('click', () => {
        setManualBillingDay('');
        log('Payment day cleared');
        modal.hide();
        setTimeout(init, 100);
      });

    modalContent
      .append(closeBtn)
      .append(title)
      .append(description)
      .append(input)
      .append(tip)
      .append(errorMessage)
      .append($('<br>'))
      .append(saveButton)
      .append(clearButton);

    modal.append(modalContent);

    // Clear error message when clicking outside
    modal.on('click', (e) => {
      if (e.target === modal[0]) {
        errorMessage.hide();
        modal.hide();
      }
    });

    return modal;
  };

  const createDonationModal = () => {
    const modal = $('<div>')
      .addClass(modalCls)
      .addClass(donationModalCls);

    const modalContent = $('<div>')
      .addClass(modalContentCls);

    const closeBtn = $('<span>')
      .addClass(modalCloseCls)
      .text('×')
      .on('click', () => modal.hide());

    const title = $('<h2>')
      .css({
        'display': 'flex',
        'align-items': 'center',
        'gap': '8px'
      })
      .append($('<span>').text('Donate').addClass('text-2xl'))
      .append(createLucideIcon({ iconName: 'heart-handshake', size: '32px', invert: true }));

    const description = $('<p>')
      .css('opacity', '0.8')
      .text('Thank you for considering a donation! Your support helps maintain and improve this script.');

    const hr = $('<hr>').addClass(hrCls);

    const bitcoinLabel = $('<p>')
      .css({
        'margin': '15px 0 10px 0',
        'font-weight': '500'
      })
      .text('Bitcoin Address:');

    const bitcoinContainer = $('<div>')
      .css({
        'display': 'flex',
        'align-items': 'center'
      });

    const bitcoinInput = $('<input>')
      .addClass(inputCls)
      .addClass(inputWithButtonCls)
      .attr({
        'type': 'text',
        'value': bitcoinAddress,
        'readonly': true
      });

    const bitcoinCopyBtn = $('<button>')
      .addClass(copyButtonCls)
      .attr('data-copy', bitcoinAddress)
      .append(createLucideIcon({ iconName: 'copy', size: '24px', invert: true }));

    bitcoinContainer.append(bitcoinInput).append(bitcoinCopyBtn);

    const paymentLabel = $('<p>')
      .css({
        'margin': '15px 0 10px 0',
        'font-weight': '500'
      })
      .text('Payment Link:');

    const paymentContainer = $('<div>')
      .css({
        'display': 'flex',
        'align-items': 'center'
      });

    const paymentInput = $('<input>')
      .addClass(inputCls)
      .addClass(inputWithButtonCls)
      .attr({
        'type': 'text',
        'value': bitcoinPaymentLink,
        'readonly': true
      });

    const paymentCopyBtn = $('<button>')
      .addClass(copyButtonCls)
      .attr('data-copy', bitcoinPaymentLink)
      .append(createLucideIcon({ iconName: 'copy', size: '24px', invert: true }));

    paymentContainer.append(paymentInput).append(paymentCopyBtn);

    modalContent
      .append(closeBtn)
      .append(title)
      .append(description)
      .append(hr)
      .append(bitcoinLabel)
      .append(bitcoinContainer)
      .append(paymentLabel)
      .append(paymentContainer);

    modal.append(modalContent);

    // Copy functionality - update to handle icon buttons
    modal.find(`.${copyButtonCls}`).on('click', async function () {
      const button = $(this);
      const textToCopy = button.attr('data-copy');
      const originalContent = button.html();

      try {
        await navigator.clipboard.writeText(textToCopy);
        button.html(createLucideIcon({ iconName: 'check', size: '16px', invert: true })[0].outerHTML);
        setTimeout(() => button.html(originalContent), 2000);
      } catch (err) {
        error('Clipboard write failed:', err);
        button.html(createLucideIcon({ iconName: 'x', size: '16px', invert: true })[0].outerHTML);
        setTimeout(() => button.html(originalContent), 2000);
      }
    });

    // Close on outside click
    modal.on('click', (e) => {
      if (e.target === modal[0]) modal.hide();
    });

    return modal;
  };

  const init = () => {
    // Only run on dashboard page
    if (!window.location.pathname.includes('/dashboard')) {
      log('Not on dashboard page, retrying in 1 second...');
      setTimeout(init, 1000);
      return;
    }

    log('Initializing on dashboard page');

    // Remove existing tracker card
    $(`.${trackerCardCls}`).remove();

    const usageData = extractUsageData();
    if (!usageData) {
      log('Could not extract usage data, retrying in 1 second...');
      setTimeout(init, 1000);
      return;
    }

    const daysInfo = calculateDaysSinceReset(usageData.resetDate);
    const trackerCard = createTrackerCard(usageData, daysInfo);

    // Insert after the original usage card
    const originalCard = getUsageCard();
    if (originalCard.length) {
      originalCard.after(trackerCard);
      log('Usage tracker card inserted successfully');

      // Add donation modal click handler
      trackerCard.find(`.${sigCls}`).on('click', () => {
        $(`.${donationModalCls}`).show();
      });
    } else {
      error('Could not find original usage card to insert tracker after');
      // Retry in a bit
      setTimeout(init, 1000);
    }
  };

  // Initialize when document is ready
  $(document).ready(() => {
    log('Usage Tracker v2 loaded');

    // Add styles
    $('head').append($('<style>').text(styles));

    // Preload icons
    ['settings', 'heart-handshake', 'copy', 'check', 'x']
      .map(iconName => createLucideIcon({ iconName }))
      .forEach(icon => $('body').append(icon))
      ;

    // Add modals to body
    $('body')
      .append(createDonationModal())
      .append(createSettingsModal());

    // Start the continuous check for dashboard page
    setTimeout(init, 1000);

    // Re-run when navigation happens (SPA)
    let lastUrl = location.href;
    new MutationObserver(() => {
      const url = location.href;
      if (url !== lastUrl) {
        lastUrl = url;
        log('URL changed, reinitializing');
        setTimeout(init, 500);
      }
    }).observe(document, { subtree: true, childList: true });

    // Debug helper - tests
    unsafeWindow.usageTracker = {
      reinit: init,
      extractUsageData: extractUsageData,
      calculateDaysSinceReset: calculateDaysSinceReset,
      getManualBillingDay: getManualBillingDay,
      setManualBillingDay: setManualBillingDay,
      createLucideIcon: createLucideIcon,
      colors: colors,
      version: '2.0'
    };
  });
})();