Floating panel to track Claude usage limits and reset times. Drag anywhere to move. Minimized by default.
// ==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);
})();