@@ -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'
@@ -18,6 +18,15 @@ import { join } from 'path'
1818type Period = 'today' | 'week' | '30days' | 'month' | 'all'
1919type View = 'dashboard' | 'optimize' | 'compare'
2020
21+ type CachedWindow = {
22+ period : Period
23+ range : {
24+ start : Date
25+ end : Date
26+ }
27+ projects : ProjectSummary [ ]
28+ }
29+
2130const PERIODS : Period [ ] = [ 'today' , 'week' , '30days' , 'month' , 'all' ]
2231const PERIOD_LABELS : Record < Period , string > = {
2332 today : 'Today' ,
@@ -105,6 +114,10 @@ function getDateRange(period: Period): { start: Date; end: Date } {
105114 }
106115}
107116
117+ function rangeCovers ( outer : { start : Date ; end : Date } , inner : { start : Date ; end : Date } ) : boolean {
118+ return outer . start <= inner . start && outer . end >= inner . end
119+ }
120+
108121type Layout = { dashWidth : number ; wide : boolean ; halfWidth : number ; barWidth : number }
109122
110123function getLayout ( columns ?: number ) : Layout {
@@ -587,8 +600,137 @@ function InteractiveDashboard({ initialProjects, initialPeriod, initialProvider,
587600 ) . size
588601 const compareAvailable = modelCount >= 2
589602 const debounceRef = useRef < ReturnType < typeof setTimeout > | null > ( null )
603+ const cacheByProviderRef = useRef ( new Map < string , CachedWindow [ ] > ( ) )
604+ const reloadSeqRef = useRef ( 0 )
605+ const preloadingRef = useRef ( new Map < string , Promise < ProjectSummary [ ] > > ( ) )
590606 const findingCount = optimizeResult ?. findings . length ?? 0
591607
608+ const providerCacheKey = useCallback ( ( provider : string ) => `${ provider } :${ noCache ? 'nocache' : 'cache' } ` , [ noCache ] )
609+ const getRangeWidth = useCallback ( ( range : { start : Date ; end : Date } ) => range . end . getTime ( ) - range . start . getTime ( ) , [ ] )
610+ const makeCacheToken = useCallback ( ( provider : string , period : Period ) => `${ providerCacheKey ( provider ) } :${ period } ` , [ providerCacheKey ] )
611+
612+ const storeCachedWindow = useCallback ( ( provider : string , period : Period , range : { start : Date ; end : Date } , projects : ProjectSummary [ ] ) => {
613+ if ( noCache ) return
614+ const key = providerCacheKey ( provider )
615+ const windows = cacheByProviderRef . current . get ( key ) ?? [ ]
616+ const normalizedRange = { start : new Date ( range . start ) , end : new Date ( range . end ) }
617+ const existing = windows . findIndex (
618+ existing => existing . period === period && existing . range . start . getTime ( ) === normalizedRange . start . getTime ( ) && existing . range . end . getTime ( ) === normalizedRange . end . getTime ( ) ,
619+ )
620+ if ( existing >= 0 ) windows . splice ( existing , 1 )
621+ windows . push ( { period, range : normalizedRange , projects } )
622+ windows . sort ( ( a , b ) => a . range . start . getTime ( ) - b . range . start . getTime ( ) )
623+ cacheByProviderRef . current . set ( key , windows )
624+ } , [ noCache , providerCacheKey ] )
625+
626+ const findCachedWindow = useCallback ( ( provider : string , range : { start : Date ; end : Date } ) => {
627+ const candidates = cacheByProviderRef . current . get ( providerCacheKey ( provider ) ) ?? [ ]
628+ let best : CachedWindow | undefined
629+ for ( const candidate of candidates ) {
630+ if ( ! rangeCovers ( candidate . range , range ) ) continue
631+ if ( ! best ) { best = candidate ; continue }
632+ if ( getRangeWidth ( candidate . range ) < getRangeWidth ( best . range ) ) {
633+ best = candidate
634+ } else if ( candidate . period !== best . period && getRangeWidth ( candidate . range ) === getRangeWidth ( best . range ) && candidate . range . start > best . range . start ) {
635+ best = candidate
636+ }
637+ }
638+ return best
639+ } , [ getRangeWidth , providerCacheKey ] )
640+
641+ const preloadWindow = useCallback ( async ( periodToLoad : Period , provider : string ) => {
642+ if ( noCache ) return
643+ const preloadKey = makeCacheToken ( provider , periodToLoad )
644+ const range = getDateRange ( periodToLoad )
645+ const cached = findCachedWindow ( provider , range )
646+ if ( cached ) return
647+ const inFlight = preloadingRef . current . get ( preloadKey )
648+ if ( inFlight ) return
649+
650+ const promise = ( async ( ) => {
651+ const projects = await parseAllSessions ( range , provider , { noCache, progress : null } )
652+ if ( ! noCache ) {
653+ storeCachedWindow ( provider , periodToLoad , range , projects )
654+ }
655+ return projects
656+ } ) ( )
657+
658+ preloadingRef . current . set ( preloadKey , promise )
659+ try {
660+ await promise
661+ } finally {
662+ preloadingRef . current . delete ( preloadKey )
663+ }
664+ } , [ findCachedWindow , makeCacheToken , noCache , storeCachedWindow ] )
665+
666+ const reloadData = useCallback ( async ( p : Period , prov : string , options ?: { silent ?: boolean } ) => {
667+ const range = getDateRange ( p )
668+ const request = ++ reloadSeqRef . current
669+ const token = makeCacheToken ( prov , p )
670+ const cachedWindow = findCachedWindow ( prov , range )
671+ if ( cachedWindow ) {
672+ const projectsFromCache = filterProjectsByName (
673+ filterProjectsByDateRange ( cachedWindow . projects , range ) ,
674+ projectFilter ,
675+ excludeFilter ,
676+ )
677+ if ( ! options ?. silent && request === reloadSeqRef . current ) {
678+ setProjects ( projectsFromCache )
679+ }
680+ return
681+ }
682+
683+ const inFlight = preloadingRef . current . get ( token )
684+ if ( inFlight ) {
685+ if ( ! options ?. silent ) setLoading ( true )
686+ try {
687+ const projects = await inFlight
688+ if ( ! noCache ) {
689+ storeCachedWindow ( prov , p , range , projects )
690+ }
691+ if ( request !== reloadSeqRef . current ) return
692+ const filtered = filterProjectsByName ( projects , projectFilter , excludeFilter )
693+ if ( ! options ?. silent ) {
694+ setProjects ( filtered )
695+ }
696+ } finally {
697+ if ( ! options ?. silent && request === reloadSeqRef . current ) setLoading ( false )
698+ }
699+ return
700+ }
701+
702+ if ( ! options ?. silent ) setLoading ( true )
703+ try {
704+ const projects = await parseAllSessions ( range , prov , { noCache, progress : null } )
705+ if ( ! noCache ) {
706+ storeCachedWindow ( prov , p , range , projects )
707+ }
708+ if ( request !== reloadSeqRef . current ) return
709+ const filtered = filterProjectsByName ( projects , projectFilter , excludeFilter )
710+ if ( ! options ?. silent ) {
711+ setProjects ( filtered )
712+ }
713+ } finally {
714+ if ( ! options ?. silent && request === reloadSeqRef . current ) setLoading ( false )
715+ }
716+ } , [ findCachedWindow , noCache , projectFilter , excludeFilter , storeCachedWindow ] )
717+
718+ useEffect ( ( ) => {
719+ if ( noCache ) return
720+ const initialRange = getDateRange ( initialPeriod )
721+ const initialKey = providerCacheKey ( initialProvider )
722+ const existing = cacheByProviderRef . current . get ( initialKey ) ?? [ ]
723+ const alreadyCached = existing . some ( entry => rangeCovers ( entry . range , initialRange ) )
724+ if ( ! alreadyCached ) {
725+ storeCachedWindow ( initialProvider , initialPeriod , initialRange , initialProjects )
726+ }
727+ } , [ initialPeriod , initialProvider , initialProjects , noCache , providerCacheKey , storeCachedWindow ] )
728+
729+ useEffect ( ( ) => {
730+ if ( noCache || period === '30days' ) return
731+ void preloadWindow ( '30days' , activeProvider )
732+ } , [ noCache , period , activeProvider , preloadWindow ] )
733+
592734 useEffect ( ( ) => {
593735 let cancelled = false
594736 async function detect ( ) {
@@ -629,18 +771,6 @@ function InteractiveDashboard({ initialProjects, initialPeriod, initialProvider,
629771 return ( ) => { cancelled = true }
630772 } , [ projects , period , optimizeAvailable ] )
631773
632- const reloadData = useCallback ( async ( p : Period , prov : string ) => {
633- setLoading ( true )
634- const range = getDateRange ( p )
635- const data = filterProjectsByName (
636- await parseAllSessions ( range , prov , { noCache, progress : null } ) ,
637- projectFilter ,
638- excludeFilter ,
639- )
640- setProjects ( data )
641- setLoading ( false )
642- } , [ excludeFilter , noCache , projectFilter ] )
643-
644774 useEffect ( ( ) => {
645775 if ( ! refreshSeconds || refreshSeconds <= 0 ) return
646776 const id = setInterval ( ( ) => { reloadData ( period , activeProvider ) } , refreshSeconds * 1000 )
0 commit comments