Greasy Fork 还支持 简体中文。

Cursor.com Usage Tracker (Enhanced)

Tracks and displays usage statistics, payment cycles, and detailed model costs for Premium models on Cursor.com.

目前為 2025-05-26 提交的版本,檢視 最新版本

  1. // ==UserScript==
  2. // @name Cursor.com Usage Tracker (Enhanced)
  3. // @author monnef, Sonnet 3.5 (via Perplexity and Cursor), some help from Cursor Tab and Cursor Small, NoahBPeterson, Sonnet 3.7, Gemini
  4. // @namespace http://monnef.eu
  5. // @version 0.5.2
  6. // @description Tracks and displays usage statistics, payment cycles, and detailed model costs for Premium models on Cursor.com.
  7. // @match https://www.cursor.com/settings
  8. // @grant GM_getValue
  9. // @grant GM_setValue
  10. // @require https://code.jquery.com/jquery-3.6.0.min.js
  11. // @license AGPL-3.0
  12. // @icon https://www.cursor.com/favicon-48x48.png
  13. // ==/UserScript==
  14.  
  15. (function () {
  16. 'use strict';
  17.  
  18. const $ = jQuery.noConflict();
  19.  
  20. const $c = (cls, parent) => $(`.${cls}`, parent);
  21. const $i = (id, parent) => $(`#${id}`, parent);
  22.  
  23. $.fn.nthParent = function (n) {
  24. return this.parents().eq(n - 1);
  25. };
  26.  
  27. const log = (...messages) => {
  28. console.log(`[UsageTracker]`, ...messages);
  29. };
  30.  
  31. const error = (...messages) => {
  32. console.error(`[UsageTracker]`, ...messages);
  33. };
  34.  
  35. const debug = (...messages) => {
  36. console.debug(`[UsageTracker Debug]`, ...messages);
  37. };
  38.  
  39. const genCssId = name => `ut-${name}`;
  40.  
  41. // --- CSS Class Names ---
  42. const sigCls = genCssId('sig');
  43. const buttonCls = genCssId('button');
  44. const buttonWhiteCls = genCssId('button-white');
  45. const buttonDarkCls = genCssId('button-dark');
  46. const mainCaptionCls = genCssId('main-caption');
  47. const modalCls = genCssId('modal');
  48. const modalContentCls = genCssId('modal-content');
  49. const modalCloseCls = genCssId('modal-close');
  50. const copyButtonCls = genCssId('copy-button');
  51. const inputCls = genCssId('input');
  52. const inputWithButtonCls = genCssId('input-with-button');
  53. const errorMessageCls = genCssId('error-message');
  54. const settingsModalCls = genCssId('settings-modal');
  55. const hrCls = genCssId('hr');
  56. const debugContainerCls = genCssId('debug-container');
  57. const hSpaceSmCls = genCssId('h-space-sm');
  58. const hSpaceMdCls = genCssId('h-space-md');
  59. const hSpaceLgCls = genCssId('h-space-lg');
  60. const flexCenterCls = genCssId('flex-center');
  61. const flexRightCls = genCssId('flex-right');
  62. const flexBetweenCls = genCssId('flex-between');
  63. const multiBarCls = genCssId('multi-bar');
  64. const barSegmentCls = genCssId('bar-segment');
  65. const tooltipCls = genCssId('tooltip');
  66. const statsContainerCls = genCssId('stats-container');
  67. const statItemCls = genCssId('stat-item');
  68.  
  69. const colors = {
  70. cursor: {
  71. blue: '#3864f6',
  72. blueDarker: '#2e53cc',
  73. lightGray: '#e5e7eb',
  74. gray: '#a7a9ac',
  75. grayDark: '#333333',
  76. green: '#63a11a', // Original green
  77. },
  78. segments: [ // A palette for the multi-segment bar
  79. '#F44336', '#E91E63', '#9C27B0', '#673AB7', '#3F51B5',
  80. '#2196F3', '#03A9F4', '#00BCD4', '#009688', '#4CAF50',
  81. '#8BC34A', '#CDDC39', '#FFEB3B', '#FFC107', '#FF9800',
  82. '#FF5722', '#795548', '#9E9E9E', '#607D8B'
  83. ]
  84. };
  85.  
  86. const styles = `
  87. .${hSpaceSmCls} { height: 5px; }
  88. .${hSpaceMdCls} { height: 10px; }
  89. .${hSpaceLgCls} { height: 20px; }
  90. .${flexCenterCls} { display: flex; justify-content: center; align-items: center; }
  91. .${flexRightCls} { display: flex; justify-content: flex-end; align-items: center; }
  92. .${flexBetweenCls} { display: flex; justify-content: space-between; align-items: center; }
  93. .${sigCls} { font-size: 0.75rem; color: ${colors.cursor.gray}; margin-left: 0.75rem; opacity: 0.2; transition: opacity 0.1s ease-in-out; }
  94. .${sigCls}:hover { opacity: 1; }
  95. .${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; }
  96. .${buttonCls}:hover { background-color: ${colors.cursor.blueDarker}; }
  97. .${buttonWhiteCls} { background-color: white; color: black; border: 1px solid ${colors.cursor.lightGray}; padding: 3px 8px; }
  98. .${buttonWhiteCls}:hover { background-color: ${colors.cursor.lightGray}; }
  99. .${buttonDarkCls} { background-color: black; color: white; border: 1px solid black; padding: 3px 8px; }
  100. .${buttonDarkCls}:hover { background-color: white; color: black; }
  101. .${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); }
  102. .${modalContentCls} { background-color: black; color: white; margin: 15% auto; padding: 15px 20px; width: 600px; border-radius: 4px; position: relative; }
  103. .${modalCloseCls} { color: white; position: absolute; top: 0px; right: 10px; font-size: 25px; font-weight: bold; cursor: pointer; }
  104. .${modalCloseCls}:hover { color: ${colors.cursor.lightGray}; }
  105. .${copyButtonCls} { margin-left: 10px; width: 5em; }
  106. .${modalContentCls} h2 { margin-bottom: 20px; }
  107. .${modalContentCls} hr { border: 0; height: 1px; background-color: ${colors.cursor.grayDark}; margin: 10px 0; }
  108. .${inputCls} { background-color: white; color: black; border: 1px solid ${colors.cursor.lightGray}; padding: 5px; width: 100%; border-radius: 4px; font-size: 14px; }
  109. .${inputWithButtonCls} { width: calc(100% - 5em - 10px); }
  110. .${errorMessageCls} { color: #ff4d4f; font-size: 14px; margin-top: 5px; }
  111. .${hrCls} { border: 0; height: 1px; background-color: #333333; /* Darker HR */ margin: 15px 0; }
  112. .${debugContainerCls} { background-color: #f0f0f0; border: 1px solid #ccc; border-radius: 4px; padding: 10px; margin-top: 10px; font-family: monospace; font-size: 12px; overflow-wrap: break-word; max-height: 200px; overflow-y: auto; }
  113. .${statsContainerCls} { display: grid; grid-template-columns: 1fr 1fr; gap: 10px 20px; margin: 15px 0; padding: 15px; background-color: #1a1a1a; border-radius: 8px; }
  114. .${statItemCls} { font-size: 14px; }
  115. .${statItemCls} .label { color: ${colors.cursor.gray}; }
  116. .${statItemCls} .value { color: white; font-weight: bold; }
  117. .${multiBarCls} { display: flex; width: 100%; height: 15px; background-color: ${colors.cursor.grayDark}; border-radius: 4px; /* REMOVED: overflow: hidden; */ margin: 10px 0; }
  118. .${barSegmentCls} { height: 100%; position: relative; transition: filter 0.2s ease-in-out; }
  119. .${barSegmentCls}:hover { filter: brightness(1.2); }
  120. .${barSegmentCls} .${tooltipCls} {
  121. visibility: hidden;
  122. width: max-content;
  123. background-color: black;
  124. color: #fff;
  125. text-align: center;
  126. border-radius: 6px;
  127. padding: 5px 10px;
  128. position: absolute;
  129. z-index: 50; /* INCREASED Z-INDEX */
  130. bottom: 150%; /* Adjusted slightly up */
  131. left: 50%;
  132. transform: translateX(-50%);
  133. opacity: 0;
  134. transition: opacity 0.3s;
  135. border: 1px solid ${colors.cursor.gray};
  136. font-size: 12px;
  137. pointer-events: none; /* Prevent tooltip from interfering with hover */
  138. }
  139. .${barSegmentCls}:hover .${tooltipCls} {
  140. visibility: visible;
  141. opacity: 1;
  142. }
  143. .${barSegmentCls} .${tooltipCls}::after {
  144. content: "";
  145. position: absolute;
  146. top: 100%;
  147. left: 50%;
  148. margin-left: -5px;
  149. border-width: 5px;
  150. border-style: solid;
  151. border-color: black transparent transparent transparent;
  152. }
  153. `;
  154.  
  155. const genHr = () => $('<hr>').addClass(hrCls);
  156.  
  157. const getUsageCard = () => {
  158. const usageHeading = $('h2:contains("Usage")');
  159. if (usageHeading.length > 0) {
  160. const card = usageHeading.closest('.rounded-2xl, .rounded-3xl');
  161. debug(`Found Usage card via h2: ${card.length > 0}`);
  162. return card.length > 0 ? card : null;
  163. }
  164. debug('Usage card not found.');
  165. return null;
  166. };
  167.  
  168. // --- Data Parsing Functions ---
  169.  
  170. /**
  171. * Finds and parses the "Recent Usage Events" table.
  172. * @returns {{ modelUsage: Record<string, { count: number, cost: number }>, totalPaidRequests: number, totalRequests: number }}
  173. */
  174. const parseUsageEventsTable = () => {
  175. const modelUsage = {};
  176. let totalPaidRequests = 0;
  177. let totalRequests = 0;
  178.  
  179. const table = $('p:contains("Recent Usage Events")').closest('div').find('table tbody tr');
  180. debug(`Found ${table.length} rows in Recent Usage Events table.`);
  181.  
  182. if (table.length === 0) {
  183. error("Could not find 'Recent Usage Events' table.");
  184. return { modelUsage, totalPaidRequests, totalRequests };
  185. }
  186.  
  187. table.each((_, row) => {
  188. const $row = $(row);
  189. const model = $row.find('td:eq(1)').text().trim();
  190. const status = $row.find('td:eq(2)').text().trim();
  191. const requestsStr = $row.find('td:eq(3)').text().trim();
  192. const requests = parseFloat(requestsStr) || 0;
  193.  
  194. if (status !== 'Errored, Not Charged' && model) { // Ensure model name exists
  195. totalRequests += requests;
  196. if (!modelUsage[model]) {
  197. modelUsage[model] = { count: 0, cost: 0 }; // Initialize cost here
  198. }
  199. modelUsage[model].count += requests;
  200.  
  201. if (status === 'Usage-based') {
  202. totalPaidRequests += requests;
  203. }
  204. }
  205. });
  206.  
  207. debug('Parsed Usage Events:', { modelUsage, totalPaidRequests, totalRequests });
  208. return { modelUsage, totalPaidRequests, totalRequests };
  209. };
  210.  
  211. /**
  212. * Finds and parses the "Current Usage" cost summary table.
  213. * @param {Record<string, { count: number, cost: number }>} modelUsage - The object to update with costs.
  214. * @returns {{ totalCost: number }}
  215. */
  216. const parseCurrentUsageCosts = (modelUsage) => {
  217. let totalCost = 0;
  218. const costTable = $('p:contains("Current Usage"):last').closest('div').find('table tbody tr'); // Find the *cost* table
  219. debug(`Found ${costTable.length} rows in Current Usage (Cost) table.`);
  220.  
  221. if (costTable.length === 0) {
  222. error("Could not find 'Current Usage' (Cost) table.");
  223. return { totalCost };
  224. }
  225.  
  226. costTable.each((_, row) => {
  227. const $row = $(row);
  228. const description = $row.find('td:eq(0)').text().trim().toLowerCase();
  229. const costStr = $row.find('td:eq(1)').text().trim().replace('$', '');
  230. const cost = parseFloat(costStr) || 0;
  231.  
  232. totalCost += cost; // Sum all costs, including negative ones (payments)
  233.  
  234. let foundModel = false;
  235. for (const model in modelUsage) {
  236. if (description.includes(model.toLowerCase())) {
  237. modelUsage[model].cost += cost; // Add cost to the model
  238. foundModel = true;
  239. }
  240. }
  241. if (!foundModel && (description.includes('extra') || description.includes('premium')) && cost > 0 ) {
  242. if (!modelUsage['Extra/Other Premium']) {
  243. modelUsage['Extra/Other Premium'] = { count: 0, cost: 0 };
  244. }
  245. modelUsage['Extra/Other Premium'].cost += cost;
  246. foundModel = true;
  247. }
  248. if (!foundModel && cost > 0) {
  249. if (!modelUsage['Other Costs']) {
  250. modelUsage['Other Costs'] = { count: 0, cost: 0 };
  251. }
  252. modelUsage['Other Costs'].cost += cost;
  253. }
  254. });
  255.  
  256. // Ensure all models in modelUsage have a cost, even if 0
  257. for (const model in modelUsage) {
  258. modelUsage[model].cost = modelUsage[model].cost || 0;
  259. }
  260.  
  261.  
  262. debug('Parsed Costs & Updated Model Usage:', { modelUsage, totalCost });
  263. return { totalCost }; // Keep returning totalCost in case we need it later, even if not displayed.
  264. };
  265.  
  266. /**
  267. * Extracts the basic "X / Y" usage if available.
  268. * @returns {{ used: number, total: number }}
  269. */
  270. const getBaseUsageData = () => {
  271. debug('Attempting to find base usage data...');
  272. const premiumLabel = $('span:contains("Premium models")').first();
  273. if (premiumLabel.length === 0) {
  274. debug('Base Premium models label not found.');
  275. return {};
  276. }
  277.  
  278. const usageSpan = premiumLabel.siblings('span').last();
  279. const usageText = usageSpan.text();
  280. debug(`Found base usage text: "${usageText}"`);
  281.  
  282. const regex = /(\d+) \/ (\d+)/;
  283. const matches = usageText.match(regex);
  284. if (matches && matches.length === 3) {
  285. const used = parseInt(matches[1], 10);
  286. const total = parseInt(matches[2], 10);
  287. debug(`Parsed base values - Used: ${used}, Total: ${total}`);
  288. return { used, total };
  289. } else {
  290. debug('Regex did not match the base usage text.');
  291. return {};
  292. }
  293. };
  294.  
  295.  
  296. // --- Display Functions ---
  297.  
  298. /**
  299. * Creates a multi-segment progress bar.
  300. * @param {Record<string, { count: number, cost: number }>} modelUsage
  301. * @returns {JQuery<HTMLElement>}
  302. */
  303. const createMultiSegmentProgressBar = (modelUsage) => {
  304. const barContainer = $('<div>').addClass(multiBarCls);
  305. const totalRequests = Object.values(modelUsage).reduce((sum, model) => sum + (model.count || 0), 0);
  306.  
  307. if (totalRequests === 0) {
  308. return barContainer.text('No usage data for bar.').css({ height: 'auto', padding: '5px' });
  309. }
  310.  
  311. let colorIndex = 0;
  312. // Sort models by count descending for better visualization
  313. const sortedModels = Object.entries(modelUsage)
  314. .filter(([_, data]) => data.count > 0)
  315. .sort(([, a], [, b]) => b.count - a.count);
  316.  
  317. for (const [model, data] of sortedModels) {
  318. const percentage = (data.count / totalRequests) * 100;
  319. const cost = data.cost.toFixed(2);
  320. const color = colors.segments[colorIndex % colors.segments.length];
  321.  
  322. const tooltipText = `${model}: ${data.count.toFixed(1)} reqs ($${cost})`;
  323. const tooltip = $('<span>').addClass(tooltipCls).text(tooltipText);
  324.  
  325. const segment = $('<div>')
  326. .addClass(barSegmentCls)
  327. .css({
  328. width: `${percentage}%`,
  329. backgroundColor: color,
  330. })
  331. .append(tooltip);
  332.  
  333. barContainer.append(segment);
  334. colorIndex++;
  335. }
  336. return barContainer;
  337. };
  338.  
  339.  
  340. /**
  341. * Displays all the new tracker data.
  342. * @param {number} paymentDay
  343. */
  344. const displayEnhancedTrackerData = (paymentDay) => {
  345. const tracker = $c(mainCaptionCls);
  346. if (tracker.length === 0) {
  347. error('Main caption not found for displaying enhanced data.');
  348. return false; // Indicate failure
  349. }
  350. // Clear previous tracker data before parsing again
  351. tracker.siblings(`.${genCssId('enhanced-tracker')}`).remove();
  352.  
  353. const { modelUsage, totalPaidRequests, totalRequests } = parseUsageEventsTable();
  354. parseCurrentUsageCosts(modelUsage); // Call this to populate costs in modelUsage
  355. const baseUsage = getBaseUsageData();
  356.  
  357. const container = $('<div>').addClass(genCssId('enhanced-tracker'));
  358. const statsContainer = $('<div>').addClass(statsContainerCls);
  359.  
  360. const addStat = (label, value) => {
  361. statsContainer.append(
  362. $('<div>').addClass(statItemCls).append(
  363. $('<span>').addClass('label').text(`${label}: `),
  364. $('<span>').addClass('value').text(value)
  365. )
  366. );
  367. }
  368.  
  369. addStat('Total Usage-Based Requests (Weighted)', totalPaidRequests.toFixed(1));
  370. addStat('Grand Total Requests (Weighted)', (totalRequests + baseUsage.total).toFixed(1));
  371.  
  372. container.append(
  373. statsContainer,
  374. $('<p>').css({ fontSize: '13px', color: colors.cursor.gray, marginTop: '10px' }).text('Model Usage Breakdown (Weighted, Hover for details):'),
  375. createMultiSegmentProgressBar(modelUsage)
  376. );
  377.  
  378. tracker.after(container);
  379. debug('Enhanced tracker data displayed.');
  380. return true; // Indicate success
  381. };
  382.  
  383.  
  384. // --- Original Script Functions (Mostly Unchanged or Slightly Modified) ---
  385.  
  386. const decorateUsageCard = () => {
  387. if ($c(mainCaptionCls).length > 0) {
  388. return true;
  389. }
  390. const usageHeading = $('h2:contains("Usage")');
  391. if (usageHeading.length > 0) {
  392. const caption = $('<div>')
  393. .addClass('font-medium gt-standard-mono text-xl/[1.375rem] font-semibold -tracking-4 md:text-2xl/[1.875rem]')
  394. .addClass(mainCaptionCls)
  395. .text('Usage Tracker');
  396.  
  397. usageHeading.after(genHr(), caption);
  398. addSettingsButton(caption); // Add settings button here
  399. debug("Added tracker after Usage heading");
  400. return true;
  401. }
  402. return false;
  403. };
  404.  
  405. const addUsageTracker = () => {
  406. const paymentDay = GM_getValue('paymentDay');
  407. return displayEnhancedTrackerData(paymentDay);
  408. };
  409.  
  410. const calculateDaysPassed = ({ today, paymentDay, disableLog = false }) => {
  411. if (!paymentDay) return null;
  412. const currentMonth = today.getMonth();
  413. const currentYear = today.getFullYear();
  414. const lastPaymentDate = new Date(currentYear, currentMonth, paymentDay);
  415. if (today < lastPaymentDate) {
  416. lastPaymentDate.setMonth(lastPaymentDate.getMonth() - 1);
  417. }
  418. const daysPassed = Math.floor((today - lastPaymentDate) / (1000 * 60 * 60 * 24));
  419. const nextPaymentDate = new Date(lastPaymentDate);
  420. nextPaymentDate.setMonth(nextPaymentDate.getMonth() + 1);
  421. const totalDays = Math.floor((nextPaymentDate - lastPaymentDate) / (1000 * 60 * 60 * 24));
  422. const res = { daysPassed, totalDays, progress: daysPassed / totalDays };
  423. if (!disableLog) {
  424. debug(`Calculated days - Passed: ${res.daysPassed}, Total: ${res.totalDays}, Progress: ${res.progress}`);
  425. }
  426. return res;
  427. };
  428.  
  429. const createModal = ({ className, title, content }) => {
  430. const modal = $('<div>').addClass(modalCls).addClass(className);
  431. const modalContent = $('<div>').addClass(modalContentCls);
  432. const closeButton = $('<span>').addClass(modalCloseCls).text('×');
  433. const titleElement = $('<h1>')
  434. .addClass('text-4xl gt-standard-mono font-medium')
  435. .text(title);
  436. modalContent.append(
  437. closeButton,
  438. titleElement,
  439. $('<div>').addClass(hSpaceMdCls),
  440. content
  441. );
  442. modal.append(modalContent);
  443. closeButton.click(() => modal.hide());
  444. $(window).click(event => {
  445. if (event.target === modal[0]) {
  446. modal.hide();
  447. }
  448. });
  449. return modal;
  450. };
  451.  
  452. const createSettingsModal = () => {
  453. const subtitle = $('<p>').text('Enter the day of the month when you are billed (1-31):');
  454. const input = $('<input>')
  455. .addClass(inputCls)
  456. .attr('type', 'number')
  457. .attr('min', '1')
  458. .attr('max', '31')
  459. .val(GM_getValue('paymentDay') || '');
  460. const tip = $('<p>')
  461. .addClass('text-sm text-gray-500 mt-1')
  462. .text('You can find your billing date via the "Manage Subscription" button on the left.');
  463. const errorMessage = $('<p>').addClass(errorMessageCls).hide();
  464. const saveAndReload = () => {
  465. const newPaymentDay = parseInt(input.val(), 10);
  466. if (newPaymentDay && newPaymentDay >= 1 && newPaymentDay <= 31) {
  467. GM_setValue('paymentDay', newPaymentDay);
  468. log(`Payment day has been set to: ${newPaymentDay}`);
  469. $c(settingsModalCls).hide();
  470. location.reload();
  471. } else {
  472. errorMessage.text('Invalid input. Please enter a number between 1 and 31.').show();
  473. }
  474. };
  475. const saveButton = $('<button>')
  476. .addClass(buttonCls)
  477. .text('Save & Reload')
  478. .click(saveAndReload);
  479. input.on('keypress', (e) => {
  480. if (e.which === 13) saveAndReload();
  481. });
  482. const madeByText = $('<p>').html( // Updated authors
  483. 'Made with ❤️ by monnef, Sonnet 3.5 & Gemini'
  484. );
  485. const content = $('<div>').append(
  486. subtitle,
  487. $('<div>').addClass(hSpaceSmCls),
  488. input,
  489. tip,
  490. errorMessage,
  491. $('<div>').addClass(hSpaceLgCls),
  492. $('<div>').addClass(flexBetweenCls).append(madeByText, saveButton)
  493. );
  494. return createModal({
  495. className: settingsModalCls,
  496. title: 'Usage Tracker Settings',
  497. content: content
  498. });
  499. };
  500.  
  501. const addSettingsButton = (mainCaption) => {
  502. const settingsButton = $('<button>')
  503. .css({
  504. position: 'absolute',
  505. top: '-10px',
  506. right: '0px',
  507. height: '29.4px',
  508. width: '29.4px',
  509. padding: '0px',
  510. filter: 'invert(1)',
  511. })
  512. .addClass(buttonWhiteCls)
  513. .attr('title', 'Usage Tracker settings')
  514. .append($(`<svg class="lucide lucide-settings" xmlns="http://www.w3.org/2000/svg" width="16px" height="16px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z" /><circle cx="12" cy="12" r="3" /></svg>`).css({ display: 'inline-block', verticalAlign: 'text-bottom' }));
  515.  
  516. settingsButton.click(() => {
  517. log('Usage Tracker settings button clicked.');
  518. $c(settingsModalCls).show();
  519. $c(settingsModalCls).find(`.${inputCls}`).focus();
  520. });
  521.  
  522. const buttonWrapper = $('<div>').css({ position: 'relative', height: '0px' });
  523. buttonWrapper.append(settingsButton);
  524.  
  525. if (mainCaption.length > 0) {
  526. mainCaption.prepend(buttonWrapper);
  527. log('Settings button wrapper added to the page');
  528. } else {
  529. log('Main caption not found, settings button not added');
  530. }
  531. };
  532.  
  533.  
  534. // --- Main Execution Logic ---
  535.  
  536. const state = {
  537. addingUsageTrackerSucceeded: false,
  538. addingUsageTrackerAttempts: 0,
  539. };
  540.  
  541. const ATTEMPTS_LIMIT = 15;
  542. const ATTEMPTS_INTERVAL = 500;
  543. const ATTEMPTS_MAX_DELAY = 4000;
  544.  
  545. const main = () => {
  546. state.addingUsageTrackerAttempts++;
  547. log(`Attempt ${state.addingUsageTrackerAttempts}...`);
  548.  
  549. const scheduleNextAttempt = () => {
  550. if (!state.addingUsageTrackerSucceeded && state.addingUsageTrackerAttempts < ATTEMPTS_LIMIT) {
  551. const delay = Math.min(ATTEMPTS_INTERVAL * (state.addingUsageTrackerAttempts), ATTEMPTS_MAX_DELAY);
  552. log(`Attempt ${state.addingUsageTrackerAttempts} failed or incomplete. Retrying in ${delay}ms...`);
  553. setTimeout(main, delay);
  554. } else if (state.addingUsageTrackerSucceeded) {
  555. log(`Attempt ${state.addingUsageTrackerAttempts} succeeded.`);
  556. } else {
  557. error(`All ${ATTEMPTS_LIMIT} attempts failed. Could not add Usage Tracker. Check selectors or page structure.`);
  558. }
  559. };
  560.  
  561. const decorationOkay = decorateUsageCard();
  562. if (!decorationOkay) {
  563. debug('Decoration failed. Will retry.');
  564. scheduleNextAttempt();
  565. return;
  566. }
  567.  
  568. const usageEventsTable = $('p:contains("Recent Usage Events")').closest('div').find('table tbody tr');
  569. if (usageEventsTable.length === 0) {
  570. debug("'Recent Usage Events' table not found yet. Will retry.");
  571. scheduleNextAttempt();
  572. return;
  573. }
  574. debug(`Found ${usageEventsTable.length} usage events.`);
  575.  
  576.  
  577. try {
  578. state.addingUsageTrackerSucceeded = addUsageTracker();
  579. } catch (e) {
  580. error("Error during addUsageTracker:", e);
  581. state.addingUsageTrackerSucceeded = false;
  582. }
  583.  
  584. scheduleNextAttempt();
  585. };
  586.  
  587. $(document).ready(() => {
  588. log('Script started');
  589. unsafeWindow.ut = { // For manual debugging
  590. jq: $,
  591. resetSettings: () => {
  592. GM_setValue('paymentDay', undefined);
  593. location.reload();
  594. },
  595. parseEvents: parseUsageEventsTable,
  596. parseCosts: parseCurrentUsageCosts,
  597. getBase: getBaseUsageData,
  598. };
  599. $('head').append($('<style>').text(styles));
  600. $('body').append(createSettingsModal());
  601. setTimeout(main, ATTEMPTS_INTERVAL); // Initial delay
  602. });
  603.  
  604. })();