first commit
This commit is contained in:
648
content.js
Normal file
648
content.js
Normal file
@@ -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 ? `
|
||||
<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.isResetDay ? 'reset-day' : ''}">
|
||||
<div class="day-tooltip">
|
||||
${day.dayFullName}: ${day.usage.toFixed(1)}% usado
|
||||
${day.isToday ? ` (${day.dayHoursElapsed.toFixed(1)}h transcurridas)` : day.isPast ? ' (completo)' : ''}
|
||||
${day.isResetDay ? `<br>Reinicio a las ${pageData.resetHour}` : ''}
|
||||
<br>Ideal para ${day.dayHoursElapsed.toFixed(0)}h: ${((day.dayHoursElapsed / 24) * day.idealUsage).toFixed(1)}%
|
||||
</div>
|
||||
<div class="day-bar">
|
||||
${day.isResetDay ? `<div class="day-bar-offset" style="width: ${day.dayStartOffsetPercent}%"></div>` : ''}
|
||||
<div class="day-bar-bg" style="left: ${day.dayStartOffsetPercent}%; width: ${(100 - day.dayStartOffsetPercent) * day.dayFillPercent / 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: ${(100 - day.dayStartOffsetPercent) * day.dayFillPercent / 100}%">
|
||||
</div>
|
||||
</div>
|
||||
<span class="day-label">${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
|
||||
|
||||
})();
|
||||
Reference in New Issue
Block a user