feat: add All Time period, avg/s per project, and Top Sessions panel

- Add 'all' period (key 5) to dashboard showing data from all time
- Daily Activity panel shows all available days when All Time is active
- Add avg cost per session column to Project breakdown
- Add Top Sessions panel highlighting the 5 most expensive sessions
This commit is contained in:
Ninym 2026-04-16 13:14:34 +02:00
parent 9794b5450a
commit cfd046a95c
2 changed files with 70 additions and 17 deletions

View file

@ -50,10 +50,11 @@ function getDateRange(period: string): { range: DateRange; label: string } {
}
}
function toPeriod(s: string): 'today' | 'week' | '30days' | 'month' {
function toPeriod(s: string): 'today' | 'week' | '30days' | 'month' | 'all' {
if (s === 'today') return 'today'
if (s === 'month') return 'month'
if (s === '30days') return '30days'
if (s === 'all') return 'all'
return 'week'
}
@ -69,7 +70,7 @@ program.hook('preAction', async () => {
program
.command('report', { isDefault: true })
.description('Interactive usage dashboard')
.option('-p, --period <period>', 'Starting period: today, week, 30days, month', 'week')
.option('-p, --period <period>', 'Starting period: today, week, 30days, month, all', 'week')
.option('--provider <provider>', 'Filter by provider: all, claude, codex, cursor', 'all')
.option('--refresh <seconds>', 'Auto-refresh interval in seconds', parseInt)
.action(async (opts) => {

View file

@ -8,14 +8,15 @@ import { parseAllSessions } from './parser.js'
import { loadPricing } from './models.js'
import { getAllProviders } from './providers/index.js'
type Period = 'today' | 'week' | '30days' | 'month'
type Period = 'today' | 'week' | '30days' | 'month' | 'all'
const PERIODS: Period[] = ['today', 'week', '30days', 'month']
const PERIODS: Period[] = ['today', 'week', '30days', 'month', 'all']
const PERIOD_LABELS: Record<Period, string> = {
today: 'Today',
week: '7 Days',
'30days': '30 Days',
month: 'This Month',
all: 'All Time',
}
const MIN_WIDE = 90
@ -37,6 +38,7 @@ const PANEL_COLORS = {
overview: '#FF8C42',
daily: '#5B9EF5',
project: '#5BF5A0',
sessions: '#FF6B6B',
model: '#E05BF5',
activity: '#F5C85B',
tools: '#5BF5E0',
@ -99,6 +101,7 @@ function getDateRange(period: Period): { start: Date; end: Date } {
case 'week': return { start: new Date(now.getFullYear(), now.getMonth(), now.getDate() - 7), end }
case '30days': return { start: new Date(now.getFullYear(), now.getMonth(), now.getDate() - 30), end }
case 'month': return { start: new Date(now.getFullYear(), now.getMonth(), 1), end }
case 'all': return { start: new Date(0), end }
}
}
@ -191,7 +194,7 @@ function DailyActivity({ projects, days = 14, pw, bw }: { projects: ProjectSumma
}
}
}
const sortedDays = Object.keys(dailyCosts).sort().slice(-days)
const sortedDays = days !== undefined ? Object.keys(dailyCosts).sort().slice(-days) : Object.keys(dailyCosts).sort()
const maxCost = Math.max(...sortedDays.map(d => dailyCosts[d] ?? 0))
return (
@ -230,20 +233,28 @@ function shortProject(encoded: string): string {
return parts.slice(-3).join('/')
}
const PROJECT_COL_AVG = 7
function ProjectBreakdown({ projects, pw, bw }: { projects: ProjectSummary[]; pw: number; bw: number }) {
const maxCost = Math.max(...projects.map(p => p.totalCostUSD))
const nw = Math.max(8, pw - bw - 23)
const nw = Math.max(8, pw - bw - 30)
return (
<Panel title="By Project" color={PANEL_COLORS.project} width={pw}>
<Text dimColor wrap="truncate-end">{''.padEnd(bw + 1 + nw)}{'cost'.padStart(8)}{'sess'.padStart(6)}</Text>
{projects.slice(0, 8).map((project, i) => (
<Text key={`${project.project}-${i}`} wrap="truncate-end">
<HBar value={project.totalCostUSD} max={maxCost} width={bw} />
<Text dimColor> {fit(shortProject(project.project), nw)}</Text>
<Text color={GOLD}>{formatCost(project.totalCostUSD).padStart(8)}</Text>
<Text>{String(project.sessions.length).padStart(6)}</Text>
</Text>
))}
<Text dimColor wrap="truncate-end">{''.padEnd(bw + 1 + nw)}{'cost'.padStart(8)}{'avg/s'.padStart(PROJECT_COL_AVG)}{'sess'.padStart(6)}</Text>
{projects.slice(0, 8).map((project, i) => {
const avgCost = project.sessions.length > 0
? formatCost(project.totalCostUSD / project.sessions.length)
: '-'
return (
<Text key={`${project.project}-${i}`} wrap="truncate-end">
<HBar value={project.totalCostUSD} max={maxCost} width={bw} />
<Text dimColor> {fit(shortProject(project.project), nw)}</Text>
<Text color={GOLD}>{formatCost(project.totalCostUSD).padStart(8)}</Text>
<Text color={GOLD}>{avgCost.padStart(PROJECT_COL_AVG)}</Text>
<Text>{String(project.sessions.length).padStart(6)}</Text>
</Text>
)
})}
</Panel>
)
}
@ -364,6 +375,42 @@ function ToolBreakdown({ projects, pw, bw, title, filterPrefix }: { projects: Pr
)
}
const TOP_SESSIONS_DATE_LEN = 10
const TOP_SESSIONS_COST_COL = 8
const TOP_SESSIONS_CALLS_COL = 6
function TopSessions({ projects, pw, bw }: { projects: ProjectSummary[]; pw: number; bw: number }) {
const allSessions = projects.flatMap(p =>
p.sessions.map(s => ({ ...s, projectName: p.project }))
)
const top = [...allSessions].sort((a, b) => b.totalCostUSD - a.totalCostUSD).slice(0, 5)
if (top.length === 0) {
return <Panel title="Top Sessions" color={PANEL_COLORS.sessions} width={pw}><Text dimColor>No sessions</Text></Panel>
}
const maxCost = top[0].totalCostUSD
const nw = Math.max(8, pw - bw - TOP_SESSIONS_COST_COL - TOP_SESSIONS_CALLS_COL - 1)
return (
<Panel title="Top Sessions" color={PANEL_COLORS.sessions} width={pw}>
<Text dimColor wrap="truncate-end">{''.padEnd(bw + 1 + nw)}{'cost'.padStart(TOP_SESSIONS_COST_COL)}{'calls'.padStart(TOP_SESSIONS_CALLS_COL)}</Text>
{top.map((session, i) => {
const date = session.firstTimestamp.slice(0, TOP_SESSIONS_DATE_LEN)
const label = `${date} ${shortProject(session.projectName)}`
return (
<Text key={`${session.sessionId}-${i}`} wrap="truncate-end">
<HBar value={session.totalCostUSD} max={maxCost} width={bw} />
<Text dimColor> {fit(label, nw - 1)}</Text>
<Text color={GOLD}>{formatCost(session.totalCostUSD).padStart(TOP_SESSIONS_COST_COL)}</Text>
<Text>{String(session.apiCalls).padStart(TOP_SESSIONS_CALLS_COL)}</Text>
</Text>
)
})}
</Panel>
)
}
function McpBreakdown({ projects, pw, bw }: { projects: ProjectSummary[]; pw: number; bw: number }) {
const mcpTotals: Record<string, number> = {}
for (const project of projects) {
@ -477,7 +524,9 @@ function StatusBar({ width, showProvider }: { width: number; showProvider?: bool
<Text color={ORANGE} bold>3</Text>
<Text dimColor> 30 days </Text>
<Text color={ORANGE} bold>4</Text>
<Text dimColor> month</Text>
<Text dimColor> month </Text>
<Text color={ORANGE} bold>5</Text>
<Text dimColor> all time</Text>
{showProvider && (
<>
<Text dimColor> </Text>
@ -508,7 +557,7 @@ function DashboardContent({ projects, period, columns, activeProvider }: { proje
}
const pw = wide ? halfWidth : dashWidth
const days = period === 'month' || period === '30days' ? 31 : 14
const days = period === 'all' ? undefined : (period === 'month' || period === '30days' ? 31 : 14)
return (
<Box flexDirection="column" width={dashWidth}>
@ -519,6 +568,8 @@ function DashboardContent({ projects, period, columns, activeProvider }: { proje
<ProjectBreakdown projects={projects} pw={pw} bw={barWidth} />
</Row>
<TopSessions projects={projects} pw={dashWidth} bw={barWidth} />
<Row wide={wide} width={dashWidth}>
<ActivityBreakdown projects={projects} pw={pw} bw={barWidth} />
<ModelBreakdown projects={projects} pw={pw} bw={barWidth} />
@ -629,6 +680,7 @@ function InteractiveDashboard({ initialProjects, initialPeriod, initialProvider,
else if (input === '2') switchPeriodImmediate('week')
else if (input === '3') switchPeriodImmediate('30days')
else if (input === '4') switchPeriodImmediate('month')
else if (input === '5') switchPeriodImmediate('all')
})
if (loading) {