commit ed2d44fc13d3ecf124036ceb15fb238fd7c42f34 Author: Carlos Narro Date: Wed Mar 18 23:11:18 2026 +0100 first commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..29378dd --- /dev/null +++ b/README.md @@ -0,0 +1,68 @@ +# Claude Usage Tracker - Extensión de Chrome + +Extensión de Chrome que mejora la página de uso de Claude (https://claude.ai/settings/usage) añadiendo: + +## Características + +### 📊 División semanal del uso +- Divide la barra de progreso "Todos los modelos" en los **7 días de la semana** +- Muestra visualmente cuánto deberías usar cada día para no quedarte sin cuota +- Indica con colores si vas bien (verde), con cuidado (amarillo) o excedido (rojo) + +### ⏱️ Información de sesión +- **Hora de inicio** de la sesión actual +- **Hora actual** +- **Duración** de la sesión + +### 📈 Estadísticas +- Uso diario ideal (≈14.29% por día) +- Promedio real de uso +- Días restantes hasta el reinicio + +## Instalación + +1. **Crear iconos PNG** (requerido): + - Abre `icons/icon.svg` en un editor de imágenes + - Exporta como PNG en tamaños: 16x16, 48x48 y 128x128 + - Guárdalos como `icon16.png`, `icon48.png` e `icon128.png` en la carpeta `icons/` + +2. **Cargar en Chrome**: + - Abre Chrome y ve a `chrome://extensions/` + - Activa el "Modo de desarrollador" (esquina superior derecha) + - Haz clic en "Cargar extensión sin empaquetar" + - Selecciona la carpeta `claude-use-extension` + +3. **Usar**: + - Ve a https://claude.ai/settings/usage + - La extensión se activará automáticamente mostrando el tracker semanal + +## Estructura de archivos + +``` +claude-use-extension/ +├── manifest.json # Configuración de la extensión +├── content.js # Script que modifica la página +├── styles.css # Estilos del tracker +├── icons/ +│ ├── icon.svg # Icono fuente (SVG) +│ ├── icon16.png # (crear manualmente) +│ ├── icon48.png # (crear manualmente) +│ └── icon128.png # (crear manualmente) +└── README.md +``` + +## Cómo funciona + +- **Detección de uso**: La extensión busca la barra de progreso en la página y extrae el porcentaje de uso actual +- **Cálculo semanal**: Asume que el uso se reinicia los lunes y calcula el uso ideal diario (100%/7 ≈ 14.29%) +- **Sesión**: Guarda la hora de inicio en `localStorage` y la mantiene si llevas menos de 4 horas + +## Personalización + +Puedes modificar `styles.css` para cambiar los colores y estilos del tracker. + +## Notas + +- La extensión solo funciona en `https://claude.ai/settings/usage` +- Los datos de sesión se guardan localmente en tu navegador +- No se envía ninguna información a servidores externos diff --git a/content.js b/content.js new file mode 100644 index 0000000..b890b9c --- /dev/null +++ b/content.js @@ -0,0 +1,648 @@ +// 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 ? ` +
+ ${boostStatus.isBoostPeriod ? '⚡ 2×' : '1×'} + ${boostStatus.isBoostPeriod ? 'Boost activo' : 'Horario normal'} + ${boostStatus.isBoostPeriod ? '1× a las' : '2× a las'} ${boostStatus.nextChangeTime} (${boostStatus.nextChangeIn}) +
+ ` : ''} + +
+ + + + + + + Uso semanal por días +
+ +
+ ${dailyData.dailyBreakdown.map(day => ` +
+
+ ${day.dayFullName}: ${day.usage.toFixed(1)}% usado + ${day.isToday ? ` (${day.dayHoursElapsed.toFixed(1)}h transcurridas)` : day.isPast ? ' (completo)' : ''} + ${day.isResetDay ? `
Reinicio a las ${pageData.resetHour}` : ''} +
Ideal para ${day.dayHoursElapsed.toFixed(0)}h: ${((day.dayHoursElapsed / 24) * day.idealUsage).toFixed(1)}% +
+
+ ${day.isResetDay ? `
` : ''} +
+
+
+
+ ${day.dayName} +
+ `).join('')} +
+ +
+
+ + ${dailyData.status === 'perfect' ? '✓ Perfecto' : dailyData.status === 'good' ? '✓ Vas bien' : dailyData.status === 'warning' ? '⚠ Atención' : '✗ Excedido'} +
+
+
+ Usado + ${usagePercent.toFixed(1)}% +
+
+ Ideal + ${dailyData.idealUsedByNow.toFixed(1)}% +
+
+ Diferencia + + ${usagePercent <= dailyData.idealUsedByNow ? '-' : '+'}${Math.abs(usagePercent - dailyData.idealUsedByNow).toFixed(1)}% + +
+
+
+ +
+ ${dailyData.willRunOut ? '⚠ Se agotará el' : '📅 Reinicio el'} + ${dailyData.estimatedEndDate ? dailyData.estimatedEndDate.toLocaleDateString('es-ES', { weekday: 'long', day: 'numeric', month: 'short' }) : '--'} + ${dailyData.estimatedEndDate ? dailyData.estimatedEndDate.toLocaleTimeString('es-ES', { hour: '2-digit', minute: '2-digit' }) : '--:--'} + ${dailyData.willRunOut ? `(en ~${Math.round(dailyData.hoursToReach100)}h)` : ''} +
+ +
+
+ Tiempo transcurrido + ${Math.floor(dailyData.hoursElapsed)}h ${Math.round((dailyData.hoursElapsed % 1) * 60)}m +
+
+ Uso/día real vs ideal + + ${(dailyData.avgUsagePerHour * 24).toFixed(2)}% / ${(dailyData.idealUsagePerHour * 24).toFixed(2)}% + +
+
+ Horas restantes + ${Math.round((168 - dailyData.hoursElapsed))}h +
+
+ +
+
+ + + + + Sesión actual +
+ +
+
+ ⏱ Tiempo +
+
+
+
+ ${Math.round(100 - (pageData.sessionTimeRemainingMin / (SESSION_DURATION_HOURS * 60)) * 100)}% +
+
+ 📊 Uso +
+
+
+
+ ${pageData.sessionUsage}% +
+
+ +
+
+ Inicio + ${pageData.sessionStartTime ? formatTime(pageData.sessionStartTime) : '--:--'} +
+
+ Fin + ${pageData.sessionEndTime ? formatTime(pageData.sessionEndTime) : '--:--'} +
+
+ Restante + ${pageData.sessionTimeRemainingMin > 0 ? Math.floor(pageData.sessionTimeRemainingMin / 60) + 'h ' + (pageData.sessionTimeRemainingMin % 60) + 'm' : '--'} +
+
+
+ +
+ + + + + Reinicio semanal: ${resetDayName} a las ${pageData.resetHour} (en ${pageData.daysUntilReset} día${pageData.daysUntilReset !== 1 ? 's' : ''}) +
+ `; + + return container; + } + + function injectTracker() { + // Remove existing tracker if present + const existing = document.getElementById('claude-usage-tracker'); + if (existing) { + existing.remove(); + } + + initSession(); + + // Parsear datos reales de la página + const pageData = parsePageData(); + + // Si no encontramos datos, no inyectar nada + if (pageData.weeklyUsage === 0 && pageData.resetDayIndex === -1) { + console.log('[Claude Usage Tracker] No se encontraron datos de uso en la página'); + return; + } + + const dailyData = calculateDailyUsage(pageData); + const tracker = createWeeklyTracker(pageData, dailyData); + + // Insertar justo DEBAJO del título principal h1 + const mainContent = document.querySelector('main'); + if (!mainContent) { + console.log('[Claude Usage Tracker] No se encontró el main'); + return; + } + + // Buscar el h1 del título principal "Ajustes" (visible en móvil) + const mobileH1 = mainContent.querySelector('h1.font-heading'); + + if (mobileH1) { + // Insertar justo después del h1 + mobileH1.after(tracker); + } else { + // Fallback: insertar al principio del main content + const contentDiv = mainContent.querySelector('.pb-8') || mainContent.querySelector('div > div'); + if (contentDiv) { + contentDiv.prepend(tracker); + } else { + mainContent.prepend(tracker); + } + } + + console.log('[Claude Usage Tracker] Inyectado en la parte superior', { + pageData, + dailyData + }); + } + + // Wait for page to fully load + function waitForContent() { + const observer = new MutationObserver((mutations, obs) => { + const main = document.querySelector('main'); + const hasContent = main && main.textContent.length > 100; + + if (hasContent) { + obs.disconnect(); + setTimeout(injectTracker, 500); // Small delay to ensure content is rendered + } + }); + + observer.observe(document.body, { + childList: true, + subtree: true + }); + + // Timeout fallback + setTimeout(() => { + observer.disconnect(); + injectTracker(); + }, 3000); + } + + // Initialize + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', waitForContent); + } else { + waitForContent(); + } + + // Update session time periodically + setInterval(() => { + const sessionValue = document.querySelector('.session-info-value'); + if (sessionValue) { + injectTracker(); + } + }, 60000); // Update every minute + +})(); diff --git a/icons/icon.svg b/icons/icon.svg new file mode 100644 index 0000000..d0393db --- /dev/null +++ b/icons/icon.svg @@ -0,0 +1,11 @@ + + + + + + + + + CLAUDE + USAGE TRACKER + diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..6194795 --- /dev/null +++ b/manifest.json @@ -0,0 +1,15 @@ +{ + "manifest_version": 3, + "name": "Claude Usage Tracker", + "version": "1.0", + "description": "Divide la barra de uso de Claude en 7 días y muestra información de sesión", + "permissions": ["storage"], + "content_scripts": [ + { + "matches": ["https://claude.ai/settings/usage*"], + "js": ["content.js"], + "css": ["styles.css"], + "run_at": "document_idle" + } + ] +} diff --git a/styles.css b/styles.css new file mode 100644 index 0000000..820edd3 --- /dev/null +++ b/styles.css @@ -0,0 +1,517 @@ +/* Claude Usage Tracker Extension Styles */ + +.claude-usage-tracker-container { + margin-top: 16px; + padding: 16px; + background: rgba(255, 255, 255, 0.05); + border-radius: 12px; + border: 1px solid rgba(255, 255, 255, 0.1); +} + +.claude-usage-tracker-title { + font-size: 14px; + font-weight: 600; + color: var(--text-100, #e5e5e5); + margin-bottom: 12px; + display: flex; + align-items: center; + gap: 8px; +} + +.claude-usage-tracker-title svg { + width: 16px; + height: 16px; +} + +/* Weekly Progress Bar */ +.weekly-progress-container { + display: flex; + gap: 4px; + height: 32px; + margin-bottom: 8px; +} + +.day-segment { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + position: relative; +} + +.day-bar { + width: 100%; + height: 20px; + background: rgba(255, 255, 255, 0.05); + border-radius: 4px; + position: relative; + overflow: hidden; +} + +.day-bar-bg { + position: absolute; + top: 0; + left: 0; + height: 100%; + background: rgba(255, 255, 255, 0.1); + border-radius: 4px; +} + +.day-bar-fill { + position: absolute; + top: 0; + left: 0; + height: 100%; + border-radius: 4px; + transition: width 0.3s ease; +} + +.day-segment.future .day-bar { + opacity: 0.4; +} + +/* Offset para el día de reinicio (hueco al principio) */ +.day-bar-offset { + position: absolute; + top: 0; + left: 0; + height: 100%; + background: repeating-linear-gradient( + -45deg, + rgba(50, 50, 50, 0.8), + rgba(50, 50, 50, 0.8) 3px, + rgba(30, 30, 30, 0.8) 3px, + rgba(30, 30, 30, 0.8) 6px + ); + border-radius: 4px 0 0 4px; + z-index: 2; +} + +.day-segment.reset-day .day-label::after { + content: ' 🔄'; + font-size: 8px; +} + +.day-bar-fill.under-budget { + background: linear-gradient(90deg, #10b981, #34d399); +} + +.day-bar-fill.on-track { + background: linear-gradient(90deg, #f59e0b, #fbbf24); +} + +.day-bar-fill.over-budget { + background: linear-gradient(90deg, #ef4444, #f87171); +} + +.day-label { + font-size: 10px; + color: var(--text-400, #a3a3a3); + margin-top: 4px; + text-transform: uppercase; +} + +.day-segment.today .day-label { + color: var(--accent-main-100, #f97316); + font-weight: 600; +} + +.day-segment.today .day-bar { + border: 1px solid var(--accent-main-100, #f97316); +} + +/* Daily Target Line */ +.daily-target-line { + position: absolute; + top: 0; + height: 100%; + width: 2px; + background: rgba(255, 255, 255, 0.5); + z-index: 1; +} + +/* Info Row */ +.usage-info-row { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 12px; + padding-top: 12px; + border-top: 1px solid rgba(255, 255, 255, 0.1); +} + +.usage-stat { + display: flex; + flex-direction: column; + align-items: center; +} + +.usage-stat-label { + font-size: 10px; + color: var(--text-400, #a3a3a3); + text-transform: uppercase; + margin-bottom: 4px; +} + +.usage-stat-value { + font-size: 14px; + font-weight: 600; + color: var(--text-100, #e5e5e5); +} + +.usage-stat-value.good { + color: #10b981; +} + +.usage-stat-value.warning { + color: #f59e0b; +} + +.usage-stat-value.danger { + color: #ef4444; +} + +/* Session Info */ +.session-info-container { + margin-top: 16px; + padding: 12px; + background: rgba(255, 255, 255, 0.03); + border-radius: 8px; + border: 1px solid rgba(255, 255, 255, 0.08); +} + +.session-info-title { + font-size: 12px; + font-weight: 600; + color: var(--text-200, #d4d4d4); + margin-bottom: 8px; + display: flex; + align-items: center; + gap: 6px; +} + +.session-info-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 12px; +} + +.session-info-item { + display: flex; + flex-direction: column; +} + +.session-info-label { + font-size: 10px; + color: var(--text-400, #a3a3a3); + margin-bottom: 2px; +} + +.session-info-value { + font-size: 13px; + font-weight: 500; + color: var(--text-100, #e5e5e5); +} + +.session-reset-text { + margin-top: 8px; + font-size: 11px; + color: var(--text-400, #a3a3a3); + font-style: italic; +} + +/* Tooltip */ +.day-tooltip { + position: absolute; + bottom: 100%; + left: 50%; + transform: translateX(-50%); + background: rgba(0, 0, 0, 0.9); + color: white; + padding: 6px 10px; + border-radius: 6px; + font-size: 11px; + white-space: nowrap; + opacity: 0; + pointer-events: none; + transition: opacity 0.2s; + z-index: 100; + margin-bottom: 4px; +} + +.day-segment:hover .day-tooltip { + opacity: 1; +} + +/* Reset Info */ +.reset-info { + display: flex; + align-items: center; + gap: 6px; + font-size: 11px; + color: var(--text-400, #a3a3a3); + margin-top: 8px; +} + +.reset-info svg { + width: 14px; + height: 14px; +} + +/* Progress summary */ +.progress-summary { + display: flex; + justify-content: space-between; + align-items: center; + font-size: 13px; + color: var(--text-200, #d4d4d4); + margin-top: 12px; + padding: 12px 16px; + background: rgba(255, 255, 255, 0.03); + border-radius: 8px; + border: 1px solid rgba(255, 255, 255, 0.08); +} + +.progress-ideal { + display: flex; + align-items: center; + gap: 6px; + font-weight: 600; + font-size: 14px; +} + +.progress-ideal-indicator { + width: 10px; + height: 10px; + border-radius: 50%; + box-shadow: 0 0 6px currentColor; +} + +.progress-ideal-indicator.perfect { + background: #06b6d4; + box-shadow: 0 0 8px #06b6d4; +} + +.progress-ideal-indicator.good { + background: #10b981; + box-shadow: 0 0 8px #10b981; +} + +.progress-ideal-indicator.warning { + background: #f59e0b; + box-shadow: 0 0 8px #f59e0b; +} + +.progress-ideal-indicator.danger { + background: #ef4444; + box-shadow: 0 0 8px #ef4444; +} + +.progress-summary .usage-values { + display: flex; + gap: 16px; + font-size: 14px; +} + +.progress-summary .usage-value { + display: flex; + flex-direction: column; + align-items: center; +} + +.progress-summary .usage-value-label { + font-size: 10px; + color: var(--text-400, #a3a3a3); + text-transform: uppercase; + margin-bottom: 2px; +} + +.progress-summary .usage-value-number { + font-size: 18px; + font-weight: 700; +} + +.progress-summary .usage-value-number.used { + color: var(--text-100, #f5f5f5); +} + +.progress-summary .usage-value-number.ideal { + color: var(--text-300, #a3a3a3); +} + +.progress-summary .usage-value-number.under { + color: #10b981; +} + +.progress-summary .usage-value-number.over { + color: #ef4444; +} + +/* Estimated end date */ +.estimated-end { + display: flex; + align-items: center; + gap: 8px; + margin-top: 12px; + padding: 10px 14px; + background: rgba(16, 185, 129, 0.1); + border: 1px solid rgba(16, 185, 129, 0.3); + border-radius: 8px; + font-size: 13px; +} + +.estimated-end.warning { + background: rgba(239, 68, 68, 0.1); + border-color: rgba(239, 68, 68, 0.3); +} + +.estimated-end-label { + color: var(--text-300, #a3a3a3); +} + +.estimated-end-date { + font-weight: 600; + color: var(--text-100, #f5f5f5); + text-transform: capitalize; +} + +.estimated-end-time { + color: var(--text-200, #d4d4d4); + font-weight: 500; +} + +.estimated-end-hours { + color: #ef4444; + font-size: 12px; + font-weight: 500; +} + +/* Session sliders */ +.session-sliders { + display: flex; + flex-direction: column; + gap: 8px; + margin: 12px 0; + padding: 12px; + background: rgba(0, 0, 0, 0.2); + border-radius: 8px; +} + +.session-slider-row { + display: flex; + align-items: center; + gap: 10px; +} + +.session-slider-label { + font-size: 12px; + color: var(--text-300, #a3a3a3); + width: 70px; + flex-shrink: 0; +} + +.session-slider-track { + flex: 1; + height: 8px; + background: rgba(255, 255, 255, 0.1); + border-radius: 4px; + position: relative; + overflow: visible; +} + +.session-slider-fill { + height: 100%; + border-radius: 4px; + transition: width 0.3s ease; +} + +.session-slider-fill.time { + background: linear-gradient(90deg, #3b82f6, #60a5fa); +} + +.session-slider-fill.usage.good { + background: linear-gradient(90deg, #10b981, #34d399); +} + +.session-slider-fill.usage.over { + background: linear-gradient(90deg, #f59e0b, #fbbf24); +} + +.session-slider-marker { + position: absolute; + top: -3px; + width: 3px; + height: 14px; + background: white; + border-radius: 2px; + transform: translateX(-50%); + box-shadow: 0 0 4px rgba(0, 0, 0, 0.5); +} + +.session-slider-value { + font-size: 12px; + font-weight: 600; + color: var(--text-200, #d4d4d4); + width: 35px; + text-align: right; + flex-shrink: 0; +} + +/* Boost 2× indicator */ +.boost-indicator { + display: flex; + align-items: center; + gap: 10px; + padding: 10px 14px; + border-radius: 8px; + margin-bottom: 12px; + font-size: 13px; +} + +.boost-indicator.active { + background: linear-gradient(135deg, rgba(16, 185, 129, 0.15), rgba(6, 182, 212, 0.15)); + border: 1px solid rgba(16, 185, 129, 0.4); +} + +.boost-indicator.inactive { + background: rgba(255, 255, 255, 0.03); + border: 1px solid rgba(255, 255, 255, 0.1); +} + +.boost-badge { + font-weight: 700; + font-size: 14px; + padding: 2px 8px; + border-radius: 4px; +} + +.boost-indicator.active .boost-badge { + background: linear-gradient(135deg, #10b981, #06b6d4); + color: white; + animation: boost-pulse 2s ease-in-out infinite; +} + +.boost-indicator.inactive .boost-badge { + background: rgba(255, 255, 255, 0.1); + color: var(--text-300, #a3a3a3); +} + +@keyframes boost-pulse { + 0%, 100% { box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.4); } + 50% { box-shadow: 0 0 8px 2px rgba(16, 185, 129, 0.3); } +} + +.boost-text { + font-weight: 600; + color: var(--text-100, #f5f5f5); +} + +.boost-indicator.inactive .boost-text { + color: var(--text-300, #a3a3a3); +} + +.boost-next { + margin-left: auto; + color: var(--text-300, #a3a3a3); + font-size: 12px; +} diff --git a/use.html b/use.html new file mode 100644 index 0000000..d5b750e --- /dev/null +++ b/use.html @@ -0,0 +1,620 @@ + + +Claude

Ajustes

Ajustes

Límites de uso del plan

Sesión actual

Se restablece en 3 h 44 min

65% usado

Todos los modelos

Se restablece vie, 4:59

52% usado

Solo Sonnet

Aún no has usado Sonnet

0% usado

Última actualización: hace 2 minutos

Uso adicional

Activa el uso adicional para seguir usando Claude si alcanzas un límite. Más información

5,45 € gastados

Se restablece el Apr 1

109% usado

5 €

Límite de gasto mensual

0,00 €

Saldo actual·

    \ No newline at end of file