.claude

Codebase Skill — Complete Operational Guide

install
source · Clone the upstream repo
git clone https://github.com/Ricardo-Marques/betaflight-tuning-helper
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/Ricardo-Marques/betaflight-tuning-helper "$T" && mkdir -p ~/.claude/skills && cp -r "$T/.claude" ~/.claude/skills/ricardo-marques-betaflight-tuning-helper-claude && rm -rf "$T"
manifest: .claude/codebase-skill.md
source content

Codebase Skill — Complete Operational Guide

KEEP THIS FILE UP TO DATE. When architectural decisions change, files move, new patterns are adopted, or conventions shift, update this file immediately. Stale guidance is worse than no guidance. After any structural change (new store, renamed file, new dependency, changed workflow), check if this file needs updating.


Quick Reference Commands

pnpm dev              # Dev server → localhost:5173
pnpm build            # tsc && vite build → dist/
pnpm lint             # ESLint (--max-warnings 0, zero tolerance)
pnpm test             # Playwright E2E (full suite, ~10 min)
pnpm test:headed      # E2E with visible browser
pnpm test:ui          # E2E Playwright UI debug mode
pnpm test:unit        # Vitest unit tests (src/**/*.test.ts)
npx tsx scripts/take-screenshots.ts      # All 8 showcase screenshots
npx tsx scripts/take-screenshots.ts 2    # Single screenshot by index
npx tsx scripts/take-screenshots.ts composite  # Re-composite from existing raws

File Map — Where to Find Everything

By Purpose

I need to...Look here
Add/change a React component
src/components/
Add/change chart logic
src/components/LogChart.tsx
+
src/components/logChart/
Add/change chart styles
src/components/LogChart.styles.ts
Add a new tuning rule
src/domain/rules/
(create new file, register in RuleEngine)
Change how analysis works
src/domain/engine/RuleEngine.ts
Add/change a Betaflight parameter
src/domain/types/Analysis.ts
(BetaflightParameter type)
Map parameter to CLI command
src/domain/utils/CliExport.ts
Add CLI option metadata/ranges
src/lib/betaflight/cliOptions.ts
Change how .bbl/.bfl files parse
src/domain/blackbox/
Change how .txt/.csv files parse
src/workers/logParser.worker.ts
Add/change a MobX store
src/stores/
Wire a new store into the app
src/stores/RootStore.ts
Add/change theme colors
src/theme/lightTheme.ts
,
src/theme/darkTheme.ts
Add/change theme type
src/theme/types.ts
(ThemeColors interface)
Add global CSS animations
src/theme/GlobalStyles.tsx
Add/change MobX reactive primitives
src/lib/mobx-reactivity/
Add/change serial (USB) communication
src/serial/
Add/change quad profile thresholds
src/domain/profiles/quadProfiles.ts
Add/change issue descriptions on chart
src/domain/utils/issueChartDescriptions.ts
Update changelog
src/data/changelog.ts
Add/change glossary terms
src/data/glossary.ts
Change build/deploy
vite.config.ts
,
.github/workflows/deploy.yml
Change virtual:changelog module
vite-plugins/changelogPlugin.ts
Add/change E2E tests
e2e/
(specs + helpers)
Add/change unit testsCo-located with source:
src/**/*.test.ts
Change Playwright config
playwright.config.ts
Change ESLint rules
eslint.config.js
Check TS config
tsconfig.json
(
useDefineForClassFields: false
)

Key Files (Sorted by Importance)

