dd-timer/docs/plans/2026-03-27-v1.2.0-implementation.md
POL Mickaël 3e485fd09b chore: normalise fins de ligne CRLF → LF dans tout le repo
Applique .gitattributes sur tous les fichiers existants.
Élimine les différences fantômes entre WSL et Windows.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-06 08:55:10 +02:00

641 lines
23 KiB
Markdown
Executable File
Raw 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.

# v1.2.0 Implementation Plan — Accouplement, Sidebar, Réappro ♂/♀
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** Ajouter un onglet Accouplement dédié, une sidebar de navigation overlay, intégrer les stats dans le dashboard, et ajouter les contraintes ♂/♀ dans la réappro.
**Architecture:** Tout le code est dans `src/index.html` (monolithique). On modifie le CSS, le HTML, et le JS inline. Pas de framework. Rendu impératif via innerHTML. État central dans `S`, persisté via `save()`.
**Tech Stack:** Vanilla JS, CSS custom properties, Electron IPC.
**Fichier principal:** `/mnt/c/Users/micka/Desktop/dd-timer/src/index.html`
---
## Task 1 : Retrait des bébés des enclos + migration
**But :** Supprimer le système de bébés des enclos et migrer les données existantes vers `S.archivedStats`.
**Files:**
- Modify: `src/index.html`
**Step 1 : Migration des données au chargement**
Dans la fonction `load()` ou juste après le chargement de `S`, ajouter une migration one-shot :
```javascript
// Migration v1.2.0 : archiver les babyHistory des enclos
if(!S._migratedBabies120){
S.enclos.forEach(enc=>{
if(enc.babyHistory&&enc.babyHistory.length){
if(!S.archivedStats)S.archivedStats={babies:[],totalMax:0};
if(!S.archivedStats.babies)S.archivedStats.babies=[];
enc.babyHistory.forEach(b=>{
S.archivedStats.babies.push({...b,enclosName:enc.name||('Enclos '+(S.enclos.indexOf(enc)+1))});
S.archivedStats.totalMax=(S.archivedStats.totalMax||0)+Math.min(5,Math.floor((b.ddCount||2)/2));
});
}
});
S._migratedBabies120=true;
save();
}
```
**Step 2 : Supprimer le code des bébés dans les enclos**
Supprimer ou commenter les fonctions et le code suivant :
- `isBabyUnlocked()` (ligne ~2095)
- `updateBabyBtnState()` (ligne ~2101)
- `maxBabiesForEnclos()` (ligne ~2115)
- `currentBabiesCount()` (ligne ~2118)
- `openBabyModal()` (ligne ~2127)
- `closeBabyModal()` (ligne ~2134)
- `modalBack()`, `modalNext()`, `renderModalStep()` (lignes ~2138-2250)
- `selectModalGen()`, `selectModalRaceByIdx()`, `selectModalRace()`, `changeCount()` (lignes ~2250-2285)
- `renderBabies()` (ligne ~2015)
- `renderBabyHistory()` (ligne ~2049)
- `switchSubTab()` (ligne ~2001)
- `removeBaby()` (ligne ~2079)
- `clearSessionBabies()` (ligne ~2087)
- Le HTML du modal bébé (lignes ~352-369)
**Step 3 : Nettoyer renderContent pour les enclos**
Dans `renderContent()` (ligne ~1465), retirer les sous-onglets "elevage"/"history" et le bouton bébé du rendu des enclos.
**Step 4 : Commit**
```
feat: retire bébés des enclos, migre données vers archivedStats
```
---
## Task 2 : Onglet Accouplement — données et constante
**But :** Créer la structure de données et le lookup inversé pour les accouplements.
**Files:**
- Modify: `src/index.html`
**Step 1 : Ajouter 'accouplement' dans SPECIAL_TABS**
Ligne ~698 :
```javascript
const SPECIAL_TABS=['dashboard','appro','inventaire','accouplement','workflows'];
// Note : 'stats' retiré (sera dans le dashboard)
```
**Step 2 : Créer le lookup inversé BREEDING_BY_PARENTS**
Après `BREEDING_RECIPES` (ligne ~677) :
```javascript
// Lookup inversé : "ParentA|ParentB" → babyRace
const BREEDING_BY_PARENTS={};
Object.entries(BREEDING_RECIPES).forEach(([baby,[a,b]])=>{
BREEDING_BY_PARENTS[a+'|'+b]=baby;
if(a!==b) BREEDING_BY_PARENTS[b+'|'+a]=baby;
});
// Lookup : pour un parent donné, quels partenaires sont possibles ?
const COMPATIBLE_PARTNERS={};
Object.entries(BREEDING_RECIPES).forEach(([baby,[a,b]])=>{
if(!COMPATIBLE_PARTNERS[a])COMPATIBLE_PARTNERS[a]=[];
COMPATIBLE_PARTNERS[a].push({partner:b,baby,gen:RACE_GEN[baby]});
if(a!==b){
if(!COMPATIBLE_PARTNERS[b])COMPATIBLE_PARTNERS[b]=[];
COMPATIBLE_PARTNERS[b].push({partner:a,baby,gen:RACE_GEN[baby]});
}
});
```
**Step 3 : Initialiser S.accouplements**
Dans le chargement de S (après `load()`) :
```javascript
if(!S.accouplements) S.accouplements=[];
```
**Step 4 : Commit**
```
feat: ajoute structures données accouplement et lookup inversé
```
---
## Task 3 : Onglet Accouplement — rendu UI
**But :** Interface complète de l'onglet accouplement.
**Files:**
- Modify: `src/index.html` (CSS + JS)
**Step 1 : CSS pour l'onglet accouplement**
Ajouter dans la section `<style>` (avant `</style>`, ligne ~313) :
```css
/* Accouplement */
.accoup-grid{display:flex;flex-wrap:wrap;gap:10px;justify-content:center}
.accoup-card{background:var(--bg2);border:2px solid var(--border);border-radius:var(--r);padding:10px;cursor:pointer;text-align:center;width:100px;transition:border-color .2s,transform .15s}
.accoup-card:hover{border-color:var(--text);transform:translateY(-2px)}
.accoup-card.selected{border-color:var(--ok);box-shadow:0 0 12px rgba(40,232,136,.25)}
.accoup-result{display:flex;align-items:center;gap:20px;justify-content:center;flex-wrap:wrap;padding:20px 0}
.accoup-arrow{font-size:2rem;color:var(--muted)}
.accoup-inputs{display:flex;gap:16px;align-items:center;justify-content:center;flex-wrap:wrap;padding:16px 0}
.accoup-input-group{display:flex;flex-direction:column;align-items:center;gap:6px}
.accoup-input-group label{font-size:.82rem;color:var(--muted)}
.accoup-input-group input{width:70px;background:var(--bg2);border:1px solid var(--border);border-radius:8px;color:var(--text);padding:8px;font:700 1rem 'Nunito',sans-serif;text-align:center}
.accoup-gen-tabs{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:12px;justify-content:center}
.accoup-gen-tab{padding:4px 12px;border-radius:8px;font-size:.78rem;font-weight:700;cursor:pointer;border:1px solid var(--border);background:var(--bg2);color:var(--muted);transition:all .2s}
.accoup-gen-tab.active{background:var(--ok);color:#fff;border-color:var(--ok)}
```
**Step 2 : État local de l'onglet**
```javascript
const accoupState={parent1:null,parent2:null,filterGen:null,couples:'',babies:''};
```
**Step 3 : Fonction renderAccouplement()**
```javascript
function renderAccouplement(){
const c=document.getElementById('enclos-content');
c.removeAttribute('data-enc');
const allRaces=Object.keys(RACE_GEN).sort((a,b)=>(RACE_GEN[a]-RACE_GEN[b])||a.localeCompare(b));
const gens=[...new Set(Object.values(RACE_GEN))].sort((a,b)=>a-b);
// Si parent1 sélectionné, filtrer les partenaires compatibles
let availableRaces=allRaces;
if(accoupState.parent1){
const partners=COMPATIBLE_PARTNERS[accoupState.parent1]||[];
availableRaces=partners.map(p=>p.partner);
}
// Filtrer par gen
const fg=accoupState.parent1?null:accoupState.filterGen;
const filtered=fg?availableRaces.filter(r=>RACE_GEN[r]===fg):availableRaces;
// Déduire le bébé
let baby=null,babyGen=null;
if(accoupState.parent1&&accoupState.parent2){
baby=BREEDING_BY_PARENTS[accoupState.parent1+'|'+accoupState.parent2]||null;
if(baby)babyGen=RACE_GEN[baby];
}
// Gen tabs (uniquement si pas de parent1 sélectionné)
let genTabs='';
if(!accoupState.parent1){
genTabs=`<div class="accoup-gen-tabs">
<div class="accoup-gen-tab ${!fg?'active':''}" onclick="accoupState.filterGen=null;renderAccouplement()">Toutes</div>
${gens.map(g=>`<div class="accoup-gen-tab ${fg===g?'active':''}" style="${fg===g?'background:'+GEN_COLORS[g]+';border-color:'+GEN_COLORS[g]:''}" onclick="accoupState.filterGen=${g};renderAccouplement()">Gen ${g}</div>`).join('')}
</div>`;
}
let html=`<div style="font-family:'Cinzel',serif;font-size:.8rem;letter-spacing:2.5px;text-transform:uppercase;color:var(--muted);margin-bottom:12px">Accouplement</div>`;
// Étape 1 ou 2 : sélection parent
const stepLabel=accoupState.parent1?'Sélectionne le Parent 2':'Sélectionne le Parent 1';
const selectedParent1=accoupState.parent1;
if(selectedParent1){
const gc1=GEN_COLORS[RACE_GEN[selectedParent1]]||'#888';
html+=`<div class="card" style="border-color:${gc1}">
<div style="display:flex;align-items:center;gap:12px;margin-bottom:10px">
<span class="card-title" style="margin:0">Parent 1</span>
<button class="btn btn-ghost" style="padding:4px 12px;font-size:.8rem" onclick="accoupState.parent1=null;accoupState.parent2=null;renderAccouplement()">✕ Changer</button>
</div>
<div style="display:flex;align-items:center;gap:10px">
${getDDImage(selectedParent1)}
<div>
<div style="font-weight:800;color:var(--text)">${selectedParent1}</div>
<span style="background:${gc1};color:#fff;font-size:.65rem;font-weight:800;padding:1px 6px;border-radius:8px">Gen ${RACE_GEN[selectedParent1]}</span>
</div>
</div>
</div>`;
}
// Résultat si les deux parents sont sélectionnés
if(baby){
const gcb=GEN_COLORS[babyGen]||'#888';
const gc2=GEN_COLORS[RACE_GEN[accoupState.parent2]]||'#888';
html+=`<div class="card" style="border-color:${gc2}">
<span class="card-title">Parent 2 — ${accoupState.parent2}</span>
<div style="display:flex;align-items:center;gap:10px">
${getDDImage(accoupState.parent2)}
<div>
<div style="font-weight:800;color:var(--text)">${accoupState.parent2}</div>
<span style="background:${gc2};color:#fff;font-size:.65rem;font-weight:800;padding:1px 6px;border-radius:8px">Gen ${RACE_GEN[accoupState.parent2]}</span>
</div>
<button class="btn btn-ghost" style="padding:4px 12px;font-size:.8rem" onclick="accoupState.parent2=null;renderAccouplement()">✕ Changer</button>
</div>
</div>`;
html+=`<div class="card" style="border-color:${gcb}">
<span class="card-title">Bébé à naître</span>
<div class="accoup-result">
${getDDImage(accoupState.parent1)}
<span class="accoup-arrow">+</span>
${getDDImage(accoupState.parent2)}
<span class="accoup-arrow">➜</span>
<div style="text-align:center">
${getDDImage(baby)}
<div style="font-weight:800;color:var(--text);margin-top:4px">${baby}</div>
<span style="background:${gcb};color:#fff;font-size:.65rem;font-weight:800;padding:1px 6px;border-radius:8px">Gen ${babyGen}</span>
</div>
</div>
<div class="accoup-inputs">
<div class="accoup-input-group">
<label>Couples accouplés</label>
<input type="number" min="1" value="${accoupState.couples}" placeholder="—"
oninput="accoupState.couples=this.value"
onfocus="this.dataset.prev=this.value;this.value=''"
onblur="if(this.value==='')this.value=this.dataset.prev">
</div>
<div class="accoup-input-group">
<label>Bébés obtenus</label>
<input type="number" min="0" value="${accoupState.babies}" placeholder="—"
oninput="accoupState.babies=this.value"
onfocus="this.dataset.prev=this.value;this.value=''"
onblur="if(this.value==='')this.value=this.dataset.prev">
</div>
</div>
<div style="text-align:center;padding-top:8px">
<button class="btn btn-start" style="padding:10px 28px;font-size:.95rem" onclick="enregistrerAccouplement()">
✅ Enregistrer
</button>
</div>
</div>`;
}else{
// Grille de sélection
html+=`<div class="card">
<span class="card-title">${stepLabel}</span>
${genTabs}
<div class="accoup-grid">
${filtered.map(race=>{
const gc=GEN_COLORS[RACE_GEN[race]]||'#888';
return`<div class="accoup-card" onclick="selectAccoupParent('${race.replace(/'/g,"\\'")}')" title="${race}">
${getDDImage(race)}
<div style="font-weight:700;font-size:.75rem;color:var(--text);margin-top:4px;line-height:1.1">${race}</div>
<span style="background:${gc};color:#fff;font-size:.6rem;font-weight:800;padding:1px 5px;border-radius:6px">Gen ${RACE_GEN[race]}</span>
</div>`;
}).join('')}
</div>
</div>`;
}
c.innerHTML=html;
}
```
**Step 4 : Fonctions d'interaction**
```javascript
function selectAccoupParent(race){
if(!accoupState.parent1){
accoupState.parent1=race;
accoupState.parent2=null;
}else{
accoupState.parent2=race;
}
accoupState.couples='';
accoupState.babies='';
renderAccouplement();
}
function enregistrerAccouplement(){
const couples=parseInt(accoupState.couples)||0;
const babies=parseInt(accoupState.babies)||0;
if(couples<=0){alert('Renseigne le nombre de couples.');return;}
const baby=BREEDING_BY_PARENTS[accoupState.parent1+'|'+accoupState.parent2];
if(!baby)return;
if(!S.accouplements)S.accouplements=[];
S.accouplements.push({
parentA:accoupState.parent1,
parentB:accoupState.parent2,
baby,
gen:RACE_GEN[baby],
couples,
babiesObtained:babies,
date:Date.now()
});
save();
// Reset
accoupState.parent1=null;
accoupState.parent2=null;
accoupState.couples='';
accoupState.babies='';
renderAccouplement();
}
```
**Step 5 : Router dans renderContent()**
Ligne ~1465, ajouter le cas :
```javascript
if(S.activeId==='accouplement'){renderAccouplement();return;}
```
**Step 6 : Ajouter l'onglet dans renderTabs()**
Dans `renderTabs()` (ligne ~1341), ajouter un onglet "Accouplement" dans les special tabs, avec icône 💑.
**Step 7 : Commit**
```
feat: ajoute onglet Accouplement avec sélection parents et enregistrement
```
---
## Task 4 : Intégrer les stats dans le Dashboard + section Paramètres
**But :** Déplacer les stats et les settings dans le dashboard. Supprimer l'onglet Stats.
**Files:**
- Modify: `src/index.html`
**Step 1 : Retirer 'stats' de SPECIAL_TABS**
Déjà fait dans Task 2 Step 1.
**Step 2 : Modifier renderDashboard()**
Après le contenu actuel du dashboard (grille des enclos), ajouter :
1. **Section Stats globales** : Reprendre le contenu de `renderStats()` (lignes ~2306-2427) mais en tant que section du dashboard plutôt qu'onglet séparé. Adapter pour inclure les données de `S.accouplements` en plus de `S.archivedStats`.
2. **Section Paramètres** : Déplacer le sélecteur de son (lignes ~1421-1433), le toggle notifs (lignes ~1435-1449) et le bouton ntfy (lignes ~1451-1457) depuis le header de `renderTabs()` vers une card "Paramètres" en bas du dashboard.
**Calcul des stats depuis S.accouplements :**
```javascript
// Agréger accouplements dans les stats
const accoupBabies={};
let accoupTotal=0,accoupCouples=0;
(S.accouplements||[]).forEach(a=>{
accoupBabies[a.baby]=(accoupBabies[a.baby]||0)+a.babiesObtained;
accoupTotal+=a.babiesObtained;
accoupCouples+=a.couples;
});
```
**Step 3 : Retirer les settings du header dans renderTabs()**
Supprimer les lignes ~1421-1457 (sound select, notif btn, ntfy btn) de `renderTabs()`.
**Step 4 : Supprimer renderStats()**
Supprimer `renderStats()` (lignes ~2306-2427) et `resetStats()` (lignes ~2290-2304) — le code est intégré dans le dashboard.
**Step 5 : Commit**
```
feat: intègre stats et paramètres dans le dashboard, retire onglet Stats
```
---
## Task 5 : Sidebar navigation
**But :** Ajouter un menu hamburger avec sidebar overlay.
**Files:**
- Modify: `src/index.html` (CSS + HTML + JS)
**Step 1 : CSS de la sidebar**
```css
/* Sidebar */
.sidebar-toggle{position:fixed;top:12px;left:12px;z-index:1100;background:var(--bg3);border:1px solid var(--border);border-radius:8px;color:var(--text);font-size:1.3rem;padding:6px 10px;cursor:pointer;transition:background .2s;line-height:1}
.sidebar-toggle:hover{background:var(--bg4)}
.sidebar-overlay{position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:1200;opacity:0;pointer-events:none;transition:opacity .25s}
.sidebar-overlay.open{opacity:1;pointer-events:all}
.sidebar{position:fixed;top:0;left:-280px;width:270px;height:100%;background:var(--bg2);border-right:1px solid var(--border);z-index:1300;transition:left .25s;padding:60px 0 20px;overflow-y:auto;display:flex;flex-direction:column;gap:2px}
.sidebar.open{left:0}
.sidebar-item{display:flex;align-items:center;gap:10px;padding:10px 20px;color:var(--muted);font-size:.88rem;font-weight:600;cursor:pointer;transition:background .15s,color .15s;border-left:3px solid transparent}
.sidebar-item:hover{background:var(--bg3);color:var(--text)}
.sidebar-item.active{color:var(--text);border-left-color:var(--ok);background:var(--bg3)}
.sidebar-sep{height:1px;background:var(--border);margin:8px 16px}
.sidebar-label{padding:8px 20px;font-size:.7rem;font-weight:800;text-transform:uppercase;letter-spacing:1.5px;color:var(--muted);opacity:.6}
```
**Step 2 : HTML — ajouter le bouton et le conteneur sidebar**
Juste après `<body>` (ou avant la section `<header>`) :
```html
<button class="sidebar-toggle" onclick="toggleSidebar()" title="Menu"></button>
<div class="sidebar-overlay" id="sidebar-overlay" onclick="closeSidebar()"></div>
<nav class="sidebar" id="sidebar"></nav>
```
**Step 3 : Ajouter un padding-left au header pour ne pas chevaucher le bouton hamburger**
Ajouter `padding-left:50px` au `header` ou au conteneur principal.
**Step 4 : JS — fonctions sidebar**
```javascript
function toggleSidebar(){
const sb=document.getElementById('sidebar');
const ov=document.getElementById('sidebar-overlay');
const open=sb.classList.toggle('open');
ov.classList.toggle('open',open);
if(open)renderSidebar();
}
function closeSidebar(){
document.getElementById('sidebar').classList.remove('open');
document.getElementById('sidebar-overlay').classList.remove('open');
}
function sidebarNav(id){
selectEnclos(id);
closeSidebar();
}
function renderSidebar(){
const sb=document.getElementById('sidebar');
let html=`<div class="sidebar-item ${S.activeId==='dashboard'?'active':''}" onclick="sidebarNav('dashboard')">📊 Dashboard</div>`;
html+=`<div class="sidebar-sep"></div>`;
html+=`<div class="sidebar-label">Enclos</div>`;
S.enclos.forEach((enc,i)=>{
const label=enc.name||('Enclos '+(i+1));
const isActive=S.activeId===enc.id;
html+=`<div class="sidebar-item ${isActive?'active':''}" onclick="sidebarNav(${enc.id})">🐉 ${label}</div>`;
});
html+=`<div class="sidebar-sep"></div>`;
html+=`<div class="sidebar-label">Outils</div>`;
html+=`<div class="sidebar-item ${S.activeId==='accouplement'?'active':''}" onclick="sidebarNav('accouplement')">💑 Accouplement</div>`;
html+=`<div class="sidebar-item ${S.activeId==='appro'?'active':''}" onclick="sidebarNav('appro')">🧬 Réappro</div>`;
html+=`<div class="sidebar-item ${S.activeId==='inventaire'?'active':''}" onclick="sidebarNav('inventaire')">📦 Inventaire</div>`;
html+=`<div class="sidebar-item ${S.activeId==='workflows'?'active':''}" onclick="sidebarNav('workflows')">📋 Workflows</div>`;
sb.innerHTML=html;
}
```
**Step 5 : Commit**
```
feat: ajoute sidebar navigation overlay avec menu hamburger
```
---
## Task 6 : Réappro ♂/♀
**But :** Ajouter les contraintes de genre dans l'arbre de réapprovisionnement.
**Files:**
- Modify: `src/index.html`
**Step 1 : Modifier calcAppro() pour assigner ♂/♀**
Dans `calcAppro()` (ligne ~2507), modifier chaque étape pour indiquer le genre :
```javascript
// Convention : parentA = ♂, parentB = ♀ (sauf si inversé par l'utilisateur)
const inverted=approState.inverted||{};
const[rawA,rawB]=BREEDING_RECIPES[race];
const isInv=inverted[race];
const a=isInv?rawB:rawA, b=isInv?rawA:rawB;
// a = ♂, b = ♀
// Pour les besoins : marquer le genre
if(!needs[a])needs[a]={total:0,m:0,f:0};
if(!needs[b])needs[b]={total:0,m:0,f:0};
needs[a].m=(needs[a].m||0)+couplesReal;
needs[a].total=(needs[a].total||0)+couplesReal;
needs[b].f=(needs[b].f||0)+couplesReal;
needs[b].total=(needs[b].total||0)+couplesReal;
```
Le `needs` actuel est un simple nombre (`needs[race]=qty`). Il faut le transformer en objet `{total, m, f}`.
**Step 2 : Toggle d'inversion ♂/♀ par étape**
Ajouter dans `approState` :
```javascript
approState.inverted={}; // race → boolean
```
Fonction :
```javascript
function toggleApproGender(race){
if(!approState.inverted)approState.inverted={};
approState.inverted[race]=!approState.inverted[race];
calcAppro();
}
```
Bouton dans le rendu de chaque étape (à côté du toggle reproducteur) :
```html
<button class="btn btn-ghost" style="padding:4px 10px;font-size:.75rem"
onclick="toggleApproGender('${race}')" title="Inverser ♂/♀">
🔄 ♂↔♀
</button>
```
**Step 3 : Affichage ♂/♀ sur les mini-cards**
Modifier la fonction `miniCard()` dans `calcAppro()` pour accepter un paramètre genre :
```javascript
function miniCard(name,qty,gen,gender){
const gc=GEN_COLORS[gen||RACE_GEN[name]]||'#888';
const genderBadge=gender==='m'?'<span style="color:#50a0ff;font-weight:800"> ♂</span>'
:gender==='f'?'<span style="color:#ff64a0;font-weight:800"> ♀</span>':'';
return`<div style="display:flex;flex-direction:column;align-items:center;gap:4px;min-width:70px">
<div style="position:relative">${getDDImage(name)}
<span style="position:absolute;top:-4px;right:-8px;background:${gc};color:#fff;font-size:0.65rem;font-weight:800;padding:1px 6px;border-radius:8px">Gen ${gen||RACE_GEN[name]}</span>
</div>
<span style="font-weight:800;font-size:0.78rem;color:var(--text);text-align:center;line-height:1.1;max-width:90px">${name}${genderBadge}</span>
<span style="font-family:'Cinzel',serif;font-size:1.1rem;font-weight:700;color:${gc}">×${qty}</span>
</div>`;
}
```
Appels : `miniCard(s.parentA, s.couples, RACE_GEN[s.parentA], 'm')` et `miniCard(s.parentB, s.couples, RACE_GEN[s.parentB], 'f')`.
**Step 4 : Résumé Gen 1 avec genres**
Modifier le rendu des matières premières Gen 1 pour afficher `×4 ♂` et `×4 ♀` séparément :
```javascript
gen1Needs.forEach(([name,data])=>{
if(data.m>0) html+=miniCard(name,data.m,1,'m');
if(data.f>0) html+=miniCard(name,data.f,1,'f');
});
```
**Step 5 : Commit**
```
feat: ajoute contraintes ♂/♀ dans réappro avec toggle d'inversion
```
---
## Task 7 : Mise à jour du rendu des tabs + nettoyage final
**But :** S'assurer que les tabs affichent correctement les nouveaux onglets et nettoyer le code mort.
**Files:**
- Modify: `src/index.html`
**Step 1 : Mettre à jour renderTabs()**
Ajouter l'onglet Accouplement (💑) dans la barre horizontale, entre Dashboard et les enclos ou entre Réappro et Inventaire. S'assurer que l'onglet Stats n'apparaît plus.
**Step 2 : Mettre à jour renderContent()**
Vérifier le routing complet :
```javascript
function renderContent(){
clearElCache();
const c=document.getElementById('enclos-content');
if(S.activeId==='dashboard'){renderDashboard();return;}
if(S.activeId==='accouplement'){renderAccouplement();return;}
if(S.activeId==='appro'){renderApprovisionnement();return;}
if(S.activeId==='inventaire'){renderInventaire();return;}
if(S.activeId==='workflows'){renderWorkflows();return;}
// Sinon c'est un enclos
const enc=activeEnclos();
if(!enc)return;
renderEnclos(enc);
}
```
**Step 3 : Nettoyage**
- Supprimer tout code mort lié aux bébés dans les enclos
- Supprimer le rendu de l'onglet Stats dans renderTabs()
- Vérifier que `save()` persiste `S.accouplements`
- Vérifier que la migration `_migratedBabies120` fonctionne correctement
**Step 4 : Mettre à jour le README (version 1.2.0)**
**Step 5 : Commit**
```
feat: nettoyage final, routing mis à jour, v1.2.0 prêt
```
---
## Ordre d'exécution
| Task | Dépendance | Description |
|------|-----------|-------------|
| 1 | Aucune | Retrait bébés + migration |
| 2 | Aucune | Données accouplement + lookup |
| 3 | Task 2 | UI onglet accouplement |
| 4 | Task 1 | Stats dans dashboard + paramètres |
| 5 | Aucune | Sidebar navigation |
| 6 | Aucune | Réappro ♂/♀ |
| 7 | Tasks 1-6 | Nettoyage + routing final |
Tasks 1, 2, 5, 6 sont indépendantes et peuvent être faites en parallèle.
Task 3 dépend de 2. Task 4 dépend de 1. Task 7 finalise le tout.