@@ -4,7 +4,7 @@ import React, { useState, useCallback, useEffect, useRef } from 'react'
44import { render , Box , Text , useInput , useApp , useWindowSize } from 'ink'
55import { CATEGORY_LABELS , type DateRange , type ProjectSummary , type TaskCategory } from './types.js'
66import { formatCost , formatTokens } from './format.js'
7- import { parseAllSessions , filterProjectsByName } from './parser.js'
7+ import { parseAllSessions , filterProjectsByDateRange , filterProjectsByName } from './parser.js'
88import { loadPricing } from './models.js'
99import { getAllProviders } from './providers/index.js'
1010import { scanAndDetect , type WasteFinding , type WasteAction , type OptimizeResult } from './optimize.js'
@@ -20,6 +20,15 @@ import { join } from 'path'
2020type Period = 'today' | 'week' | '30days' | 'month' | 'all'
2121type View = 'dashboard' | 'optimize' | 'compare'
2222
23+ type CachedWindow = {
24+ period : Period
25+ range : {
26+ start : Date
27+ end : Date
28+ }
29+ projects : ProjectSummary [ ]
30+ }
31+
2332const PERIODS : Period [ ] = [ 'today' , 'week' , '30days' , 'month' , 'all' ]
2433const PERIOD_LABELS : Record < Period , string > = {
2534 today : 'Today' ,
@@ -108,6 +117,10 @@ function getDateRange(period: Period): { start: Date; end: Date } {
108117 }
109118}
110119
120+ function rangeCovers ( outer : { start : Date ; end : Date } , inner : { start : Date ; end : Date } ) : boolean {
121+ return outer . start <= inner . start && outer . end >= inner . end
122+ }
123+
111124type Layout = { dashWidth : number ; wide : boolean ; halfWidth : number ; barWidth : number }
112125
113126function getLayout ( columns ?: number ) : Layout {
@@ -630,9 +643,156 @@ function InteractiveDashboard({ initialProjects, initialPeriod, initialProvider,
630643 ) . size
631644 const compareAvailable = modelCount >= 2
632645 const debounceRef = useRef < ReturnType < typeof setTimeout > | null > ( null )
633- const reloadGenerationRef = useRef ( 0 )
646+ const cacheByProviderRef = useRef ( new Map < string , CachedWindow [ ] > ( ) )
647+ const reloadSeqRef = useRef ( 0 )
648+ const preloadingRef = useRef ( new Map < string , Promise < ProjectSummary [ ] > > ( ) )
634649 const findingCount = optimizeResult ?. findings . length ?? 0
635650
651+ const providerCacheKey = useCallback ( ( provider : string ) => `${ provider } :${ noCache ? 'nocache' : 'cache' } ` , [ noCache ] )
652+ const getRangeWidth = useCallback ( ( range : { start : Date ; end : Date } ) => range . end . getTime ( ) - range . start . getTime ( ) , [ ] )
653+ const makeCacheToken = useCallback ( ( provider : string , period : Period ) => `${ providerCacheKey ( provider ) } :${ period } ` , [ providerCacheKey ] )
654+
655+ const storeCachedWindow = useCallback ( ( provider : string , period : Period , range : { start : Date ; end : Date } , projects : ProjectSummary [ ] ) => {
656+ if ( noCache ) return
657+ const key = providerCacheKey ( provider )
658+ const windows = cacheByProviderRef . current . get ( key ) ?? [ ]
659+ const normalizedRange = { start : new Date ( range . start ) , end : new Date ( range . end ) }
660+ const existing = windows . findIndex (
661+ existing => existing . period === period && existing . range . start . getTime ( ) === normalizedRange . start . getTime ( ) && existing . range . end . getTime ( ) === normalizedRange . end . getTime ( ) ,
662+ )
663+ if ( existing >= 0 ) windows . splice ( existing , 1 )
664+ windows . push ( { period, range : normalizedRange , projects } )
665+ windows . sort ( ( a , b ) => a . range . start . getTime ( ) - b . range . start . getTime ( ) )
666+ cacheByProviderRef . current . set ( key , windows )
667+ } , [ noCache , providerCacheKey ] )
668+
669+ const findCachedWindow = useCallback ( ( provider : string , range : { start : Date ; end : Date } ) => {
670+ const candidates = cacheByProviderRef . current . get ( providerCacheKey ( provider ) ) ?? [ ]
671+ let best : CachedWindow | undefined
672+ for ( const candidate of candidates ) {
673+ if ( ! rangeCovers ( candidate . range , range ) ) continue
674+ if ( ! best ) { best = candidate ; continue }
675+ if ( getRangeWidth ( candidate . range ) < getRangeWidth ( best . range ) ) {
676+ best = candidate
677+ } else if ( candidate . period !== best . period && getRangeWidth ( candidate . range ) === getRangeWidth ( best . range ) && candidate . range . start > best . range . start ) {
678+ best = candidate
679+ }
680+ }
681+ return best
682+ } , [ getRangeWidth , providerCacheKey ] )
683+
684+ const preloadWindow = useCallback ( async ( periodToLoad : Period , provider : string ) => {
685+ if ( noCache ) return
686+ const preloadKey = makeCacheToken ( provider , periodToLoad )
687+ const range = getDateRange ( periodToLoad )
688+ const cached = findCachedWindow ( provider , range )
689+ if ( cached ) return
690+ const inFlight = preloadingRef . current . get ( preloadKey )
691+ if ( inFlight ) return
692+
693+ const promise = ( async ( ) => {
694+ const projects = await parseAllSessions ( range , provider , { noCache, progress : null } )
695+ if ( ! noCache ) {
696+ storeCachedWindow ( provider , periodToLoad , range , projects )
697+ }
698+ return projects
699+ } ) ( )
700+
701+ preloadingRef . current . set ( preloadKey , promise )
702+ try {
703+ await promise
704+ } finally {
705+ preloadingRef . current . delete ( preloadKey )
706+ }
707+ } , [ findCachedWindow , makeCacheToken , noCache , storeCachedWindow ] )
708+
709+ const reloadData = useCallback ( async ( p : Period , prov : string , options ?: { silent ?: boolean } ) => {
710+ const range = getDateRange ( p )
711+ const request = ++ reloadSeqRef . current
712+ const token = makeCacheToken ( prov , p )
713+ const cachedWindow = findCachedWindow ( prov , range )
714+ if ( ! options ?. silent ) {
715+ setOptimizeResult ( null )
716+ }
717+
718+ if ( cachedWindow ) {
719+ const projectsFromCache = filterProjectsByName (
720+ filterProjectsByDateRange ( cachedWindow . projects , range ) ,
721+ projectFilter ,
722+ excludeFilter ,
723+ )
724+ if ( ! options ?. silent && request === reloadSeqRef . current ) {
725+ setProjects ( projectsFromCache )
726+ }
727+ if ( ! options ?. silent ) {
728+ const usage = await getPlanUsageOrNull ( )
729+ if ( request !== reloadSeqRef . current ) return
730+ setPlanUsage ( usage ?? undefined )
731+ }
732+ return
733+ }
734+
735+ const inFlight = preloadingRef . current . get ( token )
736+ if ( inFlight ) {
737+ if ( ! options ?. silent ) setLoading ( true )
738+ try {
739+ const projects = await inFlight
740+ if ( ! noCache ) {
741+ storeCachedWindow ( prov , p , range , projects )
742+ }
743+ if ( request !== reloadSeqRef . current ) return
744+ const filtered = filterProjectsByName ( projects , projectFilter , excludeFilter )
745+ if ( ! options ?. silent ) {
746+ setProjects ( filtered )
747+ }
748+ } finally {
749+ if ( ! options ?. silent && request === reloadSeqRef . current ) setLoading ( false )
750+ }
751+ if ( ! options ?. silent ) {
752+ const usage = await getPlanUsageOrNull ( )
753+ if ( request !== reloadSeqRef . current ) return
754+ setPlanUsage ( usage ?? undefined )
755+ }
756+ return
757+ }
758+
759+ if ( ! options ?. silent ) setLoading ( true )
760+ try {
761+ const projects = await parseAllSessions ( range , prov , { noCache, progress : null } )
762+ if ( ! noCache ) {
763+ storeCachedWindow ( prov , p , range , projects )
764+ }
765+ if ( request !== reloadSeqRef . current ) return
766+ const filtered = filterProjectsByName ( projects , projectFilter , excludeFilter )
767+ if ( ! options ?. silent ) {
768+ setProjects ( filtered )
769+ }
770+ } finally {
771+ if ( ! options ?. silent && request === reloadSeqRef . current ) setLoading ( false )
772+ }
773+ if ( ! options ?. silent ) {
774+ const usage = await getPlanUsageOrNull ( )
775+ if ( request !== reloadSeqRef . current ) return
776+ setPlanUsage ( usage ?? undefined )
777+ }
778+ } , [ excludeFilter , findCachedWindow , getPlanUsageOrNull , noCache , projectFilter , storeCachedWindow ] )
779+
780+ useEffect ( ( ) => {
781+ if ( noCache ) return
782+ const initialRange = getDateRange ( initialPeriod )
783+ const initialKey = providerCacheKey ( initialProvider )
784+ const existing = cacheByProviderRef . current . get ( initialKey ) ?? [ ]
785+ const alreadyCached = existing . some ( entry => rangeCovers ( entry . range , initialRange ) )
786+ if ( ! alreadyCached ) {
787+ storeCachedWindow ( initialProvider , initialPeriod , initialRange , initialProjects )
788+ }
789+ } , [ initialPeriod , initialProvider , initialProjects , noCache , providerCacheKey , storeCachedWindow ] )
790+
791+ useEffect ( ( ) => {
792+ if ( noCache || period === '30days' ) return
793+ void preloadWindow ( '30days' , activeProvider )
794+ } , [ noCache , period , activeProvider , preloadWindow ] )
795+
636796 useEffect ( ( ) => {
637797 let cancelled = false
638798 async function detect ( ) {
@@ -673,32 +833,6 @@ function InteractiveDashboard({ initialProjects, initialPeriod, initialProvider,
673833 return ( ) => { cancelled = true }
674834 } , [ projects , period , optimizeAvailable ] )
675835
676- const reloadData = useCallback ( async ( p : Period , prov : string ) => {
677- const generation = ++ reloadGenerationRef . current
678- setLoading ( true )
679- setOptimizeResult ( null )
680- try {
681- const range = getDateRange ( p )
682- const data = filterProjectsByName (
683- await parseAllSessions ( range , prov , { noCache : noCache ?? false , progress : null } ) ,
684- projectFilter ,
685- excludeFilter ,
686- )
687- if ( reloadGenerationRef . current !== generation ) return
688-
689- setProjects ( data )
690- const usage = await getPlanUsageOrNull ( )
691- if ( reloadGenerationRef . current !== generation ) return
692- setPlanUsage ( usage ?? undefined )
693- } catch ( error ) {
694- console . error ( error )
695- } finally {
696- if ( reloadGenerationRef . current === generation ) {
697- setLoading ( false )
698- }
699- }
700- } , [ excludeFilter , noCache , projectFilter ] )
701-
702836 useEffect ( ( ) => {
703837 if ( ! refreshSeconds || refreshSeconds <= 0 ) return
704838 const id = setInterval ( ( ) => { reloadData ( period , activeProvider ) } , refreshSeconds * 1000 )
0 commit comments