@@ -211,23 +211,37 @@ function civilDaysBetween(fromKey: string, toKey: string): number {
211211}
212212
213213/**
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.
214+ * The start of the viewer-local hour an instant falls in. Local, not UTC: in a
215+ * half-hour-offset zone a UTC hour starts halfway through a local one.
218216 */
219- function trailingRange ( days : number , now : Date , timezone : string ) : UsageAnalyticsWindow {
220- const from = new Date ( now . getTime ( ) - days * DAY_MS )
217+ function startOfLocalHour ( instant : Date , timezone : string ) : Date {
221218 const parts = new Intl . DateTimeFormat ( 'en-US' , {
222219 timeZone : timezone ,
223220 minute : 'numeric' ,
224221 second : 'numeric' ,
225- } ) . formatToParts ( from )
222+ } ) . formatToParts ( instant )
226223 const part = ( type : 'minute' | 'second' ) =>
227224 Number ( parts . find ( ( entry ) => entry . type === type ) ?. value ?? 0 )
228- from . setTime ( from . getTime ( ) - ( part ( 'minute' ) * 60 + part ( 'second' ) ) * 1000 )
229- from . setUTCMilliseconds ( 0 )
230- return { kind : 'range' , from, to : now }
225+ const start = new Date ( instant . getTime ( ) - ( part ( 'minute' ) * 60 + part ( 'second' ) ) * 1000 )
226+ start . setUTCMilliseconds ( 0 )
227+ return start
228+ }
229+
230+ /**
231+ * The last `days` before `to`, starting on the viewer's hour. A start mid-hour would
232+ * leave the window's first hour partial, and a partial hour can never be cached —
233+ * every view would read it from the ledger again.
234+ */
235+ function trailingRange (
236+ days : number ,
237+ to : Date ,
238+ timezone : string
239+ ) : Extract < UsageAnalyticsWindow , { kind : 'range' } > {
240+ return {
241+ kind : 'range' ,
242+ from : startOfLocalHour ( new Date ( to . getTime ( ) - days * DAY_MS ) , timezone ) ,
243+ to,
244+ }
231245}
232246
233247/**
@@ -254,14 +268,12 @@ export function resolveUsageAnalyticsWindow({
254268 const previous = resolvePreviousPeriod ( period )
255269 if ( previous ) return { kind : 'period' , period : previous }
256270 // An open period has no meaningful predecessor — deriving one from its length
257- // reaches back eight millennia — so it steps back by the display window instead.
271+ // reaches back eight millennia — so it steps back by the display window instead,
272+ // ending exactly where the current period's window starts: no hour is counted in
273+ // both, and both ends fall on the viewer's hour, so every segment can settle.
258274 if ( isUnboundedPeriod ( period ) ) {
259- const to = new Date ( now . getTime ( ) - UNBOUNDED_PERIOD_DISPLAY_DAYS * DAY_MS )
260- return {
261- kind : 'range' ,
262- from : new Date ( to . getTime ( ) - UNBOUNDED_PERIOD_DISPLAY_DAYS * DAY_MS ) ,
263- to,
264- }
275+ const current = trailingRange ( UNBOUNDED_PERIOD_DISPLAY_DAYS , now , timezone )
276+ return trailingRange ( UNBOUNDED_PERIOD_DISPLAY_DAYS , current . from , timezone )
265277 }
266278 // A stripe period carries no rule for deriving its predecessor, so fall back to
267279 // a range of the same length rather than inventing stamps that would match
@@ -282,7 +294,7 @@ export function resolveUsageAnalyticsWindow({
282294 // through the same branch, which is what keeps an unbounded period from being
283295 // scanned in full here as well.
284296 if ( ! customStart || ! customEnd ) {
285- return resolveUsageAnalyticsWindow ( { preset : 'current-period' , period, now } )
297+ return resolveUsageAnalyticsWindow ( { preset : 'current-period' , period, timezone , now } )
286298 }
287299 /**
288300 * The picker offers calendar days and sends `YYYY-MM-DD`, which arrives here
0 commit comments