700 lines
28 KiB
JavaScript
700 lines
28 KiB
JavaScript
// 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
|
||
|
||
})();
|