FileLinesRole
src/App.tsx
~613Root layout, panel resize, drag-drop, modal orchestration
src/stores/RootStore.ts
~96Composes all stores, provides React context + hooks
src/stores/AnalysisStore.ts
~200Analysis results, issue/recommendation selection, reanalyze
src/stores/LogStore.ts
~130Parsed frames, metadata, worker communication
src/stores/UIStore.ts
~294Axis, zoom, panel state, modals, mobile layout, toasts
src/stores/SettingsStore.ts
~200Imported Betaflight settings, pending/accepted values
src/stores/SerialStore.ts
~200USB serial connection, read/write to FC
src/stores/FlashDownloadStore.ts
~160Download logs from FC flash memory
src/domain/engine/RuleEngine.ts
~500Orchestrates analysis: segment → detect → dedup → recommend
src/domain/types/Analysis.ts
~245DetectedIssue, Recommendation, ParameterChange, BetaflightParameter
src/domain/types/LogFrame.ts
~167LogFrame, LogMetadata, AxisData interfaces
src/domain/types/TuningRule.ts
~60TuningRule interface (condition, detect, recommend)
src/components/RecommendationsPanel.tsx
~1429Tuning recommendations UI (tabs, CLI export, accept tune)
src/components/LogChart.tsx
~385Recharts line chart with issue markers
src/components/logChart/useChartData.ts
~100Downsample frames → chart data points
src/components/logChart/useChartInteractions.ts
~330Mouse drag/click handlers, issue selection on chart click
src/components/logChart/useIssueLabels.ts
~135Compute label positions, stacking, severity sorting
src/components/logChart/useIssuePopover.ts
~245Hover/forced popover HTML, glow effect on selection
src/components/LogChart.styles.ts
~230Styled components for chart, labels, popover, overlays
src/components/FileUpload.tsx
~770Drag-drop upload, parse progress, metadata display
src/workers/logParser.worker.ts
~400Web Worker: parses .bbl/.bfl/.txt/.csv in background
src/domain/utils/CliExport.ts
~384Map recommendations → Betaflight CLI
set
commands
src/domain/utils/FrequencyAnalysis.ts
~299Cooley-Tukey FFT, RMS, band energy
src/domain/utils/SignalAnalysis.ts
~391Bounceback detection, settling time analysis
src/lib/betaflight/cliOptions.ts
~1000Betaflight 4.5 CLI param definitions, ranges, enums

Architecture at a Glance

User uploads .bbl/.bfl/.txt/.csv file
    ↓
LogStore.uploadFile() → spawns Web Worker
    ↓
Worker parses binary/text → postMessage({ type: 'complete', frames, metadata })
    ↓
LogStore sets frames + metadata → triggers AnalysisStore.analyze()
    ↓
RuleEngine.analyzeLog():
  1. Segment log into 100ms windows (50% overlap)
  2. Classify flight phase per window (idle/hover/cruise/punch/propwash/flip/roll)
  3. Run each rule: condition() → detect() → recommend()
  4. Temporal dedup issues (100ms gap merge, then collapse by type+axis)
  5. Cross-axis correlation (annotate patterns, generate hardware recs)
  6. Frequency issue merge (collapse same-freq frameResonance/bearingNoise across axes)
  7. Temporal progression analysis (annotate trends, generate meta-issues)
  8. Generate recommendations (settings-aware, with currentValue populated)
  9. Dedup recommendations (key on parameter:axis, not title)
  10. Generate summary + flight segments
    ↓
AnalysisStore.result populated → observer() components re-render
    ↓
LogChart shows traces + issue markers
RecommendationsPanel shows issues + recommendations + CLI export

Layer Rules

  • Domain (
    src/domain/
    ): Pure TypeScript. NO React imports, NO MobX imports. Testable in isolation.
  • Stores (
    src/stores/
    ): MobX
    makeAutoObservable
    . Business logic orchestration. Can reference domain layer.
  • Components (
    src/components/
    ): React +
    observer()
    . Access stores via hooks. Emotion styled components.
  • Workers (
    src/workers/
    ): Background threads. Can import domain layer. Communicate via
    postMessage
    .

How to Implement Common Tasks

New Tuning Rule

  1. Create
    src/domain/rules/YourRule.ts
    conforming to
    TuningRule
    interface:
    export const YourRule: TuningRule = {
      id: 'your-rule',
      name: 'Your Rule',
      description: '...',
      baseConfidence: 0.7,
      issueTypes: ['yourIssueType'],
      applicableAxes: ['roll', 'pitch', 'yaw'],
      condition(window, frames) { return /* should this window be checked? */ },
      detect(window, frames, profile) { return /* DetectedIssue[] */ },
      recommend(issues, frames, profile) { return /* Recommendation[] */ },
    }
    
  2. Add issue type to
    IssueType
    union in
    src/domain/types/Analysis.ts
  3. Register rule in
    src/domain/engine/RuleEngine.ts
    constructor:
    this.registerRule(YourRule)
  4. Add chart description in
    src/domain/utils/issueChartDescriptions.ts
  5. If new parameters: add to
    BetaflightParameter
    union in
    Analysis.ts
    , map in
    CliExport.ts
  6. Add thresholds per quad profile in
    src/domain/profiles/quadProfiles.ts

