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 tests | Co-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)
| File | Lines | Role |
|---|---|---|
src/App.tsx | ~613 | Root layout, panel resize, drag-drop, modal orchestration |
src/stores/RootStore.ts | ~96 | Composes all stores, provides React context + hooks |
src/stores/AnalysisStore.ts | ~200 | Analysis results, issue/recommendation selection, reanalyze |
src/stores/LogStore.ts | ~130 | Parsed frames, metadata, worker communication |
src/stores/UIStore.ts | ~294 | Axis, zoom, panel state, modals, mobile layout, toasts |
src/stores/SettingsStore.ts | ~200 | Imported Betaflight settings, pending/accepted values |
src/stores/SerialStore.ts | ~200 | USB serial connection, read/write to FC |
src/stores/FlashDownloadStore.ts | ~160 | Download logs from FC flash memory |
src/domain/engine/RuleEngine.ts | ~500 | Orchestrates analysis: segment → detect → dedup → recommend |
src/domain/types/Analysis.ts | ~245 | DetectedIssue, Recommendation, ParameterChange, BetaflightParameter |
src/domain/types/LogFrame.ts | ~167 | LogFrame, LogMetadata, AxisData interfaces |
src/domain/types/TuningRule.ts | ~60 | TuningRule interface (condition, detect, recommend) |
src/components/RecommendationsPanel.tsx | ~1429 | Tuning recommendations UI (tabs, CLI export, accept tune) |
src/components/LogChart.tsx | ~385 | Recharts line chart with issue markers |
src/components/logChart/useChartData.ts | ~100 | Downsample frames → chart data points |
src/components/logChart/useChartInteractions.ts | ~330 | Mouse drag/click handlers, issue selection on chart click |
src/components/logChart/useIssueLabels.ts | ~135 | Compute label positions, stacking, severity sorting |
src/components/logChart/useIssuePopover.ts | ~245 | Hover/forced popover HTML, glow effect on selection |
src/components/LogChart.styles.ts | ~230 | Styled components for chart, labels, popover, overlays |
src/components/FileUpload.tsx | ~770 | Drag-drop upload, parse progress, metadata display |
src/workers/logParser.worker.ts | ~400 | Web Worker: parses .bbl/.bfl/.txt/.csv in background |
src/domain/utils/CliExport.ts | ~384 | Map recommendations → Betaflight CLI set commands |
src/domain/utils/FrequencyAnalysis.ts | ~299 | Cooley-Tukey FFT, RMS, band energy |
src/domain/utils/SignalAnalysis.ts | ~391 | Bounceback detection, settling time analysis |
src/lib/betaflight/cliOptions.ts | ~1000 | Betaflight 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/): MobXmakeAutoObservable. 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 viapostMessage.
How to Implement Common Tasks
New Tuning Rule
- Create
src/domain/rules/YourRule.tsconforming toTuningRuleinterface: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[] */ }, } - Add issue type to
IssueTypeunion insrc/domain/types/Analysis.ts - Register rule in
src/domain/engine/RuleEngine.tsconstructor:this.registerRule(YourRule) - Add chart description in
src/domain/utils/issueChartDescriptions.ts - If new parameters: add to
BetaflightParameterunion inAnalysis.ts, map inCliExport.ts - 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:
- Add field:
myStore: MyStore - Instantiate in constructor:
this.myStore = new MyStore() - Add hook:
export function useMyStore() { return useStores().myStore } - Add to
reset()if applicable
New Betaflight Parameter
- Add to
BetaflightParameterunion insrc/domain/types/Analysis.ts - Add CLI mapping in
src/domain/utils/CliExport.ts:- Per-axis →
PER_AXIS_PARAMSmap - Global →
GLOBAL_PARAM_MAP
- Per-axis →
- Add display name in
PARAMETER_DISPLAY_NAMES - Add CLI option metadata in
src/lib/betaflight/cliOptions.ts - Add value lookup in
getPidValue()orgetGlobalValue()
New Theme Color
- Add to
ThemeColorsinterface insrc/theme/types.ts - Add values in
src/theme/lightTheme.tsandsrc/theme/darkTheme.ts - Use in styled components:
${p => p.theme.colors.your.new.color}
New Modal
- Add boolean toggle to
UIStore:yourModalOpen = false - Add open/close actions in UIStore
- Create component in
src/components/YourModal.tsx - Render conditionally in
src/App.tsxbased onuiStore.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
| Rule | File | Issue Type | Detects |
|---|---|---|---|
| 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 parsersrc/domain/engine/RuleEngine.test.ts— Deduplication, segmentationsrc/domain/engine/RuleEngineProfiles.test.ts— Profile thresholdssrc/domain/rules/TrackingQualityRule.test.ts— Specific rulesrc/domain/utils/CliExport.test.ts— CLI generationsrc/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-testidattributes - No mocking: Real file uploads, real parsing, real analysis
- Upload helper:
uploadAndAnalyze(page, filePath?)— uploads BFL and waits for analysis
Key test files:
| File | Tests |
|---|---|
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:
- build —
tsc && vite build - unit-tests —
pnpm test:unit - integration-tests — Playwright across 16 shards
- deploy — GitHub Pages (all 3 above must pass)
Key Deduplication Logic
Understanding this prevents confusion when working on analysis:
Issue Deduplication (RuleEngine)
- Temporal merge: Issues of same type+axis within 100ms gap are merged into one
- Collapse: Multiple occurrences become
occurrences[]array with count - Result: One
DetectedIssueper type+axis with occurrence count
Recommendation Deduplication (RuleEngine)
- Key:
parameter:axisfromchanges[]array (NOT title string) - Multiple rules recommending same parameter change are merged
- Conflicting changes use weighted merge based on confidence
Dependencies
| Package | Version | Role |
|---|---|---|
react | 18.2.0 | UI framework |
react-dom | 18.2.0 | DOM rendering |
mobx | 6.12.0 | Reactive state management |
mobx-react-lite | 4.0.5 | observer() HOC |
@emotion/react | 11.14.0 | CSS-in-JS (css prop) |
@emotion/styled | 11.14.1 | Styled components |
recharts | 2.10.3 | SVG chart library |
@playwright/test | 1.58.2 | E2E testing |
vitest | 4.0.18 | Unit testing |
vite | 5.0.8 | Build tool |
vite-plugin-pwa | 1.2.0 | PWA manifest + service worker |
typescript | 5.3.3 | Type checking |
Critical Constraints
| Constraint | Reason |
|---|---|
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 only | Theme-aware, dynamic dark/light mode |
| Files under 300 lines | Maintainability, 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 samples | Avoid slowdown on large logs |
| Web Worker for parsing | Prevent main thread blocking on large files |
| No backend | Everything 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:
| Location | What 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.isMobileLayoutobservable (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 wrappersrc/serial/MspProtocol.ts— MSP protocol (read FC state)src/serial/CliProtocol.ts— Enter/exit CLI, read/write settingssrc/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:changelogmodule - Injects:
entries[],buildDate(ISO),buildHash(git short hash) - Watches
src/data/changelog.tsfor HMR - Used by ChangelogModal for "What's New" feature
Panel Resize Pattern (Drag Performance)
When resizing panels adjacent to the chart (expensive Recharts component):
- Freeze inner chart wrapper at current pixel width on drag start
- During drag: only change panel width via direct DOM (no MobX, no React re-renders)
- On mouseup: clear inline styles, commit final width to MobX in single
runInAction - Result: zero ResizeObserver fires, zero React re-renders during drag
Quad Profiles
5 profiles in src/domain/profiles/quadProfiles.ts:
| Profile | Multiplier Range | Notes |
|---|---|---|
| Whoop (65-85mm) | 1.5-2.5x (relaxed) | More noise tolerance |
| 3" (micro) | 1.1-1.3x | Balanced |
| 5" (baseline) | 1.0x | All thresholds calibrated here |
| 7" (long-range) | 0.7-1.3x | Varies per issue |
| X-Class (10"+) | 0.6-1.5x | More propwash, less noise |
actualThreshold = baseThreshold * profile.thresholds[issueType]