codeburn/app/renderer/components/Onboarding.tsx
iamtoruk ae18b1fd91 fix: audit batch — prefetch one-shot, turn-anchored day bucketing, warm-path collapse
- prefetch fires exactly once per provider per session; memo sized to
  detected providers so base keys never evict (was: 12 full-history
  parses every 30s forever from LRU thrash re-triggering the effect)
- day-aggregator buckets whole turns by user-message timestamp,
  matching the report/headline path: trend bars and provider breakdown
  now reconcile exactly (midnight-straddling turns were split);
  DAILY_CACHE_VERSION 13 with an IANA tz guard that rehydrates when
  the cache crosses timezones
- overview's redundant second all-provider scan collapsed (~11% faster
  warm); numbers parity-guarded
- lock takeover cleans stale hydrating.lock; SIGINT/SIGTERM release it
- scanner skips EPERM/EACCES provider dirs (progress shows skipped)
  instead of aborting; onboarding hints Full Disk Access
- rateLimited quota copy render tests; export -f json emits valid
  empty JSON when no data

Root 2023 (+4), app 327 (+3), typechecks clean.
2026-07-16 15:37:03 -07:00

110 lines
4.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 { useState } from 'react'
import { createPortal } from 'react-dom'
import { FlameMark } from './FlameMark'
import { codeburn } from '../lib/ipc'
import { motionClass } from '../lib/motion'
const COLLECT_URL = 'https://www.codeburn.app/telemetry'
type Screen = {
title: string
body: string
glyph: React.ReactNode
}
const SCREENS: Screen[] = [
{
title: 'Every agent. One dashboard.',
body: 'Claude Code, Codex, Cursor, Copilot and 20+ more: spend, sessions, models and quotas, side by side.',
glyph: <><rect x="3" y="4" width="18" height="14" rx="2" /><path d="M7 14v-3M11 14V8M15 14v-5M3 18h18" /></>,
},
{
title: 'Local-first by design.',
body: 'Usage is read from files already on your machine. No accounts, no API keys, nothing leaves your device.',
glyph: <><rect x="4.5" y="10" width="15" height="11" rx="2" /><path d="M8 10V7a4 4 0 0 1 8 0v3" /></>,
},
{
title: 'Find the waste.',
body: 'Retry tax, routing waste and task success by category: see what your agents actually deliver for the money.',
glyph: <><circle cx="12" cy="12" r="9" /><path d="M12 7v5l3.5 2" /></>,
},
]
/**
* First-launch overlay: three feature screens, then the telemetry consent
* screen. The toggle's initial position comes from the region default (EU/EEA/
* UK/CH off, elsewhere on) and nothing is transmitted until the user finishes
* here. Rendered only while the main process reports `onboarded: false`.
*/
export function Onboarding({ defaultEnabled, onDone }: { defaultEnabled: boolean; onDone: (enabled: boolean) => void }) {
const [step, setStep] = useState(0)
const [enabled, setEnabled] = useState(defaultEnabled)
const last = SCREENS.length // consent screen index
const isConsent = step === last
if (typeof document === 'undefined') return null
return createPortal(
<div className={motionClass('onboard', 'onboard-in')} role="dialog" aria-label="Welcome to CodeBurn">
<div className="onboard-card">
<div className="onboard-glyph" aria-hidden>
{isConsent
? <FlameMark size={40} />
: <svg viewBox="0 0 24 24" width="40" height="40" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">{SCREENS[step].glyph}</svg>}
</div>
{isConsent ? (
<>
<h2 className="onboard-title">Help improve CodeBurn</h2>
<p className="onboard-body">
Share anonymous usage statistics: model and provider mix, task success rates, performance and errors.
Never your prompts, code, project names or anything that identifies you.
</p>
<div className="onboard-consent">
<span id="onboard-consent-label">Anonymous telemetry</span>
<button
type="button"
role="switch"
aria-checked={enabled}
aria-labelledby="onboard-consent-label"
className={enabled ? 'switch on' : 'switch'}
onClick={() => setEnabled(value => !value)}
>
<span className="switch-knob" />
</button>
</div>
<button type="button" className="onboard-link" onClick={() => { void codeburn.openExternal?.(COLLECT_URL) }}>
What data we collect
</button>
<p className="onboard-hint">
Tip: if a provider looks empty, grant Full Disk Access in System Settings Privacy &amp; Security.
</p>
</>
) : (
<>
<h2 className="onboard-title">{SCREENS[step].title}</h2>
<p className="onboard-body">{SCREENS[step].body}</p>
</>
)}
<div className="onboard-controls">
{step > 0 ? (
<button type="button" className="onboard-btn" onClick={() => setStep(value => value - 1)}>Back</button>
) : <span className="onboard-btn-ghost" />}
<div className="onboard-dots" aria-hidden>
{[...SCREENS, null].map((_, index) => (
<span key={index} className={index === step ? 'onboard-dot on' : 'onboard-dot'} />
))}
</div>
{isConsent ? (
<button type="button" className="onboard-btn primary" onClick={() => onDone(enabled)}>Get started</button>
) : (
<button type="button" className="onboard-btn primary" onClick={() => setStep(value => value + 1)}>Next</button>
)}
</div>
</div>
</div>,
document.body,
)
}