docs(sprint 7): plan — terminal-SOC design refresh

9 binding decisions locked with user 2026-06-09 (4-question + 4-question
+ 3-question rounds). Visual direction Bloomberg / terminal SOC. Border
radius 0 except status pills and avatars. Palette kept (primary blue +
slab + canvas/paper/cloud/fog/ink), ADD success/warn semantic tokens.
Scope: 8 pages + 17 components + tokens + DESIGN.md rewrite, all in one
sprint. JetBrains Mono for data only (Inter stays for body/headers).
Light + dark both kept. Zero transitions (brutalist).

Plan validated by spec-reviewer pre-pass: APPROVED with 3 findings
addressed inline (D9 added, R2 reworded, semantic tokens promoted from
optional to locked).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Knacky
2026-06-09 18:34:45 +02:00
parent c85ece46b9
commit 5627d7dcfa

View File

@@ -1,281 +1,153 @@
# Sprint 6Engagement export (Markdown + CSV + PDF) # Sprint 7Design Refresh: Terminal-SOC Aesthetic
> Branch : `sprint/6-export` · Worktree : `.claude/worktrees/sprint-6-export` · Base : `main` @ `678ee8f` > Branch : `sprint/7-design` · Worktree : `.claude/worktrees/sprint-7-design` · Base : `main` @ `e27babe`
## §0 — Binding decisions (locked with the user 2026-06-07) ## §0 — Binding decisions (locked with user, 2026-06-09)
1. **Scope du sprint** : export d'un engagement (header + toutes ses simulations RT + SOC) vers Markdown, CSV et PDF — clôt la boucle « remplace l'utilisation d'un fichier excel plat partagé entre la redteam et les analystes SOC en fin de mission ». 1. **Visual direction**: Bloomberg / Terminal-SOC — dense, brutalist, semantic colors strong, no ornament.
2. **Formats livrés** : Markdown, CSV, PDF (3 formats). JSON exclu (redondant avec l'API existante). 2. **Border-radius**: **0 everywhere** except status pills (`rounded-pill`) and avatars (round). All buttons, cards, modals, inputs, dropdowns, tables, tags → angular.
3. **RBAC** : `admin` + `redteam` peuvent exporter. **SOC = pas d'accès** (pas de bouton dans l'UI, endpoint `/api/engagements/<eid>/export` → 403). Cohérent avec le pattern templates sprint 5 (livrable RedTeam). 3. **Palette**: KEEP current (`#024ad8` primary blue, `slab #111827`, `canvas/paper/cloud/fog/ink` light+dark vars). ADD semantic tokens `success-green` + `warn-amber` (confirmed scope add — needed for SOC-grade status legibility on dashboards and badges).
4. **Contenu de l'export** : Engagement header (name, description, dates, status, created_by, created_at) + **toutes** les simulations de l'engagement, avec leurs champs RT (name, techniques, tactics, description, commands, prerequisites, executed_at, execution_result, status) ET SOC (log_source, logs, soc_comment, incident_number). Ordre des simulations : `id ASC` (ordre de création). 4. **Scope**: Refonte globale en 1 sprint (all 8 pages + 17 components + tokens + DESIGN.md).
5. **Déclenchement UI** : un bouton **split-button dropdown** sur `EngagementDetailPage` libellé `[Export ▼]`, qui ouvre un menu `Markdown / CSV / PDF`. Click → download direct (Blob + `URL.createObjectURL`). Pas de modal de configuration. Pattern réutilisé du dropdown sprint 5 (`SimulationList`). 5. **Monospace**: data-only — JetBrains Mono for IDs, dates ISO, commands, execution output, MITRE techniques, metrics. Inter stays for body/labels/headers.
6. **Mono font**: JetBrains Mono, bundled locally via `@fontsource-variable/jetbrains-mono` (consistent with existing Inter bundle).
7. **Modes**: KEEP light + dark both. Toggle stays.
8. **Animations**: **Brutalist — zero transition**. Remove all `transition-*` utilities, focus rings sharp, hover instantaneous.
9. **Display scale reduction**: locked. `display-xxl 72→40`, `display-xl 56→32`, `display-lg 44→28`, `display-md 32→24`, `display-sm 24→20`, `display-xs 20→16`. Headers stay modest in terminal aesthetic — no editorial flourish at hero scale.
### Décisions techniques arrêtées par le team-lead (à challenger par spec-reviewer) ## §1 — Pre-work checks (team-lead, before dispatch)
6. **Endpoint backend** : **un seul** endpoint `GET /api/engagements/<eid>/export?format=md|csv|pdf` plutôt que 3 endpoints distincts. Une seule route à protéger (RBAC), un seul test d'intégration RBAC, switch sur `format` en interne. Format inconnu → **400** `{error: "format must be one of: md, csv, pdf"}`. Format manquant → **400** (pas de défaut implicite — évite l'ambiguïté). - [ ] Confirm `tasks/lessons.md` has nothing contradicting this brief
7. **Markdown** : généré via string templating Python (pas de lib externe). Simple, déterministe, testable par assertion de sous-chaînes. - [ ] Verify uncommitted `.claude/agents/frontend-builder.md` patch (Skill mandatory) is restored in worktree — sprint hygiene
8. **CSV** : généré via `csv.writer` (stdlib). Une ligne d'en-tête + N lignes simulations. Colonnes : `id, name, status, techniques (joined "|"), tactics (joined "|"), description, commands, prerequisites, executed_at, execution_result, log_source, logs, soc_comment, incident_number, created_at, updated_at`. **Pas de header engagement dans le CSV** (format machine-readable strict) ; l'engagement context sort dans le filename. - [ ] Send plan to **spec-reviewer** for 2-pass approval (vs SPEC.md, vs §0 binding decisions). MUST be APPROVED before any code touches `frontend/`.
9. **PDF** : généré via **WeasyPrint** (Python HTML→PDF, lib mature, qualité de rendu pro, dépendances système cairo/pango/gdk-pixbuf à ajouter au `python:3.12-slim` du Dockerfile). Pipeline : on génère **le même HTML** que pour le Markdown (mais wrappé en `<html>...<style>...</html>`), puis WeasyPrint le rend en PDF. Le styling CSS est inline (≤ 30 lignes : hierarchy h1/h2/h3, code-block monospace, alternance fond pour les simulations). Pas de logo / page de garde — keep it simple. - [ ] After approval: dispatch frontend-builder with this todo as brief.
10. **Filename convention** : `engagement-<id>-<slugified-name>-YYYYMMDD.{ext}`. Slugification :
```python
import unicodedata, re
normalized = unicodedata.normalize('NFKD', name).encode('ascii', 'ignore').decode()
slug = re.sub(r'[^a-z0-9]+', '-', normalized.lower()).strip('-')[:60] or "unnamed"
```
Le NFKD-strip enlève les accents proprement (`Opération` → `Operation`), le fallback `"unnamed"` couvre le edge case d'un nom 100 % non-alphanum (`"---!!!"` → `""` → `"unnamed"`). `YYYYMMDD` = `date.today().strftime('%Y%m%d')` côté serveur. Le frontend lit `Content-Disposition` pour le nom du fichier.
11. **Content-Type** : `text/markdown; charset=utf-8`, `text/csv; charset=utf-8`, `application/pdf`.
12. **Génération synchrone** : Flask renvoie le fichier dans la même requête (un engagement = quelques dizaines de simulations max, génération < 500 ms même PDF). Pas de job async.
13. **Pas de cache** : chaque export régénère depuis la DB (état toujours frais).
14. **Frontend client** : pour télécharger, on utilise un fetch avec `responseType: 'blob'`, on lit `Content-Disposition` pour le filename, puis `URL.createObjectURL` + `<a>` invisible + `click()`. Pas de navigation. Le bouton `[Export ▼]` utilise la classe **`btn-outline`** (la même que le bouton `Edit` du header existant — cohérence visuelle directe). Le dropdown wrapper réutilise le même token set que le sprint 5 `NewSimulationDropdown` (`shadow-floating` + `dark:shadow-floating-dark`, `bg-canvas` + `dark:bg-fog`).
### Points OUVERTS pour le spec-reviewer (à valider Pass 1) ## §2 — Sprint hygiene (commit #1)
- **WeasyPrint vs alternatives** : retenu pour rendu pro + pipeline HTML mutualisable. Alternatives écartées : `reportlab` (layout programmatique = beaucoup plus de code), `xhtml2pdf` (rendu inférieur), `pdfkit + wkhtmltopdf` (binaire externe en archive partielle). Le coût Dockerfile (≈ 50 MB de libs cairo/pango) est accepté. - [ ] `chore(agents): frontend-builder must invoke Skill frontend-design before UI work` — lands BEFORE design work so the agent itself triggers the Skill on first call this sprint.
- **CSV sans header engagement** : choix de pureté tabulaire (Excel-friendly direct). Le team-lead a tranché. Spec-reviewer doit confirmer ou proposer la variante "1 ligne commentaire `# Engagement: <name>`".
- **Pas de JSON export** : redondant avec l'API. À confirmer.
- **Statut `done` inclus comme tous les autres** : pas de filtre par défaut. L'utilisateur exporte toujours TOUT.
--- ## §3 — Foundation: DESIGN.md + tokens (commits #2#4)
## §1 — Backend (Sonnet · backend-builder) ### §3.1 DESIGN.md rewrite (commit #2)
### Modèle de données - [ ] Replace current HP-catalog doc (346 lines, off-brand) with terminal-SOC spec covering:
**Aucun changement** de modèle. Pas de migration. L'export est en lecture seule sur les modèles existants `Engagement` + `Simulation`. - **Overview**: brutalist BAS Purple Team console, angular surfaces, semantic color signals, data-monospace hybrid
- **Colors**: keep all existing tokens with **role redefinition** for terminal-SOC context. Primary = neutral action. Bloom-deep/coral = destructive/alert. ADD `success` (green) + `warn` (amber) — locked §0 D3 — with light + dark variants and WCAG AA contrast on slab and canvas surfaces
- **Typography**: Inter (body/headers/labels) + JetBrains Mono (data). Concrete tier table with size/weight/line-height
- **Layout**: tighter spacing (replace `section 80px``section 48px`; halve card padding on dense surfaces)
- **Shapes**: ALL radii = 0 except `rounded-pill` reserved for status badges and avatars
- **Components**: re-document `btn-*`, `text-input`, `card-*`, `badge-*`, `nav-*`, `modal-*` with brutalist specs (no shadow, hairline borders, zero transition)
- **Do's/Don'ts**: zero rounded on conteneurs; zero transitions; semantic colors only on status surfaces; mono ONLY for data, never headers
- **Iteration guide**
- [ ] Doc lives in English (in-repo).
### Services / serializers ### §3.2 Tailwind token refresh (commit #3)
- Nouveau module **`backend/app/services/export.py`** avec 3 fonctions pures testables unitairement :
- `render_engagement_markdown(engagement: Engagement, simulations: list[Simulation]) -> str`
- `render_engagement_csv(engagement: Engagement, simulations: list[Simulation]) -> str`
- `render_engagement_pdf(engagement: Engagement, simulations: list[Simulation]) -> bytes`
- Le rendu Markdown réutilise `_enrich_techniques` + `_enrich_tactics` de `serializers.py` pour avoir `[{id, name}]` au lieu de juste les IDs.
- Le rendu PDF construit l'HTML à partir d'un template Python `_render_engagement_html(engagement, simulations) -> str` (string templating, pas Jinja — KISS) **et le passe à `weasyprint.HTML(string=html).write_pdf()`. Important : le PDF est généré à partir des MÊMES DONNÉES (engagement + simulations) que le Markdown, PAS à partir du string Markdown — `_render_engagement_html` est un rendu distinct.**
- **Rendu de `created_by`** : pour Markdown et CSV, on rend la `username` seule (`engagement.created_by.username`), pas la dict `{id, username}`. Pour la cohérence du livrable handoff. Idem pour `simulation.created_by`.
- **MITRE non chargé** : si le bundle n'est pas chargé, `_enrich_techniques` retourne `tactics: []` silencieusement (cohérent avec `serialize_simulation` existant — pas de 503 dans l'export). Le render doit continuer sans crash. Test dédié exigé (cf. § Tests).
### Endpoint - [ ] `frontend/tailwind.config.ts`:
- Extension du blueprint `engagements_bp` existant. Path : `GET /api/engagements/<int:eid>/export?format=md|csv|pdf`. - `borderRadius`: keep `none: 0`, keep `pill: 9999px`. Drop or stop using `xs/sm/md/lg/xl` for surfaces — keep only if a badge variant needs `2px`
- Décorateur : `@role_required("admin", "redteam")`. - ADD `fontFamily.mono`: `['JetBrains Mono Variable', 'JetBrains Mono', 'ui-monospace', 'monospace']`
- Logique : - ADD semantic colors (locked §0): `success: { DEFAULT, soft }` (green) + `warn: { DEFAULT, soft }` (amber). Pull dark-mode variants from CSS vars too. Suggested anchors — `success #16a34a` (dark `#22c55e`), `warn #d97706` (dark `#f59e0b`); design-reviewer audits WCAG AA at both modes.
1. Charger l'engagement (404 si absent). - Reduce `display-*` scale (locked §0): `display-xxl 72px → 40px`, `display-xl 56→32`, `display-lg 44→28`, `display-md 32→24`, `display-sm 24→20`, `display-xs 20→16` — terminal headers are modest
2. Parse `format` query param. Format manquant ou inconnu → 400 `{error: "format must be one of: md, csv, pdf"}`. - Drop `tracking[0.7px]` and uppercase from `button-md` (still ALLCAPS via class but no letter-spacing)
3. Charger les simulations triées par `id ASC`. - Drop shadow tokens or keep but ensure no component class applies them
4. Appeler la fonction `render_engagement_<fmt>(engagement, simulations)`. - [ ] `frontend/src/styles/index.css`:
5. Construire la `Response` avec `Content-Type`, `Content-Disposition: attachment; filename="<slug>.<ext>"`, et le body. - Drop `font-size: 16.5px` root bump (back to `16px` standard)
- Filename helper : `_export_filename(engagement, ext) -> str` (slugifier + date). - Set body `line-height: 1.4`, tighten headings to 1.1
- Rewrite `.btn-primary/ink/outline/outline-ink`: `rounded-none`, NO `transition-colors`, keep `uppercase`, drop `tracking-[0.7px]`, keep `h-11` (touch target)
- Rewrite `.text-input`: `rounded-none`, focus border-primary sharp (no halo), no transition
- Rewrite `.card-product` and any `.card-*`: `rounded-none`, no shadow, 1px hairline border for separation
- Rewrite `.badge-pill-*`: keep `rounded-pill` ONLY here (status badges); strip uppercase if applied
- Rewrite `.modal-backdrop`: same dark backdrop, no rounded for the modal frame itself
- ADD `.mono` utility or rely on Tailwind's `font-mono` (preferred) for data cells
### Tests ### §3.3 JetBrains Mono bundle (commit #4)
**Cible : 226 → 245+ pytest passing.**
Fichiers nouveaux : - [ ] `cd frontend && npm i @fontsource-variable/jetbrains-mono`
- `backend/tests/test_export_engagement.py` — couvre l'endpoint + RBAC + format inconnu. - [ ] `frontend/src/styles/fonts.css`: add `@import '@fontsource-variable/jetbrains-mono'`
- `test_export_markdown_admin_returns_md_with_engagement_header_and_all_simulations` - [ ] No CDN. Confirms via `npm ls @fontsource-variable/jetbrains-mono`.
- `test_export_markdown_redteam_ok`
- `test_export_markdown_soc_403`
- `test_export_csv_returns_csv_with_one_row_per_simulation`
- `test_export_csv_columns_match_contract` (assert exact header row)
- `test_export_csv_escapes_special_characters` (commands avec virgule, guillemet, newline)
- `test_export_pdf_returns_pdf_magic_bytes_and_non_empty`
- `test_export_unknown_format_400`
- `test_export_missing_format_400`
- `test_export_unknown_engagement_404`
- `test_export_engagement_with_zero_simulations_renders_header_only`
- `test_export_unauthenticated_401`
- `test_export_filename_slugifies_name_and_carries_date`
- `backend/tests/test_export_render.py` — tests unitaires sur les 3 fonctions pures.
- `test_render_engagement_markdown_includes_header_fields`
- `test_render_engagement_markdown_lists_all_simulations_in_order`
- `test_render_engagement_markdown_includes_techniques_with_id_and_name`
- `test_render_engagement_markdown_includes_tactics`
- `test_render_engagement_markdown_includes_soc_fields_even_when_blank` (cohérence handoff)
- `test_render_engagement_markdown_escapes_backticks_in_commands` (fenced code block safety)
- `test_render_engagement_csv_has_header_row`
- `test_render_engagement_csv_joins_multi_techniques_with_pipe`
- `test_render_engagement_pdf_starts_with_pdf_magic` (assert `output[:4] == b'%PDF'`)
- `test_render_engagement_markdown_with_mitre_bundle_not_loaded_does_not_crash` (assert render OK et contient les technique IDs même quand le bundle MITRE est absent — sécurise les Docker cold-starts)
### Dépendances ## §4 — Component sweep (commit #5)
- `weasyprint>=60.0` ajouté à `backend/requirements.txt`.
- `docker/Dockerfile` stage Python : ajouter les libs minimales WeasyPrint pour Debian slim. **Set minimal pour text-only PDF** :
```
apt-get install -y libcairo2 libpango-1.0-0 libpangoft2-1.0-0 libharfbuzz0b libfontconfig1 shared-mime-info
```
**Note** : `libgdk-pixbuf-2.0-0` n'est requis QUE si on intègre des images dans le PDF. Notre rendu est text-only → on peut s'en passer. Le builder confirme via `weasyprint --info` dans le container après build. Documenter dans le PR.
### Livrable backend-builder (summary attendu) Rule: `rounded-*``rounded-none` unless explicitly an avatar or status pill; remove `transition-*`; data text → `font-mono`.
- **PREMIÈRE LIGNE OBLIGATOIRE** du summary (lesson sprint 5 — URL drift silencieuse interdite) :
```
endpoint final = GET /api/engagements/<int:eid>/export?format=md|csv|pdf
```
Texte EXACT, pas paraphrasé. Si le builder a choisi un autre path, il le déclare ici en deviation.
- Tous les fichiers créés/modifiés
- Contrat API précis (statuts, query params, headers de réponse) en table
- Liste des helpers réutilisés (`_enrich_techniques`, `_enrich_tactics`, `serialize_user_brief`)
- **Section "Déviations vs plan"** explicite (cf. lesson sprint 5)
- Résultats pytest + ruff + mypy
--- - [ ] `Layout.tsx`: nav-bar/utility-strip already angular — confirm. Remove `transition-colors` on theme button and hover-underline transitions. Mono font for any data label exposed (e.g. user.role pill).
- [ ] `StatusBadge.tsx`: KEEP rounded → switch to `rounded-pill` (it's a status pill, locked exception). Audit semantic mapping (planned/active/closed → semantic tokens once added).
- [ ] `SimulationStatusBadge.tsx`: same — `rounded-pill`, semantic colors aligned with new tokens.
- [ ] `FormField.tsx`: angular inputs (already via `.text-input` recipe — confirm).
- [ ] `EmptyState.tsx`: angular wrapper. No rounded illustration container.
- [ ] `ErrorState.tsx`: angular. Bloom-deep border-left if signalling.
- [ ] `LoadingState.tsx`: drop any rounded spinner background. Spinner shape ok.
- [ ] `ConfirmDialog.tsx`: angular modal. Buttons via new `.btn-*` recipes.
- [ ] `Toast.tsx`: angular. Semantic color border-left strip.
- [ ] `ExportEngagementButton.tsx` (sprint 6): angular dropdown menu. Audit `rounded-*` in the menu/item classes.
- [ ] `MitreMatrixModal.tsx`: angular modal. Cells already grid — confirm no rounded.
- [ ] `MitreTechniquePicker.tsx`: angular dropdown.
- [ ] `MitreTechniquesField.tsx`: angular chips.
- [ ] `MitreTechniqueTag.tsx`: angular tag (NOT pill — terminal tag, not a status). Decide once and apply consistently across MITRE surfaces.
- [ ] `TemplatePickerModal.tsx`: angular modal.
- [ ] `SimulationList.tsx`: angular table. Data cells (commands, executed_at, MITRE techniques) → `font-mono`.
- [ ] `ProtectedRoute.tsx`: no visual surface, skip.
## §2Frontend (Sonnet · frontend-builder) ## §5Page sweep (commit #6)
### Composants For each page: header/body/footer review, replace rounded card containers with angular hairline-bordered containers, ensure data cells use mono.
- **`ExportEngagementButton.tsx`** (nouveau) : split-button dropdown style sprint 5.
- Bouton principal `Export` (icône `Download` lucide-react) + chevron à droite (icône `ChevronDown`).
- **IMPORTANT — différence sémantique vs `NewSimulationDropdown` sprint 5** : les DEUX moitiés (label `Export` + chevron) ouvrent le dropdown. Il n'y a PAS d'action par défaut sur le click gauche (parce qu'il n'y a pas de format "défaut" évident parmi Markdown/CSV/PDF). Ce n'est PAS le même pattern que `[+ New]` (où la gauche navigue vers `/.../new` blank).
- Dropdown : 3 items "Markdown" / "CSV" / "PDF". Click → mutation download.
- Fermeture : click outside + Escape (réutiliser le hook/effet du dropdown sprint 5 dans `SimulationList`).
- Loading state : pendant la mutation, le composant affiche un spinner inline sur l'item cliqué, le dropdown reste ouvert. Désactive les 3 items pendant l'in-flight.
- Toast erreur sur 4xx/5xx.
- **`data-testid="export-dropdown"`** sur le wrapper du composant pour permettre au test-verifier d'asserter la présence/absence DOM (AC-30.1).
- Style : utiliser la classe utilitaire **`btn-outline`** (la même que le bouton `Edit` du header existant) — cohérence visuelle directe avec le header.
- **`EngagementDetailPage.tsx`** : intégrer `<ExportEngagementButton engagementId={engagement.id} />` dans le header de la page, à côté du bouton `Edit` existant. **Visible uniquement si `currentUser.role in ['admin', 'redteam']`** (gate côté UI + RBAC backend de toute façon en force) — réutiliser le helper `canEditEngagements` de `useAuth` (le même rôle set).
### API client - [ ] `LoginPage.tsx`: angular form card. Drop ornament.
- **`frontend/src/api/exports.ts`** (nouveau) : - [ ] `EngagementsListPage.tsx`: angular table container (currently `.card-product` with rounded-xl). Data cells (dates) → mono.
- `downloadEngagementExport(engagementId: number, format: 'md' | 'csv' | 'pdf'): Promise<void>` — fait un GET `/api/engagements/<id>/export?format=<fmt>` avec `responseType: 'blob'`, lit `Content-Disposition` pour le filename, crée un `Blob` + `URL.createObjectURL` + `<a>.click()`, puis `URL.revokeObjectURL`. **Contrat d'erreur** : sur réponse non-2xx, parse le JSON `{error: "..."}` du body (ou défaut "Export failed") et **throw un `Error`** avec le message — laisse le caller catcher pour le toast. - [ ] `EngagementDetailPage.tsx`: angular header section. Engagement metadata (start/end dates, IDs, created_at) in mono. Simulations table covered via SimulationList.
- Helper `parseContentDispositionFilename(header: string | undefined): string | null` (regex `filename="..."`, fallback null). - [ ] `EngagementFormPage.tsx`: angular form. Date inputs ok.
- [ ] `SimulationFormPage.tsx`: angular form. Commands textarea → mono.
- [ ] `TemplatesListPage.tsx`: angular list.
- [ ] `TemplateFormPage.tsx`: angular form. Commands field → mono.
- [ ] `UsersAdminPage.tsx`: angular table. Username column → mono (it's an ID).
### Types ## §6 — Test refresh (commit #7)
- Aucun nouveau type API (l'export retourne un Blob).
### Tests - [ ] `cd frontend && npm run test -- --run` — identify failing assertions on class names (`rounded-xl`, `card-product`, etc.). Update tests to use semantic queries (role, name, data-testid) where possible; if test asserts on visual class, update assertion to the new class.
**Cible : 121 → 130+ vitest passing.** - [ ] No new vitest tests added (visual sprint, behavior unchanged).
- [ ] Playwright e2e: should be `data-testid`-driven — run full suite to confirm no regression. If breakage, fix the testid not the test logic.
Fichiers nouveaux : ## §7 — Reviews
- `frontend/tests/ExportEngagementButton.test.tsx`
- `renders Export button with chevron`
- `clicking primary opens dropdown with three formats`
- `clicking outside closes dropdown`
- `Escape closes dropdown`
- `clicking Markdown triggers download with format=md`
- `clicking CSV triggers download with format=csv`
- `clicking PDF triggers download with format=pdf`
- `loading state disables items during in-flight`
- `error response shows toast`
- `frontend/tests/EngagementDetailPage.test.tsx` (**nouveau** — il n'existe pas encore, le builder le crée from scratch) :
- `admin sees Export button`
- `redteam sees Export button`
- `soc does NOT see Export button`
### Screenshots OBLIGATOIRES (lesson sprint 4) - [ ] **spec-reviewer** (pre-dispatch, §1): plan validated vs SPEC.md and §0 binding decisions
- `EngagementDetailPage` light + dark, dropdown fermé. - [ ] **frontend-builder** (§2-§6): implements, runs typecheck/lint/test, delivers screenshots for design-reviewer (every page + key states, light+dark)
- `EngagementDetailPage` light + dark, dropdown ouvert (3 items visibles). - [ ] **design-reviewer** (post-frontend): reviews screenshots + diff vs new DESIGN.md. Brutalist consistency, mono-discipline (only data), zero-rounded discipline.
- `EngagementDetailPage` SOC view — bouton Export ABSENT. - [ ] **code-reviewer** (post-design): reviews frontend diff for duplication, lost reuse, dead code.
- Le builder doit fournir un script Playwright authenti (réutiliser le pattern sprint 5 — `page.goto('/login') → fill → wait nav`). - [ ] **test-verifier**: skipped this sprint (no new US, no behavior change).
- [ ] **backend-builder**: idle, no work this sprint.
### Livrable frontend-builder (summary attendu) ## §8 — Git workflow
- Tous les fichiers créés/modifiés
- API contracts consommés exactement comme livrés par backend (cf. lesson sprint 5 — path drift à éviter, grep `Content-Disposition` dans la PR)
- Helpers réutilisés (`useToast`, etc.)
- Résultats vitest + typecheck + lint
- Liste des écrans capturés (light + dark, role-by-role)
--- - [ ] Branch: `sprint/7-design` (already created from origin/main)
- [ ] Commits: conventional, one per logical group (§2 to §7)
- [ ] PR via `make open-pr` (Gitea pattern, per memory)
- [ ] PR body in `tasks/pr-body-sprint-7.md`
- [ ] CHANGELOG.md sprint 7 section
## §3Acceptance tests (Sonnet · test-verifier) ## §9Risks & mitigations
**Cible : 201 → 215+ Playwright passing.** - **R1 — Tests break en masse**: many vitest specs may assert on class strings (e.g., `rounded-xl` on cards). Mitigation: update assertions to semantic queries; budget half a phase to test repair.
- **R2 — Dark mode contrast lost**: angular + new semantic colors may break WCAG AA contrast on dark slab. Mitigation: design-reviewer audits both modes; adjust the dark variant hex to meet WCAG AA. Rollback the success/warn family only if no accessible green/amber is achievable on the dark slab.
- **R3 — Mono overflow**: JetBrains Mono is wider than Inter at same px. Cell widths in tables may overflow. Mitigation: keep `table-layout: fixed` and `word-break: break-word` (pattern reused from PDF export CSS sprint 6).
- **R4 — DESIGN.md rewrite churn**: replacing 346 lines is a big diff. Mitigation: rewrite atomically in commit #2, keep token names consistent so downstream commits don't drift.
- **R5 — User taste mismatch**: "Bloomberg/SOC" may not match user's mental image. Mitigation: design-reviewer screenshots → user check-in BEFORE merge.
3 user stories à couvrir : ## §10 — Definition of Done
### US-29 — Admin/redteam exporte l'engagement en Markdown/CSV/PDF - [ ] All §0 decisions reflected in DESIGN.md + tokens + components + pages
- **AC-29.1** : login admin → engagement avec ≥ 2 simulations → click "Export" → dropdown s'ouvre. - [ ] `npm run typecheck` clean
- **AC-29.2** : click "Markdown" → download d'un `.md` avec `Content-Type: text/markdown`. Le fichier contient le nom de l'engagement, la date de début, et le nom de chaque simulation. - [ ] `npm run lint` clean
- **AC-29.3** : click "CSV" → download d'un `.csv` avec exactement N+1 **rows CSV** (1 header + N simulations). La colonne `name` contient les noms des simulations. **Note implémentation test** : compter les rows via `csv.reader` (ou équivalent JS), PAS via `file.split('\n')` — les commands multilines produisent des cells avec newlines embedded entre quotes, le line-count du fichier > row-count CSV. - [ ] `npm run test -- --run` all green
- **AC-29.4** : click "PDF" → download avec `Content-Type: application/pdf`, taille > 1 KB, magic bytes `%PDF`. - [ ] Backend untouched — `git diff origin/main -- backend/` empty
- **AC-29.5** : login redteam → mêmes 3 formats fonctionnent. - [ ] Playwright e2e green (223 baseline preserved)
- **AC-29.6** : filename respecte `engagement-<id>-<slug>-YYYYMMDD.{ext}` (assert via Content-Disposition). - [ ] Screenshots delivered (light + dark) for every page + key states
- [ ] DESIGN.md rewritten, no HP/Forma/wordmark/chevron references
- [ ] CHANGELOG.md sprint 7 section
- [ ] PR opened on Gitea
- [ ] User merges PR → sprint closed → team idle ready for sprint 8
### US-30 — SOC pas d'accès à l'export ## §11 — Lessons being applied from prior sprints
- **AC-30.1** : login SOC → engagement page → bouton "Export" **ABSENT** du DOM (pas seulement `display: none`). Assert via `expect(page.locator('[data-testid="export-dropdown"]')).not.toBeAttached()`.
- **AC-30.2** : appel direct API `GET /api/engagements/<id>/export?format=md` (Bearer SOC) → 403.
- **AC-30.3** : (sanity) appel API sans token → 401.
### US-31 — Robustesse format / engagement - **SPEC/DESIGN commit-first**: DESIGN.md rewrite is commit #2 (after sprint hygiene). No design churn mid-sprint.
- **AC-31.1** : `GET /api/engagements/<id>/export` sans `format` → 400 message friendly. - **spec-reviewer 2-pass**: APPROVED before dispatch, not after.
- **AC-31.2** : `GET /api/engagements/<id>/export?format=xml` → 400 friendly. - **Team idle policy**: 6 agents already mounted, no shutdown until PR merged.
- **AC-31.3** : `GET /api/engagements/99999/export?format=md` → 404. - **frontend-builder MUST invoke `Skill frontend-design`** before UI work (the patch commits as #1, takes effect immediately for the same sprint).
- **AC-31.4** : engagement avec 0 simulations → export OK (header seul, le CSV n'a qu'une ligne d'en-tête, le MD n'a pas de section simulation).
### Bouncing
- Si un AC échoue → bounce au builder responsable (backend ou frontend), pas de patch test-side.
---
## §4 — Reviews
### Spec-reviewer Pass 1 (avant dispatch)
- Lit ce `tasks/todo.md` § 0 + § 1 + § 2 + § 3.
- Verdict attendu : APPROVED / NEEDS-CHANGES par section.
- Points particuliers à challenger : WeasyPrint vs reportlab, CSV sans header engagement, URL drift (un seul endpoint avec query param vs 3 endpoints distincts).
### Spec-reviewer Pass 2 (après mes éventuels édits du plan)
- Re-validation des changements apportés.
- **TEAM-LEAD : ne PAS dispatcher backend tant que Pass 2 n'a pas répondu APPROVED.** Lesson sprint 5 — la patience sur le 2-pass a éliminé les addenda mid-implementation.
### Code-reviewer (après backend + frontend)
- LSP first (`goToDefinition`, `findReferences`).
- Focalise sur : pureté des render functions (testables), gestion des deps WeasyPrint dans Dockerfile, échappement CSV, filename slug, dropdown close-on-outside réutilisation.
### Design-reviewer (après screenshots frontend)
- Light + dark cohérence du dropdown Export.
- Vérifie que le bouton respecte la convention "icône + label court ≤ 8 chars" (`Export`).
- Audit alignement vs le header existant de la page.
### Test-verifier (après code-reviewer APPROVED)
- Écrit 1 spec file par US (`us29-export-formats`, `us30-export-rbac`, `us31-export-robustness`).
- Rapport pass/fail par AC.
---
## §5 — SPEC.md update (au tout début du sprint — lesson sprint 3/4/5)
Ajouter une section **§ Export d'engagement** entre § Templates de simulations et § Authentification & rôles :
> ## Export d'engagement
> Un engagement peut être exporté à tout moment dans 3 formats au choix : **Markdown** (handoff narratif), **CSV** (machine-readable, intégration tableurs), **PDF** (livrable client). L'export contient l'en-tête de l'engagement et toutes ses simulations avec les champs Red Team **et** SOC. Endpoint unique : `GET /api/engagements/<id>/export?format=md|csv|pdf`. Réservé aux rôles admin et redteam (livrable RedTeam, cohérent avec le RBAC Templates). Filename normalisé : `engagement-<id>-<slug>-YYYYMMDD.<ext>`.
**Le commit qui crée cette section doit être le PREMIER commit du sprint** (pas le dernier — sinon on rate le bug récurrent identifié dans les lessons). Le commit suivant peut être le plan lui-même (`tasks/todo.md` + `tasks/lessons.md`).
---
## §6 — Workflow git du sprint
- Branch : `sprint/6-export` (créée @ `678ee8f`).
- Commit séquence :
1. `docs(spec): add § Export d'engagement section` (le `M SPEC.md` ne doit JAMAIS rester unstaged)
2. `docs(plan): sprint 6 plan + sprint-5 lessons folded` (tasks/)
3. Commits backend (un ou deux, signés par backend-builder)
4. Commits frontend (un ou deux, signés par frontend-builder)
5. Commit post-code-review fixes (si nécessaire)
6. Commit screenshots design + e2e tests
7. Wrap-up commit team-lead : CHANGELOG + README + lessons.md sprint-6 + plan final
- PR via `make open-pr SPRINT=6 TITLE="feat: sprint 6 — engagement export (md/csv/pdf)" BODY=tasks/pr-body-sprint-6.md` (3e dogfood du wrapper sprint 4).
---
## §7 — Risk / hazard list
| # | Risk | Mitigation |
|---|---|---|
| 1 | WeasyPrint deps gonflent l'image Docker | Liste minimale documentée + WeasyPrint déjà packagé sur Debian slim ; mesurer Δ MB image build après vs avant |
| 2 | CSV mal-échappé avec commands multilines / quotes | Utiliser `csv.writer` stdlib (handles tout automatiquement), pas de string concat manuel |
| 3 | Markdown casse sur backticks dans commands | Fenced code blocks `~~~bash` (tildes au lieu de backticks pour les blocks contenant des backticks), OU escape via `markdown.escape` |
| 4 | Test PDF fragile sur le contenu | Asserter UNIQUEMENT : Content-Type, magic bytes `%PDF`, taille > 1 KB. Pas de regex sur le texte rendu (binary). |
| 5 | URL drift backend (`/export` vs `/engagements/<id>/export`) | Lesson sprint 5 — la 1re ligne du backend summary doit confirmer le path exact |
| 6 | Frontend oublie `URL.revokeObjectURL` → fuite mémoire | Test unitaire explicite : assert `revokeObjectURL` appelé après le click téléchargement |
| 7 | SPEC.md uncommitted à la fin du sprint (3 sprints en série !) | Commit SPEC.md en commit #1 du sprint, pas en wrap-up. Étape « cendrillon » du plan ci-dessus. |
---
## §8 — Definition of Done (sprint-level)
- [ ] §5 SPEC.md committed AS THE FIRST COMMIT of the sprint.
- [ ] Backend : 245+ pytest, ruff clean, mypy clean.
- [ ] Frontend : 130+ vitest, typecheck clean, lint clean.
- [ ] E2e : 215+ Playwright, 0 régression vs main.
- [ ] Screenshots fournies : EngagementDetailPage light + dark, dropdown fermé + ouvert, vue SOC sans bouton.
- [ ] Dockerfile mis à jour avec deps WeasyPrint + `make build` réussit.
- [ ] CHANGELOG.md `[Unreleased] → Sprint 6` rédigée.
- [ ] README.md « Status » bumped + section dans le tableau des features si pertinent.
- [ ] PR ouverte via `make open-pr` (pas via UI manuelle).
- [ ] `git status` au sprint-close affiche **uniquement** des fichiers ignorés (lesson récurrente).