// 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; } /** * 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 }; // Buscar todas las secciones con progressbar const sections = document.querySelectorAll('section'); for (const section of sections) { const rows = section.querySelectorAll('.flex.flex-row'); for (const row of rows) { const labelEl = row.querySelector('p.font-base.text-text-100'); const resetTextEl = row.querySelector('p.font-base.text-text-400'); const progressBar = row.querySelector('[role="progressbar"]'); if (!labelEl || !progressBar) continue; const label = labelEl.textContent.trim(); const usage = parseFloat(progressBar.getAttribute('aria-valuenow')) || 0; const resetText = resetTextEl ? resetTextEl.textContent.trim() : ''; if (label === 'Sesión actual') { data.sessionUsage = usage; data.sessionResetText = resetText; // Calcular tiempo restante y horas de inicio/fin data.sessionTimeRemainingMin = parseTimeRemaining(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 if (label === 'Todos los modelos') { data.weeklyUsage = usage; data.weeklyResetText = resetText; // Parsear "Se restablece vie, 4:59" para obtener día y hora const resetMatch = resetText.match(/Se restablece\s+(\w+),?\s*([\d:]+)/i); if (resetMatch) { const dayAbbr = resetMatch[1].toLowerCase(); data.resetHour = resetMatch[2]; // Encontrar el índice del día data.resetDayIndex = DAYS_ES.findIndex(d => d === dayAbbr || d.startsWith(dayAbbr.substring(0, 3))); } } } } // 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 */ 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); // Encontrar la fecha del último reinicio let lastResetDate = new Date(now); let daysSinceReset = todayIndex - resetDayIndex; if (daysSinceReset < 0) daysSinceReset += 7; if (daysSinceReset === 0) { // Es el día de reinicio, ¿ya pasó la hora? if (now.getHours() < resetH || (now.getHours() === resetH && now.getMinutes() < resetM)) { 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; // La semana empieza el mismo día del reinicio (ej: viernes) // El primer día es el día del reinicio, desde la hora de reinicio const startDayIndex = resetDayIndex; // Calcular la posición del día actual dentro de la semana de facturación // Posición 0 = día del reinicio, posición 1 = día siguiente, etc. let todayPositionInWeek = todayIndex - resetDayIndex; if (todayPositionInWeek < 0) todayPositionInWeek += 7; const dailyBreakdown = []; for (let i = 0; i < 7; i++) { const dayIndex = (startDayIndex + i) % 7; const isToday = (i === todayPositionInWeek); const isPast = (i < todayPositionInWeek); const isFuture = (i > todayPositionInWeek); // Calcular cuántas horas de este día han transcurrido let dayHoursElapsed = 0; if (isPast) { // Día completo ya pasado if (i === 0) { // Primer día (día del reinicio): desde hora de reinicio hasta medianoche dayHoursElapsed = 24 - (resetH + (resetM || 0) / 60); } else { dayHoursElapsed = 24; } } else if (isToday) { if (i === 0) { // Hoy es el día del reinicio: desde hora de reinicio hasta ahora dayHoursElapsed = currentHour - (resetH + (resetM || 0) / 60); if (dayHoursElapsed < 0) dayHoursElapsed = 0; } else { // Día actual normal: desde medianoche hasta ahora dayHoursElapsed = currentHour; } } // Días futuros: dayHoursElapsed = 0 // Calcular horas totales del día (para el porcentaje de llenado) let dayTotalHours = 24; if (i === 0) { // Primer día: solo cuenta desde la hora de reinicio dayTotalHours = 24 - (resetH + (resetM || 0) / 60); } // Porcentaje de llenado del día (0-100%) const dayFillPercent = dayTotalHours > 0 ? (dayHoursElapsed / dayTotalHours) * 100 : 0; // Uso asignado a este día (promedio distribuido) let usage = 0; if (dayHoursElapsed > 0 && hoursElapsed > 0) { // Distribuir el uso total proporcionalmente a las horas transcurridas usage = (dayHoursElapsed / hoursElapsed) * weeklyUsage; } // Estado basado en si el uso del día está dentro del ideal let status = 'pending'; if (dayHoursElapsed > 0) { const idealForThisDay = (dayHoursElapsed / 24) * idealDailyPercent; status = usage <= idealForThisDay * 1.1 ? 'good' : usage <= idealForThisDay * 1.5 ? 'warning' : 'danger'; } // Offset para el primer día (horas antes del reinicio que no cuentan) const dayStartOffsetPercent = (i === 0) ? ((resetH + (resetM || 0) / 60) / 24) * 100 : 0; dailyBreakdown.push({ dayIndex, dayName: DAYS_ES_DISPLAY[dayIndex], dayFullName: DAYS_FULL_ES[dayIndex], isPast, isToday, isFuture, isResetDay: i === 0, usage, idealUsage: idealDailyPercent, dayFillPercent, // % del día transcurrido (para llenar la barra) dayHoursElapsed, dayTotalHours, dayStartOffsetPercent, // % del día antes del reinicio (hueco a la izquierda) 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] || 'próximo reinicio'; const boostStatus = getBoostStatus(); container.innerHTML = ` ${boostStatus.isActive ? `