New React Component

import { observer } from 'mobx-react-lite'
import styled from '@emotion/styled'
import { useStores } from '../stores/RootStore'
import { useObservableState, useComputed, useAutorun } from '../lib/mobx-reactivity'

const Wrapper = styled.div`
  color: ${p => p.theme.colors.text.primary};
`

export const MyComponent = observer(() => {
  const { uiStore, analysisStore } = useStores()
  const [localState, setLocalState] = useObservableState(false)
  const derived = useComputed(() => analysisStore.issues.filter(i => i.severity === 'high'))

  useAutorun(() => {
    if (analysisStore.selectedIssue) {
      // side effect reacting to observable change
    }
  })

  return <Wrapper>...</Wrapper>
})

Rules:

  • ALWAYS wrap with
    observer()
  • NEVER use
    useState
    ,
    useEffect
    ,
    useMemo
    ,
    useCallback
  • ALWAYS use
    useObservableState
    ,
    useComputed
    ,
    useAutorun
  • ALWAYS use Emotion styled components (no inline styles, no CSS files, no Tailwind)
  • Keep under 300 lines — split by domain/concern

New MobX Store

import { makeAutoObservable, runInAction } from 'mobx'

export class MyStore {
  publicField: string = ''
  private privateField: SomeType | null = null

  constructor() {
    makeAutoObservable<this, 'privateField'>(this, {
      privateField: false,  // exclude from observation
    })
  }

  get computed(): string {
    return this.publicField.toUpperCase()
  }

  someAction = (value: string): void => {
    this.publicField = value
  }

  someAsyncAction = async (): Promise<void> => {
    const result = await fetch(...)
    runInAction(() => {
      this.publicField = result
    })
  }

  reset = (): void => {
    this.publicField = ''
  }
}

Then wire in

src/stores/RootStore.ts
:

  1. Add field:
    myStore: MyStore
  2. Instantiate in constructor:
    this.myStore = new MyStore()
  3. Add hook:
    export function useMyStore() { return useStores().myStore }
  4. Add to
    reset()
    if applicable

New Betaflight Parameter

  1. Add to
    BetaflightParameter
    union in
    src/domain/types/Analysis.ts
  2. Add CLI mapping in
    src/domain/utils/CliExport.ts
    :
    • Per-axis →
      PER_AXIS_PARAMS
      map
    • Global →
      GLOBAL_PARAM_MAP
  3. Add display name in
    PARAMETER_DISPLAY_NAMES
  4. Add CLI option metadata in
    src/lib/betaflight/cliOptions.ts
  5. Add value lookup in
    getPidValue()
    or
    getGlobalValue()

New Theme Color

  1. Add to
    ThemeColors
    interface in
    src/theme/types.ts
  2. Add values in
    src/theme/lightTheme.ts
    and
    src/theme/darkTheme.ts
  3. Use in styled components:
    ${p => p.theme.colors.your.new.color}

New Modal

  1. Add boolean toggle to
    UIStore
    :
    yourModalOpen = false
  2. Add open/close actions in UIStore
  3. Create component in
    src/components/YourModal.tsx
  4. Render conditionally in
    src/App.tsx
    based on
    uiStore.yourModalOpen

Store Access Hooks

useStores()              // Full RootStore (all stores)
useLogStore()            // Parsed frames, metadata, parse status
useAnalysisStore()       // Analysis results, issues, recommendations, selection
useUIStore()             // Axis, zoom, panel state, modals, toasts
useThemeStore()          // Dark/light mode, theme object
useSettingsStore()       // Imported Betaflight settings
useSerialStore()         // USB serial connection state
useFlashDownloadStore()  // Flash download progress

8 Registered Tuning Rules

