// 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+([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(/[\u0300-\u036f]/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:', resetText); } } } } // 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 ? `