From ec68336a5dd63c4513f87972ff40cc5f4a0641a7 Mon Sep 17 00:00:00 2001 From: Carlos Narro Date: Mon, 25 May 2026 13:46:39 +0200 Subject: [PATCH] Arreglar parser: anclar en texto/role en vez de clases CSS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit La página de uso de Claude eliminó el design token text-text-100, lo que rompía la búsqueda de filas por p.font-base.text-text-100 y dejaba al tracker sin inyectarse. Reescrito parsePageData para localizar cada métrica por su etiqueta de texto ("Todos los modelos", "Sesión actual"), subir hasta el ancestro con [role=progressbar] y leer aria-valuenow + texto "Se restablece". Robusto ante renombrados de clases de Claude. Co-Authored-By: Claude Opus 4.7 (1M context) --- content.js | 149 +++++++++++++++++++++++++++++++++-------------------- 1 file changed, 93 insertions(+), 56 deletions(-) diff --git a/content.js b/content.js index 72f375a..6e9fad1 100644 --- a/content.js +++ b/content.js @@ -138,6 +138,52 @@ return 0; } + /** + * Encuentra el elemento "hoja" cuyo texto coincide exactamente con el texto dado. + * Se ancla en el TEXTO, no en clases CSS, para ser robusto ante los cambios + * de design tokens de Claude (p.ej. text-text-100 desapareció en mayo 2026). + */ + function findLabelElement(text) { + let best = null; + const candidates = document.querySelectorAll('p, span, div, h2, h3, h4'); + for (const el of candidates) { + if (el.textContent.trim() === text) { + // Preferir el match más interno (menor longitud de texto = más cercano a la hoja) + if (!best || el.textContent.length <= best.textContent.length) { + best = el; + } + } + } + return best; + } + + /** + * Dada la etiqueta de una métrica (p.ej. "Todos los modelos" o "Sesión actual"), + * localiza su fila subiendo desde la etiqueta hasta el primer ancestro que + * contiene un [role="progressbar"], y devuelve { usage, resetText }. + * No depende de nombres de clase CSS de Claude. + */ + function getMetricByLabel(labelText) { + const labelEl = findLabelElement(labelText); + if (!labelEl) return null; + + let container = labelEl; + for (let i = 0; i < 10 && container; i++) { + if (container.querySelector('[role="progressbar"]')) break; + container = container.parentElement; + } + + const progressBar = container && container.querySelector('[role="progressbar"]'); + if (!progressBar) return null; + + const usage = parseFloat(progressBar.getAttribute('aria-valuenow')) || 0; + const containerText = container.innerText || container.textContent || ''; + const resetMatch = containerText.match(/Se restablece[^\n]*/); + const resetText = resetMatch ? resetMatch[0].trim() : ''; + + return { usage, resetText }; + } + /** * Parsea la información de la página real de Claude * Busca los progressbar con aria-valuenow y el texto "Se restablece..." @@ -157,63 +203,54 @@ 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); - } - } + // Sesión actual + const session = getMetricByLabel('Sesión actual'); + if (session) { + data.sessionUsage = session.usage; + data.sessionResetText = session.resetText; + + // Calcular tiempo restante y horas de inicio/fin + data.sessionTimeRemainingMin = parseTimeRemaining(session.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 { + console.log('[Claude Usage Tracker] No se encontró la métrica "Sesión actual"'); + } + + // Todos los modelos (uso semanal) + const weekly = getMetricByLabel('Todos los modelos'); + if (weekly) { + data.weeklyUsage = weekly.usage; + data.weeklyResetText = weekly.resetText; + + // Parsear "Se restablece vie, 4:59" para obtener día y hora + const resetMatch = weekly.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(/[̀-ͯ]/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:', weekly.resetText); + } + } else { + console.log('[Claude Usage Tracker] No se encontró la métrica "Todos los modelos"'); } // Calcular días hasta el reinicio y días pasados