Claude Usage Tracker v2

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

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey, το Greasemonkey ή το Violentmonkey για να εγκαταστήσετε αυτόν τον κώδικα.

You will need to install an extension such as Tampermonkey to install this script.

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey ή το Violentmonkey για να εγκαταστήσετε αυτόν τον κώδικα.

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey ή το Userscripts για να εγκαταστήσετε αυτόν τον κώδικα.

You will need to install an extension such as Tampermonkey to install this script.

Θα χρειαστεί να εγκαταστήσετε μια επέκταση διαχείρισης κώδικα χρήστη για να εγκαταστήσετε αυτόν τον κώδικα.

(Έχω ήδη έναν διαχειριστή κώδικα χρήστη, επιτρέψτε μου να τον εγκαταστήσω!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(Έχω ήδη έναν διαχειριστή στυλ χρήστη, επιτρέψτε μου να τον εγκαταστήσω!)

// ==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);

})();