fix(costs): guard routes, fix DST ranges, sync provider state, wire live updates

- add companyAccess guard to costs route
- fix effectiveProvider/activeProvider desync via sync-back useEffect
- move ROLLING_WINDOWS to module level; replace IIFE with useMemo in ProviderQuotaCard
- add NO_COMPANY sentinel to eliminate non-null assertions before enabled guard
- fix DST-unsafe 7d/30d ranges in useDateRange (use Date constructor)
- remove providerData from providerTabItems memo deps (use byProvider)
- normalize used_percent 0-1 vs 0-100 ambiguity in quota-windows service
- rename secondsToWindowLabel index param to fallback; pass explicit labels
- add 4.33 magic number comment; fix quota window key collision
- remove rounded-md from date inputs (violates --radius: 0 theme)
- wire cost_event invalidation in LiveUpdatesProvider
This commit is contained in:
Sai Shankar
2026-03-08 19:04:27 +05:30
committed by Dotta
parent 56c9d95daa
commit bc991a96b4
6 changed files with 202 additions and 141 deletions

View File

@@ -13,20 +13,28 @@ export const PRESET_LABELS: Record<DatePreset, string> = {
export const PRESET_KEYS: DatePreset[] = ["mtd", "7d", "30d", "ytd", "all", "custom"];
// note: computeRange calls new Date() at evaluation time. for sliding presets (7d, 30d, etc.)
// the window is computed once at render time and can be up to ~1 minute stale between re-renders.
// this is acceptable for a cost dashboard but means the displayed range may lag wall clock time
// slightly between poll ticks.
function computeRange(preset: DatePreset): { from: string; to: string } {
const now = new Date();
const to = now.toISOString();
// floor `to` to the nearest minute so the query key is stable across 30s refetch ticks
// (prevents a new cache entry being created on every poll cycle)
const toFloored = new Date(now);
toFloored.setSeconds(0, 0);
const to = toFloored.toISOString();
switch (preset) {
case "mtd": {
const d = new Date(now.getFullYear(), now.getMonth(), 1);
return { from: d.toISOString(), to };
}
case "7d": {
const d = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
const d = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 7, 0, 0, 0, 0);
return { from: d.toISOString(), to };
}
case "30d": {
const d = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
const d = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 30, 0, 0, 0, 0);
return { from: d.toISOString(), to };
}
case "ytd": {
@@ -59,25 +67,15 @@ export function useDateRange(): UseDateRangeResult {
const [customTo, setCustomTo] = useState("");
const { from, to } = useMemo(() => {
if (preset === "custom") {
// treat custom date strings as local-date boundaries so the full day is included
// regardless of the user's timezone. "from" starts at local midnight, "to" at 23:59:59.999.
const fromDate = customFrom ? new Date(customFrom + "T00:00:00") : null;
const toDate = customTo ? new Date(customTo + "T23:59:59.999") : null;
return {
from: fromDate ? fromDate.toISOString() : "",
to: toDate ? toDate.toISOString() : "",
};
}
const range = computeRange(preset);
// floor `to` to the nearest minute so the query key is stable across 30s refetch ticks
// (prevents a new cache entry being created on every poll cycle)
if (range.to) {
const d = new Date(range.to);
d.setSeconds(0, 0);
range.to = d.toISOString();
}
return range;
if (preset !== "custom") return computeRange(preset);
// treat custom date strings as local-date boundaries so the full day is included
// regardless of the user's timezone. "from" starts at local midnight, "to" at 23:59:59.999.
const fromDate = customFrom ? new Date(customFrom + "T00:00:00") : null;
const toDate = customTo ? new Date(customTo + "T23:59:59.999") : null;
return {
from: fromDate ? fromDate.toISOString() : "",
to: toDate ? toDate.toISOString() : "",
};
}, [preset, customFrom, customTo]);
const customReady = preset !== "custom" || (!!customFrom && !!customTo);