@@ -211,13 +211,22 @@ function civilDaysBetween(fromKey: string, toKey: string): number {
211211}
212212
213213/**
214- * The last `days` up to now, starting on the hour. A start mid-hour would leave the
215- * window's first hour partial, and a partial hour can never be cached — every view
216- * would read it from the ledger again.
214+ * The last `days` up to now, starting on the viewer's hour. A start mid-hour would
215+ * leave the window's first hour partial, and a partial hour can never be cached —
216+ * every view would read it from the ledger again. The hour is local, not UTC: in a
217+ * half-hour-offset zone a UTC hour starts halfway through a segment.
217218 */
218- function trailingRange ( days : number , now : Date ) : UsageAnalyticsWindow {
219+ function trailingRange ( days : number , now : Date , timezone : string ) : UsageAnalyticsWindow {
219220 const from = new Date ( now . getTime ( ) - days * DAY_MS )
220- from . setUTCMinutes ( 0 , 0 , 0 )
221+ const parts = new Intl . DateTimeFormat ( 'en-US' , {
222+ timeZone : timezone ,
223+ minute : 'numeric' ,
224+ second : 'numeric' ,
225+ } ) . formatToParts ( from )
226+ const part = ( type : 'minute' | 'second' ) =>
227+ Number ( parts . find ( ( entry ) => entry . type === type ) ?. value ?? 0 )
228+ from . setTime ( from . getTime ( ) - ( part ( 'minute' ) * 60 + part ( 'second' ) ) * 1000 )
229+ from . setUTCMilliseconds ( 0 )
221230 return { kind : 'range' , from, to : now }
222231}
223232
@@ -239,7 +248,7 @@ export function resolveUsageAnalyticsWindow({
239248 switch ( preset ) {
240249 case 'current-period' :
241250 return isUnboundedPeriod ( period )
242- ? trailingRange ( UNBOUNDED_PERIOD_DISPLAY_DAYS , now )
251+ ? trailingRange ( UNBOUNDED_PERIOD_DISPLAY_DAYS , now , timezone )
243252 : { kind : 'period' , period }
244253 case 'previous-period' : {
245254 const previous = resolvePreviousPeriod ( period )
@@ -265,9 +274,9 @@ export function resolveUsageAnalyticsWindow({
265274 }
266275 }
267276 case '7d' :
268- return trailingRange ( 7 , now )
277+ return trailingRange ( 7 , now , timezone )
269278 case '30d' :
270- return trailingRange ( 30 , now )
279+ return trailingRange ( 30 , now , timezone )
271280 case 'custom' : {
272281 // A partial selection is not a range, so it falls back to the current period —
273282 // through the same branch, which is what keeps an unbounded period from being
0 commit comments