'use client' import { useMemo, useState, useEffect } from 'react' import { Bar, Line, ComposedChart, CartesianGrid, XAxis, YAxis, } from 'recharts' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { ChartContainer, ChartTooltip, type ChartConfig, } from '@/components/ui/chart' import type { SessionSummary } from '@/lib/parse-logs' const chartConfig = { releases: { label: 'New Releases', color: 'var(--chart-2)' }, behind: { label: 'Versions Behind', color: 'var(--chart-5)' }, } satisfies ChartConfig function semverToNum(v: string): number { const parts = v.split('.').map(Number) return (parts[0] || 0) * 1_000_000 + (parts[1] || 0) * 1_000 + (parts[2] || 0) } // Only match stable semver (no prerelease/beta suffixes) function isStableVersion(v: string): boolean { return /^\d+\.\d+\.\d+$/.test(v) } export function VersionLagChart({ sessions }: { sessions: SessionSummary[] }) { const [npmVersions, setNpmVersions] = useState>({}) useEffect(() => { fetch('/api/cc-versions') .then((r) => r.json()) .then((data) => setNpmVersions(data)) .catch(() => {}) }, []) const { data, currentLag, currentUserVersion, currentLatestVersion, totalReleases } = useMemo(() => { const empty = { data: [], currentLag: 0, currentUserVersion: '', currentLatestVersion: '', totalReleases: 0 } if (!Object.keys(npmVersions).length || !sessions.length) return empty // Filter to stable versions only const stableVersions = Object.entries(npmVersions) .filter(([v]) => isStableVersion(v)) .map(([version, date]) => ({ version, date: date.slice(0, 10), num: semverToNum(version), })) .sort((a, b) => a.num - b.num) // Determine session date range first const sessionDates = sessions .filter(s => s.cliVersion && s.cliVersion !== 'unknown') .map(s => s.startTime.slice(0, 10)) .sort() const rangeStart = sessionDates[0] || '' const rangeEnd = sessionDates[sessionDates.length - 1] || '' // Count releases and build per-day map within the session date range let total = 0 const releasesPerDay = new Map() for (const v of stableVersions) { if (v.date >= rangeStart && v.date <= rangeEnd) { total++ releasesPerDay.set(v.date, (releasesPerDay.get(v.date) || 0) + 1) } } // Count how many stable versions are newer than userVersion as of date function countBehind(userVersion: string, date: string): number { const userNum = semverToNum(userVersion) let count = 0 for (const v of stableVersions) { if (v.date <= date && v.num > userNum) count++ } return count } function latestAtDate(date: string): string { let best = '' let bestNum = 0 for (const v of stableVersions) { if (v.date <= date && v.num > bestNum) { best = v.version bestNum = v.num } } return best } // Group sessions by date const byDate = new Map() for (const s of sessions) { if (!s.cliVersion || s.cliVersion === 'unknown') continue const date = s.startTime.slice(0, 10) if (!byDate.has(date)) byDate.set(date, []) byDate.get(date)!.push(s.cliVersion) } if (byDate.size === 0) return empty // Build date range: from first session date to last const sortedDates = Array.from(byDate.keys()).sort() const startDate = new Date(sortedDates[0]) const endDate = new Date(sortedDates[sortedDates.length - 1]) // Fill every day in the range const points: { date: string releases: number behind: number userVersion: string latestVersion: string }[] = [] let lastUserVersion = '' const d = new Date(startDate) while (d <= endDate) { const dateStr = d.toLocaleDateString('en-CA') // YYYY-MM-DD const dayVersions = byDate.get(dateStr) if (dayVersions) { // Pick most common version that day const counts = new Map() for (const v of dayVersions) { counts.set(v, (counts.get(v) || 0) + 1) } lastUserVersion = Array.from(counts.entries()).sort((a, b) => b[1] - a[1])[0][0] } if (lastUserVersion) { const latest = latestAtDate(dateStr) const behind = countBehind(lastUserVersion, dateStr) points.push({ date: dateStr.slice(5), releases: releasesPerDay.get(dateStr) || 0, behind, userVersion: lastUserVersion, latestVersion: latest, }) } d.setDate(d.getDate() + 1) } const last = points[points.length - 1] return { data: points, currentLag: last?.behind ?? 0, currentUserVersion: last?.userVersion ?? '', currentLatestVersion: last?.latestVersion ?? '', totalReleases: total, } }, [sessions, npmVersions]) if (data.length === 0) { return ( Version Freshness Your Claude Code version vs latest release over time
No version data available. Re-sync to capture CLI versions.
) } return ( Version Freshness {currentLag === 0 ? ( Up to date (v{currentUserVersion}) ) : ( v{currentUserVersion} —{' '} {currentLag} release{currentLag !== 1 ? 's' : ''} behind {' '} (latest: v{currentLatestVersion}) )} {totalReleases} stable releases { if (!active || !payload?.length) return null const d = payload[0].payload return (
{d.date}
New releases {d.releases}
Your version v{d.userVersion}
Latest v{d.latestVersion}
Behind {d.behind === 0 ? 'up to date' : `${d.behind} release${d.behind !== 1 ? 's' : ''}`}
) }} />
{/* Legend */}
Daily releases Versions behind
) }