Claude Usage Tracker v2

Floating panel to track Claude usage limits and reset times. Drag anywhere to move. Minimized by default.

Vous devrez installer une extension telle que Tampermonkey, Greasemonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Userscripts pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension de gestionnaire de script utilisateur pour installer ce script.

(J'ai déjà un gestionnaire de scripts utilisateur, laissez-moi l'installer !)

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

(J'ai déjà un gestionnaire de style utilisateur, laissez-moi l'installer!)

// ==UserScript==
// @name         Claude Usage Tracker v2
// @namespace    usage-and-quick-settings-of-claude
// @author       Yalums
// @version      2
// @description  Floating panel to track Claude usage limits and reset times. Drag anywhere to move. Minimized by default.
// @match        https://claude.ai/*
// @grant        none
// @run-at       document-start
// @license      GNU General Public License v3.0
// ==/UserScript==

(function() {
    'use strict';

    // v2: Smoother dragging + single click to toggle (no double click needed)

    function resetPositionIfNeeded() {
        const pos = JSON.parse(localStorage.getItem('claudePanel_position') || '{}');
        const left = parseInt(pos.left) || 0;
        const top = parseInt(pos.top) || 0;

        if (left > window.innerWidth - 50 || top > window.innerHeight - 50 || left < 0 || top < 0) {
            localStorage.setItem('claudePanel_position', JSON.stringify({left: "80px", top: "100px"}));
        }
    }

    const storedMinimized = localStorage.getItem('claudePanel_minimized');
    let panelState = {
        isMinimized: storedMinimized !== 'false',
        position: JSON.parse(localStorage.getItem('claudePanel_position') || '{"left":"80px","top":"100px"}')
    };

    async function getUsageData() {
        try {
            const orgsResponse = await fetch('/api/organizations', { credentials: 'include' });
            const orgs = await orgsResponse.json();
            const orgId = orgs[0]?.uuid;
            if (!orgId) return null;
            const usageResponse = await fetch(`/api/organizations/${orgId}/usage`, { credentials: 'include' });
            return await usageResponse.json();
        } catch (err) {
            return null;
        }
    }

    function formatResetTime(isoTime) {
        if (!isoTime) return 'N/A';
        const diff = new Date(isoTime) - new Date();
        const minutes = Math.floor(diff / 60000);
        const hours = Math.floor(diff / 3600000);
        const days = Math.floor(diff / 86400000);
        if (minutes < 1) return 'soon';
        if (minutes < 60) return `${minutes}m`;
        if (hours < 24) return `${hours}h`;
        return `${days}d`;
    }

    function injectStyles() {
        if (document.getElementById('claude-panel-styles')) return;

        const style = document.createElement('style');
        style.id = 'claude-panel-styles';
        style.textContent = `
            #claude-control-panel {
                position: fixed !important;
                z-index: 2147483647 !important;
                background: linear-gradient(145deg, #6366f1 0%, #7c3aed 100%) !important;
                border: none !important;
                border-radius: 14px !important;
                box-shadow: 0 4px 24px rgba(99, 102, 241, 0.4) !important;
                font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif !important;
                color: #ffffff !important;
                overflow: hidden !important;
                display: block !important;
                visibility: visible !important;
                opacity: 1 !important;
                pointer-events: auto !important;
                touch-action: none !important;
                user-select: none !important;
            }

            #claude-control-panel.dragging {
                cursor: grabbing !important;
                box-shadow: 0 12px 40px rgba(99, 102, 241, 0.6) !important;
                opacity: 0.9 !important;
            }

            /* MINIMIZED STATE */
            #claude-control-panel.minimized {
                width: 46px !important;
                height: 46px !important;
                cursor: pointer !important;
                border-radius: 13px !important;
                padding: 0 !important;
                display: flex !important;
                align-items: center !important;
                justify-content: center !important;
            }

            #claude-control-panel.minimized:not(.dragging):hover {
                box-shadow: 0 6px 28px rgba(99, 102, 241, 0.7) !important;
                transform: scale(1.06) !important;
            }

            #claude-control-panel.minimized .panel-expanded {
                display: none !important;
            }

            #claude-control-panel.minimized .panel-icon {
                display: flex !important;
                font-size: 20px !important;
                cursor: pointer !important;
                align-items: center !important;
                justify-content: center !important;
                width: 100% !important;
                height: 100% !important;
            }

            /* EXPANDED STATE */
            #claude-control-panel:not(.minimized) {
                width: 160px !important;
                cursor: grab !important;
            }

            #claude-control-panel:not(.minimized) .panel-icon {
                display: none !important;
            }

            .panel-expanded {
                display: flex !important;
                flex-direction: column !important;
            }

            /* Header */
            .panel-header-row {
                display: flex !important;
                align-items: center !important;
                justify-content: space-between !important;
                padding: 8px 10px !important;
                background: rgba(0, 0, 0, 0.15) !important;
                border-bottom: 1px solid rgba(255, 255, 255, 0.1) !important;
                cursor: pointer !important;
            }

            .panel-header-row:hover {
                background: rgba(0, 0, 0, 0.25) !important;
            }

            .panel-title {
                font-size: 11px !important;
                font-weight: 700 !important;
                display: flex !important;
                align-items: center !important;
                gap: 5px !important;
            }

            .panel-btns {
                display: flex !important;
                gap: 4px !important;
            }

            .panel-btn-sm {
                background: rgba(255, 255, 255, 0.15) !important;
                border: none !important;
                border-radius: 5px !important;
                width: 22px !important;
                height: 22px !important;
                cursor: pointer !important;
                display: flex !important;
                align-items: center !important;
                justify-content: center !important;
                color: #ffffff !important;
                font-size: 12px !important;
                transition: background 0.15s ease !important;
            }

            .panel-btn-sm:hover {
                background: rgba(255, 255, 255, 0.3) !important;
            }

            /* Content */
            .panel-content-area {
                padding: 8px 10px 10px !important;
                display: flex !important;
                flex-direction: column !important;
                gap: 6px !important;
            }

            /* Usage items - compact rows */
            .usage-row-item {
                display: flex !important;
                align-items: center !important;
                gap: 8px !important;
            }

            .usage-name {
                font-size: 10px !important;
                font-weight: 600 !important;
                width: 32px !important;
                opacity: 0.9 !important;
            }

            .usage-bar-wrap {
                flex: 1 !important;
                height: 7px !important;
                background: rgba(255, 255, 255, 0.25) !important;
                border-radius: 4px !important;
                overflow: hidden !important;
            }

            .usage-bar-inner {
                height: 100% !important;
                background: #34d399 !important;
                border-radius: 4px !important;
                transition: width 0.3s ease !important;
            }

            .usage-bar-inner.warning {
                background: #fbbf24 !important;
            }

            .usage-bar-inner.danger {
                background: #f87171 !important;
            }

            .usage-info {
                font-size: 9px !important;
                text-align: right !important;
                min-width: 38px !important;
                opacity: 0.85 !important;
            }

            .usage-pct {
                font-weight: 700 !important;
            }

            .usage-time {
                opacity: 0.7 !important;
                font-size: 8px !important;
            }

            body.panel-dragging {
                user-select: none !important;
                cursor: grabbing !important;
            }

            body.panel-dragging * {
                cursor: grabbing !important;
            }
        `;

        if (document.head) {
            document.head.appendChild(style);
        } else if (document.documentElement) {
            document.documentElement.appendChild(style);
        }
    }

    function togglePanel(panel) {
        panel.classList.toggle('minimized');
        panelState.isMinimized = panel.classList.contains('minimized');
        localStorage.setItem('claudePanel_minimized', panelState.isMinimized ? 'true' : 'false');
    }

    function createPanel() {
        const existing = document.getElementById('claude-control-panel');
        if (existing) existing.remove();

        resetPositionIfNeeded();
        panelState.position = JSON.parse(localStorage.getItem('claudePanel_position') || '{"left":"80px","top":"100px"}');

        const panel = document.createElement('div');
        panel.id = 'claude-control-panel';
        panel.className = panelState.isMinimized ? 'minimized' : '';

        panel.innerHTML = `
            <div class="panel-icon">📊</div>
            <div class="panel-expanded">
                <div class="panel-header-row" id="header-row">
                    <span class="panel-title">📊 Usage</span>
                    <div class="panel-btns">
                        <button class="panel-btn-sm" id="refresh-btn" title="Refresh">🔄</button>
                    </div>
                </div>
                <div class="panel-content-area" id="usage-content">
                    <div style="font-size: 10px; opacity: 0.8; text-align: center; padding: 4px;">Loading...</div>
                </div>
            </div>
        `;

        const left = parseInt(panelState.position.left) || 80;
        const top = parseInt(panelState.position.top) || 100;

        panel.style.left = left + 'px';
        panel.style.top = top + 'px';

        const container = document.body || document.documentElement;
        container.appendChild(panel);

        // Refresh button
        panel.querySelector('#refresh-btn').addEventListener('click', (e) => {
            e.stopPropagation();
            const content = panel.querySelector('#usage-content');
            content.innerHTML = '<div style="font-size: 10px; opacity: 0.8; text-align: center; padding: 4px;">...</div>';
            updatePanelContent(panel);
        });

        makeDraggable(panel);
        return panel;
    }

    async function updatePanelContent(panel) {
        const content = panel.querySelector('#usage-content');
        if (!content) return;

        const usageData = await getUsageData();

        if (!usageData) {
            content.innerHTML = '<div style="font-size: 10px; opacity: 0.8; text-align: center; padding: 4px;">❌ Error</div>';
            return;
        }

        let html = '';

        if (usageData.five_hour) {
            const percent = usageData.five_hour.utilization || 0;
            const barClass = percent > 80 ? 'danger' : percent > 60 ? 'warning' : '';
            const time = formatResetTime(usageData.five_hour.resets_at);
            html += `
                <div class="usage-row-item">
                    <span class="usage-name">5hr</span>
                    <div class="usage-bar-wrap"><div class="usage-bar-inner ${barClass}" style="width: ${percent}%"></div></div>
                    <span class="usage-info"><span class="usage-pct">${percent}%</span> <span class="usage-time">${time}</span></span>
                </div>
            `;
        }

        if (usageData.seven_day) {
            const percent = usageData.seven_day.utilization || 0;
            const barClass = percent > 80 ? 'danger' : percent > 60 ? 'warning' : '';
            const time = formatResetTime(usageData.seven_day.resets_at);
            html += `
                <div class="usage-row-item">
                    <span class="usage-name">7day</span>
                    <div class="usage-bar-wrap"><div class="usage-bar-inner ${barClass}" style="width: ${percent}%"></div></div>
                    <span class="usage-info"><span class="usage-pct">${percent}%</span> <span class="usage-time">${time}</span></span>
                </div>
            `;
        }

        if (usageData.seven_day_opus) {
            const percent = usageData.seven_day_opus.utilization || 0;
            const barClass = percent > 80 ? 'danger' : percent > 60 ? 'warning' : '';
            const time = formatResetTime(usageData.seven_day_opus.resets_at);
            html += `
                <div class="usage-row-item">
                    <span class="usage-name">Opus</span>
                    <div class="usage-bar-wrap"><div class="usage-bar-inner ${barClass}" style="width: ${percent}%"></div></div>
                    <span class="usage-info"><span class="usage-pct">${percent}%</span> <span class="usage-time">${time}</span></span>
                </div>
            `;
        }

        content.innerHTML = html || '<div style="font-size: 10px; opacity: 0.8; text-align: center;">No data</div>';
    }

    function makeDraggable(element) {
        let isDragging = false;
        let hasMoved = false;
        let offsetX = 0, offsetY = 0;
        let startLeft = 0, startTop = 0;

        function getPointerPosition(e) {
            if (e.touches && e.touches.length > 0) {
                return { x: e.touches[0].clientX, y: e.touches[0].clientY };
            }
            return { x: e.clientX, y: e.clientY };
        }

        function dragStart(e) {
            // Don't drag from buttons
            if (e.target.closest('.panel-btn-sm')) return;

            const pos = getPointerPosition(e);
            const rect = element.getBoundingClientRect();

            offsetX = pos.x - rect.left;
            offsetY = pos.y - rect.top;
            startLeft = rect.left;
            startTop = rect.top;

            isDragging = true;
            hasMoved = false;

            element.classList.add('dragging');
            document.body.classList.add('panel-dragging');

            e.preventDefault();
        }

        function drag(e) {
            if (!isDragging) return;
            e.preventDefault();

            const pos = getPointerPosition(e);

            let newX = pos.x - offsetX;
            let newY = pos.y - offsetY;

            // Check if moved more than 5px (threshold for "actually dragging")
            if (Math.abs(newX - startLeft) > 5 || Math.abs(newY - startTop) > 5) {
                hasMoved = true;
            }

            // Keep within viewport
            const maxX = window.innerWidth - element.offsetWidth;
            const maxY = window.innerHeight - element.offsetHeight;
            newX = Math.max(0, Math.min(newX, maxX));
            newY = Math.max(0, Math.min(newY, maxY));

            // Direct position update (smooth on modern browsers)
            element.style.left = newX + 'px';
            element.style.top = newY + 'px';
        }

        function dragEnd(e) {
            if (!isDragging) return;
            isDragging = false;

            element.classList.remove('dragging');
            document.body.classList.remove('panel-dragging');

            // Save position
            panelState.position = { left: element.style.left, top: element.style.top };
            localStorage.setItem('claudePanel_position', JSON.stringify(panelState.position));

            // SINGLE CLICK TO TOGGLE - if didn't move, toggle the panel
            if (!hasMoved) {
                togglePanel(element);
            }
        }

        element.addEventListener('mousedown', dragStart);
        document.addEventListener('mousemove', drag);
        document.addEventListener('mouseup', dragEnd);

        element.addEventListener('touchstart', dragStart, { passive: false });
        document.addEventListener('touchmove', drag, { passive: false });
        document.addEventListener('touchend', dragEnd);
    }

    function init() {
        injectStyles();
        const panel = createPanel();
        updatePanelContent(panel);
        setInterval(() => updatePanelContent(panel), 60000);
    }

    if (document.readyState === 'complete' || document.readyState === 'interactive') {
        setTimeout(init, 500);
    }

    document.addEventListener('DOMContentLoaded', () => {
        if (!document.getElementById('claude-control-panel')) {
            setTimeout(init, 500);
        }
    });

    window.addEventListener('load', () => {
        if (!document.getElementById('claude-control-panel')) {
            setTimeout(init, 1000);
        }
    });

    setTimeout(() => {
        if (!document.getElementById('claude-control-panel')) {
            init();
        }
    }, 3000);

})();