Cursor.com Usage Tracker

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

// ==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'
    };
  });
})();