Files
claude-use-extension/content.js
Carlos Narro 6eeb5f929c mejora semanal
2026-03-20 04:31:17 +01:00

700 lines
28 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 ? `
<div class="boost-indicator ${boostStatus.isBoostPeriod ? 'active' : 'inactive'}">
<span class="boost-badge">${boostStatus.isBoostPeriod ? '⚡ 2×' : '1×'}</span>
<span class="boost-text">${boostStatus.isBoostPeriod ? 'Boost activo' : 'Horario normal'}</span>
<span class="boost-next">${boostStatus.isBoostPeriod ? '1× a las' : '2× a las'} ${boostStatus.nextChangeTime} (${boostStatus.nextChangeIn})</span>
</div>
` : ''}
<div class="claude-usage-tracker-title">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect>
<line x1="16" y1="2" x2="16" y2="6"></line>
<line x1="8" y1="2" x2="8" y2="6"></line>
<line x1="3" y1="10" x2="21" y2="10"></line>
</svg>
Uso semanal por días
</div>
<div class="weekly-progress-container">
${dailyData.dailyBreakdown.map(day => `
<div class="day-segment ${day.isToday ? 'today' : ''} ${day.isFuture ? 'future' : ''} ${day.isResetDayStart ? 'reset-day-start' : ''} ${day.isResetDayEnd ? 'reset-day-end' : ''}">
<div class="day-tooltip">
${day.dayFullName}: ${day.usage.toFixed(1)}% usado (${day.dayTotalHours.toFixed(1)}h)
${day.isToday ? `${day.dayHoursElapsed.toFixed(1)}h transcurridas` : day.isPast ? ' — completo' : ''}
${day.isResetDayStart ? `<br>🔄 Desde reinicio (${pageData.resetHour})` : ''}
${day.isResetDayEnd ? `<br>⏳ Hasta reinicio (${pageData.resetHour})` : ''}
</div>
<div class="day-bar">
${day.isResetDayStart ? `<div class="day-bar-offset start" style="width: ${day.dayStartOffsetPercent}%"></div>` : ''}
${day.isResetDayEnd ? `<div class="day-bar-offset end" style="left: ${100 - day.dayEndOffsetPercent}%; width: ${day.dayEndOffsetPercent}%"></div>` : ''}
<div class="day-bar-bg" style="left: ${day.dayStartOffsetPercent}%; width: ${day.dayFillPercent * (100 - day.dayStartOffsetPercent - day.dayEndOffsetPercent) / 100}%"></div>
<div class="day-bar-fill ${day.status === 'good' ? 'under-budget' : day.status === 'warning' ? 'on-track' : day.status === 'danger' ? 'over-budget' : ''}"
style="left: ${day.dayStartOffsetPercent}%; width: ${day.dayFillPercent * (100 - day.dayStartOffsetPercent - day.dayEndOffsetPercent) / 100}%">
</div>
</div>
<span class="day-label">${day.isResetDayStart ? day.dayName + '↓' : day.isResetDayEnd ? day.dayName + '↑' : day.dayName}</span>
</div>
`).join('')}
</div>
<div class="progress-summary">
<div class="progress-ideal">
<span class="progress-ideal-indicator ${dailyData.status}"></span>
<span>${dailyData.status === 'perfect' ? '✓ Perfecto' : dailyData.status === 'good' ? '✓ Vas bien' : dailyData.status === 'warning' ? '⚠ Atención' : '✗ Excedido'}</span>
</div>
<div class="usage-values">
<div class="usage-value">
<span class="usage-value-label">Usado</span>
<span class="usage-value-number used">${usagePercent.toFixed(1)}%</span>
</div>
<div class="usage-value">
<span class="usage-value-label">Ideal</span>
<span class="usage-value-number ideal">${dailyData.idealUsedByNow.toFixed(1)}%</span>
</div>
<div class="usage-value">
<span class="usage-value-label">Diferencia</span>
<span class="usage-value-number ${usagePercent <= dailyData.idealUsedByNow ? 'under' : 'over'}">
${usagePercent <= dailyData.idealUsedByNow ? '-' : '+'}${Math.abs(usagePercent - dailyData.idealUsedByNow).toFixed(1)}%
</span>
</div>
</div>
</div>
<div class="estimated-end ${dailyData.willRunOut ? 'warning' : ''}">
<span class="estimated-end-label">${dailyData.willRunOut ? '⚠ Se agotará el' : '📅 Reinicio el'}</span>
<span class="estimated-end-date">${dailyData.estimatedEndDate ? dailyData.estimatedEndDate.toLocaleDateString('es-ES', { weekday: 'long', day: 'numeric', month: 'short' }) : '--'}</span>
<span class="estimated-end-time">${dailyData.estimatedEndDate ? dailyData.estimatedEndDate.toLocaleTimeString('es-ES', { hour: '2-digit', minute: '2-digit' }) : '--:--'}</span>
${dailyData.willRunOut ? `<span class="estimated-end-hours">(en ~${Math.round(dailyData.hoursToReach100)}h)</span>` : ''}
</div>
<div class="usage-info-row">
<div class="usage-stat">
<span class="usage-stat-label">Tiempo transcurrido</span>
<span class="usage-stat-value">${Math.floor(dailyData.hoursElapsed)}h ${Math.round((dailyData.hoursElapsed % 1) * 60)}m</span>
</div>
<div class="usage-stat">
<span class="usage-stat-label">Uso/día real vs ideal</span>
<span class="usage-stat-value ${dailyData.avgUsagePerHour <= dailyData.idealUsagePerHour * 1.1 ? 'good' : dailyData.avgUsagePerHour <= dailyData.idealUsagePerHour * 1.5 ? 'warning' : 'danger'}">
${(dailyData.avgUsagePerHour * 24).toFixed(2)}% / ${(dailyData.idealUsagePerHour * 24).toFixed(2)}%
</span>
</div>
<div class="usage-stat">
<span class="usage-stat-label">Horas restantes</span>
<span class="usage-stat-value">${Math.round((168 - dailyData.hoursElapsed))}h</span>
</div>
</div>
<div class="session-info-container">
<div class="session-info-title">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"></circle>
<polyline points="12 6 12 12 16 14"></polyline>
</svg>
Sesión actual
</div>
<div class="session-sliders">
<div class="session-slider-row">
<span class="session-slider-label">⏱ Tiempo</span>
<div class="session-slider-track">
<div class="session-slider-fill time" style="width: ${100 - (pageData.sessionTimeRemainingMin / (SESSION_DURATION_HOURS * 60)) * 100}%"></div>
<div class="session-slider-marker" style="left: ${100 - (pageData.sessionTimeRemainingMin / (SESSION_DURATION_HOURS * 60)) * 100}%"></div>
</div>
<span class="session-slider-value">${Math.round(100 - (pageData.sessionTimeRemainingMin / (SESSION_DURATION_HOURS * 60)) * 100)}%</span>
</div>
<div class="session-slider-row">
<span class="session-slider-label">📊 Uso</span>
<div class="session-slider-track">
<div class="session-slider-fill usage ${pageData.sessionUsage <= (100 - (pageData.sessionTimeRemainingMin / (SESSION_DURATION_HOURS * 60)) * 100) ? 'good' : 'over'}" style="width: ${pageData.sessionUsage}%"></div>
<div class="session-slider-marker" style="left: ${pageData.sessionUsage}%"></div>
</div>
<span class="session-slider-value">${pageData.sessionUsage}%</span>
</div>
</div>
<div class="session-info-grid">
<div class="session-info-item">
<span class="session-info-label">Inicio</span>
<span class="session-info-value">${pageData.sessionStartTime ? formatTime(pageData.sessionStartTime) : '--:--'}</span>
</div>
<div class="session-info-item">
<span class="session-info-label">Fin</span>
<span class="session-info-value">${pageData.sessionEndTime ? formatTime(pageData.sessionEndTime) : '--:--'}</span>
</div>
<div class="session-info-item">
<span class="session-info-label">Restante</span>
<span class="session-info-value">${pageData.sessionTimeRemainingMin > 0 ? Math.floor(pageData.sessionTimeRemainingMin / 60) + 'h ' + (pageData.sessionTimeRemainingMin % 60) + 'm' : '--'}</span>
</div>
</div>
</div>
<div class="reset-info">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="23 4 23 10 17 10"></polyline>
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path>
</svg>
Reinicio semanal: ${resetDayName} a las ${pageData.resetHour} (en ${pageData.daysUntilReset} día${pageData.daysUntilReset !== 1 ? 's' : ''})
</div>
`;
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
})();