@@ -252,12 +252,32 @@ function parseTimestamp(value) {
252252 if ( typeof value === 'string' ) {
253253 const trimmed = value . trim ( ) ;
254254 if ( ! trimmed ) return NaN ;
255- if ( / ^ \d + $ / . test ( trimmed ) ) return Number ( trimmed ) ;
255+ if ( / ^ \d + ( \. \d + ) ? $ / . test ( trimmed ) ) return Number ( trimmed ) ;
256256 return Date . parse ( trimmed ) ;
257257 }
258258 return NaN ;
259259}
260260
261+ // Epoch seconds stay below this cutoff until 2286; millisecond epochs are larger.
262+ const EPOCH_SECONDS_CUTOFF = 10000000000 ;
263+
264+ function normalizeTimestampMs ( ts ) {
265+ if ( ! Number . isFinite ( ts ) ) return NaN ;
266+ return ts > 0 && ts < EPOCH_SECONDS_CUTOFF ? ts * 1000 : ts ;
267+ }
268+
269+ function parseTimestampMs ( value ) {
270+ return normalizeTimestampMs ( parseTimestamp ( value ) ) ;
271+ }
272+
273+ function parseEntryTimestampMs ( entry ) {
274+ if ( ! entry || typeof entry !== 'object' ) return NaN ;
275+ const timestampTs = parseTimestampMs ( entry . timestamp ) ;
276+ if ( Number . isFinite ( timestampTs ) && timestampTs > 0 ) return timestampTs ;
277+ const fallbackTs = parseTimestampMs ( entry . ts ) ;
278+ return Number . isFinite ( fallbackTs ) && fallbackTs > 0 ? fallbackTs : NaN ;
279+ }
280+
261281function shortenHomePath ( value , homes = ALL_HOMES ) {
262282 value = normalizeProjectPath ( value ) ;
263283 if ( ! value || typeof value !== 'string' ) return value || '' ;
@@ -352,7 +372,7 @@ function parseKiloMcpServer(toolName) {
352372}
353373
354374// Disk cache for parsed Claude session files (keyed by path + mtime + size)
355- const PARSED_CACHE_FILE = path . join ( os . tmpdir ( ) , 'codedash -parsed-cache-v2 .json' ) ;
375+ const PARSED_CACHE_FILE = path . join ( os . tmpdir ( ) , 'codbash -parsed-cache.json' ) ;
356376let _parsedDiskCache = null ;
357377let _parsedDiskCacheDirty = false ;
358378// Reverse index: file path -> cache key (avoids repeated fs.statSync)
@@ -1790,7 +1810,7 @@ function loadKiroDetail(conversationId) {
17901810
17911811// Build workspace-hash -> project path mapping for VS Code workspaceStorage
17921812let _copilotWsMapCache = null ;
1793- const COPILOT_WS_MAP_CACHE_FILE = path . join ( os . tmpdir ( ) , 'codedash -copilot-ws-map.json' ) ;
1813+ const COPILOT_WS_MAP_CACHE_FILE = path . join ( os . tmpdir ( ) , 'codbash -copilot-ws-map.json' ) ;
17941814const COPILOT_WS_MAP_TTL = 600000 ; // 10 minutes
17951815
17961816function buildCopilotWorkspaceMap ( ) {
@@ -1884,7 +1904,7 @@ function parseCopilotJson(filePath) {
18841904}
18851905
18861906// Disk cache for Copilot session metadata (avoids re-scanning large files)
1887- const COPILOT_PARSED_CACHE_FILE = path . join ( os . tmpdir ( ) , 'codedash -copilot-parsed-cache.json' ) ;
1907+ const COPILOT_PARSED_CACHE_FILE = path . join ( os . tmpdir ( ) , 'codbash -copilot-parsed-cache.json' ) ;
18881908let _copilotParsedCache = null ;
18891909
18901910function _loadCopilotParsedCache ( ) {
@@ -2199,7 +2219,7 @@ function decodeCursorProjectFolderKey(proj) {
21992219// Build composerId -> project path mapping from Cursor workspace storage
22002220// Uses disk cache to avoid querying 190+ SQLite files on every startup
22012221let _cursorWsMapCache = null ;
2202- const CURSOR_WS_MAP_CACHE_FILE = path . join ( os . tmpdir ( ) , 'codedash -cursor-ws-map.json' ) ;
2222+ const CURSOR_WS_MAP_CACHE_FILE = path . join ( os . tmpdir ( ) , 'codbash -cursor-ws-map.json' ) ;
22032223const CURSOR_WS_MAP_TTL = 600000 ; // 10 minutes
22042224
22052225function buildCursorWorkspaceMap ( ) {
@@ -2523,15 +2543,15 @@ function parseCodexSessionFile(sessionFile) {
25232543 let msgCount = 0 ;
25242544 let userMsgCount = 0 ;
25252545 let firstMsg = '' ;
2526- let firstTs = stat . mtimeMs ;
2527- let lastTs = stat . mtimeMs ;
2546+ let firstTs = Infinity ;
2547+ let lastTs = - Infinity ;
25282548 const mcpSet = new Set ( ) ;
25292549
25302550 for ( const line of lines ) {
25312551 try {
25322552 const entry = JSON . parse ( line ) ;
2533- const ts = parseTimestamp ( entry . timestamp || entry . ts ) ;
2534- if ( Number . isFinite ( ts ) ) {
2553+ const ts = parseEntryTimestampMs ( entry ) ;
2554+ if ( Number . isFinite ( ts ) && ts > 0 ) {
25352555 if ( ts < firstTs ) firstTs = ts ;
25362556 if ( ts > lastTs ) lastTs = ts ;
25372557 }
@@ -2565,6 +2585,10 @@ function parseCodexSessionFile(sessionFile) {
25652585 } catch { }
25662586 }
25672587
2588+ const fallbackTs = Number . isFinite ( stat . mtimeMs ) ? stat . mtimeMs : Date . now ( ) ;
2589+ if ( ! Number . isFinite ( firstTs ) ) firstTs = fallbackTs ;
2590+ if ( ! Number . isFinite ( lastTs ) ) lastTs = firstTs ;
2591+
25682592 return {
25692593 projectPath,
25702594 msgCount,
@@ -2591,7 +2615,8 @@ function scanCodexSessions() {
25912615 const sid = d . session_id || d . sessionId || d . id ;
25922616 if ( ! sid ) continue ;
25932617 if ( importedFromClaude . has ( sid ) ) continue ; // skip — original Claude file loaded separately
2594- const ts = d . ts ? d . ts * 1000 : ( d . timestamp || Date . now ( ) ) ;
2618+ const parsedTs = parseEntryTimestampMs ( d ) ;
2619+ const ts = Number . isFinite ( parsedTs ) && parsedTs > 0 ? parsedTs : Date . now ( ) ;
25952620 if ( ! sessions . find ( s => s . id === sid ) ) {
25962621 sessions . push ( {
25972622 id : sid ,
@@ -2697,7 +2722,7 @@ function scanCodexSessions() {
26972722
26982723const _gitRootCache = { } ;
26992724// v2: collapses worktrees to main repo + ignores $HOME-as-git-root.
2700- const GIT_ROOT_CACHE_FILE = path . join ( os . tmpdir ( ) , 'codedash -gitroot-cache-v2 .json' ) ;
2725+ const GIT_ROOT_CACHE_FILE = path . join ( os . tmpdir ( ) , 'codbash -gitroot-cache.json' ) ;
27012726let _gitRootDiskCache = null ;
27022727
27032728function _loadGitRootDiskCache ( ) {
@@ -4793,7 +4818,7 @@ function getModelPricing(model) {
47934818// ── Compute real cost from session file token usage ────────
47944819
47954820// Disk cache for computed session costs
4796- const COST_CACHE_FILE = path . join ( os . tmpdir ( ) , 'codedash -cost-cache.json' ) ;
4821+ const COST_CACHE_FILE = path . join ( os . tmpdir ( ) , 'codbash -cost-cache.json' ) ;
47974822let _costDiskCache = null ;
47984823
47994824function _loadCostDiskCache ( ) {
@@ -5162,7 +5187,7 @@ function computeSessionCost(sessionId, project) {
51625187// ── Cost analytics ────────────────────────────────────────
51635188
51645189// Analytics result cache — avoids recomputing 31k sessions every request
5165- const ANALYTICS_CACHE_FILE = path . join ( os . tmpdir ( ) , 'codedash -analytics-cache.json' ) ;
5190+ const ANALYTICS_CACHE_FILE = path . join ( os . tmpdir ( ) , 'codbash -analytics-cache.json' ) ;
51665191let _analyticsCacheResult = null ;
51675192let _analyticsCacheKey = null ;
51685193
@@ -5366,8 +5391,9 @@ function _computeCostAnalytics(sessions) {
53665391 globalContextTurnCount += costData . contextTurnCount ;
53675392
53685393 // Date range
5369- const day = s . date || 'unknown' ;
5370- if ( s . date ) {
5394+ const hasValidDate = isValidLocalDay ( s . date ) ;
5395+ const day = hasValidDate ? s . date : 'unknown' ;
5396+ if ( hasValidDate ) {
53715397 if ( ! firstDate || s . date < firstDate ) firstDate = s . date ;
53725398 if ( ! lastDate || s . date > lastDate ) lastDate = s . date ;
53735399 }
@@ -5377,11 +5403,11 @@ function _computeCostAnalytics(sessions) {
53775403 byDay [ day ] . tokens += tokens ;
53785404
53795405 // By week
5380- if ( s . date ) {
5381- const d = new Date ( s . date ) ;
5406+ if ( hasValidDate ) {
5407+ const d = parseLocalDayStart ( s . date ) ;
53825408 const weekStart = new Date ( d ) ;
53835409 weekStart . setDate ( d . getDate ( ) - d . getDay ( ) ) ;
5384- const weekKey = weekStart . toISOString ( ) . slice ( 0 , 10 ) ;
5410+ const weekKey = fmtLocalDay ( weekStart . getTime ( ) ) ;
53855411 if ( ! byWeek [ weekKey ] ) byWeek [ weekKey ] = { cost : 0 , sessions : 0 } ;
53865412 byWeek [ weekKey ] . cost += cost ;
53875413 byWeek [ weekKey ] . sessions ++ ;
@@ -5394,20 +5420,20 @@ function _computeCostAnalytics(sessions) {
53945420 byProject [ proj ] . sessions ++ ;
53955421 byProject [ proj ] . tokens += tokens ;
53965422
5397- sessionCosts . push ( { id : s . id , cost, project : proj , date : s . date , last_ts : s . last_ts || 0 } ) ;
5423+ sessionCosts . push ( { id : s . id , cost, project : proj , date : hasValidDate ? s . date : '' , last_ts : s . last_ts || 0 } ) ;
53985424 }
53995425
54005426 // Sort top sessions by cost
54015427 sessionCosts . sort ( ( a , b ) => b . cost - a . cost ) ;
54025428
54035429 const days = firstDate && lastDate
5404- ? Math . max ( 1 , Math . round ( ( new Date ( lastDate ) - new Date ( firstDate ) ) / 86400000 ) + 1 )
5430+ ? Math . max ( 1 , Math . round ( ( parseLocalDayStart ( lastDate ) - parseLocalDayStart ( firstDate ) ) / 86400000 ) + 1 )
54055431 : 1 ;
54065432
54075433 // Burn rate: derived from already-computed sessionCosts — no extra IO
54085434 const now = Date . now ( ) ;
5409- const todayStr = new Date ( ) . toISOString ( ) . slice ( 0 , 10 ) ;
5410- const hoursElapsedToday = ( now - new Date ( todayStr ) . getTime ( ) ) / 3600000 ;
5435+ const todayStr = getLocalToday ( ) ;
5436+ const hoursElapsedToday = ( now - parseLocalDayStart ( todayStr ) . getTime ( ) ) / 3600000 ;
54115437 let last1hCost = 0 ;
54125438 let todayCost = 0 ;
54135439 for ( const sc of sessionCosts ) {
@@ -5786,9 +5812,58 @@ function leaderboardAgentKey(session) {
57865812 return session . tool || 'unknown' ;
57875813}
57885814
5815+ function getLocalToday ( ) {
5816+ return fmtLocalDay ( Date . now ( ) ) ;
5817+ }
5818+
5819+ function parseLocalDayStart ( day ) {
5820+ if ( typeof day !== 'string' ) return new Date ( NaN ) ;
5821+ const match = day . match ( / ^ ( \d { 4 } ) - ( \d { 2 } ) - ( \d { 2 } ) $ / ) ;
5822+ if ( ! match ) return new Date ( NaN ) ;
5823+ const parsed = new Date ( Number ( match [ 1 ] ) , Number ( match [ 2 ] ) - 1 , Number ( match [ 3 ] ) ) ;
5824+ if (
5825+ parsed . getFullYear ( ) !== Number ( match [ 1 ] ) ||
5826+ parsed . getMonth ( ) !== Number ( match [ 2 ] ) - 1 ||
5827+ parsed . getDate ( ) !== Number ( match [ 3 ] )
5828+ ) {
5829+ return new Date ( NaN ) ;
5830+ }
5831+ return parsed ;
5832+ }
5833+
5834+ function isValidLocalDay ( day ) {
5835+ return ! Number . isNaN ( parseLocalDayStart ( day ) . getTime ( ) ) ;
5836+ }
5837+
5838+ function getLocalTimezone ( ) {
5839+ try {
5840+ return Intl . DateTimeFormat ( ) . resolvedOptions ( ) . timeZone || '' ;
5841+ } catch {
5842+ return '' ;
5843+ }
5844+ }
5845+
5846+ function getUtcOffsetMinutes ( ts = Date . now ( ) ) {
5847+ return - new Date ( ts ) . getTimezoneOffset ( ) ;
5848+ }
5849+
5850+ function computeCurrentStreak ( daily , today = getLocalToday ( ) ) {
5851+ const activeDays = new Set ( ( daily || [ ] ) . map ( d => d && d . date ) . filter ( Boolean ) ) ;
5852+ const dt = parseLocalDayStart ( today ) ;
5853+ if ( Number . isNaN ( dt . getTime ( ) ) ) return 0 ;
5854+
5855+ let streak = 0 ;
5856+ for ( let i = 0 ; i < 365 ; i ++ ) {
5857+ const day = fmtLocalDay ( dt . getTime ( ) ) ;
5858+ if ( ! activeDays . has ( day ) ) break ;
5859+ streak ++ ;
5860+ dt . setDate ( dt . getDate ( ) - 1 ) ;
5861+ }
5862+ return streak ;
5863+ }
57895864
57905865// Disk cache for per-session daily message breakdown
5791- const DAILY_STATS_CACHE_FILE = path . join ( os . tmpdir ( ) , 'codedash -daily-stats-cache.json' ) ;
5866+ const DAILY_STATS_CACHE_FILE = path . join ( os . tmpdir ( ) , 'codbash -daily-stats-cache.json' ) ;
57925867let _dailyStatsDiskCache = null ;
57935868
57945869function _loadDailyStatsDiskCache ( ) {
@@ -5813,10 +5888,13 @@ function _computeSessionDailyBreakdown(s, found) {
58135888 const tsByDay = { } ;
58145889
58155890 const addMsg = ( day , ts ) => {
5891+ if ( ! day ) return ;
58165892 msgsByDay [ day ] = ( msgsByDay [ day ] || 0 ) + 1 ;
5817- if ( ! tsByDay [ day ] ) tsByDay [ day ] = { first : ts , last : ts } ;
5818- if ( ts < tsByDay [ day ] . first ) tsByDay [ day ] . first = ts ;
5819- if ( ts > tsByDay [ day ] . last ) tsByDay [ day ] . last = ts ;
5893+ const normalizedTs = typeof ts === 'number' ? normalizeTimestampMs ( ts ) : parseTimestampMs ( ts ) ;
5894+ if ( ! Number . isFinite ( normalizedTs ) || normalizedTs <= 0 ) return ;
5895+ if ( ! tsByDay [ day ] ) tsByDay [ day ] = { first : normalizedTs , last : normalizedTs } ;
5896+ if ( normalizedTs < tsByDay [ day ] . first ) tsByDay [ day ] . first = normalizedTs ;
5897+ if ( normalizedTs > tsByDay [ day ] . last ) tsByDay [ day ] . last = normalizedTs ;
58205898 } ;
58215899
58225900 try {
@@ -5867,24 +5945,27 @@ function _computeSessionDailyBreakdown(s, found) {
58675945 } else if ( found . format === 'codex' ) {
58685946 if ( entry . type === 'response_item' && entry . payload && entry . payload . role === 'user' ) {
58695947 isUser = true ;
5870- ts = s . first_ts ;
5871- const c = entry . payload . content ;
5872- if ( Array . isArray ( c ) ) { for ( const p of c ) { if ( ( p . text || '' ) . trim ( ) ) { hasText = true ; break ; } } }
5948+ ts = parseEntryTimestampMs ( entry ) ;
5949+ const content = extractContent ( entry . payload . content ) ;
5950+ hasText = ! ! ( content && content . trim ( ) && ! isSystemMessage ( content ) ) ;
58735951 } else continue ;
58745952 }
58755953
58765954 if ( ! isUser || ! hasText ) continue ;
5877- if ( ! ts || ts < 1000000000000 ) ts = s . first_ts ;
5878- const day = ( found . format === 'claude' && ts ) ? fmtLocalDay ( ts ) : ( s . date || fmtLocalDay ( s . last_ts ) ) ;
5879- addMsg ( day , ts || s . first_ts ) ;
5955+ const normalizedTs = normalizeTimestampMs ( ts ) ;
5956+ const fallbackTs = Number . isFinite ( s . first_ts ) && s . first_ts > 0 ? s . first_ts : NaN ;
5957+ const effectiveTs = Number . isFinite ( normalizedTs ) && normalizedTs > 0 ? normalizedTs : fallbackTs ;
5958+ const fallbackDay = isValidLocalDay ( s . date ) ? s . date : ( Number . isFinite ( s . last_ts ) && s . last_ts > 0 ? fmtLocalDay ( s . last_ts ) : '' ) ;
5959+ const day = Number . isFinite ( effectiveTs ) && effectiveTs > 0 ? fmtLocalDay ( effectiveTs ) : fallbackDay ;
5960+ addMsg ( day , effectiveTs ) ;
58805961 } catch { }
58815962 }
58825963 } catch { }
58835964 return { msgsByDay, tsByDay } ;
58845965}
58855966
58865967// Daily stats result cache
5887- const DAILY_RESULT_CACHE_FILE = path . join ( os . tmpdir ( ) , 'codedash -daily-result-cache-v2 .json' ) ;
5968+ const DAILY_RESULT_CACHE_FILE = path . join ( os . tmpdir ( ) , 'codbash -daily-result-cache.json' ) ;
58885969let _dailyResultCache = null ;
58895970let _dailyResultCacheKey = null ;
58905971
@@ -5965,7 +6046,7 @@ function _computeDailyStats(sessions) {
59656046 }
59666047
59676048 // Fallback for non-Claude or sessions without detail: single-day attribution
5968- const day = s . date || fmtLocalDay ( s . last_ts ) ;
6049+ const day = isValidLocalDay ( s . date ) ? s . date : ( Number . isFinite ( s . last_ts ) ? fmtLocalDay ( s . last_ts ) : 'unknown' ) ;
59696050 const d = ensureDay ( day ) ;
59706051 d . sessions ++ ;
59716052 // Use exact user_messages count if available, otherwise estimate
@@ -6018,21 +6099,11 @@ function getLeaderboardStats() {
60186099 }
60196100
60206101 // Today
6021- const today = new Date ( ) . toISOString ( ) . slice ( 0 , 10 ) ;
6102+ const today = getLocalToday ( ) ;
60226103 const todayStats = daily . find ( d => d . date === today ) || { sessions : 0 , messages : 0 , hours : 0 , cost : 0 , agents : { } } ;
60236104
60246105 // Streak (consecutive days with sessions)
6025- let streak = 0 ;
6026- const dt = new Date ( ) ;
6027- for ( let i = 0 ; i < 365 ; i ++ ) {
6028- const day = dt . toISOString ( ) . slice ( 0 , 10 ) ;
6029- if ( daily . find ( d => d . date === day ) ) {
6030- streak ++ ;
6031- dt . setDate ( dt . getDate ( ) - 1 ) ;
6032- } else {
6033- break ;
6034- }
6035- }
6106+ const streak = computeCurrentStreak ( daily , today ) ;
60366107
60376108 const result = {
60386109 anon,
@@ -6042,6 +6113,8 @@ function getLeaderboardStats() {
60426113 streak,
60436114 daily : daily . slice ( 0 , 30 ) , // last 30 days
60446115 activeDays : daily . length ,
6116+ timezone : getLocalTimezone ( ) ,
6117+ utcOffsetMinutes : getUtcOffsetMinutes ( ) ,
60456118 } ;
60466119 _lbCache = result ;
60476120 _lbCacheTs = Date . now ( ) ;
@@ -6099,6 +6172,14 @@ module.exports = {
60996172 parseClaudeStructuredMessage,
61006173 parseStructuredMessage,
61016174 isFilteredClaudeStructuredMessage,
6175+ parseCodexSessionFile,
6176+ _computeSessionDailyBreakdown,
6177+ fmtLocalDay,
6178+ getLocalToday,
6179+ parseLocalDayStart,
6180+ getLocalTimezone,
6181+ getUtcOffsetMinutes,
6182+ computeCurrentStreak,
61026183 _parseMainWorktree,
61036184 resolveGitRoot,
61046185 ALL_HOMES ,
0 commit comments