RuleFileIssue TypeDetects
BouncebackRule
src/domain/rules/BouncebackRule.ts
bounceback
Overshoot after stick release
PropwashRule
src/domain/rules/PropwashRule.ts
propwash
Low-throttle descent oscillation
WobbleRule
src/domain/rules/WobbleRule.ts
midThrottleWobble
Mid-throttle cruise wobble
TrackingQualityRule
src/domain/rules/TrackingQualityRule.ts
lowFrequencyOscillation
Gyro-setpoint tracking error
MotorSaturationRule
src/domain/rules/MotorSaturationRule.ts
motorSaturation
Motors hitting max output
DTermNoiseRule
src/domain/rules/DTermNoiseRule.ts
dtermNoise
D-term amplifying noise
HighThrottleOscillationRule
src/domain/rules/HighThrottleOscillationRule.ts
highThrottleOscillation
High-throttle vibration
GyroNoiseRule
src/domain/rules/GyroNoiseRule.ts
gyroNoise
Gyro noise floor elevation

Testing Guide

Unit Tests (Vitest)

  • Co-located with source:
    src/**/*.test.ts
  • Run:
    pnpm test:unit
  • Key test files:
    • src/domain/blackbox/BblParser.test.ts
      — Binary parser
    • src/domain/engine/RuleEngine.test.ts
      — Deduplication, segmentation
    • src/domain/engine/RuleEngineProfiles.test.ts
      — Profile thresholds
    • src/domain/rules/TrackingQualityRule.test.ts
      — Specific rule
    • src/domain/utils/CliExport.test.ts
      — CLI generation
    • src/components/logChart/useIssueLabels.test.ts
      — Label collision

