The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this project adheres to [Semantic Versioning](https://semver.org/).
-`SimulationTemplate` model (table `simulation_templates`) — UNIQUE constraint on `name`, JSON `techniques` + `tactic_ids` (default `[]`, NOT NULL via `server_default`), Text fields `description` / `commands` / `prerequisites`, FK `created_by_id` to `users`, `created_at` / `updated_at`.
- Alembic migration `0005_simulation_templates.py` — CREATE TABLE (SQLite native, no batch); downgrade via DROP TABLE.
- 5 new endpoints under `/api/templates`, all gated `@role_required("admin", "redteam")` (SOC → 403):
-`GET /api/templates` — list, sorted name ASC, serialized with enriched `techniques: [{id, name, tactics}]` and `tactics: [{id, name}]`.
-`POST /api/templates` — create. `name` required (400 if empty), unique (409 via `IntegrityError` catch, no pre-check race). `technique_ids` / `tactic_ids` validated upfront — type check `isinstance(list)` (400 with friendly message) THEN resolved against the bundle / `_TACTIC_IDS` (400 with id on unknown).
-`GET /api/templates/<tid>` — single, 404 on miss.
-`PATCH /api/templates/<tid>` — partial update. Same validations. 409 on `name` conflict; no-op rename (`name == current`) returns 200.
-`POST /api/engagements/<eid>/simulations` extended with optional `template_id`. When provided:
- Template loaded (404 on miss).
- Fields copied directly onto the new `Simulation` ORM object (`techniques`, `tactic_ids`, `description`, `commands`, `prerequisites`, and `name` if missing from body).
- **Explicit non-call to `apply_patch()` / `_resolve_*` helpers** — avoids re-hitting the MITRE bundle AND avoids triggering the auto-transition `pending → in_progress`. Status stays `pending`, engagement stays `planned` (no `_maybe_activate_engagement` call). Decorrelation: no `template_id` FK on `Simulation`, deep copy of JSON arrays.
- New helpers in `mitre.py` reused / re-exposed; new `serialize_template()` in `serializers.py` mirrors `serialize_simulation` (minus SOC fields, status, executed_at) and uses the shared `_enrich_techniques` + `_enrich_tactics` (no duplication).
- All migration tests (0003, 0004, 0005) now use `Path(__file__).resolve().parent.parent / "migrations" / "versions" / "..."` — sprint 4's hardcoded-path MAJOR is closed for the third sprint running.
- New page `TemplatesListPage` (`/admin/templates`, admin+redteam only) — table (Name / MITRE count / Created by / Updated / Actions), `+ New` CTA with Plus icon.
- New page `TemplateFormPage` (`/admin/templates/new` and `/admin/templates/:id/edit`) — single-column FormField stack (sidesteps the multi-column grid trap that broke AC-17.3 on UsersAdminPage). Includes `MitreTechniquePicker` + `MitreMatrixModal` inline (NOT `MitreTechniquesField` — that one auto-saves; template form needs batched save). Delete via `ConfirmDialog`.
- New component `TemplatePickerModal` — modal listing all templates (Name / MITRE count / Created by). Empty state when `useTemplates()` returns `[]`: "No templates available — Create one from the Templates page."
- New nav link "Templates" in `Layout.tsx` topbar — visible to admin + redteam only, masked for SOC. Mirrors the pattern used by the "Users" link.
-`SimulationList` "New" button refactored into a **split-button dropdown**: `[+ New] [▼]`. Primary half → `/.../simulations/new` (blank). Dropdown → "Blank" + "From template…". Open dropdown closes on click-outside or Escape (sprint 3 picker pattern). Empty-state `SimulationList` now also exposes the same dropdown (so users can instantiate from a template on a fresh engagement without creating a blank first).
-`dark:shadow-floating-dark` consistently applied to the new dropdown and `TemplatePickerModal` — matches the sprint 4 shadow token model. `dark:hover:bg-fog` on dropdown items for contrast.
- New types: `SimulationTemplate`, `SimulationTemplateCreateInput`, `SimulationTemplatePatchInput`. `SimulationCreateInput` extended with `template_id?: number`.
- New TanStack Query hooks (`useTemplates`, `useTemplate`, `useCreateTemplate`, `useUpdateTemplate`, `useDeleteTemplate`) with cache invalidation on mutations.
- API client `frontend/src/api/templates.ts` — 5 calls to `/api/templates*`. (Sprint-5 in-flight bug : initial commit `90fc5ba` used `/simulation-templates` paths everywhere; caught immediately, fixed in `2b70011`.)
**Acceptance tests** (Playwright, **201 passed**)
- 3 new spec files (one per US): `us26-templates-crud.spec.ts` (22 tests), `us27-instantiate-from-template.spec.ts` (14 tests), `us28-templates-nav.spec.ts` (8 tests).
- Sprint 2/3 spec adapts: `us4-engagements.spec.ts` and `us7-simulation-create.spec.ts` now use `getByTestId('new-simulation-btn')` instead of `getByRole('link', /new simulation/)` — the link became a split-button dropdown.
- 1 pre-existing flaky in `us3-users-admin AC-3.4` (DB contamination across runs) — predates sprint 5, unrelated.
### Changed
- 2026-05-28 — SPEC.md § Templates de simulations added (between § Fonctionnement and § Authentification & rôles). Spells out the decoupling rule and the SOC-zero-access RBAC.
- 2026-05-28 — `POST /api/engagements/<eid>/simulations` API contract: `name` is now optional when `template_id` is provided (falls back to `template.name`).
---
## [Sprint 4] — UI polish + workflow tightening + dark mode + process hygiene (merged 2026-05-28)
-`Simulation.tactic_ids` JSON column (default `[]`, NOT NULL via `server_default`). Sprint 3's `techniques` array is joined by a parallel `tactics` field in the serialized response.
-`PATCH /api/simulations/<sid>` accepts `{tactic_ids: ["TA0007", ...]}` (TA-format, validated against the hardcoded `_TACTIC_IDS` map — no MITRE bundle dependency for tactics since TA-ids are a stable MITRE standard). Dedup via `dict.fromkeys`. SOC sending `tactic_ids` → 403. Auto-transition `pending → in_progress` extended to non-empty `tactic_ids`.
- **Done is now terminal**: `PATCH /api/simulations/<sid>` on a `done` simulation → **409**`{error: "simulation is done — reopen first"}` (applies to all 3 roles, prioritised over RBAC field-level).
- **Reopen transition**: `POST /api/simulations/<sid>/transition {to: "review_required"}` from `done` → 200, open to **admin + redteam + soc**. Implemented as a special case before the `_ALLOWED_TRANSITIONS` dict lookup; other transitions from `done` (`→ pending` / `→ in_progress` / `→ done`) remain forbidden (409 via dict miss).
- **Engagement auto-status**: when any simulation transitions to `in_progress` (auto or manual), if `engagement.status == planned` → engagement passes to `active` in the same DB transaction. No auto-rollback. The `_maybe_activate_engagement` helper modifies and `db.session.add()`s only — the caller commits (no double-commit).
-`GET /api/mitre/matrix``tactic_id` field now returned in TA-format (`"TA0007"`) instead of the internal slug (`"discovery"`). Aligns with the PATCH endpoint contract — frontend can round-trip the same `tactic_id` between matrix display and PATCH body. Spec-drift caught by the e2e test-verifier (AC-21.6 defect).
- **Dark mode**: full Tailwind `darkMode: 'class'` plumbing, themed surface tokens via CSS variables under `:root` / `.dark` in `index.css`. Three-state cycle (`light` / `dark` / `system`) toggle in the topbar with lucide-react icons (Sun / Moon / Monitor). Persisted under localStorage key `mimic-theme` (default `system`, follows `prefers-color-scheme`). Dedicated `useTheme()` hook orchestrates the cycle + media-query listener.
- **Slab token split**: a new `slab` / `slab-text` / `slab-muted` token family stays fixed `#111827` / `#f9fafb` / `#6b7280` regardless of theme. Used for permanently-dark surfaces (utility strip, footer, modal backdrop) that must NOT invert in dark mode. The themed `ink` token is now strictly for text. `.btn-ink` uses `@apply bg-slab` (single source of truth).
- **Modal backdrop**: new `.modal-backdrop` CSS class (fixed `rgba(0,0,0,0.6)`) replaces `bg-ink/60` (which inverted in dark mode). Applies to `MitreMatrixModal` and `ConfirmDialog`.
- **Badge contrast in dark mode**: `SimulationStatusBadge` and `StatusBadge` use `text-white` (fixed) on colored backgrounds instead of `text-canvas` / `text-ink-on` (which inverted). `Toast` error uses `bg-slab text-slab-text`.
- **Dark mode shadows**: new `soft-lift-dark` and `floating-dark` token variants, applied to cards and modals via `dark:shadow-*` so the lift remains visible on dark canvas. Hairlines bumped (`#4b5563`) for better separator visibility.
- **Tactic selection in matrix**: clicking a tactic header toggles its selection in addition to techniques + sub-techniques. Tactic chips render with `bg-primary text-canvas` (filled), distinct from technique chips (`bg-primary-soft text-primary-deep`). Apply emits one combined PATCH `{technique_ids, tactic_ids}` — no two sequential calls.
- **MITRE input redesign**: replaces the prior `Add technique` + `Quick search` button pair with an inline autocomplete input + matrix icon button to the right. Chips display the reference only (`T1059.001` or `TA0007`); full technique name surfaces on `title=` hover. Empty state minimal.
- **`done` simulation UI**: form fields are fully disabled, `MitreTechniquesField` is read-only (chips without ×, input + matrix icon hidden), action bar shows ONLY a `Reopen` button (visible to all 3 roles per RBAC). Save / Mark for review / Close / Delete are hidden in the done state. A "this simulation is done and read-only" banner replaces them.
- **UsersAdminPage `Create account` form alignment** (3rd attempt — finally pixel-perfect): refactored from `FormField` + `items-end` to an explicit 3-row grid (labels / inputs+button / hints) using `grid-rows-[auto_auto_auto]`. Labels share row 1, inputs + button share row 2, hint sits alone in row 3 — the browser cannot misalign cells of different heights.
- **EngagementsListPage dedup**: single `+ New` CTA (header + empty-state share the same label).
- **Engagement query invalidation**: `useUpdateSimulation` and `useTransitionSimulation` now invalidate both the simulation queries AND `["engagement", engagement_id]` + `["engagements"]` so the engagement status badge updates without a full reload after the auto-transition.
**Process hygiene** (US-24)
- New agent definition `.claude/agents/design-reviewer.md` — read-only, runs AFTER `frontend-builder` and BEFORE `code-reviewer`. Audits alignment, DESIGN.md token usage, light/dark consistency, typography, whitespace rhythm, responsive sanity at 1280×720, button convention, V1 a11y, and inter-screen coherence.
- Updated `.claude/agents/frontend-builder.md` Definition of Done — screenshots are now MANDATORY (one per feature/state introduced or modified, light + dark when theming is in scope). A "Dev server not started" line is a hard block.
**Infra hygiene** (US-25)
-`scripts/open-pr.sh` — wraps `POST /api/v1/repos/{owner}/{repo}/pulls` on the Gitea REST API. Reads credentials from `~/.git-credentials` (same source as `git push` — no token in env). Detects host/owner/repo from `git remote get-url origin`. Validates args, prints PR URL.
- New Makefile target `open-pr TITLE="..." BODY=path/to/body.md [BASE=main]` wrapping the script. Team-lead PR creation is now automated.
- README `Make targets` table documents the new target.
**Acceptance tests** (Playwright, **158 passed**)
- 7 new spec files (one per testable US): `us17-ui-polish`, `us18-done-readonly-reopen`, `us19-engagement-auto-status`, `us20-matrix-fits-modal`, `us21-tactic-selection`, `us22-mitre-input-redesign`, `us23-dark-mode`.
- Coverage gaps from code-reviewer filled: `+N` suffix when techniques + tactics are mixed in the `SimulationList` MITRE column ; Tab focus-trap cycle in `MitreMatrixModal` ; dark-mode `localStorage` persistence across reload.
- AC-21.6 defect caught by the e2e (matrix returned slug `tactic_id`, PATCH expected TA-format) was bounced to backend-builder and resolved within the sprint.
### Changed
- 2026-05-27 — SPEC.md § Fonctionnement clarified: `done` is terminal, only Reopen (open to all 3 roles) returns to `review_required`. Engagement auto-flips `planned → active` on first simulation `in_progress`, never the reverse.
- 2026-05-27 — SPEC.md § Référentiel MITRE: added the sprint 4 `tactic_ids` (separated from `technique_ids`).
- 2026-05-27 — SPEC.md § Workflows: `design-reviewer` agent inserted between `frontend-builder` and `code-reviewer`. PR creation now via `make open-pr`.
- 2026-05-27 — Carry-over commit: sprint 3 `§ Simulation` multi-techniques edit had been left uncommitted at sprint 3 close; applied at sprint 4 start so SPEC.md and the shipped code finally agree (lesson logged in tasks/lessons.md).
-`Simulation.techniques` JSON column replaces the scalar `mitre_technique_id` / `mitre_technique_name` pair. Stored as `[{"id", "name"}]`; tactics are derived at serialize time from the MITRE service (snapshot pattern survives bundle updates).
- Alembic migration `0003_simulation_techniques_array.py` — reversible upgrade (backfill from scalars → drop scalars → enforce `NOT NULL` via `batch_alter_table`) and symmetric downgrade.
-`PATCH /api/simulations/<sid>` now accepts `{technique_ids: ["T1059", "T1059.001", ...]}` (flat list of T-IDs, parents and subs at the same level). Server validates each ID against the bundle (400 on unknown), deduplicates while preserving order, resolves names, and rejects SOC payloads (403). Returns 503 if the bundle isn't loaded.
-`GET /api/mitre/matrix` — new endpoint returning the full Enterprise tree `[{tactic_id, tactic_name, techniques: [{id, name, subtechniques: [{id, name}]}]}]`. Tactics in canonical order (Initial Access → Impact). Techniques sorted alphabetically per tactic; sub-techniques nested under their parent via dot-ID detection.
-`mitre_svc` extended with `get_tactics(id)`, `lookup_name(id)`, `get_matrix()`, and a `TACTIC_NAMES` constant fixing the cosmetic `"Command And Control"` → `"Command and Control"` (MITRE canonical capitalisation).
-`REDTEAM_FIELDS | {"technique_ids"}` SOC gate in `simulation_workflow.apply_patch` preserves the sprint 2 field-level RBAC pattern.
- Auto-transition `pending → in_progress` extended: triggers when `technique_ids` is non-empty (consistent with the "non-empty value" rule from sprint 2). Empty list does not trigger.
**Frontend** (86 vitest passing)
-`MitreTechniquesField` orchestrates multi-technique selection with **auto-save** — every add (Quick Search / matrix Apply) and every remove (× on tag chip) triggers a PATCH via `useUpdateSimulation`. Toast feedback on success/error; UI disabled during the in-flight PATCH; silent dedup if the user re-adds an already-present technique.
-`MitreTechniqueTag` — chip component (`bg-primary-soft text-primary-deep rounded-full`) with an × remove button.
-`MitreMatrixModal` — full-width modal, one column per tactic (220px fixed), horizontal scroll. Each technique top-level is clickable (toggle); a chevron expands/collapses sub-techniques rendered in cascade. Search filter (case-insensitive on id + name) auto-expands the parent of a matched sub-technique. Tactic header shows a "N selected" counter (parents + subs). Footer: Cancel + "Apply N technique(s)" (or "Clear all" when N=0 and there's an existing selection). Focus trap V1: search input auto-focus on open, Tab cycles within the modal, Escape and backdrop click both = Cancel.
-`MitreTechniquePicker` (sprint 2) clean-rewritten to a one-shot `onSelect({id, name})` signature; no incoming value props. The picker resets after each selection — the parent (`MitreTechniquesField`) handles append + dedup.
-`SimulationList` MITRE column displays `T1059 +2` when 3 techniques are selected (first id + remainder counter) or `—` when empty.
-`SimulationFormPage` — `MitreTechniquesField` replaces the old standalone `MitreTechniquePicker`. The technique state moves out of the RT form (independent auto-save cycle); the Save Red Team button still batches the other RT fields.
**Acceptance tests** (Playwright)
- 4 new spec files: `us13-multi-techniques.spec.ts`, `us14-techniques-tags.spec.ts`, `us15-mitre-matrix-modal.spec.ts`, `us16-regression-sprint2.spec.ts` — all ACs (AC-13.1 → AC-16.3) pass.
- Sprint 2 specs `us8-simulation-redteam-fill.spec.ts` and `us10-mitre-autocomplete.spec.ts` adapted to the new `techniques: []` array (no more scalar field assertions).
### Changed
- 2026-05-27 — SPEC.md § Simulation: "Type d'attaque MITRE correspondant" (singular) → "Types d'attaque MITRE correspondants (multi-techniques) — sélectionnables par autocomplete OU via la matrice ATT&CK affichée en modale. Sub-techniques supportées."
- 2026-05-27 — Breaking API change: `mitre_technique_id` and `mitre_technique_name` removed from the `Simulation` payload (both directions). Replaced by `techniques: [{id, name, tactics}]` in responses and `technique_ids: string[]` in PATCH requests. No backwards-compatibility shim (no external consumer at this stage).
-`simulation_workflow` service: field-level RBAC (SOC blocked when status ∈ {pending, in_progress}; SOC rejected if payload contains a redteam field), state machine (only forward transitions, validated by role), and auto-transition `pending → in_progress` when admin/redteam saves any non-empty redteam field.
-`mitre` service: STIX 2.1 Enterprise bundle loaded at boot, indexed by T-id + name + tactic. Ranked search (`exact-id > prefix-id > substring-name`), max 20 results. Includes sub-techniques (`T1059.001`). Boot-safe: missing/corrupt bundle logs a warning and the endpoint returns 503 instead of crashing the app.
-`make update-mitre` is now a real target — fetches the upstream STIX bundle and restarts the container if running. Bundle is committed at `backend/data/mitre/enterprise-attack.json` (~46 MB) so `make build` stays self-contained.
- Upfront validation of `executed_at` (no partial mutation on parse failure).
-`SimulationList` component rendered inside `EngagementDetailPage` (replaces the Sprint 1 placeholder). Columns: name, MITRE id, status badge, executed_at. Row click → SPA navigation via `useNavigate` (no full reload).
-`SimulationFormPage` (`/engagements/:eid/simulations/new` and `/engagements/:eid/simulations/:sid/edit`): single role-aware page with two cards ("Red Team" / "SOC"). Redteam/admin can edit all fields; SOC sees the redteam card as read-only and the SOC card disabled (with an explanatory banner) until status reaches `review_required`. Footer surfaces context-appropriate transition buttons ("Marquer en revue" / "Clôturer") and a confirmation modal for delete.
-`MitreTechniquePicker`: debounced (200 ms) autocomplete input with keyboard navigation (↑↓ / Enter / Escape), listbox accessibility, and an inline 503 error path. Selection populates both `mitre_technique_id` and `mitre_technique_name`. A `hasHydratedFromProps` ref prevents the input from being wiped mid-stroke when the parent emits `onChange(null, null)`.
-`SimulationStatusBadge`: 4 variants mapped to DESIGN.md tokens (`bg-fog`, `bg-primary-soft`, `bg-bloom-coral`, `bg-storm-deep`). Sibling of the existing `StatusBadge` rather than a forked generic — the two badges share visual scaffolding but their enums diverge.
-`ConfirmDialog`: generic modal used by the delete flow.
- TanStack Query hooks: `useEngagementSimulations`, `useSimulation`, `useCreateSimulation`, `useUpdateSimulation`, `useDeleteSimulation`, `useTransitionSimulation`, `useMitreSearch`. Mutations invalidate both the simulation detail key and the engagement-scoped list key.
-`us4-engagements.spec.ts` AC-4.9 assertion refreshed: the Sprint 1 placeholder text was correctly replaced by the new `SimulationList` (the test now asserts the new heading + "New simulation" link).
- Sprint 1 docker-hardcoded tests (`us1`, `us6`) now resolve thanks to the podman auto-detect added to those specs in the same sprint — full suite is green on both docker and podman hosts.
- E2e assertions translated to match the i18n cleanup (French → English) shipped in the post-QA fix.
**Post-QA fixes (2026-05-26)**
- All French labels in the frontend translated to English (convention: anglais partout). Affected: `SimulationList`, `SimulationFormPage`, `ConfirmDialog` strings.
-`UsersAdminPage` "Create account" form: grid alignment fixed — the password field's `hint="≥ 8 characters"` was pushing labels out of alignment with `items-end`. Now uses `items-start` + `self-end` button wrapper so labels sit at the same baseline and the Create button stays bottom-aligned.
-`SimulationFormPage` "Execution result" field: switched from single-line `TextInput` to multiline `TextArea` (5 rows).
-`SimulationFormPage` actions reorganised: single sticky action bar at the bottom of the page replaces the previous split between RT-card footer, SOC-card footer, and workflow div. Layout: Save Red Team · Save SOC · | · Mark for review · Close · (right-aligned) Delete.
- 2026-05-26 — Makefile now auto-detects the container engine (`CONTAINER_CMD ?= docker || podman`) instead of hard-coding `docker`. Override with `make <target> CONTAINER_CMD=podman` or `export CONTAINER_CMD=…`. The matching e2e tests (`us1`, `us6`) were updated to mirror the same detection so they pass on podman-only machines without an explicit `MIMIC_CONTAINER_CMD` export.
- 2026-05-26 — `admin` role widened in `SPEC.md` § Décisions techniques. The initial draft restricted admin to user-management only; after the Sprint 1 plan review surfaced the operational pain (admin would need a second `redteam` account just to manage engagements), the user decided to make admin a super-user that cumulates redteam rights on engagements/simulations.
- Initial `SPEC.md` covering project scope, simulation model, workflow, stack, and agent team.
- Technical decisions section in `SPEC.md`: 3-role auth (admin/redteam/soc), JWT Bearer, single-container Flask+React, local MITRE STIX bundle, minimal Engagement model, admin bootstrap via Makefile target.
- Sub-agent definitions under `.claude/agents/` for backend-builder, frontend-builder, spec-reviewer (project override of the built-in, covers plan-vs-spec and code-vs-spec), code-reviewer, test-verifier, devil-advocate.