

Also from Kynth Studios


Also from Kynth Studios


Also from Kynth Studios
1# Frontend Development Guide - Svelte 523## Tech Stack45- **Svelte 5** with Runes (`$state`, `$derived`, `$effect`)6- **TypeScript** - NO `any` types without justification7- **Tailwind v4.1** (native CSS only, no component libraries)8- **Vite** build, **Vitest** testing9- **i18n** - Custom implementation in `@i18n`1011## Critical Rules1213- **NEVER use `any` type**14- **NEVER create inline SVGs** - use `@lucide/svelte` icons15- **NEVER use `toISOString()` for dates** - use `getLocalDateString()`16- **NEVER use Secure Context APIs without fallback** - BirdNET-Go commonly runs on plain HTTP in home networks. `crypto.randomUUID()` and `navigator.clipboard` are undefined on non-HTTPS. Use `Math.random().toString(36).slice(2, 10)` for unique IDs. For clipboard, check `navigator.clipboard?.writeText` and fall back to textarea + `document.execCommand('copy')`17- **NEVER ship ambiguous UI states** - disabled controls, errors, and loading states must always explain _why_ to the user. A disabled Save button with no tooltip is a support ticket waiting to be filed. See [UX Design Principles](#ux-design-principles) below.18- **Use D3.js for ALL charting/plotting** - unless specific requirement for custom approach19- **Run `npm run check:all` before EVERY commit**2021## Structure2223```text24frontend/25├── src/lib/26│ ├── components/{charts,data,forms,media,ui}/27│ ├── features/{analytics,detections,settings}/28│ ├── i18n/ # i18n configuration and utilities29│ ├── pages/30│ ├── stores/31│ └── utils/32├── static/messages/ # Translation files (15 locales)33│ ├── en.json # English (primary)34│ ├── cs.json # Czech35│ ├── da.json # Danish36│ ├── de.json # German37│ ├── es.json # Spanish38│ ├── fi.json # Finnish39│ ├── fr.json # French40│ ├── hu.json # Hungarian41│ ├── it.json # Italian42│ ├── lv.json # Latvian43│ ├── nl.json # Dutch44│ ├── pl.json # Polish45│ ├── pt.json # Portuguese46│ ├── sk.json # Slovak47│ └── sv.json # Swedish48└── dist/49```5051## Internationalization (i18n)5253### Translation Files Location5455All translation files are in `frontend/static/messages/`:5657```bash58frontend/static/messages/59├── en.json # English (primary - update this first)60├── cs.json # Czech61├── da.json # Danish62├── de.json # German63├── es.json # Spanish64├── fi.json # Finnish65├── fr.json # French66├── hu.json # Hungarian67├── it.json # Italian68├── lv.json # Latvian69├── nl.json # Dutch70├── pl.json # Polish71├── pt.json # Portuguese72├── sk.json # Slovak73└── sv.json # Swedish74```7576### Adding New Translation Keys7778**CRITICAL: When adding new translation keys, you MUST update ALL language files.**79801. Add the key to `en.json` first (English is the source of truth)812. Run `npm run i18n:sync` to propagate new keys to all locale files823. Translate the new keys in each locale file (sync fills English as fallback)834. Run `npm run generate:i18n-types` to regenerate `src/lib/i18n/types.generated.ts` and commit it8485> The generated TypeScript types (`types.generated.ts`) are committed and verified in CI.86> If you edit `en.json` without regenerating them, the `generate:i18n-types:check` step87> (run in CI and in the pre-commit hook) fails. `npm run i18n:validate:full` runs the same check locally.8889```bash90# Quick check for missing keys91grep -l "newKeyName" frontend/static/messages/*.json92```9394### Usage in Components9596```svelte97<script lang="ts">98 import { t } from '$lib/i18n';99</script>100101<p>{t('about.avicommonsTitle')}</p><p>{t('about.avicommonsDescription')}</p>102```103104### Key Naming Convention105106- Use dot notation for nested keys: `section.subsection.key`107- Use camelCase for key names: `avicommonsDescription`108- Group related keys under common prefixes: `about.`, `settings.`, `notifications.`109110## Commands111112### Task Commands (from root directory)113114| Command | Purpose | When |115| ----------------------------- | -------------------------------------------- | ------------- |116| `task frontend-install` | Install frontend dependencies | Setup |117| `task frontend-typecheck` | Run TypeScript type checking | Before PR |118| `task frontend-build` | Build frontend for production with typecheck | Before commit |119| `task frontend-dev` | Start frontend development server | Development |120| `task frontend-lint` | Run comprehensive checks (npm run check:all) | Before commit |121| `task frontend-lint-fix` | Auto-fix formatting, linting, and ast-grep | After changes |122| `task frontend-ast-fix` | Auto-fix ast-grep detected issues | After changes |123| `task frontend-test` | Run frontend tests | Before PR |124| `task frontend-test-coverage` | Run frontend tests with coverage | Weekly |125| `task frontend-quality` | Run comprehensive quality checks + build | Before PR |126127## Svelte 5 Patterns128129### State Management130131```svelte132<script lang="ts">133 let count = $state(0); // Reactive state134 let double = $derived(count * 2); // Computed value135 let items = $state<Item[]>([]); // Typed arrays136137 $effect(() => {138 // Side effects139 console.log('Count changed:', count);140 });141</script>142```143144### Component Props145146```svelte147<script lang="ts">148 interface Props {149 title: string;150 count?: number;151 children?: Snippet;152 }153154 let { title, count = 0, children }: Props = $props();155</script>156```157158### Snippets (not slots)159160```svelte161<!-- Child -->162<script lang="ts">163 let { header }: { header?: Snippet } = $props();164</script>165166<!-- Parent -->167<Card>168 {#snippet header()}169 <h2>Title</h2>170 {/snippet}171</Card>172{#if header}{@render header()}{/if}173```174175## TypeScript Safety176177### ✅ REQUIRED178179```typescript180// Proper type checking181const value = map.get(key);182if (value !== undefined) {183 // Safe to use value184}185186// Iterator validation187const result = iterator.next();188if (!result.done && result.value !== undefined) {189 // Safe to use result.value190}191192// Nullish coalescing for defaults (preferred over logical OR)193const settings = {194 include: base.include ?? [], // Only null/undefined → []195 exclude: base.exclude ?? [], // Only null/undefined → []196 config: base.config ?? {}, // Only null/undefined → {}197};198199// Use logical OR only when you want to handle falsy values200const displayName = user.name || 'Anonymous'; // Handles "", null, undefined201```202203### ❌ FORBIDDEN204205```typescript206const value = map.get(key) as string; // Type assertion207const value = map.get(key)!; // Non-null assertion208let data: any; // Untyped209210// Avoid logical OR for object defaults (can cause issues with empty arrays/objects)211const config = base.config || {}; // ❌ Converts [] to {}, 0 to {}, etc.212```213214### Nullish Coalescing vs Logical OR215216```typescript217// ✅ Use ?? when you only want to handle null/undefined218const items = data.items ?? []; // Only null/undefined → []219const config = settings.config ?? {}; // Only null/undefined → {}220221// ✅ Use || when you want to handle all falsy values222const displayText = input || 'Default'; // "", 0, false, null, undefined → 'Default'223const isEnabled = flag || false; // Any falsy → false224225// Common mistake in settings derivation:226const bad = base.include || []; // ❌ Converts 0, "", false to []227const good = base.include ?? []; // ✅ Only null/undefined to []228229// Array validation example:230const items = Array.isArray(data.items) ? data.items : [];231const config = isPlainObject(data.config) ? data.config : {};232233// Guard against non-array but truthy values:234function safeArrayDefault(value: unknown, defaultValue: unknown[] = []): unknown[] {235 return Array.isArray(value) ? value : defaultValue;236}237238// Usage in settings:239const settings = {240 include: safeArrayDefault(base.include),241 exclude: safeArrayDefault(base.exclude),242};243```244245### Type Guards for Object Safety246247Always validate object types before using them, especially in settings derivation:248249```typescript250// ✅ Define reusable type guard251function isPlainObject(value: unknown): value is Record<string, unknown> {252 if (value === null || typeof value !== 'object' || Array.isArray(value)) {253 return false;254 }255256 // Check if the prototype is exactly Object.prototype or null257 const proto = Object.getPrototypeOf(value);258 return proto === null || proto === Object.prototype;259}260261// ✅ Use type guard for safe object assignment262const settings = {263 include: base.include ?? [],264 exclude: base.exclude ?? [],265 config: isPlainObject(base.config) ? base.config : {}, // Safe object validation266};267268// ❌ Unsafe direct assignment (base.config could be array, null, etc.)269const unsafe = {270 config: base.config ?? {}, // Could assign [] or other non-plain objects271};272273// ✅ Complete pattern for settings derivation274let settings = $derived(275 (() => {276 const base = $speciesSettings ?? fallbackSettings; // Use ?? for root object277278 return {279 include: Array.isArray(base.include) ? base.include : [],280 exclude: Array.isArray(base.exclude) ? base.exclude : [],281 config: isPlainObject(base.config) ? base.config : {}, // Type guard for objects282 } as SettingsType;283 })()284);285```286287## Icon Usage288289```svelte290<script>291 import { X, Search, Settings } from '@lucide/svelte';292</script>293294<!-- ✅ Correct -->295<X class="h-4 w-4" />296<Search class="h-5 w-5" />297298<!-- ❌ Wrong -->299<svg>...</svg>300```301302See `$lib/utils/ICONS.md` for common icons and usage patterns.303304## Logging305306```typescript307import { loggers } from '$lib/utils/logger';308const logger = loggers.ui; // Once per file309310logger.debug('State changed', { component: 'MyComponent' });311logger.error('Failed', error, { action: 'save' });312313// NEVER log PII: emails, passwords, tokens, personal data314```315316## Date/Time Handling317318```typescript319import { getLocalDateString, getLocalTimeString } from '$lib/utils/date';320321// ✅ Correct - local timezone322const today = getLocalDateString(); // "2024-01-15"323324// ❌ Wrong - UTC conversion325const wrong = new Date().toISOString().split('T')[0];326```327328## SSE (Server-Sent Events)329330```typescript331import { ReconnectingEventSource } from '$lib/utils/ReconnectingEventSource';332333const eventSource = new ReconnectingEventSource('/api/endpoint', {334 max_retry_time: 30000,335 withCredentials: false,336});337338eventSource.onmessage = event => {339 const data = JSON.parse(event.data);340};341342// Cleanup343eventSource.close();344```345346## CSRF Protection347348```typescript349function getCsrfToken(): string | null {350 const meta = document.querySelector('meta[name="csrf-token"]');351 if (meta?.getAttribute('content')) return meta.getAttribute('content');352353 const match = document.cookie.match(/csrf=([^;]+)/);354 return match?.[1] || null;355}356```357358## UX Design Principles359360Every interactive element needs a deliberate UX pass before it ships. Ambiguity is the most common source of avoidable support load: each confused user opens an issue, and each issue costs far more to triage than the design review would have. BirdNET-Go is a hobby project maintained by volunteers, so preventable rework comes directly out of feature work. Catch ambiguity at design time.361362### No Ambiguous Disabled States363364When a control is disabled (button, input, toggle, link), the user MUST be able to tell _why_ without guessing or clicking around. Silent disabled controls are the single biggest source of "this is broken" reports that turn out to be a missing prerequisite the user could have satisfied themselves.365366Required indicators when a control is disabled:367368- **Tooltip on hover** explaining the blocked condition (e.g., "Fill in the species name to enable Save")369- **Inline helper text** below the control when the reason is persistent (e.g., "Save is disabled until validation passes")370- **Visible status badge** when the reason is workflow-related (e.g., "Read-only mode", "Pending approval")371- **`aria-describedby`** pointing at the explanation so screen readers convey the same context as sighted users372373```svelte374<!-- Wrong: user has no idea why -->375<button disabled={!canSave}>{t('common.save')}</button>376377<!-- Correct: reason is discoverable -->378<button379 type="button"380 disabled={!canSave}381 title={!canSave ? saveBlockedReason : undefined}382 aria-describedby={!canSave ? 'save-help' : undefined}383>384 {t('common.save')}385</button>386{#if !canSave}387 <p id="save-help" class="text-sm text-base-content/70">388 {saveBlockedReason}389 </p>390{/if}391```392393Notes:394395- A `title` attribute is the minimum bar, but tooltips are invisible on touch devices. Always pair it with inline helper text or a status badge for important controls like Save, Submit, or Delete.396- Avoid native `disabled` if the user still needs keyboard focus to read the explanation. Consider `aria-disabled="true"` plus suppressed click handling so the control remains tab-focusable.397- The reason string must be specific. "Cannot save" is useless; "Threshold must be between 0 and 1 before saving" is actionable.398399### General UX Rules400401- **Every interactive element communicates its state.** Loading, saving, validating, errored, success: each gets a visible indicator with a label, not just a bare spinner.402- **Validation errors point to the offending field.** "Form has errors" is not enough. Show the specific reason next to the input (e.g., "Threshold must be between 0 and 1").403- **Destructive actions need confirmation context.** "Delete?" alone is too thin. Show what will be deleted and what the consequences are (linked records, irreversible cleanup, etc.).404- **Empty states explain how to populate them.** "No alerts configured. Click Add to create one." beats a blank list with no affordance.405- **Loading states distinguish fetching from saving from processing.** A spinner with a label ("Saving settings...") beats a bare spinner every time.406- **Confirm success explicitly.** Toast, inline checkmark, or status badge: the user should never have to guess whether their action took effect.407- **Run the cold-read test.** Would a first-time user who landed on this screen know what to do next? If not, redesign before merging.408409### Why This Matters410411A bug filed because of UI ambiguity is preventable rework. Each ambiguous disabled state, unclear error, or silent failure becomes:4124131. A user-filed GitHub issue or support thread4142. A reproduction effort by a maintainer4153. A patch and release cycle4164. Documentation or FAQ updates explaining the workaround417418Minutes of careful design at build time save hours of triage after release. Spend the minutes.419420## Accessibility Quick Reference421422### Forms423424```svelte425<label for="field">Label</label>426<input id="field" aria-describedby="field-help" />427<div id="field-help">Help text</div>428```429430### Buttons431432```svelte433<script>434 import { X } from '@lucide/svelte';435</script>436437<button aria-label="Close dialog">438 <X class="h-4 w-4" />439</button>440```441442### Status Updates443444```svelte445<div role="status" aria-live="polite">Loading...</div>446<div role="alert" aria-live="assertive">Error occurred</div>447```448449### Testing450451```bash452npm run test:a11y # Run accessibility tests453npm run test:a11y:watch # Watch mode454```455456## Settings Components457458```svelte459<script>460 import SettingsSection from '$lib/components/ui/SettingsSection.svelte';461 import { hasSettingsChanged } from '$lib/utils/settingsChanges';462463 let hasChanges = $derived(hasSettingsChanged(original, current));464</script>465466<SettingsSection title="Title" {hasChanges}>467 <!-- controls -->468</SettingsSection>469```470471## Pre-Commit Workflow472473### Automated (Husky)474475- lint-staged auto-formats staged files476- svelte-check validates TypeScript477478### Manual Checklist4794801. Check IDE Problems panel for errors4812. Run `npm run check:all`4823. Test affected functionality4834. Review accessibility warnings484485## Debug Tools486487```bash488# Screenshots489cd tools/490node screenshot.js http://localhost:8080/ui/dashboard491node screenshot.js http://localhost:8080/ui/analytics -w 1920 -h 1080492493# Legacy494node tools/test-all-pages.js495```496497## Common Patterns498499### Loading States500501```svelte502{#if loading}503 <div class="animate-spin h-5 w-5 border-2 border-blue-500 border-t-transparent rounded-full" />504{:else if error}505 <div506 role="alert"507 class="p-4 rounded-lg bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400"508 >509 {error.message}510 </div>511{:else}512 <Content />513{/if}514```515516### Dynamic Lists517518```svelte519{#each items as item (item.id)}520 <Item {item} />521{/each}522```523524## Static Analysis with ast-grep525526### Quick Commands527528```bash529npm run ast:migration # Find Svelte 4 patterns to migrate530npm run ast:best-practices # Check Svelte 5 rune usage531npm run ast:security # Security vulnerabilities (XSS, CSRF)532npm run ast:all # Run all checks (included in check:all)533```534535### Key Rules Enforced536537- **Migration**: Detects `export let`, `$:`, slots, `on:` events538- **Security**: XSS in `{@html}`, localStorage validation, password logging539- **Best Practices**: No destructuring $state, pure $derived, effect cleanup540- **Conventions**: Use icon utils, proper date formatting, logger over console541542### Using ast-grep vs grep/sed543544```bash545# ❌ grep - fragile regex546grep -r "export let" src/547548# ✅ ast-grep - syntax-aware549sg scan --pattern "export let $PROP" src/550551# ❌ sed - can break code552sed -i 's/export let/let/g' file.svelte553554# ✅ ast-grep - safe transformation555sg scan --pattern "export let $PROP = $DEFAULT" --rewrite "let { $PROP = $DEFAULT } = $props()" src/556```557558## Svelte MCP (REQUIRED)559560The Svelte MCP server provides official documentation and code validation tools. **You MUST use this for all Svelte development.**561562### Required Usage5635641. **When writing/modifying Svelte components**: Always run the Svelte autofixer (`mcp__svelte__svelte-autofixer`) on the component code before committing5652. **When unsure about Svelte 5 syntax**: Use `mcp__svelte__list-sections` and `mcp__svelte__get-documentation` to fetch official docs5663. **For code examples**: Use `mcp__svelte__playground-link` to generate shareable playground links567568### Svelte Autofixer Workflow569570```5711. Write/modify Svelte component5722. Run svelte-autofixer with the component code5733. Fix any issues reported5744. Re-run autofixer until no issues remain5755. Commit the code576```577578### Common Issues Caught by Autofixer579580- Missing keys in `{#each}` blocks581- Incorrect rune usage582- Svelte 4 patterns that should be migrated583- Accessibility issues584585## Resources586587- **Svelte MCP** - Use `mcp__svelte__*` tools for official Svelte 5/SvelteKit documentation588- WCAG: https://www.w3.org/WAI/WCAG21/quickref/589- axe DevTools browser extension for testing590- ast-grep docs: https://ast-grep.github.io/591592## Testing Best Practices593594### Mock Organization and Shared Setup595596**Problem**: Duplicating identical `vi.mock()` blocks across multiple test files creates maintenance overhead and inconsistency.597598**Solution**: Use shared test setup files for common mocks.599600#### Shared Mock Setup Pattern6016021. **Extract common mocks to `src/test/setup.ts`**:603604```javascript605// src/test/setup.ts606import '@testing-library/jest-dom';607import { vi } from 'vitest';608609// Mock API utilities (used across multiple test suites)610vi.mock('$lib/utils/api', () => ({611 api: {612 get: vi.fn().mockResolvedValue({ data: { species: [] } }),613 post: vi.fn().mockResolvedValue({ data: {} }),614 },615 ApiError: class ApiError extends Error {616 constructor(message, status, data) {617 super(message);618 this.status = status;619 this.data = data;620 }621 },622}));623624// Mock toast notifications625vi.mock('$lib/stores/toast', () => ({626 toastActions: {627 success: vi.fn(),628 error: vi.fn(),629 info: vi.fn(),630 },631}));632633// Mock internationalization634vi.mock('$lib/i18n', () => ({635 t: vi.fn(key => key),636 getLocale: vi.fn(() => 'en'),637}));638```6396402. **Configure Vitest to load setup file** (`vite.config.js`):641642```javascript643export default defineConfig({644 test: {645 environment: 'jsdom',646 globals: true,647 setupFiles: ['./src/test/setup.ts'], // ✅ Load shared TypeScript setup648 include: ['src/**/*.{test,spec}.{js,ts}'],649 },650});651```6526533. **Clean test files** - Remove duplicate mocks:654655```typescript656// ❌ Before: Duplicate mocks in every test file657import { vi } from 'vitest';658659vi.mock('$lib/utils/api', () => ({/* duplicate */}));660vi.mock('$lib/stores/toast', () => ({/* duplicate */}));661vi.mock('$lib/i18n', () => ({/* duplicate */}));662663describe('Component Tests', () => {664 // tests...665});666667// ✅ After: Clean test file with shared setup668import { describe, it, expect, beforeEach } from 'vitest';669import { render, screen } from '@testing-library/svelte';670671// Note: Common mocks are now defined in src/test/setup.ts and loaded globally via Vitest configuration672673describe('Component Tests', () => {674 beforeEach(() => {675 vi.clearAllMocks(); // Clear mock call history between tests676 });677678 // tests...679});680```681682#### When to Use Shared vs File-Specific Mocks683684**✅ Use shared setup for**:685686- API utilities (`$lib/utils/api`)687- Toast notifications (`$lib/stores/toast`)688- Internationalization (`$lib/i18n`)689- Global browser APIs (fetch, localStorage)690- Third-party libraries (MapLibre, D3)691692**✅ Use file-specific mocks for**:693694- Component-specific stores695- Test-specific mock implementations696- Mocks that need different behavior per test697698#### Setup File Best Practices699700**✅ Always use TypeScript for setup files** (`src/test/setup.ts`):701702- Provides type safety for mock definitions703- Enables IntelliSense and better IDE support704- Allows exporting typed test utilities705- Consistent with codebase TypeScript standards706707**❌ Avoid JavaScript setup files** - they lack type safety and can't use TypeScript features needed for proper mock typing.708709#### Mock Reset Patterns710711```typescript712describe('Component Tests', () => {713 beforeEach(() => {714 vi.clearAllMocks(); // Clear call history but keep implementation715 settingsActions.resetAllSettings(); // Reset store state716 });717718 afterEach(() => {719 cleanup(); // Clean up DOM after each test720 });721});722```723724#### Advanced Mock Patterns725726```typescript727// Override shared mock for specific test728beforeEach(() => {729 const { api } = await import('$lib/utils/api');730 vi.mocked(api.get).mockResolvedValue({ data: { customData: [] } });731});732733// Restore original mock734afterEach(() => {735 vi.restoreAllMocks(); // Restore to setup.js defaults736});737```738739### TypeScript in Test Files740741#### Handling `any` Types in Edge Case Testing742743When testing edge cases with intentionally malformed data, you need to use `any` types. Follow these patterns:7447451. **Use inline ESLint disable comments for intentional `any` usage**:746747```typescript748// For single line749// eslint-disable-next-line @typescript-eslint/no-explicit-any750const malformedData = 'string' as any;751752// For blocks753/* eslint-disable @typescript-eslint/no-explicit-any */754settingsActions.updateSection('realtime', {755 species: undefined as any,756 config: 'not-an-object' as any,757});758/* eslint-enable @typescript-eslint/no-explicit-any */759```7607612. **Create type helpers for test data**:762763```typescript764// Define test-specific types765type MalformedSettings = Record<string, unknown>;766type TestData = Partial<SettingsFormData> & { [key: string]: unknown };767768// Use unknown with type guards instead of any where possible769const testData: unknown = getData();770if (typeof testData === 'object' && testData !== null) {771 // Type guard ensures safe access772}773```774775#### Avoiding Common ESLint Errors7767771. **Unused Variables**:778 - Remove unused imports immediately779 - Use underscore prefix for intentionally unused variables: `_unusedVar`780 - For required but unused component references: `expect(component).toBeTruthy()`7817822. **Nullish Coalescing vs Logical OR**:783784```typescript785// ❌ Avoid - triggers ESLint warning786const count = value || 0; // Problem: treats 0 as falsy787788// ✅ Correct - use nullish coalescing789const count = value ?? 0; // Only replaces null/undefined790791// When you explicitly want logical OR behavior, add comment:792const display = value || 'default'; // eslint-disable-line @typescript-eslint/prefer-nullish-coalescing -- intentional falsy check793```7947953. **Unnecessary Conditionals**:796797```typescript798// ❌ Avoid - settings is always defined after get()799const settings = get(birdnetSettings);800if (settings) {801 // Unnecessary - get() always returns a value802 // ...803}804805// ✅ Correct - check specific properties806const settings = get(birdnetSettings);807if (settings.sensitivity !== undefined) {808 // ...809}810```8118124. **Browser APIs in Tests**:813814```typescript815// Check for API availability816if (typeof performance !== 'undefined') {817 const startTime = performance.now();818 // ...819}820821// Or use Node.js alternatives in test environment822import { performance } from 'perf_hooks'; // For Node.js823```824825#### Type Assertions in Tests826827```typescript828// For accessing nested properties in tests829const formData = get(settingsStore).formData;830const speciesConfig = (formData as TestFormData)?.realtime?.species?.config;831832// Type guard approach (preferred)833function isSpeciesSettings(value: unknown): value is SpeciesSettings {834 return typeof value === 'object' && value !== null && 'include' in value && 'exclude' in value;835}836```837838#### Test File Organization8398401. **Group ESLint disable directives at the top for file-wide issues**:841842```typescript843/* eslint-disable @typescript-eslint/no-explicit-any */844// Test file with many intentional any types845```8468472. **Use describe blocks to scope disable directives**:848849```typescript850describe('Edge Cases', () => {851 /* eslint-disable @typescript-eslint/no-explicit-any */852 // Tests with malformed data853 /* eslint-enable @typescript-eslint/no-explicit-any */854});855```8568573. **Document why `any` is necessary**:858859```typescript860// Testing malformed data structure - any is required861settingsActions.updateSection('config', {862 // eslint-disable-next-line @typescript-eslint/no-explicit-any863 data: 'string-instead-of-object' as any,864});865```866867### Performance Testing868869```typescript870// Safe performance measurement in tests871function measurePerformance(fn: () => void): number {872 if (typeof performance !== 'undefined') {873 const start = performance.now();874 fn();875 return performance.now() - start;876 }877 // Fallback for environments without performance API878 const start = Date.now();879 fn();880 return Date.now() - start;881}882```883884### Mock Data Types885886Create dedicated types for test scenarios:887888```typescript889// types/test-helpers.ts890export type DeepPartial<T> = T extends object ? { [P in keyof T]?: DeepPartial<T[P]> } : T;891892export type MalformedData =893 string | number | boolean | null | undefined | unknown[] | Record<string, unknown>;894895export type TestSettings = DeepPartial<SettingsFormData> & {896 [key: string]: MalformedData;897};898```899900### Running Linters Before Commit901902**Always run these commands before committing test files**:903904```bash905# Check formatting906npm run format:check907908# Fix formatting909npx prettier --write src/**/*.test.ts910911# Check linting912npm run lint913914# Fix auto-fixable issues915npx eslint --fix src/**/*.test.ts916917# Full check918npm run check:all919```920921### Strict TypeScript Configuration922923#### Dealing with Strict Null Checks924925When TypeScript's strict mode is enabled, be careful with store values:926927```typescript928// ❌ Problematic - assumes store always has value929const settings = get(birdnetSettings);930settings.threshold = 0.5; // Error if settings could be undefined931932// ✅ Safe access patterns933const settings = get(birdnetSettings);934if (settings) {935 settings.threshold = 0.5;936}937938// ✅ With default fallback939const settings = get(birdnetSettings) ?? createDefaultSettings();940settings.threshold = 0.5;941942// ✅ Optional chaining for nested access943const threshold = get(birdnetSettings)?.dynamicThreshold?.min ?? 0;944```945
One repository carrying more than one format is the comparison this product exists for: does anyone actually write different content in each file, or is one a copy of the other?
| Repository | Format | Stack | Covers | Score | Changed |
|---|---|---|---|---|---|
| tphakala/birdnet-go.cursor/rules/database.mdc · 1.6k | Cursor rules | databasedo-not | 45/100 | today | |
| tphakala/birdnet-go.cursor/rules/frontend.mdc · 1.6k | Cursor rules | dependenciesuido-not | 61/100 | today | |
| tphakala/birdnet-go.cursor/rules/go.mdc · 1.6k | Cursor rules | buildteststylearch+5 | 69/100 | today | |
| tphakala/birdnet-go.cursor/rules/go_test.mdc · 1.6k | Cursor rules | setupteststyletesting-strategy+1 | 56/100 | today | |
| tphakala/birdnet-goAGENTS.md · 1.6k | AGENTS.md | teststylegitdo-not+1 | 78/100 | today | |
| tphakala/birdnet-goCLAUDE.md · 1.6k | CLAUDE.md | buildtestlint-formatstyle+8 | 100/100 | today | |
| tphakala/birdnet-gofrontend/src/lib/desktop/components/CLAUDE.md · 1.6k | CLAUDE.md | teststylearchui | 70/100 | today | |
| tphakala/birdnet-gofrontend/src/lib/desktop/components/ui/CLAUDE.md · 1.6k | CLAUDE.md | styleuidocs | 54/100 | today | |
| tphakala/birdnet-gofrontend/src/lib/desktop/features/settings/CLAUDE.md · 1.6k | CLAUDE.md | buildstylearchtypes+2 | 66/100 | today | |
| tphakala/birdnet-gofrontend/static/messages/CLAUDE.md · 1.6k | CLAUDE.md | archuido-notagent-behaviour | 67/100 | today | |
| tphakala/birdnet-gofrontend/tools/CLAUDE.md · 1.6k | CLAUDE.md | no sections | 65/100 | today | |
| tphakala/birdnet-gointernal/CLAUDE.md · 1.6k | CLAUDE.md | buildteststylearch+5 | 88/100 | today | |
| tphakala/birdnet-gointernal/api/v2/CLAUDE.md · 1.6k | CLAUDE.md | teststylesecurityapi+1 | 84/100 | today | |
| tphakala/birdnet-gointernal/errors/CLAUDE.md · 1.6k | CLAUDE.md | styleuiperformancedo-not+1 | 61/100 | today |
Same format, overlapping stack, ranked by quality.
| Repository | Format | Stack | Covers | Score | Changed |
|---|---|---|---|---|---|
| stacklok/toolhiveCLAUDE.md · 2.0k | CLAUDE.md | buildteststylearch+4 | 100/100 | 14 days ago | |
| Adit-Jain-srm/NightmareNetCLAUDE.md · 46 | CLAUDE.md | buildtestlint-formatstyle+6 | 100/100 | 14 days ago | |
| tyrchen/geektime-bootcamp-aiw7/genslides/backend/CLAUDE.md · 230 | CLAUDE.md | testlint-formatstylearch+6 | 100/100 | 9 days ago | |
| nimbalyst/nimbalystpackages/android/CLAUDE.md · 1.5k | CLAUDE.md | setupbuildstylearch+2 | 100/100 | 14 days ago | |
| dotCMS/corecore-web/CLAUDE.md · 949 | CLAUDE.md | teststylearchtesting-strategy+3 | 100/100 | 14 days ago | |
| microsoft/playwrightCLAUDE.md · 95k | CLAUDE.md | buildtestlint-formatstyle+7 | 100/100 | 7 days ago | |
| tphakala/birdnet-goCLAUDE.md · 1.6k | CLAUDE.md | buildtestlint-formatstyle+8 | 100/100 | today | |
| bagisto/bagistoCLAUDE.md · 28k | CLAUDE.md | setupbuildteststyle+5 | 100/100 | 7 days ago |
A badge carrying the measured quality of the strongest agent config file in this repository, out of 100. It reads from this index every time somebody loads your page, so it changes when the measurement changes and there is nothing to keep up to date. Free, no account, and the value is not something you or we can set by hand.
[](https://rulestack.kynth.studio/configs/tphakala-birdnet-go-frontend-claude)Would rather not hotlink us? Every badge is also served in shields.io’s endpoint schema, so shields renders the image and your readers never talk to our domain:
Published by Toolproof, the masthead over this index and eight others. The method behind the number is at toolproof.kynth.studio/methodology, and the whole thing is readable as JSON with no key at /api.