E2E Tests (Playwright)

  • Located in
    e2e/*.spec.ts
  • Helpers:
    e2e/helpers.ts
    ,
    e2e/data-verification-helpers.ts
  • Sample log:
    test-logs/shortLog.BFL
  • Run:
    pnpm test
    (full),
    pnpm test:headed
    (visible browser)
  • Config:
    playwright.config.ts
    — Chromium only, 1920x1080, 60s timeout
  • Selectors: Use
    data-testid
    attributes
  • No mocking: Real file uploads, real parsing, real analysis
  • Upload helper:
    uploadAndAnalyze(page, filePath?)
    — uploads BFL and waits for analysis

Key test files:

FileTests
file-upload.spec.ts
Drag-drop, file selection, parse progress
analysis-summary.spec.ts
Summary panel, issue counts
analysis-details.spec.ts
Issue details, severity, metrics
chart-rendering.spec.ts
Lines, grid, tooltips
chart-interaction.spec.ts
Zoom, pan, scroll
issue-*.spec.ts
Issue detection, stacking, popover
navigation-*.spec.ts
Panel resize, axis switch, segments
settings-review.spec.ts
Import/export settings
data-verification-*.spec.ts
Correct issues/recommendations detected

When to run tests:

  • After changing domain logic → run unit tests + targeted E2E
  • After changing UI → run targeted E2E spec
  • Don't re-run ALL tests unless there's a strong reason they might fail

Commit Workflow

Writing Commit Messages

  • Plain language for non-technical users
  • One concern per commit — split unrelated changes
  • Good: "Show build time in user's timezone on What's New modal"
  • Bad: "Emit full ISO datetime from changelogPlugin and add formatBuildDate"

After Committing

If the change is user-facing, add an entry to

src/data/changelog.ts
:

{
  hash: 'abc1234',      // git short hash
  date: '2026-02-19',   // ISO date
  message: 'What the user sees changed',
  category: 'feature' | 'improvement' | 'fix',
}

CI/CD Pipeline

Push to

main
triggers
.github/workflows/deploy.yml
:

  1. build
    tsc && vite build
  2. unit-tests
    pnpm test:unit
  3. integration-tests — Playwright across 16 shards
  4. deploy — GitHub Pages (all 3 above must pass)

Key Deduplication Logic

Understanding this prevents confusion when working on analysis:

Issue Deduplication (RuleEngine)

  1. Temporal merge: Issues of same type+axis within 100ms gap are merged into one
  2. Collapse: Multiple occurrences become
    occurrences[]
    array with count
  3. Result: One
    DetectedIssue
    per type+axis with occurrence count

Recommendation Deduplication (RuleEngine)

  1. Key:
    parameter:axis
    from
    changes[]
    array (NOT title string)
  2. Multiple rules recommending same parameter change are merged
  3. Conflicting changes use weighted merge based on confidence

Dependencies

PackageVersionRole
react
18.2.0UI framework
react-dom
18.2.0DOM rendering
mobx
6.12.0Reactive state management
mobx-react-lite
4.0.5
observer()
HOC
@emotion/react
11.14.0CSS-in-JS (css prop)
@emotion/styled
11.14.1Styled components
recharts
2.10.3SVG chart library
@playwright/test
1.58.2E2E testing
vitest
4.0.18Unit testing
vite
5.0.8Build tool
vite-plugin-pwa
1.2.0PWA manifest + service worker
typescript
5.3.3Type checking

Critical Constraints

ConstraintReason
useDefineForClassFields: false
in tsconfig
MobX
makeAutoObservable
needs getter/setter pattern
No React hooks (useState, useEffect, etc.)MobX reactive primitives replace them
All components wrapped in
observer()
MobX reactivity requires it
Emotion styled components onlyTheme-aware, dynamic dark/light mode
Files under 300 linesMaintainability, split by domain
No
any
in domain layer
Type safety, ESLint warning-level
Chart adaptive downsampling (300–2500 pts)Progressive formula + FPS feedback loop in
useChartData.ts
FFT capped at 2048 samplesAvoid slowdown on large logs
Web Worker for parsingPrevent main thread blocking on large files
No backendEverything client-side, works offline (PWA)

Zoom System

Zoom is percentage-based (0–100%).

UIStore.zoomStart
/
zoomEnd
define the visible window.

Minimum zoom window is enforced in 3 places — all must agree:

LocationWhat it controls
UIStore.setZoom()
Safety floor when start ≥ end (0.01% absolute minimum)
useChartInteractions.ts
(wheel handler)
Scroll-to-zoom on the chart area
RangeSlider.tsx
(minWindow prop)
Handle drag + scroll-to-zoom on the slider

The minimum is dynamic based on log duration so that full zoom always shows a 0.2s window:

const minZoomPct = (0.2 / totalDuration) * 100

LogChart.tsx
computes this and passes it to
RangeSlider
via the
minWindow
prop. The chart scroll handler in
useChartInteractions
computes the same value from
logStore.duration
.


Mobile Layout

  • Breakpoint:
    max-width: 1599px
    → mobile layout
  • UIStore.isMobileLayout
    observable (media query listener)
  • Mobile: 3-tab bottom bar (Upload / Chart / Tune) via
    BottomTabBar
  • Desktop: 3-panel layout (left / chart / right) with drag-to-resize
  • Touch targets: minimum 36x36px, 18px font on
    pointer: coarse
  • No serial options on mobile (WebSerial is desktop Chrome/Edge only)

Serial Communication (USB)

  • src/serial/SerialPort.ts
    — WebSerial API wrapper
  • src/serial/MspProtocol.ts
    — MSP protocol (read FC state)
  • src/serial/CliProtocol.ts
    — Enter/exit CLI, read/write settings
  • src/serial/DataflashReader.ts
    — Download blackbox from FC flash memory
  • Chrome/Edge only (WebSerial API)
  • Flow: connect → enter CLI → dump settings → parse → show in SettingsImportModal

Vite Plugins

changelogPlugin (
vite-plugins/changelogPlugin.ts
)

  • Provides
    virtual:changelog
    module
  • Injects:
    entries[]
    ,
    buildDate
    (ISO),
    buildHash
    (git short hash)
  • Watches
    src/data/changelog.ts
    for HMR
  • Used by ChangelogModal for "What's New" feature

Panel Resize Pattern (Drag Performance)

When resizing panels adjacent to the chart (expensive Recharts component):

  1. Freeze inner chart wrapper at current pixel width on drag start
  2. During drag: only change panel width via direct DOM (no MobX, no React re-renders)
  3. On mouseup: clear inline styles, commit final width to MobX in single
    runInAction
  4. Result: zero ResizeObserver fires, zero React re-renders during drag

Quad Profiles

5 profiles in

src/domain/profiles/quadProfiles.ts
:

ProfileMultiplier RangeNotes
Whoop (65-85mm)1.5-2.5x (relaxed)More noise tolerance
3" (micro)1.1-1.3xBalanced
5" (baseline)1.0xAll thresholds calibrated here
7" (long-range)0.7-1.3xVaries per issue
X-Class (10"+)0.6-1.5xMore propwash, less noise

actualThreshold = baseThreshold * profile.thresholds[issueType]