Arreglar parser: anclar en texto/role en vez de clases CSS

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) <noreply@anthropic.com>
This commit is contained in:
Carlos Narro
2026-05-25 13:46:39 +02:00
parent 6eeb5f929c
commit ec68336a5d

View File

@@ -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');
// Sesión actual
const session = getMetricByLabel('Sesión actual');
if (session) {
data.sessionUsage = session.usage;
data.sessionResetText = session.resetText;
for (const section of sections) {
const rows = section.querySelectorAll('.flex.flex-row');
// 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;
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);
}
}
// 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