codeburn/app/renderer/sections/Optimize.tsx
iamtoruk 300e949951 feat(app): refresh cadence, instant switching, cache coherence, telemetry v1
Performance and coherence:
- Settings > General 'Refresh every' (Manual/30s/1m/3m/5m/10m), live via
  RefreshCadenceContext; Manual polls only on demand
- usePolled memoKey LRU: provider/period switches paint the last-good
  result instantly with a switching hairline while refreshing quietly
- quota TTL 5min + honest rate-limited copy on 429 backoff
- version-suffixed cache files (session-cache.v5.json, daily-cache.v12,
  auto-minted on future bumps); legacy files never written or deleted,
  adopted once when versions match: old and new binaries coexist
  without clobbering (field-observed menubar-vs-desktop ping-pong)
- advisory hydration lock: concurrent cold starts share one scan
  (wait-then-read-warm), stale/dead locks self-heal, never a
  correctness gate

Telemetry v1 + onboarding (desktop only, per owner decisions):
- first-launch onboarding (3 feature screens + consent); region-split
  default (EU/EEA/UK/CH off, elsewhere on, unknown off); nothing sends
  before consent completion or while off; dev builds never send
- anonymous install UUID, rotated on opt-out; day-granularity events,
  cost buckets only; whitelisted names; 200-event queue, 5min flush
- Settings > Privacy live toggle replaces the static claim

Zero computed-number changes. App 316/316, root 1805.

Wire contract targets api.codeburn.app/v1/telemetry (Worker follows).
2026-07-16 11:47:05 -07:00

