From 699a8566d0f4ebe72cb32a5dd2d43c5dcb695d23 Mon Sep 17 00:00:00 2001 From: Carlos Narro Date: Thu, 4 Jun 2026 16:04:46 +0200 Subject: [PATCH] =?UTF-8?q?Simplificar=20a=20"FAB=20abre=20modal=20nativo"?= =?UTF-8?q?=20y=20a=C3=B1adir=20hora=20fin=20sesi=C3=B3n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit El usuario prefiere ver la página de Uso nativa de Claude en vez de un overlay propio. Eso permite borrar mucho código: - Fuera openOverlayPanel, closeOverlayPanel, setOverlaySpinner, renderOverlayWithCache, showOverlayError. - Fuera refreshInBackground, tryOpenViaPushState/Hash/Click, closeUsageModal, waitForDialogWithData — ya no auto-abrimos en background. - Fuera formatAge y CACHE_TTL_MS y OVERLAY_ID y refreshPromise. - Fuera __claudeUsageTracker (era andamio de debug). - Fuera bloque de CSS del overlay (~120 líneas). Nuevo onFabClick: 3 intentos en cascada para abrir el modal nativo de Claude (hash → pushState+popstate → click en "Uso"). Si los 3 fallan, warn en consola y queda el manual. Y siguiendo lo que pediste, en el resumen del tracker (el que se inyecta dentro del modal) ahora aparece un bloque azul "⏱ Sesión termina a las HH:MM (en Xh Ym)" justo debajo del bloque verde del reinicio semanal. Co-Authored-By: Claude Opus 4.7 (1M context) --- content.js | 277 +++++++---------------------------------------------- styles.css | 129 ++++--------------------- 2 files changed, 53 insertions(+), 353 deletions(-) diff --git a/content.js b/content.js index c5541cf..7789c57 100644 --- a/content.js +++ b/content.js @@ -587,6 +587,14 @@ ${dailyData.willRunOut ? `(en ~${Math.round(dailyData.hoursToReach100)}h)` : ''} + ${pageData.sessionEndTime ? ` +
+ ⏱ Sesión termina + a las ${formatTime(pageData.sessionEndTime)} + (en ${Math.floor(pageData.sessionTimeRemainingMin / 60)}h ${pageData.sessionTimeRemainingMin % 60}m) +
+ ` : ''} +
Tiempo transcurrido @@ -669,13 +677,10 @@ // =========================================================================== const CACHE_KEY = 'claude_usage_cache'; - const CACHE_TTL_MS = 5 * 60 * 1000; // 5 min const FAB_ID = 'claude-usage-tracker-fab'; - const OVERLAY_ID = 'claude-usage-tracker-overlay'; const TRACKER_ID = 'claude-usage-tracker'; let dialogObserver = null; - let refreshPromise = null; // -------- Cache (chrome.storage.local) -------- // chrome.storage serializa con JSON: los Date salen como strings ISO. Al @@ -732,15 +737,6 @@ return (Date.now() - new Date(scrapedAt).getTime()) / 60000; } - function formatAge(min) { - if (!isFinite(min)) return 'sin datos'; - if (min < 1) return 'ahora'; - if (min < 60) return `hace ${Math.floor(min)} min`; - const h = Math.floor(min / 60); - const m = Math.floor(min % 60); - return `hace ${h}h ${m}m`; - } - // -------- Detección del modal de Uso -------- function findUsageDialog() { const dialogs = document.querySelectorAll('[role="dialog"]'); @@ -885,221 +881,44 @@ else fab.classList.remove('claude-usage-tracker-fab--hidden'); } + // Click en el FAB: abrir el modal de Uso de Claude. Intenta varios métodos + // por si el router no escucha hashchange (Next.js puede colgar de popstate o + // de clicks en sus propios links). El usuario se conforma con ver el panel + // nativo, así que no abrimos nada propio. async function onFabClick() { try { - const cache = await loadCache(); - openOverlayPanel(cache); - const ageMin = cache ? cacheAgeMin(cache.scrapedAt) : Infinity; - if (ageMin > CACHE_TTL_MS / 60000) { - refreshInBackground('overlay-stale'); - } - } catch (e) { - console.error('[Claude Usage Tracker] FAB click error:', e); - } - } + if (isUsageDialogOpen()) return; - // -------- Overlay propio -------- - function openOverlayPanel(cache) { - try { - closeOverlayPanel(); - const overlay = document.createElement('div'); - overlay.id = OVERLAY_ID; - overlay.className = 'claude-usage-tracker-overlay'; - overlay.addEventListener('click', e => { - if (e.target === overlay) closeOverlayPanel(); - }); - - const panel = document.createElement('div'); - panel.className = 'claude-usage-tracker-overlay-panel'; - - const header = document.createElement('div'); - header.className = 'claude-usage-tracker-overlay-header'; - const ageMin = cache ? cacheAgeMin(cache.scrapedAt) : Infinity; - header.innerHTML = ` - ${formatAge(ageMin)} -
- - -
- `; - header.querySelector('.claude-usage-tracker-overlay-close').addEventListener('click', closeOverlayPanel); - header.querySelector('.claude-usage-tracker-overlay-refresh').addEventListener('click', () => refreshInBackground('overlay-manual')); - - const body = document.createElement('div'); - body.className = 'claude-usage-tracker-overlay-body'; - - if (cache && cache.pageData) { - body.appendChild(createWeeklyTracker(cache.pageData, cache.dailyData)); - } else { - body.innerHTML = '
Sin datos aún. Refrescando…
'; - setTimeout(() => refreshInBackground('overlay-empty'), 0); - } - - panel.appendChild(header); - panel.appendChild(body); - overlay.appendChild(panel); - document.body.appendChild(overlay); - - document.addEventListener('keydown', overlayEscapeHandler); - } catch (e) { - console.error('[Claude Usage Tracker] overlay error:', e); - } - } - - function overlayEscapeHandler(e) { - if (e.key === 'Escape') closeOverlayPanel(); - } - - function closeOverlayPanel() { - const ov = document.getElementById(OVERLAY_ID); - if (ov) ov.remove(); - document.removeEventListener('keydown', overlayEscapeHandler); - } - - function setOverlaySpinner(active) { - const ov = document.getElementById(OVERLAY_ID); - if (!ov) return; - let spinner = ov.querySelector('.claude-usage-tracker-overlay-spinner'); - if (active && !spinner) { - spinner = document.createElement('span'); - spinner.className = 'claude-usage-tracker-overlay-spinner'; - spinner.textContent = '⟳ actualizando…'; - const header = ov.querySelector('.claude-usage-tracker-overlay-header'); - if (header) header.appendChild(spinner); - } else if (!active && spinner) { - spinner.remove(); - } - } - - function renderOverlayWithCache(cache) { - const ov = document.getElementById(OVERLAY_ID); - if (!ov) return; - const body = ov.querySelector('.claude-usage-tracker-overlay-body'); - const age = ov.querySelector('.claude-usage-tracker-overlay-age'); - if (body) { - body.innerHTML = ''; - if (cache && cache.pageData) { - body.appendChild(createWeeklyTracker(cache.pageData, cache.dailyData)); + // Método 1: setear hash + if (location.hash !== '#settings/usage') { + location.hash = '#settings/usage'; } else { - body.innerHTML = '
No he podido cargar los datos. Abre Uso manualmente.
'; + // ya estaba seteado pero el modal no está abierto: limpiar primero + location.hash = ''; + await new Promise(r => setTimeout(r, 100)); + location.hash = '#settings/usage'; } - } - if (age && cache) age.textContent = formatAge(cacheAgeMin(cache.scrapedAt)); - } + await new Promise(r => setTimeout(r, 600)); + if (isUsageDialogOpen()) return; - function showOverlayError(msg) { - const ov = document.getElementById(OVERLAY_ID); - if (!ov) return; - const body = ov.querySelector('.claude-usage-tracker-overlay-body'); - const hasTracker = body && body.querySelector('.claude-usage-tracker-container'); - if (hasTracker) { - // No destruir el tracker visible — mostrar aviso temporal en el header - const header = ov.querySelector('.claude-usage-tracker-overlay-header'); - if (header) { - const existing = header.querySelector('.claude-usage-tracker-overlay-notice'); - if (existing) existing.remove(); - const notice = document.createElement('span'); - notice.className = 'claude-usage-tracker-overlay-notice'; - notice.textContent = msg; - header.appendChild(notice); - setTimeout(() => { if (notice.parentNode) notice.remove(); }, 7000); - } - } else if (body) { - body.textContent = ''; - const empty = document.createElement('div'); - empty.className = 'claude-usage-tracker-overlay-empty'; - empty.textContent = msg; - body.appendChild(empty); - } - } + // Método 2: pushState + popstate + history.pushState(null, '', '#settings/usage'); + window.dispatchEvent(new PopStateEvent('popstate')); + await new Promise(r => setTimeout(r, 600)); + if (isUsageDialogOpen()) return; - // -------- Refresh: abrir modal en background, scrapear, cerrar -------- - function waitForDialogWithData(timeoutMs) { - return new Promise(resolve => { - const t0 = Date.now(); - const check = () => { - const d = findUsageDialog(); - if (d && dialogHasData(d)) return resolve(d); - if (Date.now() - t0 > timeoutMs) return resolve(null); - setTimeout(check, 80); - }; - check(); - }); - } - - async function tryOpenViaPushState() { - history.pushState(null, '', '#settings/usage'); - window.dispatchEvent(new PopStateEvent('popstate')); - return waitForDialogWithData(700); - } - - async function tryOpenViaHash() { - location.hash = '#settings/usage'; - return waitForDialogWithData(700); - } - - async function tryOpenViaClick() { - const candidates = [...document.querySelectorAll('a, button')] - .filter(el => (el.innerText || '').trim() === 'Uso'); - if (candidates.length === 0) return null; - candidates[0].click(); - return waitForDialogWithData(1500); - } - - function closeUsageModal() { - const dialog = findUsageDialog(); - if (!dialog) return; - const closeBtn = [...dialog.querySelectorAll('button')] - .find(b => /close|cerrar/i.test(b.getAttribute('aria-label') || '')); - if (closeBtn) { closeBtn.click(); return; } - document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true, cancelable: true })); - } - - async function refreshInBackground(reason) { - if (refreshPromise) return refreshPromise; - refreshPromise = (async () => { - console.log('[Claude Usage Tracker] refresh:', reason); - setOverlaySpinner(true); - - const previousHash = location.hash; - const wasOpen = isUsageDialogOpen(); - - let dialog = wasOpen ? findUsageDialog() : null; - if (!dialog) dialog = await tryOpenViaPushState(); - if (!dialog) dialog = await tryOpenViaHash(); - if (!dialog) dialog = await tryOpenViaClick(); - - if (!dialog) { - console.warn('[Claude Usage Tracker] no he podido abrir el modal automáticamente'); - setOverlaySpinner(false); - showOverlayError('No he podido refrescar automáticamente — abre Uso manualmente y vuelve.'); - refreshPromise = null; + // Método 3: clickar el enlace "Uso" si está visible + const usoBtn = [...document.querySelectorAll('a, button')] + .find(el => (el.innerText || '').trim() === 'Uso' && el.offsetParent !== null); + if (usoBtn) { + usoBtn.click(); return; } - const pageData = parsePageData(); - let cache = null; - if (pageData.weeklyUsage > 0 || pageData.resetDayIndex >= 0) { - initSession(); - const dailyData = calculateDailyUsage(pageData); - saveCache(pageData, dailyData, reason === 'overlay-manual' ? 'manual' : 'auto'); - cache = { pageData, dailyData, scrapedAt: new Date().toISOString() }; - updateFabState(cache); - } - - // Cerrar modal solo si nosotros lo abrimos - if (!wasOpen) { - closeUsageModal(); - if (location.hash !== previousHash) { - history.replaceState(null, '', location.pathname + location.search + previousHash); - } - } - - setOverlaySpinner(false); - renderOverlayWithCache(cache); - refreshPromise = null; - })(); - return refreshPromise; + console.warn('[Claude Usage Tracker] No he podido abrir el modal de Uso. Ábrelo manualmente desde el avatar.'); + } catch (e) { + console.error('[Claude Usage Tracker] FAB click error:', e); + } } // -------- Bootstrap -------- @@ -1128,30 +947,6 @@ }, 500); } - // Exposición para depuración manual desde la consola - window.__claudeUsageTracker = { - version: 'debug-fab-click', - loadCache, - saveCache, - openOverlayPanel, - closeOverlayPanel, - refreshInBackground, - findUsageDialog, - isUsageDialogOpen, - parsePageData, - injectFloatingButton, - updateFabState, - fabExists: () => !!document.getElementById(FAB_ID), - overlayExists: () => !!document.getElementById(OVERLAY_ID), - forceShowOverlay: async () => { - const cache = await loadCache(); - console.log('[CUT-DIAG forceShowOverlay] cache:', cache); - openOverlayPanel(cache); - return !!document.getElementById(OVERLAY_ID); - } - }; - console.log('[CUT-DIAG] window.__claudeUsageTracker expuesto. Métodos:', Object.keys(window.__claudeUsageTracker).join(', ')); - if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', bootstrap); } else { diff --git a/styles.css b/styles.css index 2e5b7e7..6689c8c 100644 --- a/styles.css +++ b/styles.css @@ -576,126 +576,31 @@ } /* =========================================================================== - Overlay propio — abierto al click del FAB + Resumen — hora del próximo reinicio de sesión =========================================================================== */ -.claude-usage-tracker-overlay { - position: fixed; - inset: 0; - z-index: 999999; - background: rgba(0, 0, 0, 0.55); - backdrop-filter: blur(4px); - -webkit-backdrop-filter: blur(4px); - display: flex; - align-items: flex-start; - justify-content: center; - padding: 60px 16px 40px; - overflow-y: auto; - animation: claude-tracker-fade-in 0.15s ease; -} - -@keyframes claude-tracker-fade-in { - from { opacity: 0; } - to { opacity: 1; } -} - -.claude-usage-tracker-overlay-panel { - width: 100%; - max-width: 720px; - background: #1a1a1d; - border: 1px solid rgba(255, 255, 255, 0.1); - border-radius: 14px; - box-shadow: 0 24px 64px rgba(0, 0, 0, 0.6); - padding: 16px 20px 20px; - color: #f5f5f5; - font-family: inherit; -} - -.claude-usage-tracker-overlay-header { +.session-reset-info { display: flex; align-items: center; - justify-content: space-between; - gap: 12px; - margin-bottom: 12px; - padding-bottom: 12px; - border-bottom: 1px solid rgba(255, 255, 255, 0.08); - flex-wrap: wrap; -} - -.claude-usage-tracker-overlay-age { - font-size: 12px; - color: var(--text-400, #a3a3a3); - font-style: italic; -} - -.claude-usage-tracker-overlay-actions { - display: flex; gap: 8px; - margin-left: auto; - align-items: center; -} - -.claude-usage-tracker-overlay-refresh, -.claude-usage-tracker-overlay-close { - background: rgba(255, 255, 255, 0.06); - color: #f5f5f5; - border: 1px solid rgba(255, 255, 255, 0.12); - border-radius: 8px; - padding: 6px 12px; - font-size: 12px; - font-weight: 500; - cursor: pointer; - font-family: inherit; - transition: background 0.15s ease, border-color 0.15s ease; -} - -.claude-usage-tracker-overlay-refresh:hover, -.claude-usage-tracker-overlay-close:hover { - background: rgba(255, 255, 255, 0.12); - border-color: rgba(255, 255, 255, 0.22); -} - -.claude-usage-tracker-overlay-close { - padding: 6px 10px; -} - -.claude-usage-tracker-overlay-spinner { - font-size: 12px; - color: #06b6d4; - font-style: italic; - margin-left: 8px; - animation: claude-tracker-spin-pulse 1.2s ease-in-out infinite; -} - -.claude-usage-tracker-overlay-notice { - width: 100%; margin-top: 8px; - padding: 6px 10px; - background: rgba(245, 158, 11, 0.12); - border: 1px solid rgba(245, 158, 11, 0.35); - border-radius: 6px; - font-size: 12px; - color: #f59e0b; - animation: claude-tracker-fade-in 0.2s ease; + padding: 10px 14px; + background: rgba(59, 130, 246, 0.1); + border: 1px solid rgba(59, 130, 246, 0.3); + border-radius: 8px; + font-size: 13px; } -@keyframes claude-tracker-spin-pulse { - 0%, 100% { opacity: 0.6; } - 50% { opacity: 1; } -} - -.claude-usage-tracker-overlay-body { - min-height: 80px; -} - -.claude-usage-tracker-overlay-empty { - padding: 40px 16px; - text-align: center; +.session-reset-info-label { color: var(--text-300, #a3a3a3); - font-size: 14px; } -/* Cuando el tracker se inyecta dentro de nuestro overlay, ya está dentro de un - contenedor con padding/borde; quitamos su margen superior para que pegue. */ -.claude-usage-tracker-overlay-body .claude-usage-tracker-container { - margin-top: 0; +.session-reset-info-time { + font-weight: 600; + color: var(--text-100, #f5f5f5); +} + +.session-reset-info-remaining { + color: var(--text-200, #d4d4d4); + font-weight: 500; + margin-left: auto; }