Floating widget showing claude.ai message quota usage (5h & 7d).
// ==UserScript==
// @name Claude Usage HUD
// @namespace foxbinner
// @version 2.0.0
// @description Floating widget showing claude.ai message quota usage (5h & 7d).
// @match https://claude.ai/*
// @grant none
// @run-at document-idle
// @license MIT
// ==/UserScript==
(function () {
'use strict';
if (document.getElementById('cuw')) return;
const POLL_INTERVAL_MS = 60_000;
const R = 20;
const CIRC = 2 * Math.PI * R;
const ARC = CIRC * 0.75;
function arcOffset(pct) {
const filled = ARC * (pct / 100);
return `${filled} ${CIRC - filled}`;
}
function getColor(pct) {
if (pct < 50) return '#7dbd8e';
if (pct < 75) return '#d4a847';
if (pct < 90) return '#e8825a';
return '#d95f5f';
}
function dotClass(pct) {
if (pct < 50) return 'ok';
if (pct < 75) return 'warn';
if (pct < 90) return 'hot';
return 'crit';
}
function formatReset(iso) {
if (!iso) return '';
const ms = new Date(iso) - Date.now();
if (ms <= 0) return 'Resetting…';
const h = Math.floor(ms / 3_600_000);
const m = Math.floor((ms % 3_600_000) / 60_000);
if (h >= 24) return `${Math.floor(h / 24)}d ${h % 24}h left`;
if (h > 0) return `${h}h ${m}m left`;
return `${m}m left`;
}
const style = document.createElement('style');
style.textContent = `
#cuw {
position: fixed;
bottom: 16px;
right: 16px;
z-index: 999999;
width: 180px;
border-radius: 14px;
background: #1e1d1a;
border: 1px solid rgba(255,255,255,0.13);
box-shadow:
0 2px 8px rgba(0,0,0,0.3),
0 8px 32px rgba(0,0,0,0.35),
inset 0 1px 0 rgba(255,255,255,0.08);
font-family: ui-sans-serif, system-ui, sans-serif;
overflow: hidden;
user-select: none;
cursor: default;
}
#cuw-topline {
height: 1.5px;
background: linear-gradient(90deg,
transparent 0%,
rgba(232,130,90,0.5) 40%,
rgba(212,168,71,0.4) 60%,
transparent 100%
);
}
#cuw-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 7px 11px 4px;
}
#cuw-title {
font-size: 9.5px;
font-weight: 600;
letter-spacing: 0.8px;
text-transform: uppercase;
color: rgba(255,255,255,0.55);
}
#cuw-dot {
width: 5px;
height: 5px;
border-radius: 50%;
transition: background 0.4s, box-shadow 0.4s;
}
#cuw-dot.ok { background: #7dbd8e; box-shadow: 0 0 5px #7dbd8e55; }
#cuw-dot.warn { background: #d4a847; box-shadow: 0 0 5px #d4a84766; }
#cuw-dot.hot { background: #e8825a; box-shadow: 0 0 5px #e8825a66; }
#cuw-dot.crit { background: #d95f5f; box-shadow: 0 0 5px #d95f5f88; }
#cuw-divider {
height: 1px;
background: rgba(255,255,255,0.08);
margin: 0 10px;
}
#cuw-gauges {
display: flex;
justify-content: space-around;
align-items: center;
padding: 8px 8px 10px;
gap: 6px;
}
.cuw-gauge {
display: flex;
flex-direction: column;
align-items: center;
gap: 5px;
flex: 1;
}
.cuw-arc-wrap {
position: relative;
width: 56px;
height: 56px;
}
.cuw-arc-wrap svg {
width: 100%;
height: 100%;
transform: rotate(135deg);
}
.cuw-ring-track {
fill: none;
stroke: rgba(255,255,255,0.12);
stroke-width: 3.5;
stroke-linecap: round;
stroke-dasharray: 94.25 31.41;
}
.cuw-ring-fill {
fill: none;
stroke-width: 3.5;
stroke-linecap: round;
transition: stroke-dasharray 0.8s cubic-bezier(.4,0,.2,1), stroke 0.4s;
}
.cuw-arc-center {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
transform: translateY(3px);
}
.cuw-arc-pct {
font-size: 13px;
font-weight: 700;
color: rgba(255,255,255,0.88);
font-variant-numeric: tabular-nums;
line-height: 1;
transition: color 0.4s;
}
.cuw-arc-label {
font-size: 7.5px;
font-weight: 600;
letter-spacing: 0.5px;
text-transform: uppercase;
color: rgba(255,255,255,0.5);
margin-top: 2px;
}
.cuw-reset {
font-size: 8.5px;
color: rgba(255,255,255,0.45);
text-align: center;
font-variant-numeric: tabular-nums;
line-height: 1.3;
min-height: 11px;
}
.cuw-vdiv {
width: 1px;
height: 48px;
background: rgba(255,255,255,0.1);
flex-shrink: 0;
}
`;
document.head.appendChild(style);
function makeArcSVG(id) {
return `<svg viewBox="0 0 44 44" xmlns="http://www.w3.org/2000/svg">
<circle class="cuw-ring-track" cx="22" cy="22" r="${R}"
stroke-dasharray="${ARC} ${CIRC - ARC}" />
<circle class="cuw-ring-fill" id="${id}" cx="22" cy="22" r="${R}" />
</svg>`;
}
function makeGauge(key, ringId) {
return `<div class="cuw-gauge">
<div class="cuw-arc-wrap">
${makeArcSVG(ringId)}
<div class="cuw-arc-center">
<span class="cuw-arc-pct" id="${ringId}-pct">—</span>
<span class="cuw-arc-label">${key}</span>
</div>
</div>
<div class="cuw-reset" id="${ringId}-reset"></div>
</div>`;
}
const widget = document.createElement('div');
widget.id = 'cuw';
widget.innerHTML = `
<div id="cuw-topline"></div>
<div id="cuw-header">
<span id="cuw-title">Usage</span>
<div id="cuw-dot" class="ok"></div>
</div>
<div id="cuw-divider"></div>
<div id="cuw-gauges">
${makeGauge('5h', 'cuw-5h')}
<div class="cuw-vdiv"></div>
${makeGauge('7d', 'cuw-7d')}
</div>
`;
document.body.appendChild(widget);
const dot = widget.querySelector('#cuw-dot');
const gauges = {
'5h': {
ring: widget.querySelector('#cuw-5h'),
pct: widget.querySelector('#cuw-5h-pct'),
reset: widget.querySelector('#cuw-5h-reset'),
},
'7d': {
ring: widget.querySelector('#cuw-7d'),
pct: widget.querySelector('#cuw-7d-pct'),
reset: widget.querySelector('#cuw-7d-reset'),
},
};
widget.querySelectorAll('.cuw-ring-fill').forEach(r => {
r.style.strokeDasharray = `0 ${CIRC}`;
});
function updateGauge(key, utilization, resetsAt) {
const pct = Math.min(Math.max(Math.round(utilization), 0), 100);
const color = getColor(pct);
const { ring, pct: pctEl, reset } = gauges[key];
ring.style.stroke = color;
ring.style.strokeDasharray = arcOffset(pct);
pctEl.textContent = pct + '%';
pctEl.style.color = color;
reset.textContent = formatReset(resetsAt);
return pct;
}
async function fetchUsage() {
try {
const orgsRes = await fetch('/api/organizations', {
credentials: 'include',
headers: { Accept: 'application/json' },
});
if (!orgsRes.ok) return;
const orgs = await orgsRes.json();
if (!orgs?.length) return;
const usageRes = await fetch(`/api/organizations/${orgs[0].uuid}/usage`, {
credentials: 'include',
headers: { Accept: 'application/json' },
});
if (!usageRes.ok) return;
const { five_hour, seven_day } = await usageRes.json();
const pcts = [
five_hour && updateGauge('5h', five_hour.utilization, five_hour.resets_at),
seven_day && updateGauge('7d', seven_day.utilization, seven_day.resets_at),
].filter(Boolean);
dot.className = dotClass(Math.max(...pcts, 0));
} catch (err) {
console.warn('[Claude Usage HUD]', err.message);
}
}
fetchUsage();
setInterval(fetchUsage, POLL_INTERVAL_MS);
})();