# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Règles de collaboration

- **Toujours répondre et écrire en français**
- **Ne jamais modifier les fichiers de tests sans le signaler explicitement** à l'utilisateur avant de le faire
- **Toujours écrire des tests** (unitaires + fonctionnels/régression) lors de toute correction de bug ou ajout de feature — les tests font partie de la livraison, pas une étape optionnelle
- **Toujours mettre à jour `docs/algorithmes.md`** quand un algorithme ou une formule de calcul change
- **Ne jamais utiliser de taux fixe pour les jauges** — toujours utiliser `gainedIn` / `timeToGain` avec traversée des tiers (erreur historique du monolithe)
- **Signaler explicitement** tout changement qui touche aux snapshots du timer (`snapGauges`, `snapStats`, `gaugeRecharges`) car cela peut affecter tous les calculs en cours de session
- **Les tests ne tournent pas en WSL** — toujours rappeler à l'utilisateur de lancer `npm test` depuis un terminal Windows

## Project Overview

Obsidienne (anciennement "Minuteur Dragodinde") is a Windows desktop app (Electron + Vite + TypeScript) for managing Dragodinde breeding timers in Dofus 3. French-language UI.

## Commands

- **Dev**: `npm start` (runs Vite dev server + Electron via `vite-plugin-electron`)
- **Build**: `npm run build` (Vite build + electron-builder → NSIS installer in `dist/`)
- **Test**: `npm test` (Vitest, run from Windows — native bindings incompatible with WSL)
- **Test watch**: `npm run test:watch`
- **Build scripts**: `build.bat` (auto-elevates to admin) or `build.ps1` for Windows

## Architecture

**DDD hexagonal architecture** with Vite + TypeScript. Four layers:

```
src/
  domain/          — Entities, value objects, domain services (pure TS, no deps)
  application/     — Command handlers, query handlers, CommandBus/QueryBus
  infrastructure/  — LocalStorageRepository, ElectronNotification, WebAudioAlarm
  presentation/    — Components, helpers, UIState, entry point (index.ts)
```

### Domain layer

- **`entities/Enclos.ts`** — Enclos entity with `TimerData` (includes `gaugeRecharges: Record<string, GaugeRecharge[]>`)
- **`entities/Dragodinde.ts`** — DD entity with stats, targets, `levelTarget`, `sereniteTarget`
- **`services/GaugeCalculator.ts`** — Core pure functions: `gainedIn`, `timeToGain`, `gaugeAfter`, `elapsed`, `computeGaugeState`
- **`value-objects/GaugeType.ts`** — `GAUGE_DEFS`, `STAT_DEFS`, `DEFAULT_TARGETS` (mangeoire default = 100, but UI uses 200 when `levelTarget` is null)
- **`value-objects/XpTable.ts`** — `xpForLevel`, `levelFromXp`
- **`value-objects/Tier.ts`** — `tierNum`, `tierRate`

### Application layer

Commands: `StartTimer`, `StopTimer`, `CompleteTimer`, `ResetTimer`, `CreateEnclos`, `DeleteEnclos`, `AddDragodinde`, `RemoveDragodinde`, `UpdateGauge` (toggle + level), `RechargeGauge`, `RegisterAccouplement`, `UpdateSettings`, `ResetStats`, `ReorderEnclos`, `UpdateWorkflow`, `DeleteWorkflow`, `EnclosActions` (clear, rename, reset-timer), `DragodindeActions` (rename, update-stat, seren-target, level-target, reorder)

Queries: `GetDashboard`, `GetEnclosDetail`, `GetTimerState`, `GetBreedingOptions`, `GetReapproTree`, `GetInventaire`, `GetSettings`, `GetWorkflows`

### Presentation layer

- **`components/App.ts`** — Root component, animation loop (`requestAnimationFrame`), CommandBus/QueryBus wiring
- **`components/EnclosView.ts`** — Main timer view: gauge inputs (with `input` listener for real-time updates), DD grid, "Alarme dans" display
- **`components/DragodindeCard.ts`** — Per-DD card: stat pills, targets (all with `input` listeners for real-time preview)
- **`helpers/gauge-live.ts`** — All live calculation helpers: `computeGaugeLive`, `enclosGaugeCurGl`, `enclosGlobalState`, `calcSerenEtaLive`, `calcLevelEtaLive`

### Key data flows

**Timer start** (fresh): snapshots `gaugeLevels` → `snapGauges`, DD stats → `snapStats`, clears `gaugeRecharges`
**Timer resume** (from pause): accumulates `pausedMs += now - pausedAt`, preserves snapshots
**Gauge recharge** (during timer): pushes `{ atSec, level }` to `gaugeRecharges[gid]`, updates `gaugeLevels[gid]`
**Live display**: `requestAnimationFrame` loop → `enclosGlobalState(enc)` → per-DD `computeGaugeLive` → DOM updates

### Timer calculation core — `computeGaugeState`

The central calculation function. Handles:
- Multiple gauge recharges during a session (segments between each recharge)
- Freeze at absolute stat cap (sérénité ±5000, end/mat/amour 20000, niveau 200)
- Returns `{ gained, curGl, effectiveEl }`

**Pre-start preview**: `enclosGlobalState` computes even when timer not started (`started = false`), using current `gaugeLevels` as `startGl` and `el = 0`. "Alarme dans" updates in real-time as the user types gauge values and DD targets.

**XP gauge (mangeoire)**: when the target is unreachable in a single gauge drain, countdown shows drain time instead of `∞`. Formula: `timeToGain(curGl, Math.min(xpRestante, curGl))` — consistent with "Vide en" display.

**Default level target**: when `dd.levelTarget === null`, the XP gauge defaults to targeting level 200.

### IPC channels

Main→Renderer: `app-version`, `play-alarm-sound`, `update-available`, `update-downloading`, `update-progress`, `update-ready`, `update-error`
Renderer→Main: `trigger-alarm`, `show-notification`, `send-ntfy`, `focus-window`, `install-update`, `get-version`

### Auto-update flow

Checks Gitea Releases API on startup (after 3s) and hourly. Downloads NSIS Setup .exe to temp, launches with `/S` (silent), then quits app.

### Dev vs packaged mode

When `!app.isPackaged`, userData is stored in `Obsidienne-DEV` (isolated from installed app) and a DEV badge is injected into the UI. On first packaged launch, data is auto-migrated from the old `Minuteur Dragodinde` folder.

## Tests

```
tests/
  unit/domain/        — GaugeCalculator, Enclos, Dragodinde, XpTable, etc.
  unit/application/   — commands.test.ts, queries.test.ts, CommandBus.test.ts
  unit/infrastructure/
  functional/         — breeding-workflow, enclos-management, timer-workflow
  regression/         — gauge-tier-calculation, gauge-recharge, xp-timer-display, etc.
```

Vitest with path aliases (`@domain`, `@application`, `@infrastructure`, `@presentation`). Run with `npm test` from Windows.

## Key conventions

- All UI text is in French
- TypeScript strict mode — no `any` unless unavoidable
- Immutable domain entities (functions return new objects)
- Commands mutate shared `state` object directly (no Redux-style reducers)
- `repo.save(state)` called after every state mutation
- CSS uses custom properties in `:root` (dark purple/gaming aesthetic), component styles in `src/presentation/styles/`