239 lines
9.5 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { Fragment, useState } from 'react'
import { CliErrorPanel } from '../components/CliErrorPanel'
import { EmptyNote } from '../components/EmptyState'
import { Panel } from '../components/Panel'
import { SectionSkeleton } from '../components/Skeleton'
import { SegTabs } from '../components/SegTabs'
import { StaleBanner } from '../components/StaleBanner'
import { type Polled, usePolled } from '../hooks/usePolled'
import { formatCompact, formatUsd } from '../lib/format'
import { codeburn } from '../lib/ipc'
import type { DateRange, MenubarPayload, OptimizeJsonReport, Period, SessionYieldJson, WasteAction, YieldJsonReport } from '../lib/types'
type OptimizeTab = 'waste' | 'reverts' | 'abandoned' | 'fixes'
export function Optimize({ period, provider, range = null }: { period: Period; provider: string; range?: DateRange | null }) {
const overview = usePolled<MenubarPayload>(
() => range ? codeburn.getOverview(period, provider, range) : codeburn.getOverview(period, provider),
[period, provider, range?.from, range?.to],
)
return <OptimizeContent period={period} provider={provider} range={range} overview={overview} />
}
export function OptimizeContent({
period,
provider = 'all',
range = null,
overview,
refreshToken = 0,
ready = true,
}: {
period: Period
provider?: string
range?: DateRange | null
overview: Polled<MenubarPayload>
refreshToken?: number
ready?: boolean
}) {
// Gate on app-level readiness so boot hydrates the cache once (default true
// keeps standalone renders/tests polling normally).
const optimizeReport = usePolled<OptimizeJsonReport>(
() => range ? codeburn.getOptimizeReport(period, provider, range) : codeburn.getOptimizeReport(period, provider),
[period, provider, range?.from, range?.to, refreshToken],
{ enabled: ready, memoKey: `optimize|${period}|${provider}|${range?.from ?? ''}-${range?.to ?? ''}` },
)
const yieldReport = usePolled<YieldJsonReport>(
() => range ? codeburn.getYield(period, provider, range) : codeburn.getYield(period, provider),
[period, provider, range?.from, range?.to, refreshToken],
{ enabled: ready, memoKey: `optyield|${period}|${provider}|${range?.from ?? ''}-${range?.to ?? ''}` },
)
const [tab, setTab] = useState<OptimizeTab>('waste')
if (!overview.data) {
if (overview.error) return <CliErrorPanel error={overview.error} subject="optimize findings" />
return <SectionSkeleton label="Scanning optimize findings…" rows={5} />
}
const yieldData = yieldReport.error ? null : yieldReport.data
const revertedTotal = yieldData ? formatUsd(yieldData.summary.reverted.costUSD) : '—'
const abandonedTotal = yieldData ? formatUsd(yieldData.summary.abandoned.costUSD) : '—'
const options = [
{ value: 'waste', label: `Waste ${formatUsd(overview.data.optimize.savingsUSD)}` },
{ value: 'reverts', label: `Reverts ${revertedTotal}` },
{ value: 'abandoned', label: `Abandoned ${abandonedTotal}` },
// The Fixes tab renders topFindings (capped list), so label the count that shows.
{ value: 'fixes', label: `Fixes ${overview.data.optimize.topFindings.length.toLocaleString('en-US')}` },
]
return (
<>
{overview.error && <StaleBanner error={overview.error} />}
<SegTabs
options={options}
value={tab}
onChange={value => setTab(value as OptimizeTab)}
style={{ alignSelf: 'flex-start' }}
/>
<Panel>
{tab === 'waste' ? (
<WasteRows report={optimizeReport} />
) : tab === 'reverts' ? (
<YieldRows report={yieldReport} category="reverted" empty="No reverted sessions in this range yet." />
) : tab === 'abandoned' ? (
<YieldRows report={yieldReport} category="abandoned" empty="No abandoned sessions in this range yet." />
) : (
<FixesRows data={overview.data} />
)}
</Panel>
</>
)
}
function WasteRows({ report }: { report: Polled<OptimizeJsonReport> }) {
if (!report.data) {
if (report.error) return <CliErrorPanel error={report.error} subject="optimize findings" />
return <EmptyNote>Scanning optimize findings</EmptyNote>
}
return (
<div className="opt-waste">
<div className="opt-summary">
{report.data.summary.findingCount.toLocaleString('en-US')} findings · {formatUsd(report.data.summary.potentialSavingsCostUSD)} potential · health {report.data.summary.healthScore}/100
</div>
<ActionableFindingRows findings={report.data.findings} />
</div>
)
}
type OptimizeFinding = OptimizeJsonReport['findings'][number]
const IMPACT_ICON: Record<'high' | 'medium' | 'low', string> = {
high: '↑',
medium: '→',
low: '↓',
}
function actionText(fix: WasteAction): string {
return fix.type === 'file-content' ? fix.content : fix.text
}
function ActionableFindingRows({ findings }: { findings: OptimizeFinding[] }) {
const [expandedId, setExpandedId] = useState<string | null>(null)
const [copiedId, setCopiedId] = useState<string | null>(null)
if (!findings.length) return <EmptyNote>No waste findings in this range yet.</EmptyNote>
const copyFix = async (finding: OptimizeFinding) => {
await navigator.clipboard.writeText(actionText(finding.fix))
setCopiedId(finding.id)
window.setTimeout(() => setCopiedId(current => current === finding.id ? null : current), 1_500)
}
return (
<div className="opt-findings">
{findings.map(finding => {
const expanded = expandedId === finding.id
return (
<Fragment key={finding.id}>
<button
className="opt-finding opt-finding-toggle"
type="button"
aria-expanded={expanded}
onClick={() => setExpandedId(current => current === finding.id ? null : finding.id)}
>
<span className={`opt-impact opt-impact-${finding.severity}`}>
<span aria-hidden="true">{IMPACT_ICON[finding.severity]}</span>
{finding.severity.charAt(0).toUpperCase() + finding.severity.slice(1)}
</span>
<span className="opt-finding-titlewrap">
<b className="opt-finding-title">{finding.title}</b>
{finding.trend === 'improving' && (
<span className="opt-trend opt-trend-improving">improving<span aria-hidden="true"> </span></span>
)}
</span>
<span className="opt-finding-savings">{formatUsd(finding.estimatedSavingsUSD)}</span>
<span className="opt-finding-tokens">{formatCompact(finding.tokensSaved)} tokens</span>
<span className="opt-finding-chevron" aria-hidden="true"></span>
</button>
{expanded && (
<div className="opt-finding-detail" role="region" aria-label={`${finding.title} details`}>
<p className="opt-explanation">{finding.explanation}</p>
<div className={`opt-fix opt-fix-${finding.fix.type}`}>
<div className="opt-fix-head">
<div>
<b>{finding.fix.label}</b>
{finding.fix.type === 'file-content' && <span className="opt-fix-path">{finding.fix.path}</span>}
</div>
<button className="opt-copy" type="button" onClick={() => void copyFix(finding)}>
{copiedId === finding.id ? 'Copied' : 'Copy'}
</button>
</div>
<pre className="opt-fix-code"><code>{actionText(finding.fix)}</code></pre>
</div>
</div>
)}
</Fragment>
)
})}
</div>
)
}
type Finding = MenubarPayload['optimize']['topFindings'][number]
function FindingRows({ findings, empty }: { findings: Finding[]; empty: string }) {
if (!findings.length) return <EmptyNote>{empty}</EmptyNote>
return (
<div className="opt-findings">
{findings.map((finding, i) => (
<div className="opt-finding opt-finding-legacy" key={`${finding.title}-${i}`}>
<span className="opt-finding-rank">{String(i + 1).padStart(2, '0')}</span>
<b className="opt-finding-title">{finding.title}</b>
<span className={`opt-impact opt-impact-${finding.impact}`}>
<span aria-hidden="true">{IMPACT_ICON[finding.impact]}</span>
{finding.impact.charAt(0).toUpperCase() + finding.impact.slice(1)}
</span>
<span className="opt-finding-savings">{formatUsd(finding.savingsUSD)}</span>
</div>
))}
</div>
)
}
function YieldRows({
report,
category,
empty,
}: {
report: Polled<YieldJsonReport>
category: SessionYieldJson['category']
empty: string
}) {
if (report.error || !report.data) return <EmptyNote>Yield data is unavailable right now.</EmptyNote>
const rows = report.data.details.filter(row => row.category === category)
if (!rows.length) return <EmptyNote>{empty}</EmptyNote>
return (
<>
{rows.map((row, i) => (
<div className="li" style={{ alignItems: 'flex-start' }} key={row.sessionId}>
<span className="no">{String(i + 1).padStart(2, '0')}</span>
<div className="lx">
<b>{row.project}</b>
<span>
{row.commitCount.toLocaleString('en-US')} {row.commitCount === 1 ? 'commit' : 'commits'} · {row.sessionId}
</span>
</div>
<span className="val">{formatUsd(row.costUSD)}</span>
</div>
))}
</>
)
}
function FixesRows({ data }: { data: MenubarPayload }) {
return <FindingRows findings={data.optimize.topFindings} empty="No fixes in this range yet." />
}