最近也是 DeepSeek 用起来了,看到数据的时候总忍不住想算算缓存命中的情况,但是手动算是不可能滴,脚本启动!
ds 缓存计算脚本
// ==UserScript==
// @name DeepSeek Usage - Cache Hit Rate Monitor
// @namespace https://platform.deepseek.com
// @version 1.0.0
// @description 监测 DeepSeek 用量页面的 Prompt 缓存命中率,以双胶囊形式显示累积命中率和单日命中率(hover 图表时自动更新单日数据)
// @author DeepSeek-V4-Pro
// @match https://platform.deepseek.com/*
// @grant none
// @connect platform.deepseek.com
// @run-at document-start
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// ========== State ==========
var modelData = new Map(); // Map<modelName, {total, daily}>
var uiUpdateTimer = null;
var dailyDate = ''; // currently shown daily date (yyyy-mm-dd)
// ========== Utility ==========
function computeRate(hit, miss) {
var total = hit + miss;
if (total === 0) return null;
return (hit / total) * 100;
}
function getAmount(usageArr, type) {
var entry = (usageArr || []).find(function(u) { return u.type === type; });
return entry ? parseInt(entry.amount, 10) || 0 : 0;
}
function formatRate(rate) {
if (rate === null || rate === undefined) return 'All Cache: N/A';
return 'All Cache: ' + rate.toFixed(1) + '%';
}
function rateClass(rate) {
if (rate === null || rate === undefined) return 'cache-na';
if (rate >= 80) return 'cache-high';
if (rate >= 50) return 'cache-mid';
return '';
}
function todayStr() {
var d = new Date();
var m = d.getMonth() + 1;
var day = d.getDate();
return d.getFullYear() + '-' + (m < 10 ? '0' : '') + m + '-' + (day < 10 ? '0' : '') + day;
}
function dateLabel(iso) {
// 2026-05-15 -> 5/15
var parts = iso.split('-');
return parseInt(parts[1], 10) + '/' + parseInt(parts[2], 10);
}
function getDefaultDailyDate() {
// Prefer today, fallback to last day in data
var t = todayStr();
var iter = modelData.values();
var entry = iter.next();
while (entry && !entry.done) {
if (entry.value.daily.has(t)) return t;
entry = iter.next();
}
// No today, pick the max date from first model
var first = modelData.values().next();
if (first.done) return t;
var dates = Array.from(first.value.daily.keys()).sort();
return dates.length > 0 ? dates[dates.length - 1] : t;
}
// ========== Data Processing (called by interceptors) ==========
function processApiData(bizData) {
var dataMap = new Map();
// Cumulative (total) data
(bizData.total || []).forEach(function(entry) {
var name = entry.model;
var hit = getAmount(entry.usage, 'PROMPT_CACHE_HIT_TOKEN');
var miss = getAmount(entry.usage, 'PROMPT_CACHE_MISS_TOKEN');
dataMap.set(name, {
total: { hit: hit, miss: miss, rate: computeRate(hit, miss) },
daily: new Map()
});
});
// Daily data
(bizData.days || []).forEach(function(day) {
var date = day.date;
(day.data || []).forEach(function(entry) {
var name = entry.model;
if (!dataMap.has(name)) return;
var hit = getAmount(entry.usage, 'PROMPT_CACHE_HIT_TOKEN');
var miss = getAmount(entry.usage, 'PROMPT_CACHE_MISS_TOKEN');
dataMap.get(name).daily.set(date, {
hit: hit, miss: miss, rate: computeRate(hit, miss)
});
});
});
return dataMap;
}
function scheduleUIUpdate() {
clearTimeout(uiUpdateTimer);
uiUpdateTimer = setTimeout(updateUI, 300);
}
function updateUI() {
if (modelData.size === 0) return;
clearTimeout(resetTimer);
resetTimer = null;
dailyDate = getDefaultDailyDate();
injectAllCapsules();
}
// ========== XHR + fetch Interception ==========
function handleApiResponse(json) {
if (!json || json.code !== 0 || !json.data || !json.data.biz_data) return;
var bizData = json.data.biz_data;
modelData = processApiData(bizData);
scheduleUIUpdate();
}
// --- Intercept XMLHttpRequest ---
var origSend = XMLHttpRequest.prototype.send;
var origOpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function(method, url) {
this._ds_url = url;
return origOpen.apply(this, arguments);
};
XMLHttpRequest.prototype.send = function() {
var self = this;
var url = self._ds_url;
if (url && url.indexOf('/usage/amount') !== -1) {
self.addEventListener('load', function() {
try {
var json = JSON.parse(self.responseText);
handleApiResponse(json);
} catch(e) {}
});
}
return origSend.apply(this, arguments);
};
// --- Intercept fetch ---
if (typeof window.fetch !== 'undefined') {
var origFetch = window.fetch;
window.fetch = function(url, options) {
return origFetch.apply(this, arguments).then(function(response) {
if (typeof url === 'string' && url.indexOf('/usage/amount') !== -1) {
var cloned = response.clone();
cloned.json().then(function(json) {
handleApiResponse(json);
}).catch(function() {});
}
return response;
});
};
}
// ========== DOM-dependent code (deferred) ==========
function onDomReady() {
injectCSS();
setupGlobalTooltipObserver();
waitForReact();
}
function injectCSS() {
var s = document.createElement('style');
s.textContent =
'.ds-cache-hit-badge{' +
'display:inline-block;padding:2px 10px;margin-left:8px;' +
'font-size:12px;line-height:1.5;font-weight:500;' +
'border-radius:20px;white-space:nowrap;vertical-align:middle;' +
'transition:background 0.15s ease;' +
'color:rgb(var(--ds-rgb-primary,0 0 0));' +
'background:rgb(var(--ds-rgb-elevated,255 255 255));' +
'border:1px solid rgb(var(--ds-rgb-separator,210 216 229));}' +
'.ds-cache-hit-badge.cache-na{' +
'color:rgb(var(--ds-rgb-label-2,144 148 162));border-style:dashed;}' +
'.ds-cache-hit-badge.cache-high{color:#16A34A;border-color:#16A34A;}' +
'.ds-cache-hit-badge.cache-mid{color:#D97706;border-color:#D97706;}';
(document.head || document.documentElement).appendChild(s);
}
function waitForReact() {
var attempts = 0;
function check() {
var sel = document.querySelector('select.ds-native-select__select');
var spans = document.querySelectorAll('span.ds-text--monospace.ds-text--fm.ds-text--linherit');
if (sel && spans.length > 0) {
setupMonthWatcher();
setupPageObserver();
// If we already have data (interceptor caught the API call), update UI now
if (modelData.size > 0) {
updateUI();
} else {
injectAllCapsules();
}
return;
}
if (++attempts < 20) {
setTimeout(check, 500);
}
}
check();
}
// ========== DOM Query ==========
function findTokenSections() {
var modelSpans = document.querySelectorAll(
'span.ds-text--monospace.ds-text--fm.ds-text--linherit');
var sections = [];
modelSpans.forEach(function(span) {
var modelName = span.textContent.trim();
var headerBar = span.closest('._6926780');
if (!headerBar) return;
var chartBody = headerBar.nextElementSibling;
if (!chartBody || !chartBody.classList.contains('_06da35f')) return;
var outerFlexes = chartBody.querySelectorAll(
'.ds-flex[style*="flex-direction: column"]');
var tokenContainer = null;
var echartsDiv = null;
outerFlexes.forEach(function(outerFlex) {
var innerFlexes = outerFlex.querySelectorAll(
'.ds-flex[style*="align-items: baseline"]');
innerFlexes.forEach(function(innerFlex) {
var label = innerFlex.querySelector('span.ds-text--fsp.ds-text--lsp');
if (label && label.textContent.trim() === 'Tokens') {
tokenContainer = innerFlex;
echartsDiv = outerFlex.querySelector('[_echarts_instance_]');
}
});
if (!echartsDiv) {
echartsDiv = outerFlex.querySelector('[_echarts_instance_]');
}
});
if (tokenContainer) {
sections.push({
modelName: modelName,
tokenContainer: tokenContainer,
echartsDiv: echartsDiv
});
}
});
return sections;
}
// ========== Capsule Badge Injection ==========
function createBadge(text, cssClass) {
var badge = document.createElement('span');
badge.className = 'ds-cache-hit-badge ' + cssClass;
badge.textContent = text;
return badge;
}
function injectCapsule(section, data, dailyDateOverride) {
var container = section.tokenContainer;
// Remove old badges
var old = container.querySelectorAll('.ds-cache-hit-badge');
old.forEach(function(el) { el.remove(); });
// Cumulative capsule
var rate = data ? data.total.rate : null;
container.appendChild(createBadge(formatRate(rate), rateClass(rate)));
// Daily capsule
var date = dailyDateOverride || dailyDate;
var dailyRate = null;
if (data && date) {
var d = data.daily.get(date);
if (d) dailyRate = d.rate;
}
var dailyText = (dailyRate !== null && dailyRate !== undefined)
? dateLabel(date) + ': ' + dailyRate.toFixed(1) + '%'
: dateLabel(date) + ': N/A';
container.appendChild(createBadge(dailyText, rateClass(dailyRate)));
}
function injectAllCapsules() {
var sections = findTokenSections();
sections.forEach(function(section) {
var data = modelData.get(section.modelName);
injectCapsule(section, data);
});
}
// ========== Tooltip Date Reading ==========
function getDateFromTooltip(tooltip) {
var flex = tooltip.querySelector('div[style*="display: flex"]');
if (!flex) return null;
var leftCol = flex.children[0];
if (!leftCol || !leftCol.children[0]) return null;
return leftCol.children[0].textContent.trim();
}
// ========== Global Tooltip Observer (reads date → updates daily capsule) ==========
var tipCheckTimer = null;
var resetTimer = null;
function checkAllTooltips() {
var sections = findTokenSections();
var foundDate = null;
sections.forEach(function(section) {
if (foundDate || !section.echartsDiv) return;
var tooltips = section.echartsDiv.querySelectorAll('div[style*="absolute"]');
tooltips.forEach(function(tooltip) {
if (foundDate) return;
var cs = window.getComputedStyle(tooltip);
if (cs.visibility === 'hidden' || cs.opacity === '0') return;
var d = getDateFromTooltip(tooltip);
if (d) foundDate = d;
});
});
if (foundDate) {
clearTimeout(resetTimer);
resetTimer = null;
if (foundDate !== dailyDate) {
dailyDate = foundDate;
injectAllCapsules();
}
} else if (dailyDate && dailyDate !== getDefaultDailyDate()) {
// Mouse left chart — schedule reset to today after idle period
if (!resetTimer) {
resetTimer = setTimeout(function() {
resetTimer = null;
dailyDate = getDefaultDailyDate();
injectAllCapsules();
}, 1500);
}
}
}
function setupGlobalTooltipObserver() {
var root = document.getElementById('root') || document.body;
if (!root) return;
var observer = new MutationObserver(function() {
if (tipCheckTimer) return;
tipCheckTimer = setTimeout(function() {
tipCheckTimer = null;
checkAllTooltips();
}, 60);
});
observer.observe(root, {
subtree: true,
attributes: true,
attributeFilter: ['style']
});
}
// ========== Month Select Watcher ==========
function setupMonthWatcher() {
var sel = document.querySelector('select.ds-native-select__select');
if (!sel) return;
sel.addEventListener('change', function() {
setTimeout(function() {
if (modelData.size > 0) {
setTimeout(function() {
injectAllCapsules();
}, 500);
}
}, 300);
});
}
// ========== Page Observer (SPA re-render detection) ==========
function setupPageObserver() {
var main = document.querySelector('main') || document.getElementById('root');
if (!main) return;
var debounceTimer = null;
new MutationObserver(function() {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(function() {
injectAllCapsules();
checkAllTooltips();
}, 400);
}).observe(main, { childList: true, subtree: true });
}
// ========== Entry ==========
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', onDomReady);
} else {
onDomReady();
}
})();
效果如图:
带消费记录版本
// ==UserScript==
// @name DeepSeek Usage - Cache Hit Rate Monitor
// @namespace https://platform.deepseek.com
// @version 1.3.0
// @description 监测 DeepSeek 用量页面的 Prompt 缓存命中率,以双胶囊形式显示累积命中率和单日命中率,并在日胶囊中同时显示当天消耗金额(hover 图表时自动更新单日数据)
// @match https://platform.deepseek.com/usage
// @grant none
// @run-at document-start
// ==/UserScript==
(function() {
'use strict';
// ========== State ==========
var modelData = new Map(); // Map<modelName, {total, daily}>
var costData = new Map(); // Map<modelName, {total:Number, daily:Map<date,Number>}>
var costCurrency = ''; // 'CNY' | 'USD'
var uiUpdateTimer = null;
var dailyDate = ''; // currently shown daily date (yyyy-mm-dd)
// ========== Utility ==========
function computeRate(hit, miss) {
var total = hit + miss;
if (total === 0) return null;
return (hit / total) * 100;
}
function getAmount(usageArr, type) {
var entry = (usageArr || []).find(function(u) { return u.type === type; });
return entry ? parseInt(entry.amount, 10) || 0 : 0;
}
function formatRate(rate) {
if (rate === null || rate === undefined) return 'All Cache: N/A';
return 'All Cache: ' + rate.toFixed(1) + '%';
}
function rateClass(rate) {
if (rate === null || rate === undefined) return 'cache-na';
if (rate >= 80) return 'cache-high';
if (rate >= 50) return 'cache-mid';
return '';
}
function todayStr() {
var d = new Date();
var m = d.getMonth() + 1;
var day = d.getDate();
return d.getFullYear() + '-' + (m < 10 ? '0' : '') + m + '-' + (day < 10 ? '0' : '') + day;
}
function dateLabel(iso) {
// 2026-05-15 -> 5/15
var parts = iso.split('-');
return parseInt(parts[1], 10) + '/' + parseInt(parts[2], 10);
}
function getDefaultDailyDate() {
// Prefer today, fallback to last day in data
var t = todayStr();
var iter = modelData.values();
var entry = iter.next();
while (entry && !entry.done) {
if (entry.value.daily.has(t)) return t;
entry = iter.next();
}
// No today, pick the max date from first model
var first = modelData.values().next();
if (first.done) return t;
var dates = Array.from(first.value.daily.keys()).sort();
return dates.length > 0 ? dates[dates.length - 1] : t;
}
// ========== Cost Helpers ==========
function sumUsage(usageArr) {
return (usageArr || []).reduce(function(s, u) {
var v = parseFloat(u.amount);
return s + (isNaN(v) ? 0 : v);
}, 0);
}
function currencySymbol(c) {
return c === 'USD' ? '$' : '¥';
}
function formatCost(n) {
if (n === null || n === undefined) return null;
if (n === 0) return '0';
if (n < 0.01) return n.toFixed(4);
return n.toFixed(2);
}
function pickCurrencyEntry(bizDataArr) {
if (!Array.isArray(bizDataArr) || bizDataArr.length === 0) return null;
var sums = bizDataArr.map(function(entry) {
return (entry.total || []).reduce(function(s, m) {
return s + sumUsage(m.usage);
}, 0);
});
// Prefer non-zero entry; if multiple non-zero, prefer CNY
var pick = null;
for (var i = 0; i < bizDataArr.length; i++) {
if (sums[i] > 0) {
if (!pick) pick = bizDataArr[i];
else if (bizDataArr[i].currency === 'CNY') pick = bizDataArr[i];
}
}
if (pick) return pick;
// All zero — prefer CNY entry, else first
for (var j = 0; j < bizDataArr.length; j++) {
if (bizDataArr[j].currency === 'CNY') return bizDataArr[j];
}
return bizDataArr[0];
}
// ========== Data Processing (called by interceptors) ==========
function processApiData(bizData) {
var dataMap = new Map();
// Cumulative (total) data
(bizData.total || []).forEach(function(entry) {
var name = entry.model;
var hit = getAmount(entry.usage, 'PROMPT_CACHE_HIT_TOKEN');
var miss = getAmount(entry.usage, 'PROMPT_CACHE_MISS_TOKEN');
dataMap.set(name, {
total: { hit: hit, miss: miss, rate: computeRate(hit, miss) },
daily: new Map()
});
});
// Daily data
(bizData.days || []).forEach(function(day) {
var date = day.date;
(day.data || []).forEach(function(entry) {
var name = entry.model;
if (!dataMap.has(name)) return;
var hit = getAmount(entry.usage, 'PROMPT_CACHE_HIT_TOKEN');
var miss = getAmount(entry.usage, 'PROMPT_CACHE_MISS_TOKEN');
dataMap.get(name).daily.set(date, {
hit: hit, miss: miss, rate: computeRate(hit, miss)
});
});
});
return dataMap;
}
function processCostData(bizDataArr) {
var entry = pickCurrencyEntry(bizDataArr);
if (!entry) return null;
var map = new Map();
(entry.total || []).forEach(function(m) {
map.set(m.model, { total: sumUsage(m.usage), daily: new Map() });
});
(entry.days || []).forEach(function(day) {
(day.data || []).forEach(function(m) {
if (!map.has(m.model)) return;
map.get(m.model).daily.set(day.date, sumUsage(m.usage));
});
});
return { data: map, currency: entry.currency || 'CNY' };
}
function scheduleUIUpdate() {
clearTimeout(uiUpdateTimer);
uiUpdateTimer = setTimeout(updateUI, 300);
}
function updateUI() {
if (modelData.size === 0) return;
clearTimeout(resetTimer);
resetTimer = null;
dailyDate = getDefaultDailyDate();
injectAllCapsules();
}
// ========== XHR + fetch Interception ==========
function handleAmountApiResponse(json) {
if (!json || json.code !== 0 || !json.data || !json.data.biz_data) return;
var bizData = json.data.biz_data;
modelData = processApiData(bizData);
scheduleUIUpdate();
}
function handleCostApiResponse(json) {
if (!json || json.code !== 0 || !json.data || !json.data.biz_data) return;
var result = processCostData(json.data.biz_data);
if (!result) return;
costData = result.data;
costCurrency = result.currency;
scheduleUIUpdate();
}
function routeApiResponse(url, json) {
if (!url) return;
if (url.indexOf('/usage/amount') !== -1) handleAmountApiResponse(json);
else if (url.indexOf('/usage/cost') !== -1) handleCostApiResponse(json);
}
// --- Intercept XMLHttpRequest ---
var origSend = XMLHttpRequest.prototype.send;
var origOpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function(method, url) {
this._ds_url = url;
return origOpen.apply(this, arguments);
};
XMLHttpRequest.prototype.send = function() {
var self = this;
var url = self._ds_url;
if (url && (url.indexOf('/usage/amount') !== -1 || url.indexOf('/usage/cost') !== -1)) {
self.addEventListener('load', function() {
try {
var json = JSON.parse(self.responseText);
routeApiResponse(url, json);
} catch(e) {}
});
}
return origSend.apply(this, arguments);
};
// --- Intercept fetch ---
if (typeof window.fetch !== 'undefined') {
var origFetch = window.fetch;
window.fetch = function(url, options) {
return origFetch.apply(this, arguments).then(function(response) {
if (typeof url === 'string' &&
(url.indexOf('/usage/amount') !== -1 || url.indexOf('/usage/cost') !== -1)) {
var cloned = response.clone();
cloned.json().then(function(json) {
routeApiResponse(url, json);
}).catch(function() {});
}
return response;
});
};
}
// ========== DOM-dependent code (deferred) ==========
function onDomReady() {
injectCSS();
setupGlobalTooltipObserver();
waitForReact();
}
function injectCSS() {
var s = document.createElement('style');
s.textContent =
'.ds-cache-hit-badge{' +
'display:inline-block;padding:2px 10px;margin-left:8px;' +
'font-size:12px;line-height:1.5;font-weight:500;' +
'border-radius:20px;white-space:nowrap;vertical-align:middle;' +
'transition:background 0.15s ease;' +
'color:rgb(var(--ds-rgb-primary,0 0 0));' +
'background:rgb(var(--ds-rgb-elevated,255 255 255));' +
'border:1px solid rgb(var(--ds-rgb-separator,210 216 229));}' +
'.ds-cache-hit-badge.cache-na{' +
'color:rgb(var(--ds-rgb-label-2,144 148 162));border-style:dashed;}' +
'.ds-cache-hit-badge.cache-high{color:#16A34A;border-color:#16A34A;}' +
'.ds-cache-hit-badge.cache-mid{color:#D97706;border-color:#D97706;}';
(document.head || document.documentElement).appendChild(s);
}
function waitForReact() {
var attempts = 0;
function check() {
var sel = document.querySelector('select.ds-native-select__select');
var spans = document.querySelectorAll('span.ds-text--monospace.ds-text--fm.ds-text--linherit');
if (sel && spans.length > 0) {
setupMonthWatcher();
setupPageObserver();
// If we already have data (interceptor caught the API call), update UI now
if (modelData.size > 0) {
updateUI();
} else {
injectAllCapsules();
}
return;
}
if (++attempts < 20) {
setTimeout(check, 500);
}
}
check();
}
// ========== DOM Query ==========
function findTokenSections() {
var modelSpans = document.querySelectorAll(
'span.ds-text--monospace.ds-text--fm.ds-text--linherit');
var sections = [];
modelSpans.forEach(function(span) {
var modelName = span.textContent.trim();
var headerBar = span.closest('._6926780');
if (!headerBar) return;
var chartBody = headerBar.nextElementSibling;
if (!chartBody || !chartBody.classList.contains('_06da35f')) return;
var outerFlexes = chartBody.querySelectorAll(
'.ds-flex[style*="flex-direction: column"]');
var tokenContainer = null;
var echartsDiv = null;
outerFlexes.forEach(function(outerFlex) {
var innerFlexes = outerFlex.querySelectorAll(
'.ds-flex[style*="align-items: baseline"]');
innerFlexes.forEach(function(innerFlex) {
var label = innerFlex.querySelector('span.ds-text--fsp.ds-text--lsp');
if (label && label.textContent.trim() === 'Tokens') {
tokenContainer = innerFlex;
echartsDiv = outerFlex.querySelector('[_echarts_instance_]');
}
});
if (!echartsDiv) {
echartsDiv = outerFlex.querySelector('[_echarts_instance_]');
}
});
if (tokenContainer) {
sections.push({
modelName: modelName,
tokenContainer: tokenContainer,
echartsDiv: echartsDiv
});
}
});
return sections;
}
// ========== Capsule Badge Injection ==========
function createBadge(text, cssClass) {
var badge = document.createElement('span');
badge.className = 'ds-cache-hit-badge ' + cssClass;
badge.textContent = text;
return badge;
}
function injectCapsule(section, data, dailyDateOverride) {
var container = section.tokenContainer;
// Remove old badges
var old = container.querySelectorAll('.ds-cache-hit-badge');
old.forEach(function(el) { el.remove(); });
// Cumulative capsule
var rate = data ? data.total.rate : null;
container.appendChild(createBadge(formatRate(rate), rateClass(rate)));
// Daily capsule
var date = dailyDateOverride || dailyDate;
var dailyRate = null;
if (data && date) {
var d = data.daily.get(date);
if (d) dailyRate = d.rate;
}
var dailyText = (dailyRate !== null && dailyRate !== undefined)
? dateLabel(date) + ': ' + dailyRate.toFixed(1) + '%'
: dateLabel(date) + ': N/A';
// Append daily cost
var costEntry = costData.get(section.modelName);
if (costEntry && date) {
var dailyCost = costEntry.daily.get(date);
var costStr = formatCost(dailyCost);
if (costStr !== null) {
dailyText += ' / ' + currencySymbol(costCurrency) + costStr;
}
}
container.appendChild(createBadge(dailyText, rateClass(dailyRate)));
}
function injectAllCapsules() {
var sections = findTokenSections();
sections.forEach(function(section) {
var data = modelData.get(section.modelName);
injectCapsule(section, data);
});
}
// ========== Tooltip Date Reading ==========
function getDateFromTooltip(tooltip) {
var flex = tooltip.querySelector('div[style*="display: flex"]');
if (!flex) return null;
var leftCol = flex.children[0];
if (!leftCol || !leftCol.children[0]) return null;
return leftCol.children[0].textContent.trim();
}
// ========== Global Tooltip Observer (reads date → updates daily capsule) ==========
var tipCheckTimer = null;
var resetTimer = null;
function checkAllTooltips() {
var sections = findTokenSections();
var foundDate = null;
sections.forEach(function(section) {
if (foundDate || !section.echartsDiv) return;
var tooltips = section.echartsDiv.querySelectorAll('div[style*="absolute"]');
tooltips.forEach(function(tooltip) {
if (foundDate) return;
var cs = window.getComputedStyle(tooltip);
if (cs.visibility === 'hidden' || cs.opacity === '0') return;
var d = getDateFromTooltip(tooltip);
if (d) foundDate = d;
});
});
if (foundDate) {
clearTimeout(resetTimer);
resetTimer = null;
if (foundDate !== dailyDate) {
dailyDate = foundDate;
injectAllCapsules();
}
} else if (dailyDate && dailyDate !== getDefaultDailyDate()) {
// Mouse left chart — schedule reset to today after idle period
if (!resetTimer) {
resetTimer = setTimeout(function() {
resetTimer = null;
dailyDate = getDefaultDailyDate();
injectAllCapsules();
}, 1500);
}
}
}
function setupGlobalTooltipObserver() {
var root = document.getElementById('root') || document.body;
if (!root) return;
var observer = new MutationObserver(function() {
if (tipCheckTimer) return;
tipCheckTimer = setTimeout(function() {
tipCheckTimer = null;
checkAllTooltips();
}, 60);
});
observer.observe(root, {
subtree: true,
attributes: true,
attributeFilter: ['style']
});
}
// ========== Month Select Watcher ==========
function setupMonthWatcher() {
var sel = document.querySelector('select.ds-native-select__select');
if (!sel) return;
sel.addEventListener('change', function() {
setTimeout(function() {
if (modelData.size > 0) {
setTimeout(function() {
injectAllCapsules();
}, 500);
}
}, 300);
});
}
// ========== Page Observer (SPA re-render detection) ==========
function setupPageObserver() {
var main = document.querySelector('main') || document.getElementById('root');
if (!main) return;
var debounceTimer = null;
new MutationObserver(function() {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(function() {
injectAllCapsules();
checkAllTooltips();
}, 400);
}).observe(main, { childList: true, subtree: true });
}
// ========== Entry ==========
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', onDomReady);
} else {
onDomReady();
}
})();


