Claude Usage Tracker v2

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

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

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

})();