// Claude Usage Tracker - Content Script
(function() {
'use strict';
// Días en español - índice 0 = Domingo
const DAYS_ES = ['dom', 'lun', 'mar', 'mié', 'jue', 'vie', 'sáb'];
const DAYS_ES_DISPLAY = ['Dom', 'Lun', 'Mar', 'Mié', 'Jue', 'Vie', 'Sáb'];
const DAYS_FULL_ES = ['Domingo', 'Lunes', 'Martes', 'Miércoles', 'Jueves', 'Viernes', 'Sábado'];
// Boost 2× promotion config (March 2026)
// Peak hours: 8AM - 2PM ET (Eastern Time, UTC-4 in March)
// Outside peak = 2× boost
const BOOST_END_DATE = new Date('2026-03-28T06:59:00Z'); // March 27, 11:59 PM PT = March 28, 6:59 AM UTC
const PEAK_START_UTC = 12; // 8AM ET = 12:00 UTC
const PEAK_END_UTC = 18; // 2PM ET = 18:00 UTC
// Session tracking
let sessionStartTime = null;
const SESSION_KEY = 'claude_usage_tracker_session';
function initSession() {
const stored = localStorage.getItem(SESSION_KEY);
if (stored) {
const data = JSON.parse(stored);
const storedDate = new Date(data.startTime);
const now = new Date();
if (storedDate.toDateString() === now.toDateString() &&
(now - storedDate) < 4 * 60 * 60 * 1000) {
sessionStartTime = new Date(data.startTime);
} else {
sessionStartTime = new Date();
saveSession();
}
} else {
sessionStartTime = new Date();
saveSession();
}
}
function saveSession() {
localStorage.setItem(SESSION_KEY, JSON.stringify({
startTime: sessionStartTime.toISOString()
}));
}
function formatTime(date) {
return date.toLocaleTimeString('es-ES', {
hour: '2-digit',
minute: '2-digit'
});
}
/**
* Calcula el estado del boost 2×
* Retorna { isActive, isBoostPeriod, nextChangeIn, nextChangeTime, promotionEnded }
*/
function getBoostStatus() {
const now = new Date();
// Verificar si la promoción ha terminado
if (now >= BOOST_END_DATE) {
return { isActive: false, isBoostPeriod: false, promotionEnded: true };
}
const utcHour = now.getUTCHours();
const utcMinutes = now.getUTCMinutes();
// Peak hours: 12:00 - 18:00 UTC (8AM - 2PM ET)
const isPeakHour = utcHour >= PEAK_START_UTC && utcHour < PEAK_END_UTC;
const isBoostPeriod = !isPeakHour; // Boost es cuando NO es hora pico
// Calcular tiempo hasta el próximo cambio
let nextChangeHour;
if (isPeakHour) {
// Estamos en pico, próximo cambio es cuando termine (18:00 UTC)
nextChangeHour = PEAK_END_UTC;
} else {
// Estamos en boost, próximo cambio es cuando empiece el pico
if (utcHour < PEAK_START_UTC) {
nextChangeHour = PEAK_START_UTC;
} else {
// Después de las 18:00 UTC, el próximo pico es mañana a las 12:00 UTC
nextChangeHour = PEAK_START_UTC + 24;
}
}
const minutesUntilChange = (nextChangeHour * 60) - (utcHour * 60 + utcMinutes);
const hoursUntilChange = Math.floor(minutesUntilChange / 60);
const minsUntilChange = minutesUntilChange % 60;
// Calcular hora local del próximo cambio
const nextChangeTime = new Date(now);
nextChangeTime.setUTCHours(nextChangeHour % 24, 0, 0, 0);
if (nextChangeHour >= 24) {
nextChangeTime.setDate(nextChangeTime.getDate() + 1);
}
return {
isActive: true,
isBoostPeriod,
nextChangeIn: `${hoursUntilChange}h ${minsUntilChange}m`,
nextChangeTime: formatTime(nextChangeTime),
promotionEnded: false
};
}
function formatDuration(ms) {
const hours = Math.floor(ms / (1000 * 60 * 60));
const minutes = Math.floor((ms % (1000 * 60 * 60)) / (1000 * 60));
if (hours > 0) {
return `${hours}h ${minutes}m`;
}
return `${minutes}m`;
}
const SESSION_DURATION_HOURS = 5; // Las sesiones de Claude duran 5 horas
/**
* Parsea el tiempo restante desde texto como "Se restablece en 3 h 44 min"
* Retorna el tiempo restante en minutos
*/
function parseTimeRemaining(text) {
const match = text.match(/(\d+)\s*h\s*(\d+)\s*min/i);
if (match) {
return parseInt(match[1]) * 60 + parseInt(match[2]);
}
// Solo minutos: "Se restablece en 44 min"
const minMatch = text.match(/(\d+)\s*min/i);
if (minMatch) {
return parseInt(minMatch[1]);
}
// Solo horas: "Se restablece en 3 h"
const hourMatch = text.match(/(\d+)\s*h/i);
if (hourMatch) {
return parseInt(hourMatch[1]) * 60;
}
return 0;
}
/**
* Encuentra el elemento "hoja" cuyo texto coincide exactamente con el texto dado.
* Se ancla en el TEXTO, no en clases CSS, para ser robusto ante los cambios
* de design tokens de Claude (p.ej. text-text-100 desapareció en mayo 2026).
*/
function findLabelElement(text) {
let best = null;
const candidates = document.querySelectorAll('p, span, div, h2, h3, h4');
for (const el of candidates) {
if (el.textContent.trim() === text) {
// Preferir el match más interno (menor longitud de texto = más cercano a la hoja)
if (!best || el.textContent.length <= best.textContent.length) {
best = el;
}
}
}
return best;
}
/**
* Dada la etiqueta de una métrica (p.ej. "Todos los modelos" o "Sesión actual"),
* localiza su fila subiendo desde la etiqueta hasta el primer ancestro que
* contiene un [role="progressbar"], y devuelve { usage, resetText }.
* No depende de nombres de clase CSS de Claude.
*/
function getMetricByLabel(labelText) {
const labelEl = findLabelElement(labelText);
if (!labelEl) return null;
let container = labelEl;
for (let i = 0; i < 10 && container; i++) {
if (container.querySelector('[role="progressbar"]')) break;
container = container.parentElement;
}
const progressBar = container && container.querySelector('[role="progressbar"]');
if (!progressBar) return null;
const usage = parseFloat(progressBar.getAttribute('aria-valuenow')) || 0;
const containerText = container.innerText || container.textContent || '';
const resetMatch = containerText.match(/Se restablece[^\n]*/);
const resetText = resetMatch ? resetMatch[0].trim() : '';
return { usage, resetText };
}
/**
* Parsea la información de la página real de Claude
* Busca los progressbar con aria-valuenow y el texto "Se restablece..."
*/
function parsePageData() {
const data = {
sessionUsage: 0,
sessionResetText: '',
sessionTimeRemainingMin: 0,
sessionStartTime: null,
sessionEndTime: null,
weeklyUsage: 0,
weeklyResetText: '',
resetDayIndex: -1,
resetHour: '',
daysUntilReset: 0,
daysPassed: 0
};
// Sesión actual
const session = getMetricByLabel('Sesión actual');
if (session) {
data.sessionUsage = session.usage;
data.sessionResetText = session.resetText;
// Calcular tiempo restante y horas de inicio/fin
data.sessionTimeRemainingMin = parseTimeRemaining(session.resetText);
if (data.sessionTimeRemainingMin > 0) {
const now = new Date();
const sessionDurationMin = SESSION_DURATION_HOURS * 60;
const elapsedMin = sessionDurationMin - data.sessionTimeRemainingMin;
// Hora de inicio = ahora - tiempo transcurrido
data.sessionStartTime = new Date(now.getTime() - elapsedMin * 60 * 1000);
// Hora de fin = ahora + tiempo restante
data.sessionEndTime = new Date(now.getTime() + data.sessionTimeRemainingMin * 60 * 1000);
}
} else {
console.log('[Claude Usage Tracker] No se encontró la métrica "Sesión actual"');
}
// Todos los modelos (uso semanal)
const weekly = getMetricByLabel('Todos los modelos');
if (weekly) {
data.weeklyUsage = weekly.usage;
data.weeklyResetText = weekly.resetText;
// Parsear "Se restablece vie, 4:59" para obtener día y hora
const resetMatch = weekly.resetText.match(/Se restablece\s+([a-záéíóúñü]+),?\s*([\d:]+)/i);
if (resetMatch) {
const dayAbbr = resetMatch[1].toLowerCase();
data.resetHour = resetMatch[2];
// Encontrar el índice del día (comparar primeras 3 letras sin acentos)
const normalize = s => s.normalize('NFD').replace(/[̀-ͯ]/g, '');
const dayAbbrNorm = normalize(dayAbbr);
data.resetDayIndex = DAYS_ES.findIndex(d => {
const dNorm = normalize(d);
return dNorm === dayAbbrNorm || dNorm.startsWith(dayAbbrNorm.substring(0, 3)) || dayAbbrNorm.startsWith(dNorm.substring(0, 3));
});
console.log('[Claude Usage Tracker] Reset parsed:', dayAbbr, '->', data.resetDayIndex, 'hour:', data.resetHour);
} else {
console.log('[Claude Usage Tracker] Reset text no match:', weekly.resetText);
}
} else {
console.log('[Claude Usage Tracker] No se encontró la métrica "Todos los modelos"');
}
// Calcular días hasta el reinicio y días pasados
if (data.resetDayIndex >= 0) {
const now = new Date();
const todayIndex = now.getDay(); // 0 = Domingo, 1 = Lunes, etc.
// Calcular días hasta el reinicio
let daysUntil = data.resetDayIndex - todayIndex;
if (daysUntil <= 0) {
daysUntil += 7;
}
// Si es el mismo día pero ya pasó la hora, es la próxima semana
if (daysUntil === 7 || (daysUntil === 0 && data.resetHour)) {
const [resetH, resetM] = data.resetHour.split(':').map(Number);
if (now.getHours() > resetH || (now.getHours() === resetH && now.getMinutes() >= resetM)) {
daysUntil = 7;
}
}
data.daysUntilReset = daysUntil;
data.daysPassed = 7 - daysUntil;
}
return data;
}
/**
* Calcula el desglose diario basado en los datos reales
* Usa HORAS transcurridas para mayor precisión
* Divide el día de reinicio en dos partes: inicio (después del reinicio) y fin (antes del reinicio)
*/
function calculateDailyUsage(pageData) {
const { weeklyUsage, resetDayIndex, resetHour } = pageData;
const now = new Date();
const todayIndex = now.getDay();
const currentHour = now.getHours() + now.getMinutes() / 60;
// Calcular el momento exacto del último reinicio
const [resetH, resetM] = (resetHour || '5:00').split(':').map(Number);
const resetHourDecimal = resetH + (resetM || 0) / 60;
// Encontrar la fecha del último reinicio (usar effectiveResetDayIndex más abajo)
const effectiveResetIdx = resetDayIndex >= 0 ? resetDayIndex : 5;
let lastResetDate = new Date(now);
let daysSinceReset = todayIndex - effectiveResetIdx;
if (daysSinceReset < 0) daysSinceReset += 7;
if (daysSinceReset === 0) {
// Es el día de reinicio, ¿ya pasó la hora?
if (currentHour < resetHourDecimal) {
daysSinceReset = 7; // Aún no ha reiniciado, usar el de la semana pasada
}
}
lastResetDate.setDate(now.getDate() - daysSinceReset);
lastResetDate.setHours(resetH, resetM || 0, 0, 0);
// Horas totales transcurridas desde el reinicio
const msElapsed = now - lastResetDate;
const hoursElapsed = msElapsed / (1000 * 60 * 60);
const totalWeekHours = 7 * 24; // 168 horas
// Porcentaje de la semana transcurrido
const weekProgressPercent = (hoursElapsed / totalWeekHours) * 100;
// Uso ideal hasta ahora (proporcional a las horas transcurridas)
const idealUsedByNow = weekProgressPercent;
// Uso ideal por día (para referencia visual)
const idealDailyPercent = 100 / 7;
// Promedio real de uso por hora
const avgUsagePerHour = hoursElapsed > 0 ? weeklyUsage / hoursElapsed : 0;
const idealUsagePerHour = 100 / totalWeekHours;
// Nombre del día de reinicio (con fallback, reutilizar effectiveResetIdx)
const effectiveResetDayIndex = effectiveResetIdx;
const resetDayName = DAYS_ES_DISPLAY[effectiveResetDayIndex] || 'Vie';
const resetDayFullName = DAYS_FULL_ES[effectiveResetDayIndex] || 'Viernes';
const dailyBreakdown = [];
// Estructura: 8 segmentos
// 0: Día reinicio (inicio) - desde resetHour hasta 24:00
// 1-6: Días completos (sáb, dom, lun, mar, mié, jue si reinicio es viernes)
// 7: Día reinicio (fin) - desde 00:00 hasta resetHour
for (let i = 0; i < 8; i++) {
let dayIndex, dayName, dayFullName, dayTotalHours, isResetDayStart, isResetDayEnd;
if (i === 0) {
// Primer segmento: día de reinicio (parte después del reinicio)
dayIndex = effectiveResetDayIndex;
dayName = resetDayName;
dayFullName = resetDayFullName + ' (inicio)';
dayTotalHours = 24 - resetHourDecimal;
isResetDayStart = true;
isResetDayEnd = false;
} else if (i === 7) {
// Último segmento: día de reinicio (parte antes del reinicio)
dayIndex = effectiveResetDayIndex;
dayName = resetDayName;
dayFullName = resetDayFullName + ' (fin)';
dayTotalHours = resetHourDecimal;
isResetDayStart = false;
isResetDayEnd = true;
} else {
// Días intermedios (completos)
dayIndex = (effectiveResetDayIndex + i) % 7;
dayName = DAYS_ES_DISPLAY[dayIndex];
dayFullName = DAYS_FULL_ES[dayIndex];
dayTotalHours = 24;
isResetDayStart = false;
isResetDayEnd = false;
}
// Calcular posición del día actual
let segmentPosition;
if (todayIndex === effectiveResetDayIndex) {
// Hoy es el día de reinicio
if (currentHour >= resetHourDecimal) {
segmentPosition = 0; // Estamos en la parte de inicio
} else {
segmentPosition = 7; // Estamos en la parte de fin (antes del reinicio)
}
} else {
// Día normal
let pos = todayIndex - effectiveResetDayIndex;
if (pos <= 0) pos += 7;
segmentPosition = pos;
}
const isToday = (i === segmentPosition);
const isPast = (i < segmentPosition);
const isFuture = (i > segmentPosition);
// Calcular horas transcurridas en este segmento
let dayHoursElapsed = 0;
if (isPast) {
dayHoursElapsed = dayTotalHours;
} else if (isToday) {
if (i === 0) {
// Hoy es el día de reinicio, parte inicio
dayHoursElapsed = currentHour - resetHourDecimal;
if (dayHoursElapsed < 0) dayHoursElapsed = 0;
} else if (i === 7) {
// Hoy es el día de reinicio, parte fin
dayHoursElapsed = currentHour;
} else {
// Día normal
dayHoursElapsed = currentHour;
}
}
// Días futuros: dayHoursElapsed = 0
// Porcentaje de llenado del segmento (0-100%)
const dayFillPercent = dayTotalHours > 0 ? (dayHoursElapsed / dayTotalHours) * 100 : 0;
// Uso asignado a este segmento (promedio distribuido)
let usage = 0;
if (dayHoursElapsed > 0 && hoursElapsed > 0) {
usage = (dayHoursElapsed / hoursElapsed) * weeklyUsage;
}
// Estado basado en si el uso está dentro del ideal
let status = 'pending';
if (dayHoursElapsed > 0) {
const idealForThisSegment = (dayHoursElapsed / totalWeekHours) * 100;
const actualForThisSegment = usage;
status = actualForThisSegment <= idealForThisSegment * 1.1 ? 'good' :
actualForThisSegment <= idealForThisSegment * 1.5 ? 'warning' : 'danger';
}
// Offset visual (solo para el segmento de inicio del día de reinicio)
const dayStartOffsetPercent = isResetDayStart ? (resetHourDecimal / 24) * 100 : 0;
// Offset final (solo para el segmento de fin del día de reinicio)
const dayEndOffsetPercent = isResetDayEnd ? ((24 - resetHourDecimal) / 24) * 100 : 0;
dailyBreakdown.push({
dayIndex,
dayName,
dayFullName,
isPast,
isToday,
isFuture,
isResetDayStart,
isResetDayEnd,
usage,
idealUsage: (dayTotalHours / totalWeekHours) * 100,
dayFillPercent,
dayHoursElapsed,
dayTotalHours,
dayStartOffsetPercent,
dayEndOffsetPercent,
status
});
}
// Calcular fecha estimada de fin del uso
const usageRatePerHour = hoursElapsed > 0 ? weeklyUsage / hoursElapsed : 0;
const hoursToReach100 = usageRatePerHour > 0 ? (100 - weeklyUsage) / usageRatePerHour : Infinity;
const hoursUntilReset = totalWeekHours - hoursElapsed;
let estimatedEndDate = null;
let willRunOut = false;
if (usageRatePerHour > 0 && hoursToReach100 < hoursUntilReset) {
// Se acabará antes del reinicio
willRunOut = true;
estimatedEndDate = new Date(now.getTime() + hoursToReach100 * 60 * 60 * 1000);
} else {
// Llegará al reinicio con uso disponible
estimatedEndDate = new Date(lastResetDate.getTime() + 7 * 24 * 60 * 60 * 1000);
}
// Determinar estado con más granularidad
const diffPercent = weeklyUsage - idealUsedByNow;
let status = 'perfect'; // Por debajo del ideal
if (diffPercent > 0 && diffPercent <= 5) status = 'good'; // Hasta 5% por encima
else if (diffPercent > 5 && diffPercent <= 15) status = 'warning'; // 5-15% por encima
else if (diffPercent > 15) status = 'danger'; // Más del 15% por encima
return {
dailyBreakdown,
idealDailyPercent,
idealUsedByNow,
hoursElapsed,
avgUsagePerHour,
idealUsagePerHour,
hoursUntilReset,
estimatedEndDate,
willRunOut,
hoursToReach100,
status
};
}
function createWeeklyTracker(pageData, dailyData) {
const container = document.createElement('div');
container.className = 'claude-usage-tracker-container';
container.id = 'claude-usage-tracker';
const usagePercent = pageData.weeklyUsage;
const resetDayName = DAYS_FULL_ES[pageData.resetDayIndex >= 0 ? pageData.resetDayIndex : 5] || 'próximo reinicio';
const boostStatus = getBoostStatus();
container.innerHTML = `
${boostStatus.isActive ? `
';
setTimeout(() => refreshInBackground('overlay-empty'), 0);
}
panel.appendChild(header);
panel.appendChild(body);
overlay.appendChild(panel);
document.body.appendChild(overlay);
document.addEventListener('keydown', overlayEscapeHandler);
console.log('[CUT-DIAG] overlay appended OK. body.lastChild.id=', document.body.lastChild?.id);
} catch (e) {
console.error('[CUT-DIAG] openOverlayPanel ERROR:', e);
}
}
function overlayEscapeHandler(e) {
if (e.key === 'Escape') closeOverlayPanel();
}
function closeOverlayPanel() {
const ov = document.getElementById(OVERLAY_ID);
if (ov) ov.remove();
document.removeEventListener('keydown', overlayEscapeHandler);
}
function setOverlaySpinner(active) {
const ov = document.getElementById(OVERLAY_ID);
if (!ov) return;
let spinner = ov.querySelector('.claude-usage-tracker-overlay-spinner');
if (active && !spinner) {
spinner = document.createElement('span');
spinner.className = 'claude-usage-tracker-overlay-spinner';
spinner.textContent = '⟳ actualizando…';
const header = ov.querySelector('.claude-usage-tracker-overlay-header');
if (header) header.appendChild(spinner);
} else if (!active && spinner) {
spinner.remove();
}
}
function renderOverlayWithCache(cache) {
const ov = document.getElementById(OVERLAY_ID);
if (!ov) return;
const body = ov.querySelector('.claude-usage-tracker-overlay-body');
const age = ov.querySelector('.claude-usage-tracker-overlay-age');
if (body) {
body.innerHTML = '';
if (cache && cache.pageData) {
body.appendChild(createWeeklyTracker(cache.pageData, cache.dailyData));
} else {
body.innerHTML = '
No he podido cargar los datos. Abre Uso manualmente.
';
}
}
if (age && cache) age.textContent = formatAge(cacheAgeMin(cache.scrapedAt));
}
function showOverlayError(msg) {
const ov = document.getElementById(OVERLAY_ID);
if (!ov) return;
const body = ov.querySelector('.claude-usage-tracker-overlay-body');
if (body) body.innerHTML = `
${msg}
`;
}
// -------- Refresh: abrir modal en background, scrapear, cerrar --------
function waitForDialogWithData(timeoutMs) {
return new Promise(resolve => {
const t0 = Date.now();
const check = () => {
const d = findUsageDialog();
if (d && dialogHasData(d)) return resolve(d);
if (Date.now() - t0 > timeoutMs) return resolve(null);
setTimeout(check, 80);
};
check();
});
}
async function tryOpenViaPushState() {
history.pushState(null, '', '#settings/usage');
window.dispatchEvent(new PopStateEvent('popstate'));
return waitForDialogWithData(700);
}
async function tryOpenViaHash() {
location.hash = '#settings/usage';
return waitForDialogWithData(700);
}
async function tryOpenViaClick() {
const candidates = [...document.querySelectorAll('a, button')]
.filter(el => (el.innerText || '').trim() === 'Uso');
if (candidates.length === 0) return null;
candidates[0].click();
return waitForDialogWithData(1500);
}
function closeUsageModal() {
const dialog = findUsageDialog();
if (!dialog) return;
const closeBtn = [...dialog.querySelectorAll('button')]
.find(b => /close|cerrar/i.test(b.getAttribute('aria-label') || ''));
if (closeBtn) { closeBtn.click(); return; }
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true, cancelable: true }));
}
async function refreshInBackground(reason) {
if (refreshPromise) return refreshPromise;
refreshPromise = (async () => {
console.log('[Claude Usage Tracker] refresh:', reason);
setOverlaySpinner(true);
const previousHash = location.hash;
const wasOpen = isUsageDialogOpen();
let dialog = wasOpen ? findUsageDialog() : null;
if (!dialog) dialog = await tryOpenViaPushState();
if (!dialog) dialog = await tryOpenViaHash();
if (!dialog) dialog = await tryOpenViaClick();
if (!dialog) {
console.warn('[Claude Usage Tracker] no he podido abrir el modal automáticamente');
setOverlaySpinner(false);
showOverlayError('No he podido refrescar automáticamente — abre Uso manualmente y vuelve.');
refreshPromise = null;
return;
}
const pageData = parsePageData();
let cache = null;
if (pageData.weeklyUsage > 0 || pageData.resetDayIndex >= 0) {
initSession();
const dailyData = calculateDailyUsage(pageData);
saveCache(pageData, dailyData, reason === 'overlay-manual' ? 'manual' : 'auto');
cache = { pageData, dailyData, scrapedAt: new Date().toISOString() };
updateFabState(cache);
}
// Cerrar modal solo si nosotros lo abrimos
if (!wasOpen) {
closeUsageModal();
if (location.hash !== previousHash) {
history.replaceState(null, '', location.pathname + location.search + previousHash);
}
}
setOverlaySpinner(false);
renderOverlayWithCache(cache);
refreshPromise = null;
})();
return refreshPromise;
}
// -------- Bootstrap --------
function bootstrap() {
injectFloatingButton();
startGlobalDialogWatcher();
window.addEventListener('hashchange', () => {
updateFabVisibility();
const dialog = findUsageDialog();
if (dialog) observeDialogForData(dialog, () => injectTrackerInDialog(dialog));
});
window.addEventListener('popstate', updateFabVisibility);
// Sanity check: re-inyectar FAB si React lo eliminó
setInterval(() => {
injectFloatingButton();
updateFabVisibility();
}, 2000);
// Si el modal ya estaba abierto al cargar
setTimeout(() => {
const dialog = findUsageDialog();
if (dialog) observeDialogForData(dialog, () => injectTrackerInDialog(dialog));
updateFabVisibility();
}, 500);
}
// Exposición para depuración manual desde la consola
window.__claudeUsageTracker = {
version: 'debug-fab-click',
loadCache,
saveCache,
openOverlayPanel,
closeOverlayPanel,
refreshInBackground,
findUsageDialog,
isUsageDialogOpen,
parsePageData,
injectFloatingButton,
updateFabState,
fabExists: () => !!document.getElementById(FAB_ID),
overlayExists: () => !!document.getElementById(OVERLAY_ID),
forceShowOverlay: async () => {
const cache = await loadCache();
console.log('[CUT-DIAG forceShowOverlay] cache:', cache);
openOverlayPanel(cache);
return !!document.getElementById(OVERLAY_ID);
}
};
console.log('[CUT-DIAG] window.__claudeUsageTracker expuesto. Métodos:', Object.keys(window.__claudeUsageTracker).join(', '));
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', bootstrap);
} else {
bootstrap();
}
})();