Cursor Dashboard Enhancer

Fixed chart selector to handle dynamic chart container IDs, making it work on date range changes.

  1. // ==UserScript==
  2. // @name Cursor Dashboard Enhancer
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.0
  5. // @description Fixed chart selector to handle dynamic chart container IDs, making it work on date range changes.
  6. // @author Gemini 2.5 Pro, NoahBPeterson
  7. // @match https://www.cursor.com/dashboard
  8. // @match https://cursor.com/dashboard
  9. // @grant none
  10. // @run-at document-idle
  11. // @icon https://www.cursor.com/favicon-48x48.png
  12. // @license MIT
  13. // ==/UserScript==
  14.  
  15. (function() { // Main Tampermonkey IIFE START
  16. 'use_strict';
  17.  
  18. let injectionDone = false;
  19. // This is the key fix: Use a generic attribute selector.
  20. const CHART_HOST_SELECTOR = 'div[data-highcharts-chart]';
  21.  
  22. const DATE_RANGE_BUTTON_CONTAINER_SELECTOR = '.min-w-\\[180px\\] .flex.items-center.gap-2';
  23. const DATE_RANGE_BUTTON_SELECTOR = 'button';
  24. const METRIC_CARDS_PARENT_SELECTOR = '.mb-4.grid.grid-cols-2';
  25. const METRIC_CARD_SELECTOR = 'div.flex.cursor-pointer';
  26. const DELAY_AFTER_MOUSEOVER_FOR_POLL_MS = 50;
  27.  
  28. let mainChartContentObserver = null;
  29. let initialElementsObserver = null;
  30. let eventTriggerDebounceTimer = null;
  31.  
  32. let handlePotentialChartChangeGlobal;
  33. let setupDynamicListenersGlobal;
  34. let fetchInvoiceDataAndApply;
  35. let injectAndInitializeGlobal;
  36. let triggerFakeMouseInteraction;
  37.  
  38.  
  39. const codeToInject = function() {
  40.  
  41. // Use the same generic selector inside the injected code.
  42. const chartContainerSelector_Injected = 'div[data-highcharts-chart]';
  43. const pathSelector = 'path.highcharts-graph';
  44. const POLLING_INTERVAL_MS = 75;
  45. let persistentPollTimer = null;
  46. const HC = window._Highcharts || window.Highcharts;
  47.  
  48. function getUTCDateKey_v1733(timestamp) {
  49. const date = new Date(timestamp);
  50. const year = date.getUTCFullYear();
  51. const month = (date.getUTCMonth() + 1).toString().padStart(2, '0');
  52. const day = date.getUTCDate().toString().padStart(2, '0');
  53. return `${year}-${month}-${day}`;
  54. }
  55.  
  56. function parseMonthDayCategory_v1733(categoryString, referenceYear) {
  57. if (typeof categoryString !== 'string') return null;
  58. const dateMatch = categoryString.match(/([a-zA-Z]+)\s+(\d+)/i);
  59. if (dateMatch) {
  60. const monthNames = ["jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"];
  61. const monthIndex = monthNames.indexOf(dateMatch[1].toLowerCase());
  62. if (monthIndex !== -1) {
  63. const day = parseInt(dateMatch[2], 10);
  64. if (!isNaN(day) && day >= 1 && day <= 31) {
  65. return { year: referenceYear, month: monthIndex, day: day };
  66. }
  67. }
  68. }
  69. return null;
  70. }
  71.  
  72. function buildDateToXValueMap_v1733(chart, referenceYearForCategories) {
  73. const dateToXMap = new Map();
  74. let earliestDateFound = null;
  75. let latestDateFound = null;
  76.  
  77. if (!chart) { console.warn("MapBuilder_v1733: Chart object is null."); return { map: dateToXMap, firstDate: null, lastDate: null }; }
  78. if (!chart.series || chart.series.length === 0) { console.warn("MapBuilder_v1733: Chart has no series."); return { map: dateToXMap, firstDate: null, lastDate: null }; }
  79. if (!chart.xAxis || !chart.xAxis[0]) { console.warn("MapBuilder_v1733: Chart has no xAxis[0]."); return { map: dateToXMap, firstDate: null, lastDate: null }; }
  80.  
  81. const existingSeries = chart.series.find(s => s.visible && s.data && s.data.length > 0 && s.xAxis);
  82. if (!existingSeries) { console.warn("MapBuilder_v1733: No suitable existing series found."); return { map: dateToXMap, firstDate: null, lastDate: null }; }
  83.  
  84. const xAxis = existingSeries.xAxis;
  85.  
  86. existingSeries.data.forEach((point) => {
  87. if (!point || typeof point.x === 'undefined') return;
  88.  
  89. let dateKeyToStore;
  90. let displayCategory = typeof point.category === 'string' ? point.category : (xAxis.categories && xAxis.categories[point.x]);
  91. let pointDateForMinMax = null;
  92.  
  93. if (xAxis.options.type === 'datetime') {
  94. pointDateForMinMax = new Date(point.x);
  95. dateKeyToStore = getUTCDateKey_v1733(point.x);
  96. } else if (typeof displayCategory === 'string') {
  97. const parsedCatDate = parseMonthDayCategory_v1733(displayCategory, referenceYearForCategories);
  98. if (parsedCatDate) {
  99. pointDateForMinMax = new Date(Date.UTC(parsedCatDate.year, parsedCatDate.month, parsedCatDate.day));
  100. dateKeyToStore = getUTCDateKey_v1733(pointDateForMinMax.getTime());
  101. } else if (displayCategory.match(/^\d{4}-\d{2}-\d{2}$/)) {
  102. dateKeyToStore = displayCategory;
  103. const parts = displayCategory.split('-');
  104. if (parts.length === 3) pointDateForMinMax = new Date(Date.UTC(parseInt(parts[0],10), parseInt(parts[1],10)-1, parseInt(parts[2],10)));
  105. }
  106. } else if (Array.isArray(xAxis.categories) && xAxis.categories[point.x]) {
  107. displayCategory = xAxis.categories[point.x];
  108. const parsedCatDate = parseMonthDayCategory_v1733(displayCategory, referenceYearForCategories);
  109. if (parsedCatDate) {
  110. pointDateForMinMax = new Date(Date.UTC(parsedCatDate.year, parsedCatDate.month, parsedCatDate.day));
  111. dateKeyToStore = getUTCDateKey_v1733(pointDateForMinMax.getTime());
  112. } else if (typeof displayCategory === 'string' && displayCategory.match(/^\d{4}-\d{2}-\d{2}$/)) {
  113. dateKeyToStore = displayCategory;
  114. const parts = displayCategory.split('-');
  115. if (parts.length === 3) pointDateForMinMax = new Date(Date.UTC(parseInt(parts[0],10), parseInt(parts[1],10)-1, parseInt(parts[2],10)));
  116. }
  117. }
  118.  
  119. if (pointDateForMinMax && !isNaN(pointDateForMinMax.getTime())) {
  120. if (!earliestDateFound || pointDateForMinMax.getTime() < earliestDateFound.getTime()) {
  121. earliestDateFound = new Date(pointDateForMinMax.getTime());
  122. }
  123. if (!latestDateFound || pointDateForMinMax.getTime() > latestDateFound.getTime()) {
  124. latestDateFound = new Date(pointDateForMinMax.getTime());
  125. }
  126. }
  127.  
  128. if (dateKeyToStore && !dateToXMap.has(dateKeyToStore)) {
  129. dateToXMap.set(dateKeyToStore, point.x);
  130. }
  131. });
  132. return { map: dateToXMap, firstDate: earliestDateFound, lastDate: latestDateFound };
  133. }
  134. window.buildDateToXValueMap_v1733 = buildDateToXValueMap_v1733;
  135.  
  136. function parsePathD(d){const p=[];if(!d)return p;const c=d.match(/[MLHVCSQTAZ][^MLHVCSQTAZ]*/ig);if(!c)return p;let x=0,y=0;c.forEach(s=>{const t=s[0],a=s.substring(1).trim();if(t.toUpperCase()!=="Z"&&!a&&s.length>1)return;const e=a?a.split(/[\s,]+/).map(parseFloat):[];if(e.some(isNaN))return;if(t==="M"||t==="m")for(let n=0;n<e.length;n+=2){if(n+1>=e.length)break;let o=e[n],r=e[n+1];t==="m"&&(o+=x,r+=y),p.push({x:o,y:r}),x=o,y=r}else if(t==="L"||t==="l")for(let n=0;n<e.length;n+=2){if(n+1>=e.length)break;let o=e[n],r=e[n+1];t==="l"&&(o+=x,r+=y),p.push({x:o,y:r}),x=o,y=r}else if(t==="H"||t==="h")for(let n=0;n<e.length;n++){let o=e[n];t==="h"&&(o+=x),p.push({x:o,y:y}),x=o}else if(t==="V"||t==="v")for(let n=0;n<e.length;n++){let o=e[n];t==="v"&&(o+=y),p.push({x:x,y:o}),y=o}});return p};
  137. function getControlPoints(p0,p1,p2,p3){const x1=p1.x+(p2.x-p0.x)/6,y1=p1.y+(p2.y-p0.y)/6,x2=p2.x-(p3.x-p1.x)/6,y2=p2.y-(p3.y-p1.y)/6;return[{x:x1,y:y1},{x:x2,y:y2}]};
  138. function pointsToSplinePath(p){if(p.length<2)return"";let d=`M ${p[0].x.toFixed(3)} ${p[0].y.toFixed(3)}`;if(p.length===2)return d+=` L ${p[1].x.toFixed(3)} ${p[1].y.toFixed(3)}`;for(let i=0;i<p.length-1;i++){const p0=p[i===0?0:i-1],p1=p[i],p2=p[i+1],p3=p[i+2<p.length?i+2:p.length-1],cps=getControlPoints(p0,p1,p2,p3);d+=` C ${cps[0].x.toFixed(3)} ${cps[0].y.toFixed(3)}, ${cps[1].x.toFixed(3)} ${cps[1].y.toFixed(3)}, ${p2.x.toFixed(3)} ${p2.y.toFixed(3)}`}return d};
  139.  
  140. function smoothAllChartPaths() {
  141. const el = document.querySelector(chartContainerSelector_Injected);
  142. if (!el || !HC) return;
  143. el.querySelectorAll(pathSelector).forEach((pEl) => {
  144. const d = pEl.getAttribute('d');
  145. if (!d || d.toUpperCase().includes('C') || pEl.dataset.smoothedByV1733 === 'true') return;
  146. const pts = parsePathD(d);
  147. if (pts.length < 2) return;
  148. const nD = pointsToSplinePath(pts);
  149. if (nD && nD !== d) { pEl.setAttribute('d', nD); pEl.dataset.smoothedByV1733 = 'true';}
  150. });
  151. }
  152. if (persistentPollTimer) clearInterval(persistentPollTimer);
  153. persistentPollTimer = setInterval(smoothAllChartPaths, POLLING_INTERVAL_MS);
  154. window.forceChartCheckBySmoother_v1733 = smoothAllChartPaths;
  155.  
  156. function ensureSecondaryYAxis_v1733(chart, axisId, axisTitle) {
  157. let yAxisObj = chart.yAxis.find(axis => axis.options.id === axisId);
  158. if (!yAxisObj) {
  159. yAxisObj = chart.addAxis({ id: axisId, title: { text: axisTitle }, opposite: true, gridLineWidth: 0, min: 0 }, false, true);
  160. yAxisObj = chart.yAxis.find(axis => axis.options.id === axisId);
  161. }
  162. if (!yAxisObj || typeof yAxisObj.index === 'undefined') {
  163. console.error(`Injected Script (v1.7.33): Y-axis '${axisId}' problem. Index: ${yAxisObj ? yAxisObj.index : 'N/A'}.`);
  164. return -1;
  165. }
  166. return yAxisObj.index;
  167. }
  168. window.ensureSecondaryYAxis_v1733 = ensureSecondaryYAxis_v1733;
  169.  
  170. window.logInjectedChartAxes_v1733=function(chart,stage){if(chart&&chart.xAxis){chart.xAxis.forEach((axis,i)=>{console.log(` xAxis[${i}]: id='${axis.options.id}', type='${axis.options.type}', index=${axis.index}, categories=${axis.categories?axis.categories.length:"none"}, min=${axis.min}, max=${axis.max}, dataMin=${axis.dataMin}, dataMax=${axis.dataMax}`)})}else{console.log(" No xAxes found.")}if(chart&&chart.yAxis){chart.yAxis.forEach((axis,i)=>{console.log(` yAxis[${i}]: id='${axis.options.id}', title='${axis.options.title?.text}', index=${axis.index}, opposite=${axis.options.opposite}, min=${axis.min}, max=${axis.max}`)})}else{console.log(" No yAxes found.")}};
  171.  
  172. function processAndAddDailyUsageSeries_v1733(responseData, seriesConfig, dateToXValueMapParam) {
  173. if(!HC||!HC.charts)return!1;const chart=HC.charts.find(c=>c&&c.renderTo===document.querySelector(chartContainerSelector_Injected));if(!chart||!chart.xAxis||!chart.xAxis[0])return!1;
  174.  
  175. let dailyCountsPerDateKey = {};
  176. if (responseData && Array.isArray(responseData.usageEvents)) {
  177. responseData.usageEvents.forEach(event => {
  178. let tsStr = event.timestamp;
  179. if (typeof tsStr !== 'string' || tsStr.length === 0) return;
  180. let tsNum = parseInt(tsStr, 10);
  181. if (!isNaN(tsNum) && tsNum > 0) {
  182. const dateKey = getUTCDateKey_v1733(tsNum);
  183. dailyCountsPerDateKey[dateKey] = (dailyCountsPerDateKey[dateKey] || 0) + 1;
  184. }
  185. });
  186. }
  187.  
  188. let transformedDataPoints = [];
  189. dateToXValueMapParam.forEach((originalX, dateKey_fromMap) => {
  190. const count = dailyCountsPerDateKey[dateKey_fromMap] || 0;
  191. transformedDataPoints.push([originalX, count]);
  192. });
  193.  
  194. if (transformedDataPoints.length === 0 && dateToXValueMapParam.size > 0) {
  195. console.warn(`DailyUsage_v1733: No data points generated for ${seriesConfig.name}, map had ${dateToXValueMapParam.size} entries.`);
  196. } else if (transformedDataPoints.length === 0 && dateToXValueMapParam.size === 0) {
  197. console.warn(`DailyUsage_v1733: No data points and map is empty for ${seriesConfig.name}.`);
  198. return false;
  199. }
  200.  
  201. transformedDataPoints.sort((a,b)=>a[0]-b[0]);
  202. const seriesOptions={name:seriesConfig.name,data:transformedDataPoints,type:seriesConfig.type||"line",color:seriesConfig.color,yAxis:0,tooltip:{pointFormatter:function(){const pointXDate = (this.series.xAxis.options.type === 'datetime') ? this.x : (this.series.xAxis.categories ? this.series.xAxis.categories[this.x] : this.x); return `<span style="color:${this.color}">●</span> ${this.series.name}: <b>${this.y}</b> events (${typeof pointXDate === 'number' && this.series.xAxis.options.type === 'datetime' ? HC.dateFormat('%b %e', pointXDate) : pointXDate})<br/>`}},marker:seriesConfig.marker||{enabled:transformedDataPoints.length<30,radius:3}};
  203. let existingSeries=chart.series.find(s=>s.name===seriesConfig.name&&s.yAxis.index===0);existingSeries?existingSeries.update(seriesOptions,!1):chart.addSeries(seriesOptions,!1);return!0;
  204. };
  205. window.processAndAddDailyUsageSeries_v1733 = processAndAddDailyUsageSeries_v1733;
  206.  
  207. function processAndAddDailyCostSeries_v1733(responseData, seriesConfig, dateToXValueMapParam, yAxisIndexForDollars) {
  208. if (!HC || !HC.charts) { return false; }
  209. const chart = HC.charts.find(c => c && c.renderTo === document.querySelector(chartContainerSelector_Injected));
  210. if (!chart) { return false; }
  211. if (typeof yAxisIndexForDollars !== 'number' || yAxisIndexForDollars < 0) { console.error(`DailyCost_v1733: Invalid Y-idx (${yAxisIndexForDollars}).`); return false; }
  212.  
  213. let dailyPriceCentsPerDateKey = {};
  214. if (responseData && Array.isArray(responseData.usageEvents)) {
  215. responseData.usageEvents.forEach(event => {
  216. let tsStr=event.timestamp,priceCents=event.priceCents;
  217. if("string"!=typeof tsStr||0===tsStr.length||"number"!=typeof priceCents)return;
  218. let tsNum=parseInt(tsStr,10);
  219. if(!isNaN(tsNum)&&tsNum>0){
  220. const dateKey = getUTCDateKey_v1733(tsNum);
  221. dailyPriceCentsPerDateKey[dateKey]=(dailyPriceCentsPerDateKey[dateKey]||0)+priceCents;
  222. }
  223. });
  224. }
  225.  
  226. let transformedDataPoints = [];
  227. dateToXValueMapParam.forEach((originalX, dateKey_fromMap) => {
  228. const costInCents = dailyPriceCentsPerDateKey[dateKey_fromMap] || 0;
  229. transformedDataPoints.push([originalX, parseFloat((costInCents / 100).toFixed(2))]);
  230. });
  231.  
  232. if (transformedDataPoints.length === 0 && dateToXValueMapParam.size > 0) {
  233. console.warn(`DailyCost_v1733: No data points generated for ${seriesConfig.name}, map had ${dateToXValueMapParam.size} entries.`);
  234. } else if (transformedDataPoints.length === 0 && dateToXValueMapParam.size === 0) {
  235. console.warn(`DailyCost_v1733: No data points and map is empty for ${seriesConfig.name}.`);
  236. return false;
  237. }
  238.  
  239. transformedDataPoints.sort((a,b)=>a[0]-b[0]);
  240. const seriesOptions={name:seriesConfig.name,data:transformedDataPoints,type:seriesConfig.type||"line",color:seriesConfig.color,yAxis:yAxisIndexForDollars,tooltip:{pointFormatter:function(){const pointXDate = (this.series.xAxis.options.type === 'datetime') ? this.x : (this.series.xAxis.categories ? this.series.xAxis.categories[this.x] : this.x); return `<span style="color:${this.color}">●</span> ${this.series.name}: <b>$${this.y.toFixed(2)}</b> (${typeof pointXDate === 'number' && this.series.xAxis.options.type === 'datetime' ? HC.dateFormat('%b %e', pointXDate) : pointXDate})<br/>`}},marker:seriesConfig.marker||{enabled:transformedDataPoints.length<30,radius:2}};
  241. console.log(`Injected Script (v1.7.33): LOGGING DATA for series '${seriesConfig.name}' (points: ${seriesOptions.data.length}, yAxisIndex: ${seriesOptions.yAxis}): First 5:`,JSON.parse(JSON.stringify(seriesOptions.data.slice(0,5))));
  242. let existingSeries=chart.series.find(s=>s.name===seriesConfig.name&&s.yAxis.index===yAxisIndexForDollars);existingSeries?existingSeries.update(seriesOptions,!1):chart.addSeries(seriesOptions,!1);return true;
  243. };
  244. window.processAndAddDailyCostSeries_v1733 = processAndAddDailyCostSeries_v1733;
  245.  
  246. function processAndAddPricingSeries_v1733(responseData, dateToXValueMapParam) {
  247. const HC_local=window._Highcharts||window.Highcharts;if(!HC_local||!responseData||!responseData.pricingDescription||"string"!=typeof responseData.pricingDescription.description)return!1;const pricingString=responseData.pricingDescription.description,chart=HC_local.charts.find(c=>c&&c.renderTo===document.querySelector(chartContainerSelector_Injected));if(!chart||!chart.xAxis||!chart.xAxis[0])return!1;const includedRequestsMatch=pricingString.match(/(\d+)\s+requests\s+per\s+day\s+included/i);let pricingSeriesChanged=!1;if(includedRequestsMatch&&includedRequestsMatch[1]){const includedRequests=parseInt(includedRequestsMatch[1],10),xAxis=chart.xAxis[0],extremes=xAxis.getExtremes();let xMin=xAxis.min,xMax=xAxis.max;if("number"!=typeof xMin||"number"!=typeof xMax||xMin===xMax)if(dateToXValueMapParam&&dateToXValueMapParam.size>0){const mappedXValues=Array.from(dateToXValueMapParam.values()).filter(x=>"number"==typeof x);mappedXValues.length>0?(xMin=Math.min(...mappedXValues),xMax=Math.max(...mappedXValues)):(xMin=extremes.dataMin,xMax=extremes.dataMax)}else xMin=extremes.dataMin,xMax=extremes.dataMax;if("number"!=typeof xMin||"number"!=typeof xMax||xMin===xMax)xMin=0,xMax=dateToXValueMapParam.size>0?dateToXValueMapParam.size-1:chart.xAxis[0].categories?chart.xAxis[0].categories.length-1:10;if("number"==typeof xMin&&"number"==typeof xMax&&xMax>=xMin){const quotaData=[[xMin,includedRequests],[xMax,includedRequests]],seriesName="Daily Included Requests Quota",seriesOptions={name:seriesName,data:quotaData,type:"line",color:"forestgreen",dashStyle:"shortdash",marker:{enabled:!1},zIndex:1,yAxis:0};console.log(`Injected Script (v1.7.33): LOGGING DATA for series '${seriesName}' (points: ${seriesOptions.data.length}, yAxisIndex: ${seriesOptions.yAxis}):`,JSON.parse(JSON.stringify(seriesOptions.data.slice(0,5))));let existingQuotaSeries=chart.series.find(s=>s.name===seriesName&&s.yAxis.index===0);existingQuotaSeries?existingQuotaSeries.update(seriesOptions,!1):chart.addSeries(seriesOptions,!1),pricingSeriesChanged=!0}}return pricingSeriesChanged
  248. };
  249. window.processAndAddPricingSeries_v1733 = processAndAddPricingSeries_v1733;
  250.  
  251. if(window.smoothCursorChartPollerReadyCallbacks_v1733) {
  252. window.smoothCursorChartPollerReadyCallbacks_v1733.forEach(cb => cb());
  253. delete window.smoothCursorChartPollerReadyCallbacks_v1733;
  254. }
  255. window.isSmoothCursorChartPollerReady_v1733 = true;
  256. console.log('Injected Script (v1.7.33): Initialized. HC object available:', !!HC);
  257. };
  258.  
  259. async function fetchAndProcessMonthData(month, year, contextChart, isPrimaryCall) {
  260. const requestPayload = { month, year, includeUsageEvents: true };
  261. try {
  262. const response = await fetch('/api/dashboard/get-monthly-invoice', {
  263. method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': '*/*', },
  264. body: JSON.stringify(requestPayload)
  265. });
  266. if (!response.ok) { console.error(`TM Script (v1.7.33): HTTP error ${response.status} for ${year}-${month+1}`); return null; }
  267. const data = await response.json();
  268. data.requestContext = { month, year, isPrimaryCall };
  269. return data;
  270. } catch (error) { console.error(`TM Script (v1.7.33): Network error for ${year}-${month+1}:`, error); return null; }
  271. }
  272.  
  273. fetchInvoiceDataAndApply = async function() {
  274. console.log("Tampermonkey Script (v1.7.33): fetchInvoiceDataAndApply triggered.");
  275. const HC_instance = window._Highcharts || window.Highcharts;
  276. if (!HC_instance) {
  277. console.warn("TM Script (v1.7.33): Highcharts instance not found at start of fetchInvoiceDataAndApply.");
  278. return;
  279. }
  280.  
  281. const initialChartContainer = document.querySelector(CHART_HOST_SELECTOR);
  282. if (!initialChartContainer || !initialChartContainer.isConnected) {
  283. console.warn("TM Script (v1.7.33): Initial chart container not found or disconnected. Aborting.");
  284. return;
  285. }
  286. let chartForMapBuilding = null;
  287. if (HC_instance.charts) {
  288. chartForMapBuilding = HC_instance.charts.find(c => c && c.renderTo === initialChartContainer);
  289. }
  290. if (!chartForMapBuilding) {
  291. console.warn("TM Script (v1.7.33): Initial chart instance for map building not found. Aborting.");
  292. if (HC_instance.charts) HC_instance.charts.forEach((ch,i) => console.log(` Debug: Available chart ${i} renders to ${ch ? ch.renderTo?.id : 'N/A'}`));
  293. return;
  294. }
  295. console.log(`TM Script (v1.7.33): Initial chart acquired (title: "${chartForMapBuilding.options.title?.text || 'N/A'}", categories: ${chartForMapBuilding.xAxis[0].categories ? chartForMapBuilding.xAxis[0].categories.length : 'N/A'}).`);
  296.  
  297. let apiYear = new Date().getFullYear();
  298. let apiMonth = new Date().getMonth();
  299.  
  300. if (typeof window.buildDateToXValueMap_v1733 === 'function') {
  301. const mapInfoResult = window.buildDateToXValueMap_v1733(chartForMapBuilding, apiYear);
  302. if (mapInfoResult && mapInfoResult.lastDate) {
  303. apiYear = mapInfoResult.lastDate.getUTCFullYear();
  304. apiMonth = mapInfoResult.lastDate.getUTCMonth();
  305. console.log(`TM Script (v1.7.33): API call will primarily target month/year from chart's last date: ${apiYear}-${String(apiMonth + 1).padStart(2,'0')}`);
  306. } else {
  307. console.warn(`TM Script (v1.7.33): Defaulting API call. mapInfoResult.lastDate was ${mapInfoResult ? (mapInfoResult.lastDate ? mapInfoResult.lastDate.toISOString().substring(0,10) : 'null') : 'undefined'}`);
  308. }
  309. } else {
  310. console.warn("TM Script (v1.7.33): buildDateToXValueMap_v1733 not found. Defaulting API call params.");
  311. }
  312.  
  313. const apiResponses = [];
  314. console.log(`TM Script (v1.7.33): Fetching primary data for ${apiYear}-${String(apiMonth + 1).padStart(2,'0')}`);
  315. const primaryData = await fetchAndProcessMonthData(apiMonth, apiYear, chartForMapBuilding, true);
  316. if (primaryData) {
  317. apiResponses.push(primaryData);
  318. } else {
  319. console.warn(`TM Script (v1.7.33): Primary data fetch failed for ${apiYear}-${String(apiMonth+1).padStart(2,'0')}. Aborting.`);
  320. return;
  321. }
  322.  
  323. if (typeof window.buildDateToXValueMap_v1733 === 'function') {
  324. const mapInfoResultAgain = window.buildDateToXValueMap_v1733(chartForMapBuilding, apiYear);
  325. if (mapInfoResultAgain && mapInfoResultAgain.firstDate) {
  326. const firstDayOfPrimaryDataMonth = new Date(Date.UTC(apiYear, apiMonth, 1));
  327. if (mapInfoResultAgain.firstDate.getTime() < firstDayOfPrimaryDataMonth.getTime()) {
  328. let prevMonth = apiMonth - 1;
  329. let prevYear = apiYear;
  330. if (prevMonth < 0) { prevMonth = 11; prevYear--; }
  331. console.log(`TM Script (v1.7.33): Chart's earliest date (${mapInfoResultAgain.firstDate.toISOString().substring(0,10)}) is before primary data month's first day (${firstDayOfPrimaryDataMonth.toISOString().substring(0,10)}). Fetching data for ${prevYear}-${String(prevMonth+1).padStart(2,'0')}`);
  332. const secondaryData = await fetchAndProcessMonthData(prevMonth, prevYear, chartForMapBuilding, false);
  333. if (secondaryData) apiResponses.push(secondaryData);
  334. } else {
  335. console.log(`TM Script (v1.7.33): Chart's earliest date (${mapInfoResultAgain.firstDate.toISOString().substring(0,10)}) is NOT before primary data month's first day (${firstDayOfPrimaryDataMonth.toISOString().substring(0,10)}). No secondary fetch needed.`);
  336. }
  337. } else {
  338. console.warn(`TM Script (v1.7.33): Could not determine chart's first date to check for previous month's data need.`);
  339. }
  340. }
  341.  
  342. if (apiResponses.length === 0) { console.warn("TM Script (v1.7.33): No data fetched from any source. Aborting."); return; }
  343.  
  344. const combinedUsageEvents = [];
  345. apiResponses.forEach(resp => { if (resp && resp.usageEvents) combinedUsageEvents.push(...resp.usageEvents); });
  346. console.log(`TM Script (v1.7.33): Combined ${combinedUsageEvents.length} usage events from ${apiResponses.length} API responses.`);
  347.  
  348. const finalResponseData = {
  349. usageEvents: combinedUsageEvents,
  350. items: primaryData.items || [],
  351. pricingDescription: primaryData.pricingDescription || {},
  352. };
  353.  
  354. const finalChartContainer = document.querySelector(CHART_HOST_SELECTOR);
  355. if (!finalChartContainer || !finalChartContainer.isConnected) {
  356. console.warn(`TM Script (v1.7.33): Final chart container not found or disconnected before series processing. Aborting.`);
  357. return;
  358. }
  359. let chartForSeriesProcessing = null;
  360. if (HC_instance.charts) {
  361. chartForSeriesProcessing = HC_instance.charts.find(c => c && c.renderTo === finalChartContainer);
  362. }
  363. if (!chartForSeriesProcessing) {
  364. console.warn(`TM Script (v1.7.33): Chart instance for series processing not found. Aborting.`);
  365. if (HC_instance.charts) HC_instance.charts.forEach((ch,i) => console.log(` Debug: Available chart ${i} renders to ${ch ? ch.renderTo?.id : 'N/A'}, isConnected: ${ch && ch.renderTo ? ch.renderTo.isConnected : 'N/A'}`));
  366. return;
  367. }
  368. console.log(`TM Script (v1.7.33): Chart for series processing acquired (title: "${chartForSeriesProcessing.options.title?.text || 'N/A'}", categories: ${chartForSeriesProcessing.xAxis[0].categories ? chartForSeriesProcessing.xAxis[0].categories.length : 'N/A'}).`);
  369.  
  370. const finalMapBuildResult = typeof window.buildDateToXValueMap_v1733 === 'function' ? window.buildDateToXValueMap_v1733(chartForSeriesProcessing, apiYear) : null;
  371. const dateToXValueMapForSeries = finalMapBuildResult ? finalMapBuildResult.map : new Map();
  372.  
  373. if (!finalMapBuildResult || dateToXValueMapForSeries.size === 0) {
  374. console.warn("TM Script (v1.7.33): Final dateToXValueMap for series is empty or couldn't be built. Aborting series addition.");
  375. return;
  376. }
  377. console.log(`TM Script (v1.7.33): Final dateToXValueMap (size ${dateToXValueMapForSeries.size}) obtained. First: ${finalMapBuildResult.firstDate ? finalMapBuildResult.firstDate.toISOString().substring(0,10) : 'N/A'}, Last: ${finalMapBuildResult.lastDate ? finalMapBuildResult.lastDate.toISOString().substring(0,10) : 'N/A'}`);
  378.  
  379. if (typeof window.logInjectedChartAxes_v1733 === 'function') { window.logInjectedChartAxes_v1733(chartForSeriesProcessing, "Before Any Script Operations This Cycle"); }
  380.  
  381. let yAxisIndexForDollars = -1;
  382. if (typeof window.ensureSecondaryYAxis_v1733 === 'function') {
  383. yAxisIndexForDollars = window.ensureSecondaryYAxis_v1733(chartForSeriesProcessing, 'dollarsYAxis_v1733', 'Cost (Dollars)');
  384. if (yAxisIndexForDollars !== -1) {
  385. const refreshedChart = HC_instance.charts.find(c => c && c.renderTo === finalChartContainer);
  386. if (refreshedChart) {
  387. chartForSeriesProcessing = refreshedChart;
  388. } else {
  389. console.error("TM Script (v1.7.33): Chart lost after Y-axis ensure. Critical error. Aborting.");
  390. return;
  391. }
  392. } else {
  393. console.error("TM Script (v1.7.33): Failed to ensure secondary Y-axis. Aborting.");
  394. return;
  395. }
  396. } else { console.error("TM Script (v1.7.33): ensureSecondaryYAxis_v1733 not found. Aborting."); return; }
  397.  
  398. if (typeof window.logInjectedChartAxes_v1733 === 'function') { window.logInjectedChartAxes_v1733(chartForSeriesProcessing, "After ensureSecondaryYAxis in wrapper"); }
  399.  
  400. let anySeriesDataChanged = false;
  401. if (typeof window.processAndAddDailyUsageSeries_v1733 === 'function') {
  402. if(window.processAndAddDailyUsageSeries_v1733(finalResponseData, { name: 'Daily Event Count', type: 'line', color: '#3498db', }, dateToXValueMapForSeries)) { anySeriesDataChanged = true; }
  403. }
  404. if (typeof window.processAndAddDailyCostSeries_v1733 === 'function') {
  405. if(window.processAndAddDailyCostSeries_v1733(finalResponseData, { name: 'Daily Usage Cost ($)', type: 'line', color: '#e67e22' }, dateToXValueMapForSeries, yAxisIndexForDollars )) { anySeriesDataChanged = true; }
  406. }
  407. if (typeof window.processAndAddPricingSeries_v1733 === 'function') {
  408. if(window.processAndAddPricingSeries_v1733(finalResponseData, dateToXValueMapForSeries)) { anySeriesDataChanged = true; }
  409. }
  410.  
  411. if (anySeriesDataChanged) {
  412. console.log("Tampermonkey Script (v1.7.33): Requesting final chart redraw.");
  413. if (typeof window.logInjectedChartAxes_v1733 === 'function') { window.logInjectedChartAxes_v1733(chartForSeriesProcessing, "After All Series Processing, Before Final Redraw"); }
  414. chartForSeriesProcessing.redraw();
  415. if (typeof window.logInjectedChartAxes_v1733 === 'function') { setTimeout(() => { const finalChart = HC_instance.charts.find(c => c && c.renderTo === finalChartContainer); if(finalChart) window.logInjectedChartAxes_v1733(finalChart, "After Final Redraw"); }, 100); }
  416. setTimeout(() => window.forceChartCheckBySmoother_v1733 && window.forceChartCheckBySmoother_v1733(), 200);
  417. } else { console.log("Tampermonkey Script (v1.7.33): No series data changes required a final redraw."); }
  418. };
  419.  
  420. triggerFakeMouseInteraction=function(elementSelector){const targetElement=document.querySelector(elementSelector);if(targetElement){const e=new MouseEvent("mouseenter",{bubbles:!0,cancelable:!0,view:window}),t=new MouseEvent("mouseover",{bubbles:!0,cancelable:!0,view:window}),n=targetElement.querySelector('div[id^="highcharts-"]');(n||targetElement).dispatchEvent(e),(n||targetElement).dispatchEvent(t)}};
  421. handlePotentialChartChangeGlobal=function(){triggerFakeMouseInteraction(CHART_HOST_SELECTOR);clearTimeout(eventTriggerDebounceTimer);eventTriggerDebounceTimer=setTimeout(()=>{if("function"==typeof window.forceChartCheckBySmoother_v1733){const e=document.querySelector(CHART_HOST_SELECTOR);e&&e.querySelectorAll("path.highcharts-graph").forEach(el=>{el.dataset.smoothedByV1733="false"}),window.forceChartCheckBySmoother_v1733()}},DELAY_AFTER_MOUSEOVER_FOR_POLL_MS)};
  422. setupDynamicListenersGlobal=function(){if(mainChartContentObserver)mainChartContentObserver.disconnect();const e=document.querySelector(CHART_HOST_SELECTOR);if(e){mainChartContentObserver=new MutationObserver(()=>{window.handlePotentialChartChangeGlobal()});mainChartContentObserver.observe(e,{childList:!0,subtree:!0})}const t=[document.querySelector(METRIC_CARDS_PARENT_SELECTOR),document.querySelector(DATE_RANGE_BUTTON_CONTAINER_SELECTOR)].filter(el=>el);t.forEach(el=>{const listenerKey=`_hasSmoothClickListener_v1733_${el.id||el.className.replace(/\s+/g,"_").substring(0,30)}`;if(!el[listenerKey]){el.addEventListener("click",event=>{console.log(`TM Script (v1.7.33): Click detected on interactive element (class: ${el.className}), scheduling full update.`);let currentTarget=event.target;const relevantSelector=el.matches(METRIC_CARDS_PARENT_SELECTOR)?METRIC_CARD_SELECTOR:DATE_RANGE_BUTTON_SELECTOR;while(currentTarget&&currentTarget!==el){if(currentTarget.matches&&currentTarget.matches(relevantSelector)){window.handlePotentialChartChangeGlobal();setTimeout(()=>window.fetchInvoiceDataAndApply(),500);break}currentTarget=currentTarget.parentElement}});el[listenerKey]=!0}})}
  423. injectAndInitializeGlobal=function(){
  424. if(injectionDone)return;
  425. if(document.body){
  426. const scriptId="tampermonkeyChartLogic_v1_7_33";
  427. const existingScript=document.getElementById(scriptId);if(existingScript)existingScript.remove();
  428. const scriptEl=document.createElement("script");scriptEl.id=scriptId;scriptEl.type="text/javascript";
  429. scriptEl.textContent=`(${codeToInject.toString()})();`;document.body.appendChild(scriptEl);injectionDone=!0;
  430. window.smoothCursorChartPollerReadyCallbacks_v1733=window.smoothCursorChartPollerReadyCallbacks_v1733||[];
  431. const initCallback=()=>{console.log("TM Script (v1.7.33): Injected code ready, running initial setup.");window.handlePotentialChartChangeGlobal();window.setupDynamicListenersGlobal();window.fetchInvoiceDataAndApply()};
  432. window.smoothCursorChartPollerReadyCallbacks_v1733.push(initCallback);
  433. if(window.isSmoothCursorChartPollerReady_v1733&&"function"==typeof window.forceChartCheckBySmoother_v1733){
  434. console.log("TM Script (v1.7.33): Injected code was already ready, executing pending callbacks.");
  435. window.smoothCursorChartPollerReadyCallbacks_v1733.forEach(cb=>cb());
  436. delete window.smoothCursorChartPollerReadyCallbacks_v1733;
  437. }
  438. setTimeout(()=>{const tempScript=document.getElementById(scriptId);if(tempScript&&tempScript.parentNode)tempScript.parentNode.removeChild(tempScript)},2000)
  439. }else setTimeout(window.injectAndInitializeGlobal,200)
  440. };
  441. window.handlePotentialChartChangeGlobal = handlePotentialChartChangeGlobal;
  442. window.setupDynamicListenersGlobal = setupDynamicListenersGlobal;
  443. window.fetchInvoiceDataAndApply = fetchInvoiceDataAndApply;
  444. window.injectAndInitializeGlobal = injectAndInitializeGlobal;
  445.  
  446. initialElementsObserver=new MutationObserver((mutations,observer)=>{const chartHost=document.querySelector(CHART_HOST_SELECTOR),dateButtons=document.querySelector(DATE_RANGE_BUTTON_CONTAINER_SELECTOR),metricCards=document.querySelector(METRIC_CARDS_PARENT_SELECTOR);if(chartHost&&(dateButtons||metricCards)){observer.disconnect();initialElementsObserver=null;window.injectAndInitializeGlobal()}});
  447. const chartHostInitial=document.querySelector(CHART_HOST_SELECTOR),dateButtonsInitial=document.querySelector(DATE_RANGE_BUTTON_CONTAINER_SELECTOR),metricCardsInitial=document.querySelector(METRIC_CARDS_PARENT_SELECTOR);
  448. if(chartHostInitial&&(dateButtonsInitial||metricCardsInitial)){console.log("TM Script (v1.7.33): Initial elements already present, injecting script immediately.");if(initialElementsObserver){initialElementsObserver.disconnect();initialElementsObserver=null}window.injectAndInitializeGlobal()}else if(initialElementsObserver){initialElementsObserver.observe(document.documentElement,{childList:!0,subtree:true});}
  449.  
  450. })();