Files
mimic/tasks/todo.md

209 lines
10 KiB
Markdown
Raw Permalink Normal View History

# Sprint 13 — SimulationFormPage : layout en 3 tabs (Red Team / SOC / Tâche C2)
**Base** : `sprint/12-i18n-fr` (PR #14 ouverte, pas encore mergée). Sprint 13 enchaîne directement.
**Worktree** : `.claude/worktrees/sprint-13-sim-tabs/` (dédié — conforme à `feedback_worktree_per_sprint`).
**Branch** : `sprint/13-sim-tabs`.
**Scope** : frontend uniquement. Backend intouché. Aligné sur le pattern Tabs du sprint 11 (EngagementDetailPage 3→2 tabs) appliqué à SimulationFormPage.
---
## Décisions binding (lockées par user)
1. **Mode création** : 3 tabs visibles mais SOC + Tâche C2 **désactivés** (`aria-disabled="true"`, non-clickables, opacité réduite). Pas de banner ni d'empty-state — juste tab disabled.
2. **Tab SOC** : désactivé tant que la sim n'est pas en `review_required` (ou plus). Force l'opérateur à passer par le RT pour avancer (Mark for review depuis sticky bar RT).
3. **Sticky bar entièrement contextuelle** par tab :
- **RT tab** : `[Save, Mark for review]` (et `[Delete]` global, voir plus bas)
- **SOC tab** : `[Save SOC, Close]`
- **C2 tab** : aucune sticky bar — actions inline (`Execute via C2`, `Import C2 history`) déjà dans le tab content
4. **Count pill sur tab Tâche C2** : nombre exact de tâches C2 (depuis le hook `useSimulationC2Tasks` existant). Caché si 0 ou en mode création.
## Décisions secondaires (à appliquer par défaut)
- **Default active tab** : Red Team (premier opérateur surface).
- **Wiring URL** : `useHashTab('red-team')` (réutilise sprint 11). Hashes : `#red-team`, `#soc`, `#c2`.
- **Delete + Reopen** : pas mentionnés dans la sticky bar contextuelle user. Décision team-lead :
- **Delete** sur le tab **RT** (action destructrice côté RT — c'est l'auteur).
- **Reopen** sur le tab **SOC** (cohérent avec Close, même actor flow). Visible quand status `done`.
- **AlertBanner** (sprint 12) : `Done banner` reste visible au-dessus des tabs (status info, pas tab-spécifique). `SOC-blocked banner` disparaît (remplacé par le mécanisme de tab désactivé).
- **C2TasksPanel** (sprint 8) : déplace son rendering DANS le panel du tab C2. Plus en-dessous de la 2-col.
- **Header** (back link, title, status badge) : reste **au-dessus** des tabs, intouché.
## Contraintes constantes
- Primary `#024ad8` Electric Blue intouchée.
- Brutalisme intact : `rounded-none` (exceptions documentées), zéro `transition-*`, zéro `shadow-*`.
- DESIGN.md amendments additives uniquement (sub-section "Sub-page tabs with disabled state" si besoin).
- Tous les nouveaux labels passent par `i18n/fr.json` (cf. sprint 12) — pas de string EN hardcodée.
- `pnpm build` MANDATORY avant push (leçon `feedback_css_apply_tokens`).
---
## Task 1 — Extend Tabs primitive : support `disabled` per item
**File** : `frontend/src/components/Tabs.tsx`
Current `TabItem` :
```tsx
interface TabItem<T extends string> { id: T; label: string; count?: number; }
```
Target :
```tsx
interface TabItem<T extends string> { id: T; label: string; count?: number; disabled?: boolean; }
```
Comportement quand `disabled` :
- `<button>` reçoit `disabled={true}` et `aria-disabled="true"`
- Class CSS `.tab-underline-disabled` ajoutée (nouvelle recipe ci-dessous)
- Le hover ne change pas la couleur (`hover:text-graphite` ou rien)
- ArrowLeft/ArrowRight nav skippent les tabs disabled (cycle vers le tab actif suivant non-disabled)
- Clicks ignorés (`onClick={() => !disabled && onChange(item.id)}`)
- `aria-selected` reste `false` quand disabled
**New recipe** dans `frontend/src/styles/index.css` :
```css
.tab-underline-disabled {
@apply text-graphite opacity-50 cursor-not-allowed;
}
```
(Override le hover du `.tab-underline` parent ; pas de `border-primary` sur l'active state quand disabled — n'arrivera jamais puisque `aria-selected=false`.)
**Tests** : extend `Tabs.test.tsx` :
- Tab avec `disabled: true` ne fire pas `onChange` au click
- ArrowRight/ArrowLeft skippent les tabs disabled
- `aria-disabled="true"` présent
- Classe `.tab-underline-disabled` appliquée
DESIGN.md : ajouter 1 ligne dans la sub-section `Sub-page tabs` documentant la variante `disabled`.
---
## Task 2 — SimulationFormPage : refactor en 3 tabs
**File** : `frontend/src/pages/SimulationFormPage.tsx`
**Layout cible (edit mode)** :
```
[Header : ← Retour à l'engagement | titre sim | status badge]
[AlertBanner Done si done (au-dessus des tabs, status info global)]
[Tabs : Red Team | SOC (disabled si !=review_required+) | Tâche C2 (count pill)]
[Panel du tab actif :
- Red Team : tous les fields RT (Name, MITRE Techniques, Description, Commands, Prerequisites, Executed at, Execution result) + actions inline (Execute via C2, Import C2 history déjà existants en sprint 8/12)
- SOC : tous les fields SOC (Log source, Logs, SOC comment, Incident number)
- Tâche C2 : C2TasksPanel (sprint 8) directement, sans wrapper
]
[Sticky bar bottom (contextuel selon tab actif) :
- RT : [Save] [Mark for review (si pending/in_progress)] ... [Delete (toujours)]
- SOC : [Save SOC] [Close (si review_required)] [Reopen (si done)]
- C2 : aucune sticky bar (display:none ou pas de render)
]
```
**Layout en mode création (new)** :
```
[Header : ← Retour à l'engagement | "Nouvelle simulation"]
[Tabs : Red Team (active) | SOC (disabled) | Tâche C2 (disabled, pas de count pill)]
[Panel Red Team : champs simplifiés (juste Name + MITRE Techniques minimum)]
[Sticky bar bottom : [Create simulation] [Cancel]]
```
**Wiring** :
- `useHashTab<TabId>('red-team')``TabId = 'red-team' | 'soc' | 'c2'`.
- Items du Tabs :
```tsx
const items: TabItem<TabId>[] = [
{ id: 'red-team', label: t('simulation.form.tab.redTeam') },
{
id: 'soc',
label: t('simulation.form.tab.soc'),
disabled: !editing || sim.status === 'pending' || sim.status === 'in_progress',
},
{
id: 'c2',
label: t('simulation.form.tab.c2'),
count: editing ? c2Tasks?.length : undefined,
disabled: !editing,
},
];
```
- Le hook `useHashTab` doit fallback proprement : si `#soc` mais tab disabled, retomber sur `'red-team'`. **Vérifie le comportement actuel** (sprint 11 a peut-être déjà ça, sinon ajout dans la même PR).
**Sticky bar contextuel** :
- Wrapper `<div className="sticky bottom-0 …">` avec contenu conditionnel `{activeTab === 'red-team' && <RtActions />}{activeTab === 'soc' && <SocActions />}{activeTab === 'c2' && null}`.
- Ne pas rendre le wrapper du tout si C2 actif (pas de sticky div vide).
- Réutiliser les boutons existants (Save/Mark/Save SOC/Close/Reopen/Delete) — juste les regrouper.
**Banners** :
- `Done banner` (success) : garde au-dessus des tabs, indépendant du tab actif.
- `SOC-blocked banner` (warn) : **supprime**. Le mécanisme de tab désactivé remplace.
**C2TasksPanel** : déplacer son rendering directement dans `{activeTab === 'c2' && <C2TasksPanel … />}`. Conserver toutes les props existantes. Le `[Execute via C2]` et `[Import C2 history]` qui étaient dans la carte Red Team du sprint 8 — décision : **les laisser dans le tab Red Team** (ce sont des actions RT qui déclenchent le C2). Le tab C2 montre juste le panel des tâches.
**Tests** : `SimulationFormPage.test.tsx` — refactor important :
- Les anciens tests "RT card visible AND SOC card visible simultaneously" → maintenant on test "RT tab content visible, SOC tab content via switch-tab".
- Tests Mark/Close/Reopen : naviguer vers le bon tab avant d'asserter le bouton.
- Tests `data-testid="soc-blocked-banner"` : ce testid disparaît — ajuster ou supprimer le test.
- Add : tab SOC `disabled` quand status pending → test que click ne change pas le tab actif.
- Add : count pill sur tab C2 reflète le nombre de tasks.
## Task 3 — i18n keys nouvelles + DESIGN.md
**`fr.json`** ajouter :
```json
"simulation.form.tab.redTeam": "Red Team",
"simulation.form.tab.soc": "SOC",
"simulation.form.tab.c2": "Tâche C2"
```
(`Red Team` reste idiom, `SOC` acronyme — cohérent avec les conventions sprint 12.)
**DESIGN.md** § Navigation > Sub-page tabs : ajouter 1 paragraphe sur la variante `disabled` (visual : opacity-50 + cursor-not-allowed, aria-disabled, skipped par arrow-keys).
---
## Counts attendus
| Métrique | Valeur |
|---|---|
| Fichiers source modifiés | 3 (`Tabs.tsx`, `SimulationFormPage.tsx`, `index.css` recipe) |
| Fichiers source touched (juste i18n keys) | 1 (`fr.json`) |
| Fichiers tests modifiés | 2 (`Tabs.test.tsx`, `SimulationFormPage.test.tsx`) |
| Nouveaux fichiers | 0 |
| DESIGN.md amendments | 1 paragraphe additive |
---
## Commits plan (~4 commits)
1. `feat(frontend): Tabs primitive supports per-item disabled state` (Tabs.tsx + recipe + Tabs.test.tsx)
2. `feat(frontend): SimulationFormPage 3-tab layout (Red Team / SOC / C2 tasks)` (SimulationFormPage.tsx + tests + i18n keys + DESIGN.md amendment)
3. `refactor(frontend): contextual sticky bar per active tab on SimulationFormPage` (extraction des actions par tab, supprimer SOC-blocked banner)
4. `docs(changelog): sprint 13 simulation tabs entry`
Collapse à 2-3 si plus efficace.
---
## DoD
1. SimulationFormPage en edit mode : 3 tabs visibles (Red Team default, SOC, Tâche C2). Count pill sur C2 = `c2Tasks.length`.
2. Tabs SOC + C2 désactivés en mode création.
3. Tab SOC désactivé tant que status ∉ `{review_required, done}`.
4. Sticky bar contextuelle : RT [Save + Mark for review + Delete], SOC [Save SOC + Close + Reopen], C2 [aucune sticky bar].
5. `useHashTab` gère le fallback gracieux quand hash pointe vers un tab disabled (retombe sur `'red-team'`).
6. Tabs primitive : ArrowLeft/ArrowRight skippent les tabs disabled.
7. AlertBanner Done garde sa place au-dessus des tabs (status global). SOC-blocked banner supprimé.
8. C2TasksPanel rendu dans le tab C2 panel.
9. `[Execute via C2]` + `[Import C2 history]` restent dans le tab Red Team.
10. Tests : Tabs.test.tsx + SimulationFormPage.test.tsx adaptés, baseline ≥ 245 vitest, +N nouveaux tests sur disabled tabs + count pill + tab gating.
11. `tsc --noEmit` + `eslint --max-warnings=0` + `pnpm build` clean.
12. CHANGELOG.md entrée sprint 13.
## Out of scope (explicite)
- ❌ Refactor des sub-forms RT / SOC (juste déplacement dans le tab)
- ❌ Nouveau pattern Tabs ailleurs (uniquement SimulationFormPage cette sprint)
- ❌ Changes backend
- ❌ Color / brutalism / token changes
- ❌ Multi-langue (FR uniquement, infra sprint 12)