Cursor.com Usage Tracker

Tracks and displays the usage statistics and payment cycles for Premium models on Cursor.com, helping users monitor their subscriptions and usage limits.

Fra og med 15.11.2024. Se den nyeste version.

// ==UserScript==
// @name         Cursor.com Usage Tracker
// @author       monnef, Sonnet 3.5 (via Perplexity and Cursor), some help from Cursor Tab and Cursor Small
// @namespace    http://monnef.eu
// @version      0.2
// @description  Tracks and displays the usage statistics and payment cycles for Premium models on Cursor.com, helping users monitor their subscriptions and usage limits.
// @match        https://www.cursor.com/settings
// @grant        GM_getValue
// @grant        GM_setValue
// @require      https://code.jquery.com/jquery-3.6.0.min.js
// @license      AGPL-3.0
// ==/UserScript==

(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]`, ...messages);
  };

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

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

  const sigCls = genCssId('sig');
  const buttonCls = genCssId('button');
  const buttonWhiteCls = genCssId('button-white');
  const buttonDarkCls = genCssId('button-dark');
  const mainCaptionCls = genCssId('main-caption');
  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 errorMessageCls = genCssId('error-message');
  const settingsModalCls = genCssId('settings-modal');
  const donationModalCls = genCssId('donation-modal');
  const hrCls = genCssId('hr');

  const hSpaceSmCls = genCssId('h-space-sm');
  const hSpaceMdCls = genCssId('h-space-md');
  const hSpaceLgCls = genCssId('h-space-lg');

  const flexCenterCls = genCssId('flex-center');
  const flexRightCls = genCssId('flex-right');
  const flexBetweenCls = genCssId('flex-between');

  const colors = {
    cursor: {
      blue: '#3864f6',
      blueDarker: '#2e53cc',
      lightGray: '#e5e7eb',
      gray: '#a7a9ac',
      grayDark: '#333333',
    }
  };

  const styles = `
    .${hSpaceSmCls} {
      height: 5px;
    }

    .${hSpaceMdCls} {
      height: 10px;
    }

    .${hSpaceLgCls} {
      height: 20px;
    }

    .${flexCenterCls} {
      display: flex;
      justify-content: center;
      align-items: center;
    }

    .${flexRightCls} {
      display: flex;
      justify-content: flex-end;
      align-items: center;
    }

    .${flexBetweenCls} {
      display: flex;
      justify-content: space-between;
      align-items: center;
    }

    .${sigCls} {
      font-size: 0.75rem;
      color: ${colors.cursor.gray};
      margin-left: 0.75rem;
      opacity: 0.2;
      transition: opacity 0.1s ease-in-out;
      cursor: pointer;
    }
    .${sigCls}:hover {
      opacity: 1;
    }

    .${buttonCls}, .${buttonWhiteCls}, .${buttonDarkCls} {
      background-color: ${colors.cursor.blue};
      color: white;
      font-size: 14px;
      border: none;
      border-radius: 4px;
      cursor: pointer;
      padding: 4.25px 8px;
      font-weight: 400;
    }
    .${buttonCls}:hover {
      background-color: ${colors.cursor.blueDarker};
    }

    .${buttonWhiteCls} {
      background-color: white;
      color: black;
      border: 1px solid ${colors.cursor.lightGray};
      padding: 3px 8px;
    }
    .${buttonWhiteCls}:hover {
      background-color: ${colors.cursor.lightGray};
    }

    .${buttonDarkCls} {
      background-color: black;
      color: white;
      border: 1px solid black;
      padding: 3px 8px;
    }
    .${buttonDarkCls}:hover {
      background-color: white;
      color: black;
    }

    .${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: black;
      color: white;
      margin: 15% auto;
      padding: 15px 20px;
      width: 600px;
      border-radius: 4px;
      position: relative;
    }

    .${modalCloseCls} {
      color: white;
      position: absolute;
      top: 0px;
      right: 10px;
      font-size: 25px;
      font-weight: bold;
      cursor: pointer;
    }

    .${modalCloseCls}:hover {
      color: ${colors.cursor.lightGray};
    }

    .${copyButtonCls} {
      margin-left: 10px;
      width: 5em;
    }

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

    .${modalContentCls} p {
    }

    .${modalContentCls} hr {
      border: 0;
      height: 1px;
      background-color: ${colors.cursor.grayDark};
      margin: 10px 0;
    }

    .${inputCls} {
      background-color: white;
      color: black;
      border: 1px solid ${colors.cursor.lightGray};
      padding: 5px;
      width: 100%;
      border-radius: 4px;
      font-size: 14px;
    }

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

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

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

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

  const getUsageCard = () => $('.col-span-2 > div:has(h2:contains("Usage"))');

  const decorateUsageCard = () => {
    if ($c(mainCaptionCls).length > 0) return true;

    const usageCard = getUsageCard();

    if (!usageCard.length) {
      log('Usage card not found. Not decorating.');
      return false;
    }
    log('Usage card found. Decorating.', usageCard);

    const caption = $('<div>')
      .addClass('font-medium gt-standard-mono text-xl/[1.375rem] font-semibold -tracking-4 md:text-2xl/[1.875rem]')
      .addClass(mainCaptionCls)
      .text('Usage Tracker');
    const sig = $('<span>')
      .addClass(sigCls)
      .text('by monnef')
      .attr('title', 'Enjoying this script? Consider a small donation.')
      ;
    caption.append(sig);
    usageCard.append(genHr(), caption);
    addSettingsButton(caption);

    sig.click(() => {
      log('Signature clicked, showing donation modal');
      $c(donationModalCls).show();
    });

    if ($c(donationModalCls).length === 0) {
      $('body').append(createDonationModal());
    }

    // Add message for unset billing date
    const paymentDay = GM_getValue('paymentDay');
    if (!paymentDay) {
      const message = $('<div>')
        .addClass(getSettingsTextClassName())
        .html('Billing date not set. Please set it in the <strong>Usage Tracker settings</strong> to see usage statistics (top right of this section).');
      usageCard.append(
        // $('<div>').addClass(hSpaceMdCls),
        message
      );
    }

    return true;
  };

  const addUsageTracker = () => {
    const paymentDay = GM_getValue('paymentDay');
    log('Checking payment day.');

    if (!paymentDay) {
      log('Payment day not set. Not adding tracker.');
      return false;
    }

    log(`Payment day is set to: ${paymentDay}`);

    const usageData = getUsageData();
    if (usageData.used !== undefined && usageData.total !== undefined) {
      log(`Retrieved usage data: ${JSON.stringify(usageData)}`);
      displayTrackerData(usageData, paymentDay);
      return true;
    } else {
      log('Failed to retrieve usage data.');
      return false;
    }
  };

  const getPremiumModelsLabel = () => $('span:contains("Premium models")');

  /**
   * @returns {{ used: number, total: number }}
   */
  const getUsageData = () => {
    log('Attempting to find usage data...');
    const usageElement = getPremiumModelsLabel().siblings('span');

    if (usageElement.length === 0) {
      log('Usage element not found.');
      return {};
    }

    const usageText = usageElement.text();
    log(`Extracted text: "${usageText}"`);

    const regex = /(\d+) \/ (\d+)/;
    const matches = usageText.match(regex);
    if (matches && matches.length === 3) {
      const used = parseInt(matches[1], 10);
      const total = parseInt(matches[2], 10);
      log(`Parsed values - Used: ${used}, Total: ${total}`);
      return { used, total };
    } else {
      log('Regex did not match the extracted text.');
      return {};
    }
  };

  /**
   * @param {{ used: number, total: number }} usageData
   * @param {number} paymentDay
   * @returns {void}
   */
  const displayTrackerData = (usageData, paymentDay) => {
    const daysInfo = calculateDaysPassed({ today: new Date(), paymentDay });
    const hueShift = 170;

    if (daysInfo) {
      const { premiumRequestsPerDay, remainingUsesPerDay } = calculateMetrics(usageData, daysInfo);
      const progressBar = createProgressBar({
        value: daysInfo.daysPassed,
        max: daysInfo.totalDays,
        label: 'Days',
        rightLabel: `${daysInfo.daysPassed} / ${daysInfo.totalDays}`,
        textMiddle: ' days passed out of ',
        textAfter: ' days in the current billing cycle.',
        hueShift,
      });
      const usageCard = getUsageCard();
      usageCard.append(progressBar);
      log('Progress bar added to the page');

      // Create new progress bars for the new metrics
      const premiumRequestsBar = createProgressBar({
        value: premiumRequestsPerDay.toFixed(2),
        max: (usageData.total / daysInfo.totalDays).toFixed(2),
        label: 'Premium Requests/Day',
        rightLabel: `${premiumRequestsPerDay.toFixed(2)} / ${(usageData.total / daysInfo.totalDays).toFixed(2)}`,
        textBefore: 'So far you have used ',
        textMiddle: ' requests per day on average.',
        hideMax: true,
        hueShift,
      });

      const remainingUsesBar = createProgressBar({
        value: remainingUsesPerDay.toFixed(2),
        max: (usageData.total / daysInfo.totalDays).toFixed(2),
        label: 'Remaining Uses/Day',
        rightLabel: `${remainingUsesPerDay.toFixed(2)} / ${(usageData.total / daysInfo.totalDays).toFixed(2)}`,
        textBefore: 'You have ',
        textMiddle: ' uses per day available for the remaining ',
        textAfter: ` days.`,
        overrideMax: `${daysInfo.totalDays - daysInfo.daysPassed}`,
        hueShift,
      });

      // Append new progress bars to the usage card
      usageCard.append(premiumRequestsBar, remainingUsesBar);
    }
  };

  /**
   * @param {{ used: number, total: number }} usageData
   * @param {{ daysPassed: number, totalDays: number }} daysInfo
   * @returns {{ premiumRequestsPerDay: number, remainingUsesPerDay: number }}
   */
  const calculateMetrics = (usageData, daysInfo) => {
    const premiumRequestsPerDay = daysInfo.daysPassed > 0 
      ? (usageData.used / daysInfo.daysPassed) 
      : 0; // Avoid division by zero
    const remainingUsesPerDay = (usageData.total - usageData.used) / (daysInfo.totalDays - daysInfo.daysPassed || 1); // Prevent division by zero

    return { premiumRequestsPerDay, remainingUsesPerDay };
  };

  /**
   * @param {{ today: Date, paymentDay: number }} params
   * @returns {{ daysPassed: number, totalDays: number }}
   */
  const calculateDaysPassed = ({ today, paymentDay, disableLog = false }) => {
    const currentMonth = today.getMonth();
    const currentYear = today.getFullYear();
    const lastPaymentDate = new Date(currentYear, currentMonth, paymentDay);

    if (today < lastPaymentDate) {
      lastPaymentDate.setMonth(lastPaymentDate.getMonth() - 1);
    }

    const daysPassed = Math.floor((today - lastPaymentDate) / (1000 * 60 * 60 * 24));
    const nextPaymentDate = new Date(lastPaymentDate);
    nextPaymentDate.setMonth(nextPaymentDate.getMonth() + 1);
    const totalDays = Math.floor((nextPaymentDate - lastPaymentDate) / (1000 * 60 * 60 * 24));

    const res = { daysPassed, totalDays, progress: daysPassed / totalDays };
    if (!disableLog) {
      log(`Calculated days - Passed: ${res.daysPassed}, Total: ${res.totalDays}, Progress: ${res.progress}`);
    }
    return res;
  };

  const TEST_calculateDaysPassed = () => {
    const testCases = [
      { today: new Date(2023, 9, 15), paymentDay: 15, expected: { daysPassed: 0, totalDays: 31 } },
      { today: new Date(2023, 9, 16), paymentDay: 15, expected: { daysPassed: 1, totalDays: 31 } },
      { today: new Date(2023, 10, 1), paymentDay: 15, expected: { daysPassed: 17, totalDays: 31 } },
      { today: new Date(2023, 10, 16), paymentDay: 15, expected: { daysPassed: 1, totalDays: 30 } },
      { today: new Date(2023, 9, 1), paymentDay: 15, expected: { daysPassed: 16, totalDays: 30 } },
      { today: new Date(2023, 12, 29), paymentDay: 2, expected: { daysPassed: 27, totalDays: 31 } },
    ];

    return testCases.every(({ today, paymentDay, expected }) => {
      const result = calculateDaysPassed({ today, paymentDay, disableLog: true });
      const passed = result.daysPassed === expected.daysPassed && result.totalDays === expected.totalDays;

      if (!passed) {
        log(`Test failed for today: ${today.toDateString()}, paymentDay: ${paymentDay}. Expected: ${JSON.stringify(expected)}, got: ${JSON.stringify(result)}`);
      }

      return passed;
    });
  };

  const TEST_calculateMetrics = () => {
    const testCases = [
      { usageData: { used: 10, total: 100 }, daysInfo: { daysPassed: 5, totalDays: 30 }, expected: { premiumRequestsPerDay: 2, remainingUsesPerDay: 3.6 } },
      { usageData: { used: 0, total: 100 }, daysInfo: { daysPassed: 0, totalDays: 30 }, expected: { premiumRequestsPerDay: 0, remainingUsesPerDay: 3.3333333333333335 } },
      { usageData: { used: 50, total: 100 }, daysInfo: { daysPassed: 10, totalDays: 30 }, expected: { premiumRequestsPerDay: 5, remainingUsesPerDay: 2.5 } },
    ];

    return testCases.every(({ usageData, daysInfo, expected }) => {
      const result = calculateMetrics(usageData, daysInfo);
      const passed = result.premiumRequestsPerDay === expected.premiumRequestsPerDay && result.remainingUsesPerDay === expected.remainingUsesPerDay;

      if (!passed) {
        log(`Test failed for usageData: ${JSON.stringify(usageData)}, daysInfo: ${JSON.stringify(daysInfo)}. Expected: ${JSON.stringify(expected)}, got: ${JSON.stringify(result)}`);
      }

      return passed;
    });
  };

  const TEST = () => {
    const tests = {
      calculateDaysPassed: TEST_calculateDaysPassed(),
      calculateMetrics: TEST_calculateMetrics(),
    };
    const allPassed = Object.values(tests).every(testRes => testRes);
    if (allPassed) {
      log('TEST: All tests passed successfully.', tests);
    } else {
      log('TEST: Some tests failed. Check the logs above for details.', tests);
    }
  };

  /**
   * @returns {string|null}
   */
  const getClassNameByPrefix = prefix => {
    const classes = $('[class]')
      .map((_, element) => $(element).attr('class').match(new RegExp(`\\b${prefix}\\S+`, 'g')) || [])
      .get();

    if (classes.length === 0) {
      log('No classes found');
      return null;
    }

    const firstClass = classes[0];
    if (classes.some(cls => cls !== firstClass)) {
      error(`Classes are not the same: ${classes.join(', ')}`);
    }

    return firstClass;
  };

  /**
   * @returns {string|null}
   */
  const getSettingsTextClassName = () => '[&_b]:md:font-semibold font-mono text-sm/[0.875rem] tracking-4 md:text-sm/[1.25rem]';


  /**
   * @returns {string}
   */
  const getInfoNumberClassName = () => 'font-bold inline';

  /**
   * @param {{ value: number, max: number, label: string, rightLabel: string, textBefore?: string, textMiddle?: string, textAfter?: string, hideMax?: boolean, overrideMax?: string, hueShift?: number }} params
   * @returns {string}
   */
  const createProgressBar = ({ value, max, label, rightLabel, textBefore = '', textMiddle = '', textAfter = '', hideMax = false, overrideMax, hueShift }) => {
    const percentage = (value / max) * 100;
    return `<div class='flex flex-col gap-1.5 flex-1'>
              <div class='flex items-center justify-between gap-2'>
                  <div class='gt-standard-mono text-sm font-medium flex items-center gap-1'>
                      <span class='truncate'>${label}</span>
                  </div>
                  <div class='font-mono text-sm font-semibold truncate'>${rightLabel}</div>
              </div>
              <div class='w-full rounded-full bg-brand-borders overflow-hidden' style='height: 6px;'>
                  <div class='h-full' style='background-color: rgb(99, 161, 26); width: ${percentage.toFixed(2)}%; filter: hue-rotate(${hueShift ?? 0}deg);' title='${percentage.toFixed(0)}%'></div>
              </div>
              <div class='flex items-center gap-1'>
                  <div class='${getSettingsTextClassName()}'>
                      ${textBefore}<span class='${getInfoNumberClassName()}'>${value}</span>${textMiddle}<span class='${getInfoNumberClassName()}' ${hideMax ? 'style="display: none;"' : ''}>${overrideMax ? overrideMax : max}</span>${textAfter}
                  </div>
              </div>
          </div>`;
  };

  /**
   * @param {{ className: string, title: string, content: JQuery<HTMLElement> }} params
   * @returns {JQuery<HTMLElement>}
   */
  const createModal = ({ className, title, titleIconAfter, content }) => {
    const modal = $('<div>').addClass(modalCls).addClass(className);
    const modalContent = $('<div>').addClass(modalContentCls);
    const closeButton = $('<span>').addClass(modalCloseCls).text('×');

    const titleElement = $('<h1>')
      .addClass('text-4xl gt-standard-mono font-medium')
      .text(title);

    if (titleIconAfter) {
      titleElement.append(
        createLucideIcon({ iconName: titleIconAfter, size: '32px', invert: true })
          .css({ marginLeft: '10px' })
      );
    }
    ;

    modalContent.append(
      closeButton,
      titleElement,
      $('<div>').addClass(hSpaceMdCls),
      content
    );
    modal.append(modalContent);

    closeButton.click(() => modal.hide());
    $(window).click(event => {
      if (event.target === modal[0]) {
        modal.hide();
      }
    });

    return modal;
  };

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

    const input = $('<input>')
      .addClass(inputCls)
      .attr('type', 'number')
      .attr('min', '1')
      .attr('max', '31')
      .val(GM_getValue('paymentDay') || '');
    
    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 = $('<p>').addClass(errorMessageCls).hide();

    const saveAndReload = () => {
      const newPaymentDay = parseInt(input.val(), 10);
      if (newPaymentDay && newPaymentDay >= 1 && newPaymentDay <= 31) {
        GM_setValue('paymentDay', newPaymentDay);
        log(`Payment day has been set to: ${newPaymentDay}`);
        $c(settingsModalCls).hide();
        location.reload(); // Refresh to update tracker
      } else {
        errorMessage.text('Invalid input. Please enter a number between 1 and 31.').show();
      }
    };

    const saveButton = $('<button>')
      .addClass(buttonCls)
      .text('Save & Reload')
      .click(saveAndReload);

    // Add keypress event listener to the input
    input.on('keypress', (e) => {
      if (e.which === 13) { // 13 is the Enter key code
        saveAndReload();
      }
    });

    const madeByText = $('<p>').append(
      'Made with ',
      createLucideIcon({ iconName: 'heart', size: '14px', invert: true }),
      ' by monnef & Sonnet 3.5'
    );

    const content = $('<div>').append(
      subtitle,
      $('<div>').addClass(hSpaceSmCls),
      input,
      tip,
      errorMessage,
      $('<div>').addClass(hSpaceLgCls),
      $('<div>').addClass(flexBetweenCls).append(madeByText, saveButton)
    );

    const modal = createModal({
      className: settingsModalCls,
      title: 'Usage Tracker Settings',
      content: content
    });

    return modal;
  };

  const createDonationModal = () => {
    const subtitle = $('<p>').text('Thank you for considering a donation! Your support is appreciated.');

    const createCopyButton = (text, successMessage) => {
      const button = $('<button>')
        .addClass(buttonDarkCls)
        .addClass(copyButtonCls)
        .text('Copy');

      button.click(async () => {
        try {
          await navigator.clipboard.writeText(text);
          button.text(successMessage);
          setTimeout(() => button.text('Copy'), 2000);
        } catch (err) {
          error('Clipboard write failed:', err);
          button.text('Failed');
          setTimeout(() => button.text('Copy'), 2000);
        }
      });

      return button;
    };

    const addressText = $('<p>').text('Bitcoin Address:');
    const addressInput = $('<input>')
      .addClass(inputCls)
      .addClass(inputWithButtonCls)
      .attr('type', 'text')
      .val(bitcoinAddress)
      .prop('readonly', true);
    const copyAddressButton = createCopyButton(bitcoinAddress, 'Copied!');

    const paymentLinkText = $('<p>').text('Payment link:');
    const paymentLinkInput = $('<input>')
      .addClass(inputCls)
      .addClass(inputWithButtonCls)
      .attr('type', 'text')
      .val(bitcoinPaymentLink)
      .prop('readonly', true);
    const copyPaymentLinkButton = createCopyButton(bitcoinPaymentLink, 'Copied!');

    const content = $('<div>').append(
      subtitle,
      genHr(),
      addressText,
      addressInput,
      copyAddressButton,
      genHr(),
      paymentLinkText,
      paymentLinkInput,
      copyPaymentLinkButton,
      $('<div>').addClass(hSpaceMdCls)
    );

    return createModal({
      className: donationModalCls,
      title: 'Donate',
      titleIconAfter: 'heart-handshake',
      content: content
    });
  };

  const createLucideIcon = ({ iconName, size = '16px', invert = false }) => {
    return $('<img>')
      .attr('src', `https://unpkg.com/lucide-static@latest/icons/${iconName}.svg`)
      .css({
        width: size,
        height: size,
        display: 'inline-block',
        verticalAlign: 'text-bottom',
        filter: invert ? 'invert(1)' : 'none'
      });
  };
  const addSettingsButton = (mainCaption) => {
    const settingsButton = $('<button>')
      .css({
        position: 'absolute',
        top: '-10px',
        right: '0px',
        height: '29.4px',
        width: '29.4px',
        padding: '0px',
      })
      .addClass(buttonWhiteCls)
      .attr('title', 'Usage Tracker settings')
      .append(createLucideIcon({ iconName: 'settings' }));

    settingsButton.click(() => {
      log('Usage Tracker settings button clicked.');
      $c(settingsModalCls).show();
      const inputField = $c(settingsModalCls).find(`.${inputCls}`);
      inputField.focus();
    });

    const buttonWrapper = $('<div>').css({ position: 'relative', height: '0px' });
    buttonWrapper.append(settingsButton);

    if (mainCaption.length > 0) {
      mainCaption.prepend(buttonWrapper);
      log('Settings button wrapper added to the page');
    } else {
      log('Main caption not found, settings button not added');
    }
  };

  const state = {
    addingUsageTrackerSucceeded: false,
    addingUsageTrackerAttempts: 0,
  };

  const ATTEMPTS_LIMIT = 10;
  const ATTEMPTS_INTERVAL = 250;
  const ATTEMPTS_MAX_DELAY = 3000;

  /**
   * @returns {void}
   */
  const main = () => {
    const scheduleNextAttempt = () => {
      if (!state.addingUsageTrackerSucceeded && state.addingUsageTrackerAttempts < ATTEMPTS_LIMIT) {
        const delay = Math.min(ATTEMPTS_INTERVAL * (2 ** (state.addingUsageTrackerAttempts - 1)), ATTEMPTS_MAX_DELAY);
        log(`Attempt ${state.addingUsageTrackerAttempts} of ${ATTEMPTS_LIMIT} failed. Retrying in ${delay}ms...`);
        setTimeout(main, delay);
      } else if (state.addingUsageTrackerSucceeded) {
        log(`Attempt ${state.addingUsageTrackerAttempts} of ${ATTEMPTS_LIMIT} succeeded.`);
      } else {
        log(`Attempt ${state.addingUsageTrackerAttempts} of ${ATTEMPTS_LIMIT} failed. No more attempts.`);
      }
    };

    const decorationOkay = decorateUsageCard();
    const paymentDay = GM_getValue('paymentDay');

    if (decorationOkay && !paymentDay) {
      log('Payment day not set. Not adding tracker.');
      return;
    }

    if (!decorationOkay) {
      log('Decoration failed. Try again later.');
      scheduleNextAttempt();
      state.addingUsageTrackerAttempts++;
      return;
    }

    state.addingUsageTrackerSucceeded = addUsageTracker();
    state.addingUsageTrackerAttempts++;

    if (state.addingUsageTrackerAttempts === 1) {
      const paymentDay = GM_getValue('paymentDay');

      if (!paymentDay) {
        log('Payment day not set. Not adding tracker.');
        return;
      }
    }

    scheduleNextAttempt();
  };

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

  $(document).ready(() => {
    log('Script started');
    unsafeWindow.ut = {
      jq: $,
      resetSettings: () => {
        GM_setValue('paymentDay', undefined);
        location.reload();
      }
    };
    $('head').append($('<style>').text(styles));
    $('body').append(createSettingsModal());
    $('body').append(createDonationModal());
    setTimeout(() => {
      TEST();
      main();
    }, ATTEMPTS_INTERVAL);
  });
})();