Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save TamasNo1/54929ad91e7e569947d5cc44da419fe2 to your computer and use it in GitHub Desktop.

Select an option

Save TamasNo1/54929ad91e7e569947d5cc44da419fe2 to your computer and use it in GitHub Desktop.
Adds a simple linear weekly usage graph (100->0 over 7 days) + an X marker for current remaining usage.
// ==UserScript==
// @name ChatGPT Codex Usage: Weekly Linear Usage Graph
// @namespace https://chatgpt.com/
// @version 0.0.3
// @description Adds a linear weekly usage graph (100->0 over 7 days) + an X marker for current remaining usage.
// @match https://chatgpt.com/codex/settings/usage*
// @run-at document-idle
// @grant none
// ==/UserScript==
(() => {
"use strict";
const clamp = (v, min, max) => Math.min(max, Math.max(min, v));
function escapeXML(s) {
return String(s)
.replaceAll("&", "&")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&apos;");
}
function parseRemainingPercentFromCard(cardEl) {
const txt = cardEl.textContent || "";
const m = txt.match(/(\d+)\s*%\s*remaining/i);
if (!m) return null;
return clamp(parseInt(m[1], 10), 0, 100);
}
function inferNextResetFromTimeOnly(timeStr) {
// timeStr like "10:35 PM"
const m = timeStr.match(/^\s*(\d{1,2}):(\d{2})\s*([AP]M)\s*$/i);
if (!m) return null;
let hh = parseInt(m[1], 10);
const mm = parseInt(m[2], 10);
const ap = m[3].toUpperCase();
// Convert to 24h
if (ap === "AM") {
if (hh === 12) hh = 0;
} else {
if (hh !== 12) hh += 12;
}
const now = new Date();
const d = new Date(now);
d.setSeconds(0, 0);
d.setHours(hh, mm, 0, 0);
// If already passed today, next reset is tomorrow at that time
if (d.getTime() <= now.getTime()) {
d.setDate(d.getDate() + 1);
}
return d;
}
function parseResetDateFromCard(cardEl) {
const txt = cardEl.textContent || "";
// Format A: "Resets Feb 9, 2026 10:35 PM"
const full = txt.match(
/Resets\s+([A-Za-z]{3,}\s+\d{1,2},\s+\d{4}\s+\d{1,2}:\d{2}\s+[AP]M)/i
);
if (full) {
const d = new Date(full[1]); // local parse
return isNaN(d.getTime()) ? null : d;
}
// Format B: "Resets 10:35 PM"
const timeOnly = txt.match(/Resets\s+(\d{1,2}:\d{2}\s*[AP]M)\b/i);
if (timeOnly) {
const d = inferNextResetFromTimeOnly(timeOnly[1]);
return d && !isNaN(d.getTime()) ? d : null;
}
return null;
}
function findWeeklyUsageCard() {
const articles = Array.from(document.querySelectorAll("article"));
return (
articles.find((a) => {
const t = (a.textContent || "").toLowerCase();
return t.includes("weekly usage limit") && t.includes("resets");
}) || null
);
}
function formatTick(d) {
const date = d.toLocaleDateString(undefined, { month: "short", day: "numeric" });
const time = d.toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" });
return { date, time };
}
function createChartSVG({ remainingPct, resetDate, periodStart, now, width = 520, height = 200 }) {
// Margins (left is larger so Y axis labels have room)
const m = { l: 60, r: 24, t: 34, b: 52 };
const innerW = width - m.l - m.r;
const innerH = height - m.t - m.b;
const t0 = periodStart.getTime();
const t1 = resetDate.getTime();
const tn = now.getTime();
const xOfTime = (t) => m.l + clamp((t - t0) / (t1 - t0), 0, 1) * innerW;
const yOfPct = (pct) => m.t + (1 - clamp(pct / 100, 0, 1)) * innerH;
// Model line: 100 -> 0 across the period
const x0 = xOfTime(t0);
const x1 = xOfTime(t1);
const y100 = yOfPct(100);
const y0 = yOfPct(0);
// Current marker: (now, remainingPct)
const xNow = xOfTime(tn);
const yNow = yOfPct(remainingPct);
// X ticks every 24h from the start timestamp (includes time)
const ticks = [];
for (let i = 0; i <= 7; i++) {
const ti = t0 + i * 24 * 60 * 60 * 1000;
const di = new Date(ti);
const { date, time } = formatTick(di);
ticks.push({ x: xOfTime(ti), date, time });
}
// Y ticks
const yTicks = [0, 50, 100].map((v) => ({ v, y: yOfPct(v) }));
// X marker drawing
const xSize = 8;
const xMark = `
<line x1="${xNow - xSize}" y1="${yNow - xSize}" x2="${xNow + xSize}" y2="${yNow + xSize}" stroke="currentColor" stroke-width="2" />
<line x1="${xNow - xSize}" y1="${yNow + xSize}" x2="${xNow + xSize}" y2="${yNow - xSize}" stroke="currentColor" stroke-width="2" />
`;
const title = "Weekly usage (linear model)";
return `
<svg
width="100%"
viewBox="0 0 ${width} ${height}"
preserveAspectRatio="xMidYMid meet"
style="display:block; max-width:100%; width:100%; height:auto;"
role="img"
aria-label="${escapeXML(title)}"
>
<style>
.bg { fill: white; }
.frame { fill: none; stroke: rgba(15,23,42,0.18); stroke-width: 1; }
.grid { stroke: rgba(15,23,42,0.08); stroke-width: 1; }
.text { fill: rgba(15,23,42,0.75); font: 12px ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Arial; }
.textMuted { fill: rgba(15,23,42,0.55); font: 11px ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Arial; }
.lineModel { stroke: rgba(34,197,94,0.95); stroke-width: 2.5; fill: none; }
.marker { color: rgba(239,68,68,0.95); }
.dot { fill: rgba(239,68,68,0.95); }
</style>
<rect class="bg" x="0" y="0" width="${width}" height="${height}" rx="14" />
<text class="text" x="${m.l}" y="${m.t - 12}">${escapeXML(title)}</text>
<rect class="frame" x="${m.l}" y="${m.t}" width="${innerW}" height="${innerH}" rx="10" />
${yTicks
.map(
(t) => `
<line class="grid" x1="${m.l}" y1="${t.y}" x2="${m.l + innerW}" y2="${t.y}" />
<text class="textMuted" x="${m.l - 16}" y="${t.y + 4}" text-anchor="end">${t.v}%</text>
`
)
.join("")}
${ticks
.map(
(t) => `
<line class="grid" x1="${t.x}" y1="${m.t}" x2="${t.x}" y2="${m.t + innerH}" />
<text class="textMuted" x="${t.x}" y="${m.t + innerH + 16}" text-anchor="middle">
<tspan x="${t.x}" dy="0">${escapeXML(t.date)}</tspan>
<tspan x="${t.x}" dy="12">${escapeXML(t.time)}</tspan>
</text>
`
)
.join("")}
<path class="lineModel" d="M ${x0} ${y100} L ${x1} ${y0}" />
<g class="marker">
${xMark}
<circle class="dot" cx="${xNow}" cy="${yNow}" r="2.5" />
</g>
</svg>
`;
}
function injectChart(cardEl, svgMarkup) {
const existing = cardEl.querySelector("[data-tm-usage-chart]");
if (existing) existing.remove();
const wrap = document.createElement("div");
wrap.dataset.tmUsageChart = "1";
wrap.style.marginTop = "12px";
// Padding around the chart
wrap.style.padding = "10px";
wrap.style.boxSizing = "border-box";
wrap.style.overflow = "visible";
const inner = document.createElement("div");
inner.style.width = "100%";
inner.style.maxWidth = "100%";
inner.innerHTML = svgMarkup;
wrap.appendChild(inner);
cardEl.appendChild(wrap);
}
function render() {
const card = findWeeklyUsageCard();
if (!card) return;
const remainingPct = parseRemainingPercentFromCard(card);
const resetDate = parseResetDateFromCard(card);
if (remainingPct == null || !resetDate) return;
// Weekly period start is exactly 7 days before reset timestamp
const periodStart = new Date(resetDate.getTime() - 7 * 24 * 60 * 60 * 1000);
const now = new Date();
const svg = createChartSVG({ remainingPct, resetDate, periodStart, now });
injectChart(card, svg);
}
render();
// SPA-safe rerender
const mo = new MutationObserver(() => {
if (render._raf) cancelAnimationFrame(render._raf);
render._raf = requestAnimationFrame(render);
});
mo.observe(document.documentElement, { childList: true, subtree: true });
})();
@TamasNo1
Copy link
Author

TamasNo1 commented Feb 9, 2026

This is how it looks in your codex dashboard
Screenshot 2026-02-09 at 10 24 34

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment