Files
mimic/tasks/todo.md

10 KiB

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 :

interface TabItem<T extends string> { id: T; label: string; count?: number; }

Target :

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 :

.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 :
    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 :

"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)