fix(quota): clear usage after reported reset

This commit is contained in:
Supra4E8C
2026-07-31 11:07:29 +08:00
parent ea0652c51c
commit 66d52992e8
3 changed files with 23 additions and 11 deletions
@@ -350,24 +350,20 @@ function Lane({ lane, span, now, mode, cells, nowPercent, resolvedTheme }: LaneP
title={`${lane.displayName}\n${formatDay(window.startMs)} ${formatTime(
window.startMs
)}${formatDay(window.endMs)} ${formatTime(window.endMs)}${
window.state === 'live' && lane.remaining !== null
? `\n${lane.remaining}% remaining`
: ''
window.remaining !== null ? `\n${window.remaining}% remaining` : ''
}`}
>
{/* Consumed portion of the *current* window only — a past or
future window has no meaningful fill. */}
{window.state === 'live' && lane.remaining !== null && (
{/* Only the API-reported current window has meaningful usage;
projected windows intentionally have no fill. */}
{window.remaining !== null && (
<span
className={styles.windowFill}
style={{ width: `${100 - lane.remaining}%` }}
style={{ width: `${100 - window.remaining}%` }}
/>
)}
{showLabel && (
<span className={styles.windowLabel}>
{window.state === 'live' && lane.remaining !== null
? `${lane.remaining}% · `
: ''}
{window.remaining !== null ? `${window.remaining}% · ` : ''}
{endText}
</span>
)}
+6 -1
View File
@@ -42,7 +42,7 @@ export interface TimelineLane {
anchorMs: number | null;
/** Window length in hours. */
periodHours: number | null;
/** Remaining percent for the window being drawn, or null when unknown. */
/** Remaining percent reported for the window ending at `anchorMs`. */
remaining: number | null;
limits: TimelineLimit[];
}
@@ -55,6 +55,8 @@ export interface TimelineWindow {
leftPercent: number;
widthPercent: number;
state: 'past' | 'live' | 'next';
/** Remaining percent only when this is the API-reported current window. */
remaining: number | null;
}
/**
@@ -164,6 +166,9 @@ export function projectLane(
leftPercent: left,
widthPercent: right - left,
state,
// `lane.remaining` belongs to the current payload window ending at the
// anchor. Once that reset passes, projected windows must not reuse it.
remaining: state === 'live' && window.endMs === lane.anchorMs ? lane.remaining : null,
};
})
.filter((window): window is TimelineWindow => window !== null);
+11
View File
@@ -121,10 +121,21 @@ describe('projectLane', () => {
expect(live.length).toBe(1);
expect(live[0].startMs).toBeLessThanOrEqual(now);
expect(live[0].endMs).toBeGreaterThan(now);
expect(live[0].remaining).toBe(40);
expect(windows.filter((w) => w.state === 'past').every((w) => w.endMs <= now)).toBe(true);
expect(windows.filter((w) => w.state === 'next').every((w) => w.startMs > now)).toBe(true);
});
test('does not carry stale remaining usage past the reported reset', () => {
const nowAfterReset = at(2026, 6, 30, 12);
const windows = projectLane(lane(), span.startMs, span.endMs, nowAfterReset, 'weekly');
const live = windows.find((window) => window.state === 'live');
expect(live).toBeDefined();
expect(live?.startMs).toBe(at(2026, 6, 29, 20));
expect(live?.remaining).toBeNull();
});
test('clips bars to the visible span', () => {
const windows = projectLane(lane(), span.startMs, span.endMs, at(2026, 6, 29, 12), 'weekly');
for (const window of windows) {