From 64b953055f44f6bd8e034ba22ce81f89034f3257 Mon Sep 17 00:00:00 2001 From: Valerii Kovalskii Date: Wed, 8 Apr 2026 10:06:15 +0300 Subject: [PATCH] =?UTF-8?q?fix:=20daily=20stats=20=E2=80=94=20distribute?= =?UTF-8?q?=20multi-day=20sessions=20correctly?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Sessions spanning multiple days now split messages/hours/cost across each day - Use local timezone for date boundaries (was UTC, causing off-by-one) - Cap hours at 16h/day per session (realistic limit) - Today stats now show actual today portion, not entire session Co-Authored-By: Claude Opus 4.6 (1M context) --- src/data.js | 55 ++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 42 insertions(+), 13 deletions(-) diff --git a/src/data.js b/src/data.js index 243dbfc..00e4691 100644 --- a/src/data.js +++ b/src/data.js @@ -2240,23 +2240,52 @@ function getOrCreateAnonId() { function getDailyStats(sessions) { const byDay = {}; + const ensureDay = (date) => { + if (!byDay[date]) byDay[date] = { date, sessions: 0, messages: 0, hours: 0, cost: 0, agents: {} }; + return byDay[date]; + }; + for (const s of sessions) { - if (!s.date) continue; - if (!byDay[s.date]) byDay[s.date] = { date: s.date, sessions: 0, messages: 0, hours: 0, cost: 0, agents: {} }; - const d = byDay[s.date]; - d.sessions++; - d.messages += (s.detail_messages || s.messages || 0); - // Hours = time between first and last message - const durationMs = (s.last_ts || 0) - (s.first_ts || 0); - if (durationMs > 0) d.hours += durationMs / 3600000; - // Cost + if (!s.first_ts || !s.last_ts) continue; + const msgs = s.detail_messages || s.messages || 0; + const totalMs = Math.max(s.last_ts - s.first_ts, 0); + + // Cost per session (computed once) const costData = computeSessionCost(s.id, s.project); - if (costData && costData.cost) d.cost += costData.cost; - // Agent breakdown + const sessionCost = (costData && costData.cost) || 0; + + // Distribute across all days the session spans (local timezone) + const startDay = new Date(s.first_ts); + const endDay = new Date(s.last_ts); + const days = []; + const fmtDay = (d) => d.getFullYear() + '-' + String(d.getMonth()+1).padStart(2,'0') + '-' + String(d.getDate()).padStart(2,'0'); + const dt = new Date(startDay.getFullYear(), startDay.getMonth(), startDay.getDate()); + const endDt = new Date(endDay.getFullYear(), endDay.getMonth(), endDay.getDate()); + while (dt <= endDt) { + days.push(fmtDay(dt)); + dt.setDate(dt.getDate() + 1); + } + if (days.length === 0) days.push(s.date || fmtDay(new Date(s.last_ts))); + + const numDays = days.length; + const msgsPerDay = Math.ceil(msgs / numDays); + const hoursPerDay = (totalMs / 3600000) / numDays; + // Cap hours at 16h/day max (can't code more than that) + const cappedHours = Math.min(hoursPerDay, 16); + const costPerDay = sessionCost / numDays; const tool = s.tool || 'unknown'; - if (!d.agents[tool]) d.agents[tool] = 0; - d.agents[tool]++; + + for (const day of days) { + const d = ensureDay(day); + d.sessions++; + d.messages += msgsPerDay; + d.hours += cappedHours; + d.cost += costPerDay; + d.agents[tool] = (d.agents[tool] || 0) + 1; + } } + // Round hours to 1 decimal + for (const d of Object.values(byDay)) d.hours = Math.round(d.hours * 10) / 10; return Object.values(byDay).sort((a, b) => b.date.localeCompare(a.date)); }