@@ -65,7 +65,6 @@ import type { GatewayRequestHandlers, RespondFn } from "./types.js";
6565const COST_USAGE_CACHE_TTL_MS = 30_000 ;
6666const COST_USAGE_CACHE_MAX = 256 ;
6767const SESSIONS_USAGE_AGENT_LOAD_CONCURRENCY = 12 ;
68- const DAY_MS = 24 * 60 * 60 * 1000 ;
6968
7069type DateRange = { startMs : number ; endMs : number } ;
7170// Keep validation and parsed timestamps in one result so handlers cannot forward
@@ -169,21 +168,28 @@ const parseDateParts = (raw: unknown): DateParts | undefined => {
169168 return { year, monthIndex, day } ;
170169} ;
171170
172- const datePartsToEndMs = ( parts : DateParts , interpretation : DateInterpretation ) : number => {
171+ const shiftDateParts = ( parts : DateParts , days : number ) : DateParts => {
172+ const shifted = new Date ( Date . UTC ( parts . year , parts . monthIndex , parts . day + days ) ) ;
173+ return {
174+ year : shifted . getUTCFullYear ( ) ,
175+ monthIndex : shifted . getUTCMonth ( ) ,
176+ day : shifted . getUTCDate ( ) ,
177+ } ;
178+ } ;
179+
180+ const datePartsToStartMs = ( parts : DateParts , interpretation : DateInterpretation ) : number => {
173181 const { year, monthIndex, day } = parts ;
174182 if ( interpretation . mode === "gateway" ) {
175- return new Date ( year , monthIndex , day + 1 ) . getTime ( ) - 1 ;
183+ return new Date ( year , monthIndex , day ) . getTime ( ) ;
176184 }
177185 if ( interpretation . mode === "specific" ) {
178- return Date . UTC ( year , monthIndex , day + 1 ) - interpretation . utcOffsetMinutes * 60 * 1000 - 1 ;
186+ return Date . UTC ( year , monthIndex , day ) - interpretation . utcOffsetMinutes * 60 * 1000 ;
179187 }
180- return Date . UTC ( year , monthIndex , day + 1 ) - 1 ;
188+ return Date . UTC ( year , monthIndex , day ) ;
181189} ;
182190
183- const parseDateEndToMs = ( raw : unknown , interpretation : DateInterpretation ) : number | undefined => {
184- const parts = parseDateParts ( raw ) ;
185- return parts ? datePartsToEndMs ( parts , interpretation ) : undefined ;
186- } ;
191+ const datePartsToEndMs = ( parts : DateParts , interpretation : DateInterpretation ) : number =>
192+ datePartsToStartMs ( shiftDateParts ( parts , 1 ) , interpretation ) - 1 ;
187193
188194// usage.cost / sessions.usage accept optional startDate/endDate. parseDateParts returns
189195// undefined for both absent and invalid input, so an explicitly supplied but unparseable
@@ -258,6 +264,25 @@ const resolveDayBucketUtcOffsetMinutes = (interpretation: DateInterpretation) =>
258264 ? interpretation . utcOffsetMinutes
259265 : 0 ;
260266
267+ const getDateParts = ( date : Date , interpretation : DateInterpretation ) : DateParts => {
268+ if ( interpretation . mode === "gateway" ) {
269+ return { year : date . getFullYear ( ) , monthIndex : date . getMonth ( ) , day : date . getDate ( ) } ;
270+ }
271+ if ( interpretation . mode === "specific" ) {
272+ const shifted = new Date ( date . getTime ( ) + interpretation . utcOffsetMinutes * 60 * 1000 ) ;
273+ return {
274+ year : shifted . getUTCFullYear ( ) ,
275+ monthIndex : shifted . getUTCMonth ( ) ,
276+ day : shifted . getUTCDate ( ) ,
277+ } ;
278+ }
279+ return {
280+ year : date . getUTCFullYear ( ) ,
281+ monthIndex : date . getUTCMonth ( ) ,
282+ day : date . getUTCDate ( ) ,
283+ } ;
284+ } ;
285+
261286/**
262287 * Parse a date string (YYYY-MM-DD) to start-of-day timestamp based on interpretation mode.
263288 * Returns undefined if invalid.
@@ -270,53 +295,17 @@ const parseDateToMs = (
270295 if ( ! parts ) {
271296 return undefined ;
272297 }
273- const { year, monthIndex, day } = parts ;
274- if ( interpretation . mode === "gateway" ) {
275- const ms = new Date ( year , monthIndex , day ) . getTime ( ) ;
276- return Number . isNaN ( ms ) ? undefined : ms ;
277- }
278- if ( interpretation . mode === "specific" ) {
279- const ms = Date . UTC ( year , monthIndex , day ) - interpretation . utcOffsetMinutes * 60 * 1000 ;
280- return Number . isNaN ( ms ) ? undefined : ms ;
281- }
282- const ms = Date . UTC ( year , monthIndex , day ) ;
283- return Number . isNaN ( ms ) ? undefined : ms ;
284- } ;
285-
286- const getTodayStartMs = ( now : Date , interpretation : DateInterpretation ) : number => {
287- if ( interpretation . mode === "gateway" ) {
288- return new Date ( now . getFullYear ( ) , now . getMonth ( ) , now . getDate ( ) ) . getTime ( ) ;
289- }
290- if ( interpretation . mode === "specific" ) {
291- const shifted = new Date ( now . getTime ( ) + interpretation . utcOffsetMinutes * 60 * 1000 ) ;
292- return (
293- Date . UTC ( shifted . getUTCFullYear ( ) , shifted . getUTCMonth ( ) , shifted . getUTCDate ( ) ) -
294- interpretation . utcOffsetMinutes * 60 * 1000
295- ) ;
296- }
297- return Date . UTC ( now . getUTCFullYear ( ) , now . getUTCMonth ( ) , now . getUTCDate ( ) ) ;
298+ return datePartsToStartMs ( parts , interpretation ) ;
298299} ;
299300
300301const formatDateLabel = ( ms : number , interpretation : DateInterpretation ) : string => {
301- const date = new Date ( ms ) ;
302- if ( interpretation . mode === "gateway" ) {
303- return formatDateParts ( date . getFullYear ( ) , date . getMonth ( ) , date . getDate ( ) ) ;
304- }
305- if ( interpretation . mode === "specific" ) {
306- const shifted = new Date ( ms + interpretation . utcOffsetMinutes * 60 * 1000 ) ;
307- return formatDateParts ( shifted . getUTCFullYear ( ) , shifted . getUTCMonth ( ) , shifted . getUTCDate ( ) ) ;
308- }
309- return formatDateParts ( date . getUTCFullYear ( ) , date . getUTCMonth ( ) , date . getUTCDate ( ) ) ;
302+ const parts = getDateParts ( new Date ( ms ) , interpretation ) ;
303+ return formatDateParts ( parts . year , parts . monthIndex , parts . day ) ;
310304} ;
311305
312306const formatDateParts = ( year : number , monthIndex : number , day : number ) : string =>
313307 `${ year } -${ String ( monthIndex + 1 ) . padStart ( 2 , "0" ) } -${ String ( day ) . padStart ( 2 , "0" ) } ` ;
314308
315- const formatExplicitDateLabel = ( raw : unknown ) : string | undefined => {
316- const parts = parseDateParts ( raw ) ;
317- return parts ? formatDateParts ( parts . year , parts . monthIndex , parts . day ) : undefined ;
318- } ;
319-
320309const parseDays = ( raw : unknown ) : number | undefined => {
321310 if ( typeof raw === "number" && Number . isFinite ( raw ) ) {
322311 return Math . floor ( raw ) ;
@@ -349,6 +338,15 @@ const resolveRangeDays = (raw: unknown): number | "all" | undefined => {
349338 return undefined ;
350339} ;
351340
341+ const resolveTrailingDays = (
342+ endDateParts : DateParts ,
343+ days : number ,
344+ interpretation : DateInterpretation ,
345+ ) : DateRange => ( {
346+ startMs : datePartsToStartMs ( shiftDateParts ( endDateParts , - ( days - 1 ) ) , interpretation ) ,
347+ endMs : datePartsToEndMs ( endDateParts , interpretation ) ,
348+ } ) ;
349+
352350/**
353351 * Get date range from params (startDate/endDate or days).
354352 * Falls back to last 30 days if not provided.
@@ -371,45 +369,37 @@ const resolveDateRange = (params: {
371369
372370 const now = new Date ( ) ;
373371 const interpretation = resolveDateInterpretation ( params ) ;
374- const todayStartMs = getTodayStartMs ( now , interpretation ) ;
375- const todayEndMs = todayStartMs + DAY_MS - 1 ;
372+ const todayDateParts = getDateParts ( now , interpretation ) ;
373+ const todayEndMs = datePartsToEndMs ( todayDateParts , interpretation ) ;
376374
377- const startMs = parseDateToMs ( params . startDate , interpretation ) ;
378- const endMs = parseDateToMs ( params . endDate , interpretation ) ;
375+ const startDateParts = parseDateParts ( params . startDate ) ;
376+ const endDateParts = parseDateParts ( params . endDate ) ;
379377
380- if ( startMs !== undefined && endMs !== undefined ) {
381- if ( startMs > endMs ) {
378+ if ( startDateParts && endDateParts ) {
379+ const startMs = datePartsToStartMs ( startDateParts , interpretation ) ;
380+ const endStartMs = datePartsToStartMs ( endDateParts , interpretation ) ;
381+ if ( startMs > endStartMs ) {
382382 return { ok : false , error : "startDate must not be after endDate" } ;
383383 }
384- const endOfDayMs = parseDateEndToMs ( params . endDate , interpretation ) ;
385- if ( endOfDayMs === undefined ) {
386- return {
387- ok : false ,
388- error : "invalid endDate: expected a valid YYYY-MM-DD calendar date" ,
389- } ;
390- }
391- return { ok : true , value : { startMs, endMs : endOfDayMs } } ;
384+ return { ok : true , value : { startMs, endMs : datePartsToEndMs ( endDateParts , interpretation ) } } ;
392385 }
393386
394387 const rangeDays = resolveRangeDays ( params . range ) ;
395388 if ( rangeDays === "all" ) {
396389 return { ok : true , value : { startMs : 0 , endMs : todayEndMs } } ;
397390 }
398391 if ( rangeDays !== undefined ) {
399- const start = todayStartMs - ( rangeDays - 1 ) * DAY_MS ;
400- return { ok : true , value : { startMs : start , endMs : todayEndMs } } ;
392+ return { ok : true , value : resolveTrailingDays ( todayDateParts , rangeDays , interpretation ) } ;
401393 }
402394
403395 const days = parseDays ( params . days ) ;
404396 if ( days !== undefined ) {
405397 const clampedDays = Math . max ( 1 , days ) ;
406- const start = todayStartMs - ( clampedDays - 1 ) * DAY_MS ;
407- return { ok : true , value : { startMs : start , endMs : todayEndMs } } ;
398+ return { ok : true , value : resolveTrailingDays ( todayDateParts , clampedDays , interpretation ) } ;
408399 }
409400
410401 // Default to last 30 days
411- const defaultStartMs = todayStartMs - 29 * DAY_MS ;
412- return { ok : true , value : { startMs : defaultStartMs , endMs : todayEndMs } } ;
402+ return { ok : true , value : resolveTrailingDays ( todayDateParts , 30 , interpretation ) } ;
413403} ;
414404
415405type DiscoveredSessionWithAgent = DiscoveredSession & { agentId : string } ;
@@ -945,7 +935,6 @@ export const testApi = {
945935 parseUtcOffsetToMinutes,
946936 resolveDateInterpretation,
947937 parseDateToMs,
948- getTodayStartMs,
949938 parseDays,
950939 resolveDateRange,
951940 discoverAllSessionsForUsage,
@@ -1503,19 +1492,10 @@ export const usageHandlers: GatewayRequestHandlers = {
15031492 ...tail ,
15041493 } ;
15051494
1506- const explicitStartDate = formatExplicitDateLabel ( p . startDate ) ;
1507- const explicitEndDate = formatExplicitDateLabel ( p . endDate ) ;
1508- let responseStartDate = formatDateLabel ( startMs , dateInterpretation ) ;
1509- let responseEndDate = formatDateLabel ( endMs , dateInterpretation ) ;
1510- if ( explicitStartDate !== undefined && explicitEndDate !== undefined ) {
1511- responseStartDate = explicitStartDate ;
1512- responseEndDate = explicitEndDate ;
1513- }
1514-
15151495 const result : SessionsUsageResult = {
15161496 updatedAt : now ,
1517- startDate : responseStartDate ,
1518- endDate : responseEndDate ,
1497+ startDate : formatDateLabel ( startMs , dateInterpretation ) ,
1498+ endDate : formatDateLabel ( endMs , dateInterpretation ) ,
15191499 sessions,
15201500 totals : aggregateTotals ,
15211501 aggregates,
0 commit comments