feat(m6): missions + snapshot CRUD, membership visibility, status state machine
Adds the mission layer that materialises template snapshots, plus the SPA
list / 3-step wizard / detail page.
Backend:
- app/services/missions.py — create_mission snapshots scenarios, tests, MITRE
tags in a 4-query write; list/get apply a non-admin membership filter that
collapses to 404 (no existence leak); status state machine enforces
draft → in_progress → completed → archived with archived as a sink; the
non-admin creator is auto-added as role_hint='red' to retain visibility.
- app/api/missions.py — 8 endpoints (list, get, create, update, add
scenarios, set members, transition, soft-delete) with strict pydantic
schemas. The transition endpoint splits the perm gate manually so
archive requires mission.archive while other targets use mission.update.
- app/api/users.py — new GET /users/roster returning (id, email,
display_name) only, gated by user.read OR mission.create OR
mission.update — lets non-admin wizard users see assignable peers
without exposing the admin /users payload.
- app/api/diag.py — /diag/reset truncates the mission_* tables before the
template tables because the source_*_template_id FKs are ON DELETE SET
NULL, which is cheaper to short-circuit by removing the children first.
Frontend:
- lib/missions.ts — typed client, queryKey factory, status accent map.
- pages/MissionsListPage.tsx — list cards with status accent + filters
(q, client, status).
- pages/MissionsCreatePage.tsx — 3-step wizard (meta → scenarios → members)
with member roster fed by /users/roster.
- pages/MissionDetailPage.tsx — header + transition buttons (legal next
states only) + Tests/Members/Synthesis/Export tabs.
- Routes + nav entry (visible to anyone with mission.read or admin).
Tests:
- backend/tests/test_missions.py — 22 pytest covering snapshot fidelity,
MITRE propagation, membership visibility, transition state machine,
perm gating, member set replace, append scenarios, soft-delete, partial
update, inverted-date rejection.
- e2e/tests/m6-missions.spec.ts — 5 Playwright (snapshot freezing, non-admin
visibility, status transitions + 409, SPA wizard end-to-end, list filter).
Docs:
- CHANGELOG, tasks/testing-m6.md, tasks/lessons.md (snapshot tradeoffs,
membership=404 pattern, /diag/reset order, auto-creator add).
- README + tasks/todo.md updated.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 15:07:32 +02:00
|
|
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
fix(m6): mission detail page can now edit metadata, append scenarios, edit members
The M6 SPA shipped the create wizard but the detail page was read-only —
even though the backend already exposed PUT /missions/{id}, POST
/missions/{id}/scenarios, and PUT /missions/{id}/members. So once a
mission was created you couldn't fix a typo in the client name, add a
scenario you forgot, or change member assignments without curl.
Added three modals on the detail page, gated by `is_admin ||
mission.update`:
- Edit metadata (header button, 3xl modal): name + client + dates +
markdown description, same validation as the wizard step 1.
- Add scenarios (Tests tab): scenario picker matching wizard step 2,
calls POST /missions/{id}/scenarios which appends snapshots at
current_max_position + 1.
- Edit members (Members tab): roster + red/blue toggles, calls
PUT /missions/{id}/members (full-set replace), pre-populated with
the current member set.
The detail page now imports useAuth so `canEdit` is computed once and
shared between the three buttons.
E2E: new "detail page edits metadata, appends scenarios, edits members"
spec exercises the three modals end-to-end. M6 e2e count is now 6.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 07:37:06 +02:00
|
|
|
import { useEffect, useMemo, useState } from 'react';
|
feat(m7): per-test execution — red/blue zones, evidence pipeline, activity poll
DoD M7 (spec §F5 + §F6 + §F8 + tasks/todo.md M7) covered end-to-end:
Backend
- New migration `91a4e7c6d2f3` adds `mission_tests.last_actor_id` (FK users
ON DELETE SET NULL) and `ix_mission_tests_updated_at` for the polling query.
- `detection_levels`: 4 default rows seeded at boot, `GET /detection-levels`
read-only (CRUD lands in M8).
- `mission_tests` service + `missions` API extension:
- `GET /missions/{id}/tests/{test_id}` — full detail incl. evidence list
- `PUT /missions/{id}/tests/{test_id}` — patch red/blue fields with per-field
perm classification (`mission.write_red_fields` vs `mission.write_blue_fields`)
- `POST /missions/{id}/tests/{test_id}/transition` — pending↔skipped/blocked
and pending→executed→reviewed_by_blue (+ undo paths), side-aware perm gate
that fires *before* idempotency, `executed_at` auto-stamped on the way in
- `GET /missions/{id}/activity?since=<ISO>` — drives the 15 s polling badge
- `evidence` service + top-level `/evidence/<id>` API:
- Streaming upload, SHA256 chunk-by-chunk, 25 MB cap, ext+MIME whitelist
- Content-addressed storage at ${EVIDENCE_DIR}/<mission>/<test>/<sha256><ext>
- Atomic `os.replace`, hex-validated SHA path component, root-dir guard
- Membership-aware (404 on miss/forbidden, no existence leak)
- `/diag/reset` now wipes ${EVIDENCE_DIR}/* in test mode (symlink-safe) and
re-seeds detection levels as a safety net.
Frontend
- `lib/missions.ts` — M7 types + queryKey factory + state-machine matrix.
- `pages/MissionTestPage.tsx` — two-zone layout: red border (command, output,
comment, mark-executed + override toggle) and cyan border (detection-level
select, comment, drag-and-drop evidence dropzone). Last-touched badge polls
/activity every 15 s, gated on document.visibilityState. Per-field disable
based on the user's red/blue perms (server stays the arbiter).
- `pages/MissionDetailPage.tsx` — test rows link to the new per-test page.
- `App.tsx` — registers /missions/:id/tests/:testId behind RequireAuth.
- `HomePage.tsx` — hero + roadmap card bumped to M7; next is M8.
Tests
- `backend/tests/test_mission_tests.py` — 27 pytest tests (red/blue field
gating, state-machine matrix incl. idempotent-side enforcement, executed_at
override, 24/26 MB upload + SHA256, MIME/ext whitelist, soft-delete hide,
activity polling with URL-encoded `since`, membership 404 vs admin bypass,
cross-mission evidence access).
- `e2e/tests/m7-execution.spec.ts` — 5 Playwright tests against the live stack
(red-only/blue-only API gating, mark-executed + reviewed_by_blue side
enforcement, 24 MB/26 MB upload + SHA256 round-trip, SPA per-test page save
+ transition, non-member 404 message). afterAll restores stable admin and
re-syncs MITRE.
Docs
- CHANGELOG.md: M7 section + post-M7 review-pass subsection.
- README.md: status, feature blurb, roadmap, testing-m7 link.
- tasks/testing-m7.md: manual + automated procedure with transition matrix
and perm-gating table.
- tasks/lessons.md: M7 retrospectives (LogRecord `created` trap, URL-encoded
query timestamps, perm-before-flush, atomic move, polling visibility gate).
Test count: 133 pytest / 49 Playwright, all green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 08:16:48 +02:00
|
|
|
import { Link, useNavigate, useParams } from 'react-router-dom';
|
feat(m6): missions + snapshot CRUD, membership visibility, status state machine
Adds the mission layer that materialises template snapshots, plus the SPA
list / 3-step wizard / detail page.
Backend:
- app/services/missions.py — create_mission snapshots scenarios, tests, MITRE
tags in a 4-query write; list/get apply a non-admin membership filter that
collapses to 404 (no existence leak); status state machine enforces
draft → in_progress → completed → archived with archived as a sink; the
non-admin creator is auto-added as role_hint='red' to retain visibility.
- app/api/missions.py — 8 endpoints (list, get, create, update, add
scenarios, set members, transition, soft-delete) with strict pydantic
schemas. The transition endpoint splits the perm gate manually so
archive requires mission.archive while other targets use mission.update.
- app/api/users.py — new GET /users/roster returning (id, email,
display_name) only, gated by user.read OR mission.create OR
mission.update — lets non-admin wizard users see assignable peers
without exposing the admin /users payload.
- app/api/diag.py — /diag/reset truncates the mission_* tables before the
template tables because the source_*_template_id FKs are ON DELETE SET
NULL, which is cheaper to short-circuit by removing the children first.
Frontend:
- lib/missions.ts — typed client, queryKey factory, status accent map.
- pages/MissionsListPage.tsx — list cards with status accent + filters
(q, client, status).
- pages/MissionsCreatePage.tsx — 3-step wizard (meta → scenarios → members)
with member roster fed by /users/roster.
- pages/MissionDetailPage.tsx — header + transition buttons (legal next
states only) + Tests/Members/Synthesis/Export tabs.
- Routes + nav entry (visible to anyone with mission.read or admin).
Tests:
- backend/tests/test_missions.py — 22 pytest covering snapshot fidelity,
MITRE propagation, membership visibility, transition state machine,
perm gating, member set replace, append scenarios, soft-delete, partial
update, inverted-date rejection.
- e2e/tests/m6-missions.spec.ts — 5 Playwright (snapshot freezing, non-admin
visibility, status transitions + 409, SPA wizard end-to-end, list filter).
Docs:
- CHANGELOG, tasks/testing-m6.md, tasks/lessons.md (snapshot tradeoffs,
membership=404 pattern, /diag/reset order, auto-creator add).
- README + tasks/todo.md updated.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 15:07:32 +02:00
|
|
|
|
fix(m6): mission detail page can now edit metadata, append scenarios, edit members
The M6 SPA shipped the create wizard but the detail page was read-only —
even though the backend already exposed PUT /missions/{id}, POST
/missions/{id}/scenarios, and PUT /missions/{id}/members. So once a
mission was created you couldn't fix a typo in the client name, add a
scenario you forgot, or change member assignments without curl.
Added three modals on the detail page, gated by `is_admin ||
mission.update`:
- Edit metadata (header button, 3xl modal): name + client + dates +
markdown description, same validation as the wizard step 1.
- Add scenarios (Tests tab): scenario picker matching wizard step 2,
calls POST /missions/{id}/scenarios which appends snapshots at
current_max_position + 1.
- Edit members (Members tab): roster + red/blue toggles, calls
PUT /missions/{id}/members (full-set replace), pre-populated with
the current member set.
The detail page now imports useAuth so `canEdit` is computed once and
shared between the three buttons.
E2E: new "detail page edits metadata, appends scenarios, edits members"
spec exercises the three modals end-to-end. M6 e2e count is now 6.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 07:37:06 +02:00
|
|
|
import { MarkdownField } from '@/components/MarkdownField';
|
feat(m6): missions + snapshot CRUD, membership visibility, status state machine
Adds the mission layer that materialises template snapshots, plus the SPA
list / 3-step wizard / detail page.
Backend:
- app/services/missions.py — create_mission snapshots scenarios, tests, MITRE
tags in a 4-query write; list/get apply a non-admin membership filter that
collapses to 404 (no existence leak); status state machine enforces
draft → in_progress → completed → archived with archived as a sink; the
non-admin creator is auto-added as role_hint='red' to retain visibility.
- app/api/missions.py — 8 endpoints (list, get, create, update, add
scenarios, set members, transition, soft-delete) with strict pydantic
schemas. The transition endpoint splits the perm gate manually so
archive requires mission.archive while other targets use mission.update.
- app/api/users.py — new GET /users/roster returning (id, email,
display_name) only, gated by user.read OR mission.create OR
mission.update — lets non-admin wizard users see assignable peers
without exposing the admin /users payload.
- app/api/diag.py — /diag/reset truncates the mission_* tables before the
template tables because the source_*_template_id FKs are ON DELETE SET
NULL, which is cheaper to short-circuit by removing the children first.
Frontend:
- lib/missions.ts — typed client, queryKey factory, status accent map.
- pages/MissionsListPage.tsx — list cards with status accent + filters
(q, client, status).
- pages/MissionsCreatePage.tsx — 3-step wizard (meta → scenarios → members)
with member roster fed by /users/roster.
- pages/MissionDetailPage.tsx — header + transition buttons (legal next
states only) + Tests/Members/Synthesis/Export tabs.
- Routes + nav entry (visible to anyone with mission.read or admin).
Tests:
- backend/tests/test_missions.py — 22 pytest covering snapshot fidelity,
MITRE propagation, membership visibility, transition state machine,
perm gating, member set replace, append scenarios, soft-delete, partial
update, inverted-date rejection.
- e2e/tests/m6-missions.spec.ts — 5 Playwright (snapshot freezing, non-admin
visibility, status transitions + 409, SPA wizard end-to-end, list filter).
Docs:
- CHANGELOG, tasks/testing-m6.md, tasks/lessons.md (snapshot tradeoffs,
membership=404 pattern, /diag/reset order, auto-creator add).
- README + tasks/todo.md updated.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 15:07:32 +02:00
|
|
|
import { Alert } from '@/components/ui/Alert';
|
|
|
|
|
import { Button } from '@/components/ui/Button';
|
|
|
|
|
import { Card } from '@/components/ui/Card';
|
fix(m6): mission detail page can now edit metadata, append scenarios, edit members
The M6 SPA shipped the create wizard but the detail page was read-only —
even though the backend already exposed PUT /missions/{id}, POST
/missions/{id}/scenarios, and PUT /missions/{id}/members. So once a
mission was created you couldn't fix a typo in the client name, add a
scenario you forgot, or change member assignments without curl.
Added three modals on the detail page, gated by `is_admin ||
mission.update`:
- Edit metadata (header button, 3xl modal): name + client + dates +
markdown description, same validation as the wizard step 1.
- Add scenarios (Tests tab): scenario picker matching wizard step 2,
calls POST /missions/{id}/scenarios which appends snapshots at
current_max_position + 1.
- Edit members (Members tab): roster + red/blue toggles, calls
PUT /missions/{id}/members (full-set replace), pre-populated with
the current member set.
The detail page now imports useAuth so `canEdit` is computed once and
shared between the three buttons.
E2E: new "detail page edits metadata, appends scenarios, edits members"
spec exercises the three modals end-to-end. M6 e2e count is now 6.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 07:37:06 +02:00
|
|
|
import { Modal } from '@/components/ui/Modal';
|
feat(m6): missions + snapshot CRUD, membership visibility, status state machine
Adds the mission layer that materialises template snapshots, plus the SPA
list / 3-step wizard / detail page.
Backend:
- app/services/missions.py — create_mission snapshots scenarios, tests, MITRE
tags in a 4-query write; list/get apply a non-admin membership filter that
collapses to 404 (no existence leak); status state machine enforces
draft → in_progress → completed → archived with archived as a sink; the
non-admin creator is auto-added as role_hint='red' to retain visibility.
- app/api/missions.py — 8 endpoints (list, get, create, update, add
scenarios, set members, transition, soft-delete) with strict pydantic
schemas. The transition endpoint splits the perm gate manually so
archive requires mission.archive while other targets use mission.update.
- app/api/users.py — new GET /users/roster returning (id, email,
display_name) only, gated by user.read OR mission.create OR
mission.update — lets non-admin wizard users see assignable peers
without exposing the admin /users payload.
- app/api/diag.py — /diag/reset truncates the mission_* tables before the
template tables because the source_*_template_id FKs are ON DELETE SET
NULL, which is cheaper to short-circuit by removing the children first.
Frontend:
- lib/missions.ts — typed client, queryKey factory, status accent map.
- pages/MissionsListPage.tsx — list cards with status accent + filters
(q, client, status).
- pages/MissionsCreatePage.tsx — 3-step wizard (meta → scenarios → members)
with member roster fed by /users/roster.
- pages/MissionDetailPage.tsx — header + transition buttons (legal next
states only) + Tests/Members/Synthesis/Export tabs.
- Routes + nav entry (visible to anyone with mission.read or admin).
Tests:
- backend/tests/test_missions.py — 22 pytest covering snapshot fidelity,
MITRE propagation, membership visibility, transition state machine,
perm gating, member set replace, append scenarios, soft-delete, partial
update, inverted-date rejection.
- e2e/tests/m6-missions.spec.ts — 5 Playwright (snapshot freezing, non-admin
visibility, status transitions + 409, SPA wizard end-to-end, list filter).
Docs:
- CHANGELOG, tasks/testing-m6.md, tasks/lessons.md (snapshot tradeoffs,
membership=404 pattern, /diag/reset order, auto-creator add).
- README + tasks/todo.md updated.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 15:07:32 +02:00
|
|
|
import { SectionHeader } from '@/components/ui/SectionHeader';
|
|
|
|
|
import { Tag } from '@/components/ui/Tag';
|
fix(m6): mission detail page can now edit metadata, append scenarios, edit members
The M6 SPA shipped the create wizard but the detail page was read-only —
even though the backend already exposed PUT /missions/{id}, POST
/missions/{id}/scenarios, and PUT /missions/{id}/members. So once a
mission was created you couldn't fix a typo in the client name, add a
scenario you forgot, or change member assignments without curl.
Added three modals on the detail page, gated by `is_admin ||
mission.update`:
- Edit metadata (header button, 3xl modal): name + client + dates +
markdown description, same validation as the wizard step 1.
- Add scenarios (Tests tab): scenario picker matching wizard step 2,
calls POST /missions/{id}/scenarios which appends snapshots at
current_max_position + 1.
- Edit members (Members tab): roster + red/blue toggles, calls
PUT /missions/{id}/members (full-set replace), pre-populated with
the current member set.
The detail page now imports useAuth so `canEdit` is computed once and
shared between the three buttons.
E2E: new "detail page edits metadata, appends scenarios, edits members"
spec exercises the three modals end-to-end. M6 e2e count is now 6.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 07:37:06 +02:00
|
|
|
import { TextField } from '@/components/ui/TextField';
|
|
|
|
|
import { ApiError, apiDelete, apiGet, apiPost, apiPut } from '@/lib/api';
|
|
|
|
|
import { useAuth } from '@/lib/auth';
|
feat(m6): missions + snapshot CRUD, membership visibility, status state machine
Adds the mission layer that materialises template snapshots, plus the SPA
list / 3-step wizard / detail page.
Backend:
- app/services/missions.py — create_mission snapshots scenarios, tests, MITRE
tags in a 4-query write; list/get apply a non-admin membership filter that
collapses to 404 (no existence leak); status state machine enforces
draft → in_progress → completed → archived with archived as a sink; the
non-admin creator is auto-added as role_hint='red' to retain visibility.
- app/api/missions.py — 8 endpoints (list, get, create, update, add
scenarios, set members, transition, soft-delete) with strict pydantic
schemas. The transition endpoint splits the perm gate manually so
archive requires mission.archive while other targets use mission.update.
- app/api/users.py — new GET /users/roster returning (id, email,
display_name) only, gated by user.read OR mission.create OR
mission.update — lets non-admin wizard users see assignable peers
without exposing the admin /users payload.
- app/api/diag.py — /diag/reset truncates the mission_* tables before the
template tables because the source_*_template_id FKs are ON DELETE SET
NULL, which is cheaper to short-circuit by removing the children first.
Frontend:
- lib/missions.ts — typed client, queryKey factory, status accent map.
- pages/MissionsListPage.tsx — list cards with status accent + filters
(q, client, status).
- pages/MissionsCreatePage.tsx — 3-step wizard (meta → scenarios → members)
with member roster fed by /users/roster.
- pages/MissionDetailPage.tsx — header + transition buttons (legal next
states only) + Tests/Members/Synthesis/Export tabs.
- Routes + nav entry (visible to anyone with mission.read or admin).
Tests:
- backend/tests/test_missions.py — 22 pytest covering snapshot fidelity,
MITRE propagation, membership visibility, transition state machine,
perm gating, member set replace, append scenarios, soft-delete, partial
update, inverted-date rejection.
- e2e/tests/m6-missions.spec.ts — 5 Playwright (snapshot freezing, non-admin
visibility, status transitions + 409, SPA wizard end-to-end, list filter).
Docs:
- CHANGELOG, tasks/testing-m6.md, tasks/lessons.md (snapshot tradeoffs,
membership=404 pattern, /diag/reset order, auto-creator add).
- README + tasks/todo.md updated.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 15:07:32 +02:00
|
|
|
import {
|
|
|
|
|
MISSION_STATUS_ACCENT,
|
|
|
|
|
MISSION_STATUS_LABEL,
|
|
|
|
|
missionKeys,
|
fix(m6): mission detail page can now edit metadata, append scenarios, edit members
The M6 SPA shipped the create wizard but the detail page was read-only —
even though the backend already exposed PUT /missions/{id}, POST
/missions/{id}/scenarios, and PUT /missions/{id}/members. So once a
mission was created you couldn't fix a typo in the client name, add a
scenario you forgot, or change member assignments without curl.
Added three modals on the detail page, gated by `is_admin ||
mission.update`:
- Edit metadata (header button, 3xl modal): name + client + dates +
markdown description, same validation as the wizard step 1.
- Add scenarios (Tests tab): scenario picker matching wizard step 2,
calls POST /missions/{id}/scenarios which appends snapshots at
current_max_position + 1.
- Edit members (Members tab): roster + red/blue toggles, calls
PUT /missions/{id}/members (full-set replace), pre-populated with
the current member set.
The detail page now imports useAuth so `canEdit` is computed once and
shared between the three buttons.
E2E: new "detail page edits metadata, appends scenarios, edits members"
spec exercises the three modals end-to-end. M6 e2e count is now 6.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 07:37:06 +02:00
|
|
|
type AddScenariosPayload,
|
|
|
|
|
type MemberPayload,
|
feat(m6): missions + snapshot CRUD, membership visibility, status state machine
Adds the mission layer that materialises template snapshots, plus the SPA
list / 3-step wizard / detail page.
Backend:
- app/services/missions.py — create_mission snapshots scenarios, tests, MITRE
tags in a 4-query write; list/get apply a non-admin membership filter that
collapses to 404 (no existence leak); status state machine enforces
draft → in_progress → completed → archived with archived as a sink; the
non-admin creator is auto-added as role_hint='red' to retain visibility.
- app/api/missions.py — 8 endpoints (list, get, create, update, add
scenarios, set members, transition, soft-delete) with strict pydantic
schemas. The transition endpoint splits the perm gate manually so
archive requires mission.archive while other targets use mission.update.
- app/api/users.py — new GET /users/roster returning (id, email,
display_name) only, gated by user.read OR mission.create OR
mission.update — lets non-admin wizard users see assignable peers
without exposing the admin /users payload.
- app/api/diag.py — /diag/reset truncates the mission_* tables before the
template tables because the source_*_template_id FKs are ON DELETE SET
NULL, which is cheaper to short-circuit by removing the children first.
Frontend:
- lib/missions.ts — typed client, queryKey factory, status accent map.
- pages/MissionsListPage.tsx — list cards with status accent + filters
(q, client, status).
- pages/MissionsCreatePage.tsx — 3-step wizard (meta → scenarios → members)
with member roster fed by /users/roster.
- pages/MissionDetailPage.tsx — header + transition buttons (legal next
states only) + Tests/Members/Synthesis/Export tabs.
- Routes + nav entry (visible to anyone with mission.read or admin).
Tests:
- backend/tests/test_missions.py — 22 pytest covering snapshot fidelity,
MITRE propagation, membership visibility, transition state machine,
perm gating, member set replace, append scenarios, soft-delete, partial
update, inverted-date rejection.
- e2e/tests/m6-missions.spec.ts — 5 Playwright (snapshot freezing, non-admin
visibility, status transitions + 409, SPA wizard end-to-end, list filter).
Docs:
- CHANGELOG, tasks/testing-m6.md, tasks/lessons.md (snapshot tradeoffs,
membership=404 pattern, /diag/reset order, auto-creator add).
- README + tasks/todo.md updated.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 15:07:32 +02:00
|
|
|
type Mission,
|
fix(m6): mission detail page can now edit metadata, append scenarios, edit members
The M6 SPA shipped the create wizard but the detail page was read-only —
even though the backend already exposed PUT /missions/{id}, POST
/missions/{id}/scenarios, and PUT /missions/{id}/members. So once a
mission was created you couldn't fix a typo in the client name, add a
scenario you forgot, or change member assignments without curl.
Added three modals on the detail page, gated by `is_admin ||
mission.update`:
- Edit metadata (header button, 3xl modal): name + client + dates +
markdown description, same validation as the wizard step 1.
- Add scenarios (Tests tab): scenario picker matching wizard step 2,
calls POST /missions/{id}/scenarios which appends snapshots at
current_max_position + 1.
- Edit members (Members tab): roster + red/blue toggles, calls
PUT /missions/{id}/members (full-set replace), pre-populated with
the current member set.
The detail page now imports useAuth so `canEdit` is computed once and
shared between the three buttons.
E2E: new "detail page edits metadata, appends scenarios, edits members"
spec exercises the three modals end-to-end. M6 e2e count is now 6.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 07:37:06 +02:00
|
|
|
type MissionRoleHint,
|
feat(m6): missions + snapshot CRUD, membership visibility, status state machine
Adds the mission layer that materialises template snapshots, plus the SPA
list / 3-step wizard / detail page.
Backend:
- app/services/missions.py — create_mission snapshots scenarios, tests, MITRE
tags in a 4-query write; list/get apply a non-admin membership filter that
collapses to 404 (no existence leak); status state machine enforces
draft → in_progress → completed → archived with archived as a sink; the
non-admin creator is auto-added as role_hint='red' to retain visibility.
- app/api/missions.py — 8 endpoints (list, get, create, update, add
scenarios, set members, transition, soft-delete) with strict pydantic
schemas. The transition endpoint splits the perm gate manually so
archive requires mission.archive while other targets use mission.update.
- app/api/users.py — new GET /users/roster returning (id, email,
display_name) only, gated by user.read OR mission.create OR
mission.update — lets non-admin wizard users see assignable peers
without exposing the admin /users payload.
- app/api/diag.py — /diag/reset truncates the mission_* tables before the
template tables because the source_*_template_id FKs are ON DELETE SET
NULL, which is cheaper to short-circuit by removing the children first.
Frontend:
- lib/missions.ts — typed client, queryKey factory, status accent map.
- pages/MissionsListPage.tsx — list cards with status accent + filters
(q, client, status).
- pages/MissionsCreatePage.tsx — 3-step wizard (meta → scenarios → members)
with member roster fed by /users/roster.
- pages/MissionDetailPage.tsx — header + transition buttons (legal next
states only) + Tests/Members/Synthesis/Export tabs.
- Routes + nav entry (visible to anyone with mission.read or admin).
Tests:
- backend/tests/test_missions.py — 22 pytest covering snapshot fidelity,
MITRE propagation, membership visibility, transition state machine,
perm gating, member set replace, append scenarios, soft-delete, partial
update, inverted-date rejection.
- e2e/tests/m6-missions.spec.ts — 5 Playwright (snapshot freezing, non-admin
visibility, status transitions + 409, SPA wizard end-to-end, list filter).
Docs:
- CHANGELOG, tasks/testing-m6.md, tasks/lessons.md (snapshot tradeoffs,
membership=404 pattern, /diag/reset order, auto-creator add).
- README + tasks/todo.md updated.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 15:07:32 +02:00
|
|
|
type MissionStatus,
|
fix(m6): mission detail page can now edit metadata, append scenarios, edit members
The M6 SPA shipped the create wizard but the detail page was read-only —
even though the backend already exposed PUT /missions/{id}, POST
/missions/{id}/scenarios, and PUT /missions/{id}/members. So once a
mission was created you couldn't fix a typo in the client name, add a
scenario you forgot, or change member assignments without curl.
Added three modals on the detail page, gated by `is_admin ||
mission.update`:
- Edit metadata (header button, 3xl modal): name + client + dates +
markdown description, same validation as the wizard step 1.
- Add scenarios (Tests tab): scenario picker matching wizard step 2,
calls POST /missions/{id}/scenarios which appends snapshots at
current_max_position + 1.
- Edit members (Members tab): roster + red/blue toggles, calls
PUT /missions/{id}/members (full-set replace), pre-populated with
the current member set.
The detail page now imports useAuth so `canEdit` is computed once and
shared between the three buttons.
E2E: new "detail page edits metadata, appends scenarios, edits members"
spec exercises the three modals end-to-end. M6 e2e count is now 6.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 07:37:06 +02:00
|
|
|
type SetMembersPayload,
|
feat(m6): missions + snapshot CRUD, membership visibility, status state machine
Adds the mission layer that materialises template snapshots, plus the SPA
list / 3-step wizard / detail page.
Backend:
- app/services/missions.py — create_mission snapshots scenarios, tests, MITRE
tags in a 4-query write; list/get apply a non-admin membership filter that
collapses to 404 (no existence leak); status state machine enforces
draft → in_progress → completed → archived with archived as a sink; the
non-admin creator is auto-added as role_hint='red' to retain visibility.
- app/api/missions.py — 8 endpoints (list, get, create, update, add
scenarios, set members, transition, soft-delete) with strict pydantic
schemas. The transition endpoint splits the perm gate manually so
archive requires mission.archive while other targets use mission.update.
- app/api/users.py — new GET /users/roster returning (id, email,
display_name) only, gated by user.read OR mission.create OR
mission.update — lets non-admin wizard users see assignable peers
without exposing the admin /users payload.
- app/api/diag.py — /diag/reset truncates the mission_* tables before the
template tables because the source_*_template_id FKs are ON DELETE SET
NULL, which is cheaper to short-circuit by removing the children first.
Frontend:
- lib/missions.ts — typed client, queryKey factory, status accent map.
- pages/MissionsListPage.tsx — list cards with status accent + filters
(q, client, status).
- pages/MissionsCreatePage.tsx — 3-step wizard (meta → scenarios → members)
with member roster fed by /users/roster.
- pages/MissionDetailPage.tsx — header + transition buttons (legal next
states only) + Tests/Members/Synthesis/Export tabs.
- Routes + nav entry (visible to anyone with mission.read or admin).
Tests:
- backend/tests/test_missions.py — 22 pytest covering snapshot fidelity,
MITRE propagation, membership visibility, transition state machine,
perm gating, member set replace, append scenarios, soft-delete, partial
update, inverted-date rejection.
- e2e/tests/m6-missions.spec.ts — 5 Playwright (snapshot freezing, non-admin
visibility, status transitions + 409, SPA wizard end-to-end, list filter).
Docs:
- CHANGELOG, tasks/testing-m6.md, tasks/lessons.md (snapshot tradeoffs,
membership=404 pattern, /diag/reset order, auto-creator add).
- README + tasks/todo.md updated.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 15:07:32 +02:00
|
|
|
type TransitionPayload,
|
fix(m6): mission detail page can now edit metadata, append scenarios, edit members
The M6 SPA shipped the create wizard but the detail page was read-only —
even though the backend already exposed PUT /missions/{id}, POST
/missions/{id}/scenarios, and PUT /missions/{id}/members. So once a
mission was created you couldn't fix a typo in the client name, add a
scenario you forgot, or change member assignments without curl.
Added three modals on the detail page, gated by `is_admin ||
mission.update`:
- Edit metadata (header button, 3xl modal): name + client + dates +
markdown description, same validation as the wizard step 1.
- Add scenarios (Tests tab): scenario picker matching wizard step 2,
calls POST /missions/{id}/scenarios which appends snapshots at
current_max_position + 1.
- Edit members (Members tab): roster + red/blue toggles, calls
PUT /missions/{id}/members (full-set replace), pre-populated with
the current member set.
The detail page now imports useAuth so `canEdit` is computed once and
shared between the three buttons.
E2E: new "detail page edits metadata, appends scenarios, edits members"
spec exercises the three modals end-to-end. M6 e2e count is now 6.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 07:37:06 +02:00
|
|
|
type UpdateMissionPayload,
|
feat(m6): missions + snapshot CRUD, membership visibility, status state machine
Adds the mission layer that materialises template snapshots, plus the SPA
list / 3-step wizard / detail page.
Backend:
- app/services/missions.py — create_mission snapshots scenarios, tests, MITRE
tags in a 4-query write; list/get apply a non-admin membership filter that
collapses to 404 (no existence leak); status state machine enforces
draft → in_progress → completed → archived with archived as a sink; the
non-admin creator is auto-added as role_hint='red' to retain visibility.
- app/api/missions.py — 8 endpoints (list, get, create, update, add
scenarios, set members, transition, soft-delete) with strict pydantic
schemas. The transition endpoint splits the perm gate manually so
archive requires mission.archive while other targets use mission.update.
- app/api/users.py — new GET /users/roster returning (id, email,
display_name) only, gated by user.read OR mission.create OR
mission.update — lets non-admin wizard users see assignable peers
without exposing the admin /users payload.
- app/api/diag.py — /diag/reset truncates the mission_* tables before the
template tables because the source_*_template_id FKs are ON DELETE SET
NULL, which is cheaper to short-circuit by removing the children first.
Frontend:
- lib/missions.ts — typed client, queryKey factory, status accent map.
- pages/MissionsListPage.tsx — list cards with status accent + filters
(q, client, status).
- pages/MissionsCreatePage.tsx — 3-step wizard (meta → scenarios → members)
with member roster fed by /users/roster.
- pages/MissionDetailPage.tsx — header + transition buttons (legal next
states only) + Tests/Members/Synthesis/Export tabs.
- Routes + nav entry (visible to anyone with mission.read or admin).
Tests:
- backend/tests/test_missions.py — 22 pytest covering snapshot fidelity,
MITRE propagation, membership visibility, transition state machine,
perm gating, member set replace, append scenarios, soft-delete, partial
update, inverted-date rejection.
- e2e/tests/m6-missions.spec.ts — 5 Playwright (snapshot freezing, non-admin
visibility, status transitions + 409, SPA wizard end-to-end, list filter).
Docs:
- CHANGELOG, tasks/testing-m6.md, tasks/lessons.md (snapshot tradeoffs,
membership=404 pattern, /diag/reset order, auto-creator add).
- README + tasks/todo.md updated.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 15:07:32 +02:00
|
|
|
} from '@/lib/missions';
|
fix(m6): mission detail page can now edit metadata, append scenarios, edit members
The M6 SPA shipped the create wizard but the detail page was read-only —
even though the backend already exposed PUT /missions/{id}, POST
/missions/{id}/scenarios, and PUT /missions/{id}/members. So once a
mission was created you couldn't fix a typo in the client name, add a
scenario you forgot, or change member assignments without curl.
Added three modals on the detail page, gated by `is_admin ||
mission.update`:
- Edit metadata (header button, 3xl modal): name + client + dates +
markdown description, same validation as the wizard step 1.
- Add scenarios (Tests tab): scenario picker matching wizard step 2,
calls POST /missions/{id}/scenarios which appends snapshots at
current_max_position + 1.
- Edit members (Members tab): roster + red/blue toggles, calls
PUT /missions/{id}/members (full-set replace), pre-populated with
the current member set.
The detail page now imports useAuth so `canEdit` is computed once and
shared between the three buttons.
E2E: new "detail page edits metadata, appends scenarios, edits members"
spec exercises the three modals end-to-end. M6 e2e count is now 6.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 07:37:06 +02:00
|
|
|
import type {
|
|
|
|
|
ScenarioTemplate,
|
|
|
|
|
ScenarioTemplateListResponse,
|
|
|
|
|
} from '@/lib/templates';
|
|
|
|
|
import { templateKeys } from '@/lib/templates';
|
feat(m6): missions + snapshot CRUD, membership visibility, status state machine
Adds the mission layer that materialises template snapshots, plus the SPA
list / 3-step wizard / detail page.
Backend:
- app/services/missions.py — create_mission snapshots scenarios, tests, MITRE
tags in a 4-query write; list/get apply a non-admin membership filter that
collapses to 404 (no existence leak); status state machine enforces
draft → in_progress → completed → archived with archived as a sink; the
non-admin creator is auto-added as role_hint='red' to retain visibility.
- app/api/missions.py — 8 endpoints (list, get, create, update, add
scenarios, set members, transition, soft-delete) with strict pydantic
schemas. The transition endpoint splits the perm gate manually so
archive requires mission.archive while other targets use mission.update.
- app/api/users.py — new GET /users/roster returning (id, email,
display_name) only, gated by user.read OR mission.create OR
mission.update — lets non-admin wizard users see assignable peers
without exposing the admin /users payload.
- app/api/diag.py — /diag/reset truncates the mission_* tables before the
template tables because the source_*_template_id FKs are ON DELETE SET
NULL, which is cheaper to short-circuit by removing the children first.
Frontend:
- lib/missions.ts — typed client, queryKey factory, status accent map.
- pages/MissionsListPage.tsx — list cards with status accent + filters
(q, client, status).
- pages/MissionsCreatePage.tsx — 3-step wizard (meta → scenarios → members)
with member roster fed by /users/roster.
- pages/MissionDetailPage.tsx — header + transition buttons (legal next
states only) + Tests/Members/Synthesis/Export tabs.
- Routes + nav entry (visible to anyone with mission.read or admin).
Tests:
- backend/tests/test_missions.py — 22 pytest covering snapshot fidelity,
MITRE propagation, membership visibility, transition state machine,
perm gating, member set replace, append scenarios, soft-delete, partial
update, inverted-date rejection.
- e2e/tests/m6-missions.spec.ts — 5 Playwright (snapshot freezing, non-admin
visibility, status transitions + 409, SPA wizard end-to-end, list filter).
Docs:
- CHANGELOG, tasks/testing-m6.md, tasks/lessons.md (snapshot tradeoffs,
membership=404 pattern, /diag/reset order, auto-creator add).
- README + tasks/todo.md updated.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 15:07:32 +02:00
|
|
|
|
|
|
|
|
const TABS = ['tests', 'members', 'synthesis', 'export'] as const;
|
|
|
|
|
type Tab = (typeof TABS)[number];
|
|
|
|
|
|
|
|
|
|
const ALLOWED_TRANSITIONS: Record<MissionStatus, MissionStatus[]> = {
|
|
|
|
|
draft: ['in_progress', 'archived'],
|
|
|
|
|
in_progress: ['completed', 'archived'],
|
|
|
|
|
completed: ['archived'],
|
|
|
|
|
archived: [],
|
|
|
|
|
};
|
|
|
|
|
|
2026-05-13 15:14:57 +02:00
|
|
|
const TRANSITION_BUTTON_ACCENT: Record<MissionStatus, 'cyan' | 'orange' | 'green' | 'teal'> = {
|
|
|
|
|
draft: 'cyan',
|
|
|
|
|
in_progress: 'orange',
|
|
|
|
|
completed: 'green',
|
|
|
|
|
archived: 'teal',
|
|
|
|
|
};
|
|
|
|
|
|
fix(m6): mission detail page can now edit metadata, append scenarios, edit members
The M6 SPA shipped the create wizard but the detail page was read-only —
even though the backend already exposed PUT /missions/{id}, POST
/missions/{id}/scenarios, and PUT /missions/{id}/members. So once a
mission was created you couldn't fix a typo in the client name, add a
scenario you forgot, or change member assignments without curl.
Added three modals on the detail page, gated by `is_admin ||
mission.update`:
- Edit metadata (header button, 3xl modal): name + client + dates +
markdown description, same validation as the wizard step 1.
- Add scenarios (Tests tab): scenario picker matching wizard step 2,
calls POST /missions/{id}/scenarios which appends snapshots at
current_max_position + 1.
- Edit members (Members tab): roster + red/blue toggles, calls
PUT /missions/{id}/members (full-set replace), pre-populated with
the current member set.
The detail page now imports useAuth so `canEdit` is computed once and
shared between the three buttons.
E2E: new "detail page edits metadata, appends scenarios, edits members"
spec exercises the three modals end-to-end. M6 e2e count is now 6.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 07:37:06 +02:00
|
|
|
interface RosterUser {
|
|
|
|
|
id: string;
|
|
|
|
|
email: string;
|
|
|
|
|
display_name: string | null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface RosterResponse {
|
|
|
|
|
items: RosterUser[];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface MemberSelection {
|
|
|
|
|
user_id: string;
|
|
|
|
|
role_hint: MissionRoleHint;
|
|
|
|
|
}
|
|
|
|
|
|
feat(m6): missions + snapshot CRUD, membership visibility, status state machine
Adds the mission layer that materialises template snapshots, plus the SPA
list / 3-step wizard / detail page.
Backend:
- app/services/missions.py — create_mission snapshots scenarios, tests, MITRE
tags in a 4-query write; list/get apply a non-admin membership filter that
collapses to 404 (no existence leak); status state machine enforces
draft → in_progress → completed → archived with archived as a sink; the
non-admin creator is auto-added as role_hint='red' to retain visibility.
- app/api/missions.py — 8 endpoints (list, get, create, update, add
scenarios, set members, transition, soft-delete) with strict pydantic
schemas. The transition endpoint splits the perm gate manually so
archive requires mission.archive while other targets use mission.update.
- app/api/users.py — new GET /users/roster returning (id, email,
display_name) only, gated by user.read OR mission.create OR
mission.update — lets non-admin wizard users see assignable peers
without exposing the admin /users payload.
- app/api/diag.py — /diag/reset truncates the mission_* tables before the
template tables because the source_*_template_id FKs are ON DELETE SET
NULL, which is cheaper to short-circuit by removing the children first.
Frontend:
- lib/missions.ts — typed client, queryKey factory, status accent map.
- pages/MissionsListPage.tsx — list cards with status accent + filters
(q, client, status).
- pages/MissionsCreatePage.tsx — 3-step wizard (meta → scenarios → members)
with member roster fed by /users/roster.
- pages/MissionDetailPage.tsx — header + transition buttons (legal next
states only) + Tests/Members/Synthesis/Export tabs.
- Routes + nav entry (visible to anyone with mission.read or admin).
Tests:
- backend/tests/test_missions.py — 22 pytest covering snapshot fidelity,
MITRE propagation, membership visibility, transition state machine,
perm gating, member set replace, append scenarios, soft-delete, partial
update, inverted-date rejection.
- e2e/tests/m6-missions.spec.ts — 5 Playwright (snapshot freezing, non-admin
visibility, status transitions + 409, SPA wizard end-to-end, list filter).
Docs:
- CHANGELOG, tasks/testing-m6.md, tasks/lessons.md (snapshot tradeoffs,
membership=404 pattern, /diag/reset order, auto-creator add).
- README + tasks/todo.md updated.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 15:07:32 +02:00
|
|
|
function useMission(id: string) {
|
|
|
|
|
return useQuery({
|
|
|
|
|
queryKey: missionKeys.detail(id),
|
|
|
|
|
queryFn: () => apiGet<Mission>(`/missions/${id}`),
|
|
|
|
|
enabled: !!id,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
fix(m6): mission detail page can now edit metadata, append scenarios, edit members
The M6 SPA shipped the create wizard but the detail page was read-only —
even though the backend already exposed PUT /missions/{id}, POST
/missions/{id}/scenarios, and PUT /missions/{id}/members. So once a
mission was created you couldn't fix a typo in the client name, add a
scenario you forgot, or change member assignments without curl.
Added three modals on the detail page, gated by `is_admin ||
mission.update`:
- Edit metadata (header button, 3xl modal): name + client + dates +
markdown description, same validation as the wizard step 1.
- Add scenarios (Tests tab): scenario picker matching wizard step 2,
calls POST /missions/{id}/scenarios which appends snapshots at
current_max_position + 1.
- Edit members (Members tab): roster + red/blue toggles, calls
PUT /missions/{id}/members (full-set replace), pre-populated with
the current member set.
The detail page now imports useAuth so `canEdit` is computed once and
shared between the three buttons.
E2E: new "detail page edits metadata, appends scenarios, edits members"
spec exercises the three modals end-to-end. M6 e2e count is now 6.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 07:37:06 +02:00
|
|
|
function useScenarioCatalogue(enabled: boolean) {
|
|
|
|
|
return useQuery({
|
|
|
|
|
queryKey: templateKeys.scenarios(''),
|
|
|
|
|
queryFn: () =>
|
|
|
|
|
apiGet<ScenarioTemplateListResponse>('/scenario-templates?limit=500'),
|
|
|
|
|
enabled,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function useRoster(enabled: boolean) {
|
|
|
|
|
return useQuery({
|
|
|
|
|
queryKey: ['users', 'roster'],
|
|
|
|
|
queryFn: () => apiGet<RosterResponse>('/users/roster'),
|
|
|
|
|
enabled,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
feat(m6): missions + snapshot CRUD, membership visibility, status state machine
Adds the mission layer that materialises template snapshots, plus the SPA
list / 3-step wizard / detail page.
Backend:
- app/services/missions.py — create_mission snapshots scenarios, tests, MITRE
tags in a 4-query write; list/get apply a non-admin membership filter that
collapses to 404 (no existence leak); status state machine enforces
draft → in_progress → completed → archived with archived as a sink; the
non-admin creator is auto-added as role_hint='red' to retain visibility.
- app/api/missions.py — 8 endpoints (list, get, create, update, add
scenarios, set members, transition, soft-delete) with strict pydantic
schemas. The transition endpoint splits the perm gate manually so
archive requires mission.archive while other targets use mission.update.
- app/api/users.py — new GET /users/roster returning (id, email,
display_name) only, gated by user.read OR mission.create OR
mission.update — lets non-admin wizard users see assignable peers
without exposing the admin /users payload.
- app/api/diag.py — /diag/reset truncates the mission_* tables before the
template tables because the source_*_template_id FKs are ON DELETE SET
NULL, which is cheaper to short-circuit by removing the children first.
Frontend:
- lib/missions.ts — typed client, queryKey factory, status accent map.
- pages/MissionsListPage.tsx — list cards with status accent + filters
(q, client, status).
- pages/MissionsCreatePage.tsx — 3-step wizard (meta → scenarios → members)
with member roster fed by /users/roster.
- pages/MissionDetailPage.tsx — header + transition buttons (legal next
states only) + Tests/Members/Synthesis/Export tabs.
- Routes + nav entry (visible to anyone with mission.read or admin).
Tests:
- backend/tests/test_missions.py — 22 pytest covering snapshot fidelity,
MITRE propagation, membership visibility, transition state machine,
perm gating, member set replace, append scenarios, soft-delete, partial
update, inverted-date rejection.
- e2e/tests/m6-missions.spec.ts — 5 Playwright (snapshot freezing, non-admin
visibility, status transitions + 409, SPA wizard end-to-end, list filter).
Docs:
- CHANGELOG, tasks/testing-m6.md, tasks/lessons.md (snapshot tradeoffs,
membership=404 pattern, /diag/reset order, auto-creator add).
- README + tasks/todo.md updated.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 15:07:32 +02:00
|
|
|
function formatDateRange(start: string | null, end: string | null): string {
|
|
|
|
|
if (!start && !end) return 'No dates set';
|
|
|
|
|
if (start && end) return `${start} → ${end}`;
|
|
|
|
|
return start ?? end ?? '';
|
|
|
|
|
}
|
|
|
|
|
|
fix(m6): mission detail page can now edit metadata, append scenarios, edit members
The M6 SPA shipped the create wizard but the detail page was read-only —
even though the backend already exposed PUT /missions/{id}, POST
/missions/{id}/scenarios, and PUT /missions/{id}/members. So once a
mission was created you couldn't fix a typo in the client name, add a
scenario you forgot, or change member assignments without curl.
Added three modals on the detail page, gated by `is_admin ||
mission.update`:
- Edit metadata (header button, 3xl modal): name + client + dates +
markdown description, same validation as the wizard step 1.
- Add scenarios (Tests tab): scenario picker matching wizard step 2,
calls POST /missions/{id}/scenarios which appends snapshots at
current_max_position + 1.
- Edit members (Members tab): roster + red/blue toggles, calls
PUT /missions/{id}/members (full-set replace), pre-populated with
the current member set.
The detail page now imports useAuth so `canEdit` is computed once and
shared between the three buttons.
E2E: new "detail page edits metadata, appends scenarios, edits members"
spec exercises the three modals end-to-end. M6 e2e count is now 6.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 07:37:06 +02:00
|
|
|
// --------------------------------------------------------------------------- //
|
|
|
|
|
// Metadata edit modal //
|
|
|
|
|
// --------------------------------------------------------------------------- //
|
|
|
|
|
|
|
|
|
|
interface MetaEditModalProps {
|
|
|
|
|
mission: Mission;
|
|
|
|
|
open: boolean;
|
|
|
|
|
onClose: () => void;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function MetaEditModal({ mission, open, onClose }: MetaEditModalProps) {
|
|
|
|
|
const qc = useQueryClient();
|
|
|
|
|
const [name, setName] = useState(mission.name);
|
|
|
|
|
const [client, setClient] = useState(mission.client_target ?? '');
|
|
|
|
|
const [dateStart, setDateStart] = useState(mission.date_start ?? '');
|
|
|
|
|
const [dateEnd, setDateEnd] = useState(mission.date_end ?? '');
|
|
|
|
|
const [description, setDescription] = useState(mission.description_md ?? '');
|
|
|
|
|
|
|
|
|
|
// Reset form whenever the modal opens with a (potentially newer) mission.
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!open) return;
|
|
|
|
|
setName(mission.name);
|
|
|
|
|
setClient(mission.client_target ?? '');
|
|
|
|
|
setDateStart(mission.date_start ?? '');
|
|
|
|
|
setDateEnd(mission.date_end ?? '');
|
|
|
|
|
setDescription(mission.description_md ?? '');
|
|
|
|
|
}, [open, mission]);
|
|
|
|
|
|
|
|
|
|
const update = useMutation({
|
|
|
|
|
mutationFn: (body: UpdateMissionPayload) =>
|
|
|
|
|
apiPut<Mission>(`/missions/${mission.id}`, body),
|
|
|
|
|
onSuccess: () => {
|
|
|
|
|
qc.invalidateQueries({ queryKey: missionKeys.detail(mission.id) });
|
|
|
|
|
qc.invalidateQueries({ queryKey: missionKeys.listPrefix() });
|
|
|
|
|
onClose();
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const apiErr = update.error instanceof ApiError ? update.error : null;
|
|
|
|
|
const nameInvalid = name.trim().length === 0;
|
|
|
|
|
const datesInvalid = dateStart && dateEnd && dateEnd < dateStart;
|
|
|
|
|
|
|
|
|
|
function submit() {
|
|
|
|
|
update.mutate({
|
|
|
|
|
name: name.trim(),
|
|
|
|
|
client_target: client.trim() || null,
|
|
|
|
|
date_start: dateStart || null,
|
|
|
|
|
date_end: dateEnd || null,
|
|
|
|
|
description_md: description.trim() || null,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Modal
|
|
|
|
|
open={open}
|
|
|
|
|
onClose={onClose}
|
|
|
|
|
title={mission.name}
|
|
|
|
|
accent="cyan"
|
|
|
|
|
size="3xl"
|
|
|
|
|
testid="mission-edit-meta-modal"
|
|
|
|
|
>
|
|
|
|
|
<div className="flex flex-col gap-3 min-w-0">
|
|
|
|
|
{apiErr && <Alert accent="red">{apiErr.message}</Alert>}
|
|
|
|
|
<div className="grid grid-cols-1 gap-3 md:grid-cols-2">
|
|
|
|
|
<TextField
|
|
|
|
|
label="Name"
|
|
|
|
|
required
|
|
|
|
|
value={name}
|
|
|
|
|
onChange={(e) => setName(e.target.value)}
|
|
|
|
|
data-testid="meta-edit-name"
|
|
|
|
|
/>
|
|
|
|
|
<TextField
|
|
|
|
|
label="Client / target"
|
|
|
|
|
value={client}
|
|
|
|
|
onChange={(e) => setClient(e.target.value)}
|
|
|
|
|
data-testid="meta-edit-client"
|
|
|
|
|
/>
|
|
|
|
|
<TextField
|
|
|
|
|
label="Start date"
|
|
|
|
|
type="date"
|
|
|
|
|
value={dateStart}
|
|
|
|
|
onChange={(e) => setDateStart(e.target.value)}
|
|
|
|
|
data-testid="meta-edit-date-start"
|
|
|
|
|
/>
|
|
|
|
|
<TextField
|
|
|
|
|
label="End date"
|
|
|
|
|
type="date"
|
|
|
|
|
value={dateEnd}
|
|
|
|
|
onChange={(e) => setDateEnd(e.target.value)}
|
|
|
|
|
data-testid="meta-edit-date-end"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<MarkdownField
|
|
|
|
|
label="ROE / Description"
|
|
|
|
|
value={description}
|
|
|
|
|
onChange={setDescription}
|
|
|
|
|
data-testid="meta-edit-description"
|
|
|
|
|
/>
|
|
|
|
|
{datesInvalid && (
|
|
|
|
|
<p className="font-mono text-2xs text-red" data-testid="meta-edit-date-error">
|
|
|
|
|
End date must be on or after start date.
|
|
|
|
|
</p>
|
|
|
|
|
)}
|
|
|
|
|
<div className="flex items-center justify-end gap-2 pt-2">
|
|
|
|
|
<Button accent="teal" variant="ghost" onClick={onClose}>
|
|
|
|
|
Cancel
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
accent="green"
|
|
|
|
|
onClick={submit}
|
|
|
|
|
disabled={nameInvalid || !!datesInvalid || update.isPending}
|
|
|
|
|
data-testid="meta-edit-save"
|
|
|
|
|
>
|
|
|
|
|
{update.isPending ? 'Saving…' : 'Save'}
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</Modal>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --------------------------------------------------------------------------- //
|
|
|
|
|
// Add-scenarios modal //
|
|
|
|
|
// --------------------------------------------------------------------------- //
|
|
|
|
|
|
|
|
|
|
interface AddScenariosModalProps {
|
|
|
|
|
mission: Mission;
|
|
|
|
|
open: boolean;
|
|
|
|
|
onClose: () => void;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function AddScenariosModal({ mission, open, onClose }: AddScenariosModalProps) {
|
|
|
|
|
const qc = useQueryClient();
|
|
|
|
|
const [selected, setSelected] = useState<string[]>([]);
|
|
|
|
|
const catalogue = useScenarioCatalogue(open);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (open) setSelected([]);
|
|
|
|
|
}, [open]);
|
|
|
|
|
|
|
|
|
|
const add = useMutation({
|
|
|
|
|
mutationFn: (body: AddScenariosPayload) =>
|
|
|
|
|
apiPost<Mission>(`/missions/${mission.id}/scenarios`, body),
|
|
|
|
|
onSuccess: () => {
|
|
|
|
|
qc.invalidateQueries({ queryKey: missionKeys.detail(mission.id) });
|
|
|
|
|
qc.invalidateQueries({ queryKey: missionKeys.listPrefix() });
|
|
|
|
|
onClose();
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const apiErr = add.error instanceof ApiError ? add.error : null;
|
|
|
|
|
|
|
|
|
|
function toggle(id: string) {
|
|
|
|
|
setSelected((prev) =>
|
|
|
|
|
prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id],
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function submit() {
|
|
|
|
|
add.mutate({ scenario_template_ids: selected });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const totalTestsToAdd = useMemo(() => {
|
|
|
|
|
if (!catalogue.data) return 0;
|
|
|
|
|
const by_id = new Map<string, ScenarioTemplate>(
|
|
|
|
|
catalogue.data.items.map((sc) => [sc.id, sc] as const),
|
|
|
|
|
);
|
|
|
|
|
return selected.reduce((acc, id) => acc + (by_id.get(id)?.tests_count ?? 0), 0);
|
|
|
|
|
}, [selected, catalogue.data]);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Modal
|
|
|
|
|
open={open}
|
|
|
|
|
onClose={onClose}
|
|
|
|
|
title={`Add scenarios to ${mission.name}`}
|
|
|
|
|
accent="cyan"
|
|
|
|
|
size="3xl"
|
|
|
|
|
testid="mission-add-scenarios-modal"
|
|
|
|
|
>
|
|
|
|
|
<div className="flex flex-col gap-3 min-w-0">
|
|
|
|
|
{apiErr && <Alert accent="red">{apiErr.message}</Alert>}
|
|
|
|
|
{catalogue.isError && <Alert accent="red">Failed to load scenarios.</Alert>}
|
|
|
|
|
{catalogue.isLoading && (
|
|
|
|
|
<p className="font-mono text-xs text-text-dim">Loading…</p>
|
|
|
|
|
)}
|
|
|
|
|
<p className="font-mono text-2xs text-text-dim">
|
|
|
|
|
{selected.length} scenario{selected.length === 1 ? '' : 's'} ·{' '}
|
|
|
|
|
{totalTestsToAdd} test{totalTestsToAdd === 1 ? '' : 's'} will be appended
|
|
|
|
|
after the current {mission.scenarios_count}.
|
|
|
|
|
</p>
|
|
|
|
|
<ul
|
|
|
|
|
className="grid grid-cols-1 gap-2 md:grid-cols-2"
|
|
|
|
|
data-testid="add-scenarios-picker"
|
|
|
|
|
>
|
|
|
|
|
{catalogue.data?.items.map((sc) => {
|
|
|
|
|
const isSelected = selected.includes(sc.id);
|
|
|
|
|
return (
|
|
|
|
|
<li key={sc.id}>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
className={`w-full rounded-md border ${
|
|
|
|
|
isSelected ? 'border-cyan text-cyan' : 'border-border text-text'
|
|
|
|
|
} bg-bg-card p-3 text-left font-mono text-xs hover:border-cyan`}
|
|
|
|
|
onClick={() => toggle(sc.id)}
|
|
|
|
|
data-testid={`add-scenario-toggle-${sc.id}`}
|
|
|
|
|
aria-pressed={isSelected}
|
|
|
|
|
>
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<span className="text-text-bright">{sc.name}</span>
|
|
|
|
|
<Tag accent="purple">{sc.tests_count} tests</Tag>
|
|
|
|
|
</div>
|
|
|
|
|
{sc.description && (
|
|
|
|
|
<p className="mt-1 text-text-dim">{sc.description}</p>
|
|
|
|
|
)}
|
|
|
|
|
</button>
|
|
|
|
|
</li>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</ul>
|
|
|
|
|
{catalogue.data && catalogue.data.items.length === 0 && (
|
|
|
|
|
<p className="font-mono text-xs text-text-dim">
|
|
|
|
|
No scenarios in the catalogue yet.
|
|
|
|
|
</p>
|
|
|
|
|
)}
|
|
|
|
|
<div className="flex items-center justify-end gap-2 pt-2">
|
|
|
|
|
<Button accent="teal" variant="ghost" onClick={onClose}>
|
|
|
|
|
Cancel
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
accent="green"
|
|
|
|
|
onClick={submit}
|
|
|
|
|
disabled={selected.length === 0 || add.isPending}
|
|
|
|
|
data-testid="add-scenarios-save"
|
|
|
|
|
>
|
|
|
|
|
{add.isPending ? 'Adding…' : `Add ${selected.length} scenario${selected.length === 1 ? '' : 's'}`}
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</Modal>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --------------------------------------------------------------------------- //
|
|
|
|
|
// Edit-members modal //
|
|
|
|
|
// --------------------------------------------------------------------------- //
|
|
|
|
|
|
|
|
|
|
interface EditMembersModalProps {
|
|
|
|
|
mission: Mission;
|
|
|
|
|
open: boolean;
|
|
|
|
|
onClose: () => void;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function EditMembersModal({ mission, open, onClose }: EditMembersModalProps) {
|
|
|
|
|
const qc = useQueryClient();
|
|
|
|
|
const roster = useRoster(open);
|
|
|
|
|
const [members, setMembers] = useState<MemberSelection[]>([]);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!open) return;
|
|
|
|
|
setMembers(
|
|
|
|
|
mission.members.map((m) => ({
|
|
|
|
|
user_id: m.user_id,
|
|
|
|
|
role_hint: m.role_hint,
|
|
|
|
|
})),
|
|
|
|
|
);
|
|
|
|
|
}, [open, mission]);
|
|
|
|
|
|
|
|
|
|
const save = useMutation({
|
|
|
|
|
mutationFn: (body: SetMembersPayload) =>
|
|
|
|
|
apiPut<Mission>(`/missions/${mission.id}/members`, body),
|
|
|
|
|
onSuccess: () => {
|
|
|
|
|
qc.invalidateQueries({ queryKey: missionKeys.detail(mission.id) });
|
|
|
|
|
qc.invalidateQueries({ queryKey: missionKeys.listPrefix() });
|
|
|
|
|
onClose();
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const apiErr = save.error instanceof ApiError ? save.error : null;
|
|
|
|
|
|
|
|
|
|
function setRole(user_id: string, role_hint: MissionRoleHint) {
|
|
|
|
|
setMembers((prev) =>
|
|
|
|
|
prev.some((m) => m.user_id === user_id)
|
|
|
|
|
? prev.map((m) => (m.user_id === user_id ? { ...m, role_hint } : m))
|
|
|
|
|
: [...prev, { user_id, role_hint }],
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function remove(user_id: string) {
|
|
|
|
|
setMembers((prev) => prev.filter((m) => m.user_id !== user_id));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function submit() {
|
|
|
|
|
const payload: SetMembersPayload = {
|
|
|
|
|
members: members.map(
|
|
|
|
|
(m): MemberPayload => ({ user_id: m.user_id, role_hint: m.role_hint }),
|
|
|
|
|
),
|
|
|
|
|
};
|
|
|
|
|
save.mutate(payload);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Modal
|
|
|
|
|
open={open}
|
|
|
|
|
onClose={onClose}
|
|
|
|
|
title={`Members of ${mission.name}`}
|
|
|
|
|
accent="cyan"
|
|
|
|
|
size="3xl"
|
|
|
|
|
testid="mission-edit-members-modal"
|
|
|
|
|
>
|
|
|
|
|
<div className="flex flex-col gap-3 min-w-0">
|
|
|
|
|
{apiErr && <Alert accent="red">{apiErr.message}</Alert>}
|
|
|
|
|
{roster.isError && <Alert accent="red">Failed to load roster.</Alert>}
|
|
|
|
|
{roster.isLoading && (
|
|
|
|
|
<p className="font-mono text-xs text-text-dim">Loading users…</p>
|
|
|
|
|
)}
|
|
|
|
|
<ul className="flex flex-col gap-2" data-testid="edit-members-picker">
|
|
|
|
|
{roster.data?.items.map((u) => {
|
|
|
|
|
const selected = members.find((m) => m.user_id === u.id);
|
|
|
|
|
return (
|
|
|
|
|
<li
|
|
|
|
|
key={u.id}
|
|
|
|
|
className="flex flex-wrap items-center justify-between gap-2 rounded-md border border-border bg-bg-card p-3"
|
|
|
|
|
data-testid={`edit-member-row-${u.id}`}
|
|
|
|
|
>
|
|
|
|
|
<div>
|
|
|
|
|
<p className="font-mono text-xs text-text-bright">
|
|
|
|
|
{u.display_name ?? u.email}
|
|
|
|
|
</p>
|
|
|
|
|
{u.display_name && (
|
|
|
|
|
<p className="font-mono text-2xs text-text-dim">{u.email}</p>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<Button
|
|
|
|
|
accent="red"
|
|
|
|
|
variant={selected?.role_hint === 'red' ? 'solid' : 'outline'}
|
|
|
|
|
onClick={() => setRole(u.id, 'red')}
|
|
|
|
|
data-testid={`edit-member-${u.id}-red`}
|
|
|
|
|
>
|
|
|
|
|
Red
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
accent="cyan"
|
|
|
|
|
variant={selected?.role_hint === 'blue' ? 'solid' : 'outline'}
|
|
|
|
|
onClick={() => setRole(u.id, 'blue')}
|
|
|
|
|
data-testid={`edit-member-${u.id}-blue`}
|
|
|
|
|
>
|
|
|
|
|
Blue
|
|
|
|
|
</Button>
|
|
|
|
|
{selected && (
|
|
|
|
|
<Button
|
|
|
|
|
accent="rose"
|
|
|
|
|
variant="ghost"
|
|
|
|
|
onClick={() => remove(u.id)}
|
|
|
|
|
data-testid={`edit-member-${u.id}-clear`}
|
|
|
|
|
>
|
|
|
|
|
✕
|
|
|
|
|
</Button>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</li>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</ul>
|
|
|
|
|
<div className="flex items-center justify-end gap-2 pt-2">
|
|
|
|
|
<Button accent="teal" variant="ghost" onClick={onClose}>
|
|
|
|
|
Cancel
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
accent="green"
|
|
|
|
|
onClick={submit}
|
|
|
|
|
disabled={save.isPending}
|
|
|
|
|
data-testid="edit-members-save"
|
|
|
|
|
>
|
|
|
|
|
{save.isPending ? 'Saving…' : 'Save members'}
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</Modal>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --------------------------------------------------------------------------- //
|
|
|
|
|
// Main page //
|
|
|
|
|
// --------------------------------------------------------------------------- //
|
|
|
|
|
|
feat(m6): missions + snapshot CRUD, membership visibility, status state machine
Adds the mission layer that materialises template snapshots, plus the SPA
list / 3-step wizard / detail page.
Backend:
- app/services/missions.py — create_mission snapshots scenarios, tests, MITRE
tags in a 4-query write; list/get apply a non-admin membership filter that
collapses to 404 (no existence leak); status state machine enforces
draft → in_progress → completed → archived with archived as a sink; the
non-admin creator is auto-added as role_hint='red' to retain visibility.
- app/api/missions.py — 8 endpoints (list, get, create, update, add
scenarios, set members, transition, soft-delete) with strict pydantic
schemas. The transition endpoint splits the perm gate manually so
archive requires mission.archive while other targets use mission.update.
- app/api/users.py — new GET /users/roster returning (id, email,
display_name) only, gated by user.read OR mission.create OR
mission.update — lets non-admin wizard users see assignable peers
without exposing the admin /users payload.
- app/api/diag.py — /diag/reset truncates the mission_* tables before the
template tables because the source_*_template_id FKs are ON DELETE SET
NULL, which is cheaper to short-circuit by removing the children first.
Frontend:
- lib/missions.ts — typed client, queryKey factory, status accent map.
- pages/MissionsListPage.tsx — list cards with status accent + filters
(q, client, status).
- pages/MissionsCreatePage.tsx — 3-step wizard (meta → scenarios → members)
with member roster fed by /users/roster.
- pages/MissionDetailPage.tsx — header + transition buttons (legal next
states only) + Tests/Members/Synthesis/Export tabs.
- Routes + nav entry (visible to anyone with mission.read or admin).
Tests:
- backend/tests/test_missions.py — 22 pytest covering snapshot fidelity,
MITRE propagation, membership visibility, transition state machine,
perm gating, member set replace, append scenarios, soft-delete, partial
update, inverted-date rejection.
- e2e/tests/m6-missions.spec.ts — 5 Playwright (snapshot freezing, non-admin
visibility, status transitions + 409, SPA wizard end-to-end, list filter).
Docs:
- CHANGELOG, tasks/testing-m6.md, tasks/lessons.md (snapshot tradeoffs,
membership=404 pattern, /diag/reset order, auto-creator add).
- README + tasks/todo.md updated.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 15:07:32 +02:00
|
|
|
export function MissionDetailPage() {
|
|
|
|
|
const params = useParams();
|
|
|
|
|
const missionId = params.id ?? '';
|
|
|
|
|
const navigate = useNavigate();
|
|
|
|
|
const qc = useQueryClient();
|
fix(m6): mission detail page can now edit metadata, append scenarios, edit members
The M6 SPA shipped the create wizard but the detail page was read-only —
even though the backend already exposed PUT /missions/{id}, POST
/missions/{id}/scenarios, and PUT /missions/{id}/members. So once a
mission was created you couldn't fix a typo in the client name, add a
scenario you forgot, or change member assignments without curl.
Added three modals on the detail page, gated by `is_admin ||
mission.update`:
- Edit metadata (header button, 3xl modal): name + client + dates +
markdown description, same validation as the wizard step 1.
- Add scenarios (Tests tab): scenario picker matching wizard step 2,
calls POST /missions/{id}/scenarios which appends snapshots at
current_max_position + 1.
- Edit members (Members tab): roster + red/blue toggles, calls
PUT /missions/{id}/members (full-set replace), pre-populated with
the current member set.
The detail page now imports useAuth so `canEdit` is computed once and
shared between the three buttons.
E2E: new "detail page edits metadata, appends scenarios, edits members"
spec exercises the three modals end-to-end. M6 e2e count is now 6.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 07:37:06 +02:00
|
|
|
const { state } = useAuth();
|
|
|
|
|
|
|
|
|
|
const canEdit =
|
|
|
|
|
state.user?.is_admin ||
|
|
|
|
|
state.user?.permissions.includes('mission.update') ||
|
|
|
|
|
false;
|
feat(m6): missions + snapshot CRUD, membership visibility, status state machine
Adds the mission layer that materialises template snapshots, plus the SPA
list / 3-step wizard / detail page.
Backend:
- app/services/missions.py — create_mission snapshots scenarios, tests, MITRE
tags in a 4-query write; list/get apply a non-admin membership filter that
collapses to 404 (no existence leak); status state machine enforces
draft → in_progress → completed → archived with archived as a sink; the
non-admin creator is auto-added as role_hint='red' to retain visibility.
- app/api/missions.py — 8 endpoints (list, get, create, update, add
scenarios, set members, transition, soft-delete) with strict pydantic
schemas. The transition endpoint splits the perm gate manually so
archive requires mission.archive while other targets use mission.update.
- app/api/users.py — new GET /users/roster returning (id, email,
display_name) only, gated by user.read OR mission.create OR
mission.update — lets non-admin wizard users see assignable peers
without exposing the admin /users payload.
- app/api/diag.py — /diag/reset truncates the mission_* tables before the
template tables because the source_*_template_id FKs are ON DELETE SET
NULL, which is cheaper to short-circuit by removing the children first.
Frontend:
- lib/missions.ts — typed client, queryKey factory, status accent map.
- pages/MissionsListPage.tsx — list cards with status accent + filters
(q, client, status).
- pages/MissionsCreatePage.tsx — 3-step wizard (meta → scenarios → members)
with member roster fed by /users/roster.
- pages/MissionDetailPage.tsx — header + transition buttons (legal next
states only) + Tests/Members/Synthesis/Export tabs.
- Routes + nav entry (visible to anyone with mission.read or admin).
Tests:
- backend/tests/test_missions.py — 22 pytest covering snapshot fidelity,
MITRE propagation, membership visibility, transition state machine,
perm gating, member set replace, append scenarios, soft-delete, partial
update, inverted-date rejection.
- e2e/tests/m6-missions.spec.ts — 5 Playwright (snapshot freezing, non-admin
visibility, status transitions + 409, SPA wizard end-to-end, list filter).
Docs:
- CHANGELOG, tasks/testing-m6.md, tasks/lessons.md (snapshot tradeoffs,
membership=404 pattern, /diag/reset order, auto-creator add).
- README + tasks/todo.md updated.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 15:07:32 +02:00
|
|
|
|
|
|
|
|
const [tab, setTab] = useState<Tab>('tests');
|
fix(m6): mission detail page can now edit metadata, append scenarios, edit members
The M6 SPA shipped the create wizard but the detail page was read-only —
even though the backend already exposed PUT /missions/{id}, POST
/missions/{id}/scenarios, and PUT /missions/{id}/members. So once a
mission was created you couldn't fix a typo in the client name, add a
scenario you forgot, or change member assignments without curl.
Added three modals on the detail page, gated by `is_admin ||
mission.update`:
- Edit metadata (header button, 3xl modal): name + client + dates +
markdown description, same validation as the wizard step 1.
- Add scenarios (Tests tab): scenario picker matching wizard step 2,
calls POST /missions/{id}/scenarios which appends snapshots at
current_max_position + 1.
- Edit members (Members tab): roster + red/blue toggles, calls
PUT /missions/{id}/members (full-set replace), pre-populated with
the current member set.
The detail page now imports useAuth so `canEdit` is computed once and
shared between the three buttons.
E2E: new "detail page edits metadata, appends scenarios, edits members"
spec exercises the three modals end-to-end. M6 e2e count is now 6.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 07:37:06 +02:00
|
|
|
const [editMeta, setEditMeta] = useState(false);
|
|
|
|
|
const [addScenarios, setAddScenarios] = useState(false);
|
|
|
|
|
const [editMembers, setEditMembers] = useState(false);
|
feat(m6): missions + snapshot CRUD, membership visibility, status state machine
Adds the mission layer that materialises template snapshots, plus the SPA
list / 3-step wizard / detail page.
Backend:
- app/services/missions.py — create_mission snapshots scenarios, tests, MITRE
tags in a 4-query write; list/get apply a non-admin membership filter that
collapses to 404 (no existence leak); status state machine enforces
draft → in_progress → completed → archived with archived as a sink; the
non-admin creator is auto-added as role_hint='red' to retain visibility.
- app/api/missions.py — 8 endpoints (list, get, create, update, add
scenarios, set members, transition, soft-delete) with strict pydantic
schemas. The transition endpoint splits the perm gate manually so
archive requires mission.archive while other targets use mission.update.
- app/api/users.py — new GET /users/roster returning (id, email,
display_name) only, gated by user.read OR mission.create OR
mission.update — lets non-admin wizard users see assignable peers
without exposing the admin /users payload.
- app/api/diag.py — /diag/reset truncates the mission_* tables before the
template tables because the source_*_template_id FKs are ON DELETE SET
NULL, which is cheaper to short-circuit by removing the children first.
Frontend:
- lib/missions.ts — typed client, queryKey factory, status accent map.
- pages/MissionsListPage.tsx — list cards with status accent + filters
(q, client, status).
- pages/MissionsCreatePage.tsx — 3-step wizard (meta → scenarios → members)
with member roster fed by /users/roster.
- pages/MissionDetailPage.tsx — header + transition buttons (legal next
states only) + Tests/Members/Synthesis/Export tabs.
- Routes + nav entry (visible to anyone with mission.read or admin).
Tests:
- backend/tests/test_missions.py — 22 pytest covering snapshot fidelity,
MITRE propagation, membership visibility, transition state machine,
perm gating, member set replace, append scenarios, soft-delete, partial
update, inverted-date rejection.
- e2e/tests/m6-missions.spec.ts — 5 Playwright (snapshot freezing, non-admin
visibility, status transitions + 409, SPA wizard end-to-end, list filter).
Docs:
- CHANGELOG, tasks/testing-m6.md, tasks/lessons.md (snapshot tradeoffs,
membership=404 pattern, /diag/reset order, auto-creator add).
- README + tasks/todo.md updated.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 15:07:32 +02:00
|
|
|
const detail = useMission(missionId);
|
|
|
|
|
|
|
|
|
|
const transition = useMutation({
|
|
|
|
|
mutationFn: (body: TransitionPayload) =>
|
|
|
|
|
apiPost<Mission>(`/missions/${missionId}/transition`, body),
|
|
|
|
|
onSuccess: () => {
|
|
|
|
|
qc.invalidateQueries({ queryKey: missionKeys.detail(missionId) });
|
2026-05-13 15:14:57 +02:00
|
|
|
qc.invalidateQueries({ queryKey: missionKeys.listPrefix() });
|
feat(m6): missions + snapshot CRUD, membership visibility, status state machine
Adds the mission layer that materialises template snapshots, plus the SPA
list / 3-step wizard / detail page.
Backend:
- app/services/missions.py — create_mission snapshots scenarios, tests, MITRE
tags in a 4-query write; list/get apply a non-admin membership filter that
collapses to 404 (no existence leak); status state machine enforces
draft → in_progress → completed → archived with archived as a sink; the
non-admin creator is auto-added as role_hint='red' to retain visibility.
- app/api/missions.py — 8 endpoints (list, get, create, update, add
scenarios, set members, transition, soft-delete) with strict pydantic
schemas. The transition endpoint splits the perm gate manually so
archive requires mission.archive while other targets use mission.update.
- app/api/users.py — new GET /users/roster returning (id, email,
display_name) only, gated by user.read OR mission.create OR
mission.update — lets non-admin wizard users see assignable peers
without exposing the admin /users payload.
- app/api/diag.py — /diag/reset truncates the mission_* tables before the
template tables because the source_*_template_id FKs are ON DELETE SET
NULL, which is cheaper to short-circuit by removing the children first.
Frontend:
- lib/missions.ts — typed client, queryKey factory, status accent map.
- pages/MissionsListPage.tsx — list cards with status accent + filters
(q, client, status).
- pages/MissionsCreatePage.tsx — 3-step wizard (meta → scenarios → members)
with member roster fed by /users/roster.
- pages/MissionDetailPage.tsx — header + transition buttons (legal next
states only) + Tests/Members/Synthesis/Export tabs.
- Routes + nav entry (visible to anyone with mission.read or admin).
Tests:
- backend/tests/test_missions.py — 22 pytest covering snapshot fidelity,
MITRE propagation, membership visibility, transition state machine,
perm gating, member set replace, append scenarios, soft-delete, partial
update, inverted-date rejection.
- e2e/tests/m6-missions.spec.ts — 5 Playwright (snapshot freezing, non-admin
visibility, status transitions + 409, SPA wizard end-to-end, list filter).
Docs:
- CHANGELOG, tasks/testing-m6.md, tasks/lessons.md (snapshot tradeoffs,
membership=404 pattern, /diag/reset order, auto-creator add).
- README + tasks/todo.md updated.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 15:07:32 +02:00
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const remove = useMutation({
|
|
|
|
|
mutationFn: () => apiDelete<{ ok: true }>(`/missions/${missionId}`),
|
|
|
|
|
onSuccess: () => {
|
2026-05-13 15:14:57 +02:00
|
|
|
qc.invalidateQueries({ queryKey: missionKeys.listPrefix() });
|
feat(m6): missions + snapshot CRUD, membership visibility, status state machine
Adds the mission layer that materialises template snapshots, plus the SPA
list / 3-step wizard / detail page.
Backend:
- app/services/missions.py — create_mission snapshots scenarios, tests, MITRE
tags in a 4-query write; list/get apply a non-admin membership filter that
collapses to 404 (no existence leak); status state machine enforces
draft → in_progress → completed → archived with archived as a sink; the
non-admin creator is auto-added as role_hint='red' to retain visibility.
- app/api/missions.py — 8 endpoints (list, get, create, update, add
scenarios, set members, transition, soft-delete) with strict pydantic
schemas. The transition endpoint splits the perm gate manually so
archive requires mission.archive while other targets use mission.update.
- app/api/users.py — new GET /users/roster returning (id, email,
display_name) only, gated by user.read OR mission.create OR
mission.update — lets non-admin wizard users see assignable peers
without exposing the admin /users payload.
- app/api/diag.py — /diag/reset truncates the mission_* tables before the
template tables because the source_*_template_id FKs are ON DELETE SET
NULL, which is cheaper to short-circuit by removing the children first.
Frontend:
- lib/missions.ts — typed client, queryKey factory, status accent map.
- pages/MissionsListPage.tsx — list cards with status accent + filters
(q, client, status).
- pages/MissionsCreatePage.tsx — 3-step wizard (meta → scenarios → members)
with member roster fed by /users/roster.
- pages/MissionDetailPage.tsx — header + transition buttons (legal next
states only) + Tests/Members/Synthesis/Export tabs.
- Routes + nav entry (visible to anyone with mission.read or admin).
Tests:
- backend/tests/test_missions.py — 22 pytest covering snapshot fidelity,
MITRE propagation, membership visibility, transition state machine,
perm gating, member set replace, append scenarios, soft-delete, partial
update, inverted-date rejection.
- e2e/tests/m6-missions.spec.ts — 5 Playwright (snapshot freezing, non-admin
visibility, status transitions + 409, SPA wizard end-to-end, list filter).
Docs:
- CHANGELOG, tasks/testing-m6.md, tasks/lessons.md (snapshot tradeoffs,
membership=404 pattern, /diag/reset order, auto-creator add).
- README + tasks/todo.md updated.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 15:07:32 +02:00
|
|
|
navigate('/missions');
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const apiErr = detail.error instanceof ApiError ? detail.error : null;
|
|
|
|
|
const m = detail.data;
|
|
|
|
|
|
|
|
|
|
if (apiErr) {
|
|
|
|
|
return (
|
|
|
|
|
<section>
|
|
|
|
|
<SectionHeader prefix="Mission" highlight="Not found" accent="rose" />
|
|
|
|
|
<Alert accent="rose">{apiErr.message}</Alert>
|
|
|
|
|
</section>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
if (!m) {
|
|
|
|
|
return <p className="font-mono text-xs text-text-dim">Loading mission…</p>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const accent = MISSION_STATUS_ACCENT[m.status];
|
|
|
|
|
const allowedNext = ALLOWED_TRANSITIONS[m.status];
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<section data-testid={`mission-detail-${m.id}`}>
|
|
|
|
|
<div className="flex items-baseline justify-between flex-wrap gap-3">
|
|
|
|
|
<SectionHeader prefix="Mission" highlight={m.name} accent={accent} />
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<Tag accent={accent}>{MISSION_STATUS_LABEL[m.status]}</Tag>
|
fix(m6): mission detail page can now edit metadata, append scenarios, edit members
The M6 SPA shipped the create wizard but the detail page was read-only —
even though the backend already exposed PUT /missions/{id}, POST
/missions/{id}/scenarios, and PUT /missions/{id}/members. So once a
mission was created you couldn't fix a typo in the client name, add a
scenario you forgot, or change member assignments without curl.
Added three modals on the detail page, gated by `is_admin ||
mission.update`:
- Edit metadata (header button, 3xl modal): name + client + dates +
markdown description, same validation as the wizard step 1.
- Add scenarios (Tests tab): scenario picker matching wizard step 2,
calls POST /missions/{id}/scenarios which appends snapshots at
current_max_position + 1.
- Edit members (Members tab): roster + red/blue toggles, calls
PUT /missions/{id}/members (full-set replace), pre-populated with
the current member set.
The detail page now imports useAuth so `canEdit` is computed once and
shared between the three buttons.
E2E: new "detail page edits metadata, appends scenarios, edits members"
spec exercises the three modals end-to-end. M6 e2e count is now 6.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 07:37:06 +02:00
|
|
|
{canEdit && (
|
|
|
|
|
<Button
|
|
|
|
|
accent="cyan"
|
|
|
|
|
variant="outline"
|
|
|
|
|
onClick={() => setEditMeta(true)}
|
|
|
|
|
data-testid="mission-edit-meta"
|
|
|
|
|
>
|
|
|
|
|
Edit
|
|
|
|
|
</Button>
|
|
|
|
|
)}
|
feat(m6): missions + snapshot CRUD, membership visibility, status state machine
Adds the mission layer that materialises template snapshots, plus the SPA
list / 3-step wizard / detail page.
Backend:
- app/services/missions.py — create_mission snapshots scenarios, tests, MITRE
tags in a 4-query write; list/get apply a non-admin membership filter that
collapses to 404 (no existence leak); status state machine enforces
draft → in_progress → completed → archived with archived as a sink; the
non-admin creator is auto-added as role_hint='red' to retain visibility.
- app/api/missions.py — 8 endpoints (list, get, create, update, add
scenarios, set members, transition, soft-delete) with strict pydantic
schemas. The transition endpoint splits the perm gate manually so
archive requires mission.archive while other targets use mission.update.
- app/api/users.py — new GET /users/roster returning (id, email,
display_name) only, gated by user.read OR mission.create OR
mission.update — lets non-admin wizard users see assignable peers
without exposing the admin /users payload.
- app/api/diag.py — /diag/reset truncates the mission_* tables before the
template tables because the source_*_template_id FKs are ON DELETE SET
NULL, which is cheaper to short-circuit by removing the children first.
Frontend:
- lib/missions.ts — typed client, queryKey factory, status accent map.
- pages/MissionsListPage.tsx — list cards with status accent + filters
(q, client, status).
- pages/MissionsCreatePage.tsx — 3-step wizard (meta → scenarios → members)
with member roster fed by /users/roster.
- pages/MissionDetailPage.tsx — header + transition buttons (legal next
states only) + Tests/Members/Synthesis/Export tabs.
- Routes + nav entry (visible to anyone with mission.read or admin).
Tests:
- backend/tests/test_missions.py — 22 pytest covering snapshot fidelity,
MITRE propagation, membership visibility, transition state machine,
perm gating, member set replace, append scenarios, soft-delete, partial
update, inverted-date rejection.
- e2e/tests/m6-missions.spec.ts — 5 Playwright (snapshot freezing, non-admin
visibility, status transitions + 409, SPA wizard end-to-end, list filter).
Docs:
- CHANGELOG, tasks/testing-m6.md, tasks/lessons.md (snapshot tradeoffs,
membership=404 pattern, /diag/reset order, auto-creator add).
- README + tasks/todo.md updated.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 15:07:32 +02:00
|
|
|
{allowedNext.map((target) => (
|
|
|
|
|
<Button
|
|
|
|
|
key={target}
|
2026-05-13 15:14:57 +02:00
|
|
|
accent={TRANSITION_BUTTON_ACCENT[target]}
|
feat(m6): missions + snapshot CRUD, membership visibility, status state machine
Adds the mission layer that materialises template snapshots, plus the SPA
list / 3-step wizard / detail page.
Backend:
- app/services/missions.py — create_mission snapshots scenarios, tests, MITRE
tags in a 4-query write; list/get apply a non-admin membership filter that
collapses to 404 (no existence leak); status state machine enforces
draft → in_progress → completed → archived with archived as a sink; the
non-admin creator is auto-added as role_hint='red' to retain visibility.
- app/api/missions.py — 8 endpoints (list, get, create, update, add
scenarios, set members, transition, soft-delete) with strict pydantic
schemas. The transition endpoint splits the perm gate manually so
archive requires mission.archive while other targets use mission.update.
- app/api/users.py — new GET /users/roster returning (id, email,
display_name) only, gated by user.read OR mission.create OR
mission.update — lets non-admin wizard users see assignable peers
without exposing the admin /users payload.
- app/api/diag.py — /diag/reset truncates the mission_* tables before the
template tables because the source_*_template_id FKs are ON DELETE SET
NULL, which is cheaper to short-circuit by removing the children first.
Frontend:
- lib/missions.ts — typed client, queryKey factory, status accent map.
- pages/MissionsListPage.tsx — list cards with status accent + filters
(q, client, status).
- pages/MissionsCreatePage.tsx — 3-step wizard (meta → scenarios → members)
with member roster fed by /users/roster.
- pages/MissionDetailPage.tsx — header + transition buttons (legal next
states only) + Tests/Members/Synthesis/Export tabs.
- Routes + nav entry (visible to anyone with mission.read or admin).
Tests:
- backend/tests/test_missions.py — 22 pytest covering snapshot fidelity,
MITRE propagation, membership visibility, transition state machine,
perm gating, member set replace, append scenarios, soft-delete, partial
update, inverted-date rejection.
- e2e/tests/m6-missions.spec.ts — 5 Playwright (snapshot freezing, non-admin
visibility, status transitions + 409, SPA wizard end-to-end, list filter).
Docs:
- CHANGELOG, tasks/testing-m6.md, tasks/lessons.md (snapshot tradeoffs,
membership=404 pattern, /diag/reset order, auto-creator add).
- README + tasks/todo.md updated.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 15:07:32 +02:00
|
|
|
onClick={() => transition.mutate({ status: target })}
|
|
|
|
|
data-testid={`mission-transition-${target}`}
|
|
|
|
|
disabled={transition.isPending}
|
|
|
|
|
>
|
|
|
|
|
→ {MISSION_STATUS_LABEL[target]}
|
|
|
|
|
</Button>
|
|
|
|
|
))}
|
|
|
|
|
<Button
|
|
|
|
|
accent="rose"
|
|
|
|
|
variant="ghost"
|
|
|
|
|
onClick={() => {
|
|
|
|
|
if (
|
|
|
|
|
window.confirm(
|
|
|
|
|
`Soft-delete mission "${m.name}"? An admin can restore from the trash.`,
|
|
|
|
|
)
|
|
|
|
|
) {
|
|
|
|
|
remove.mutate();
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
data-testid="mission-delete"
|
|
|
|
|
disabled={remove.isPending}
|
|
|
|
|
>
|
|
|
|
|
Delete
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<Card className="mb-4">
|
|
|
|
|
<dl className="grid grid-cols-2 gap-3 md:grid-cols-4 font-mono text-2xs">
|
|
|
|
|
<div>
|
|
|
|
|
<dt className="text-text-dim uppercase tracking-wider2">Client</dt>
|
|
|
|
|
<dd className="text-text-bright">{m.client_target ?? '—'}</dd>
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<dt className="text-text-dim uppercase tracking-wider2">Dates</dt>
|
|
|
|
|
<dd className="text-text-bright">
|
|
|
|
|
{formatDateRange(m.date_start, m.date_end)}
|
|
|
|
|
</dd>
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<dt className="text-text-dim uppercase tracking-wider2">Scenarios</dt>
|
|
|
|
|
<dd className="text-text-bright">{m.scenarios_count}</dd>
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<dt className="text-text-dim uppercase tracking-wider2">Tests</dt>
|
|
|
|
|
<dd className="text-text-bright">{m.tests_count}</dd>
|
|
|
|
|
</div>
|
|
|
|
|
</dl>
|
|
|
|
|
{m.description_md && (
|
|
|
|
|
<pre className="mt-3 whitespace-pre-wrap font-mono text-xs text-text">{m.description_md}</pre>
|
|
|
|
|
)}
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
<nav className="flex gap-1 border-b border-border mb-4" aria-label="Mission tabs">
|
|
|
|
|
{TABS.map((t) => (
|
|
|
|
|
<button
|
|
|
|
|
key={t}
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() => setTab(t)}
|
|
|
|
|
data-testid={`mission-tab-${t}`}
|
|
|
|
|
className={`px-3 py-2 font-mono text-2xs uppercase tracking-wider2 ${
|
|
|
|
|
tab === t
|
|
|
|
|
? 'text-cyan border-b-2 border-cyan -mb-px'
|
|
|
|
|
: 'text-text-dim hover:text-text-bright'
|
|
|
|
|
}`}
|
|
|
|
|
>
|
|
|
|
|
{t}
|
|
|
|
|
</button>
|
|
|
|
|
))}
|
|
|
|
|
</nav>
|
|
|
|
|
|
|
|
|
|
{tab === 'tests' && (
|
|
|
|
|
<Card>
|
fix(m6): mission detail page can now edit metadata, append scenarios, edit members
The M6 SPA shipped the create wizard but the detail page was read-only —
even though the backend already exposed PUT /missions/{id}, POST
/missions/{id}/scenarios, and PUT /missions/{id}/members. So once a
mission was created you couldn't fix a typo in the client name, add a
scenario you forgot, or change member assignments without curl.
Added three modals on the detail page, gated by `is_admin ||
mission.update`:
- Edit metadata (header button, 3xl modal): name + client + dates +
markdown description, same validation as the wizard step 1.
- Add scenarios (Tests tab): scenario picker matching wizard step 2,
calls POST /missions/{id}/scenarios which appends snapshots at
current_max_position + 1.
- Edit members (Members tab): roster + red/blue toggles, calls
PUT /missions/{id}/members (full-set replace), pre-populated with
the current member set.
The detail page now imports useAuth so `canEdit` is computed once and
shared between the three buttons.
E2E: new "detail page edits metadata, appends scenarios, edits members"
spec exercises the three modals end-to-end. M6 e2e count is now 6.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 07:37:06 +02:00
|
|
|
<div className="flex items-baseline justify-between flex-wrap gap-2 mb-3">
|
|
|
|
|
<p className="font-mono text-2xs text-text-dim">
|
|
|
|
|
Snapshots are frozen at append time — editing a source template
|
|
|
|
|
does not propagate.
|
|
|
|
|
</p>
|
|
|
|
|
{canEdit && (
|
|
|
|
|
<Button
|
|
|
|
|
accent="cyan"
|
|
|
|
|
onClick={() => setAddScenarios(true)}
|
|
|
|
|
data-testid="mission-add-scenarios"
|
|
|
|
|
>
|
|
|
|
|
+ Add scenarios
|
|
|
|
|
</Button>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
feat(m6): missions + snapshot CRUD, membership visibility, status state machine
Adds the mission layer that materialises template snapshots, plus the SPA
list / 3-step wizard / detail page.
Backend:
- app/services/missions.py — create_mission snapshots scenarios, tests, MITRE
tags in a 4-query write; list/get apply a non-admin membership filter that
collapses to 404 (no existence leak); status state machine enforces
draft → in_progress → completed → archived with archived as a sink; the
non-admin creator is auto-added as role_hint='red' to retain visibility.
- app/api/missions.py — 8 endpoints (list, get, create, update, add
scenarios, set members, transition, soft-delete) with strict pydantic
schemas. The transition endpoint splits the perm gate manually so
archive requires mission.archive while other targets use mission.update.
- app/api/users.py — new GET /users/roster returning (id, email,
display_name) only, gated by user.read OR mission.create OR
mission.update — lets non-admin wizard users see assignable peers
without exposing the admin /users payload.
- app/api/diag.py — /diag/reset truncates the mission_* tables before the
template tables because the source_*_template_id FKs are ON DELETE SET
NULL, which is cheaper to short-circuit by removing the children first.
Frontend:
- lib/missions.ts — typed client, queryKey factory, status accent map.
- pages/MissionsListPage.tsx — list cards with status accent + filters
(q, client, status).
- pages/MissionsCreatePage.tsx — 3-step wizard (meta → scenarios → members)
with member roster fed by /users/roster.
- pages/MissionDetailPage.tsx — header + transition buttons (legal next
states only) + Tests/Members/Synthesis/Export tabs.
- Routes + nav entry (visible to anyone with mission.read or admin).
Tests:
- backend/tests/test_missions.py — 22 pytest covering snapshot fidelity,
MITRE propagation, membership visibility, transition state machine,
perm gating, member set replace, append scenarios, soft-delete, partial
update, inverted-date rejection.
- e2e/tests/m6-missions.spec.ts — 5 Playwright (snapshot freezing, non-admin
visibility, status transitions + 409, SPA wizard end-to-end, list filter).
Docs:
- CHANGELOG, tasks/testing-m6.md, tasks/lessons.md (snapshot tradeoffs,
membership=404 pattern, /diag/reset order, auto-creator add).
- README + tasks/todo.md updated.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 15:07:32 +02:00
|
|
|
{m.scenarios.length === 0 ? (
|
|
|
|
|
<p className="font-mono text-xs text-text-dim">
|
|
|
|
|
No scenarios snapshotted yet.
|
fix(m6): mission detail page can now edit metadata, append scenarios, edit members
The M6 SPA shipped the create wizard but the detail page was read-only —
even though the backend already exposed PUT /missions/{id}, POST
/missions/{id}/scenarios, and PUT /missions/{id}/members. So once a
mission was created you couldn't fix a typo in the client name, add a
scenario you forgot, or change member assignments without curl.
Added three modals on the detail page, gated by `is_admin ||
mission.update`:
- Edit metadata (header button, 3xl modal): name + client + dates +
markdown description, same validation as the wizard step 1.
- Add scenarios (Tests tab): scenario picker matching wizard step 2,
calls POST /missions/{id}/scenarios which appends snapshots at
current_max_position + 1.
- Edit members (Members tab): roster + red/blue toggles, calls
PUT /missions/{id}/members (full-set replace), pre-populated with
the current member set.
The detail page now imports useAuth so `canEdit` is computed once and
shared between the three buttons.
E2E: new "detail page edits metadata, appends scenarios, edits members"
spec exercises the three modals end-to-end. M6 e2e count is now 6.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 07:37:06 +02:00
|
|
|
{canEdit && ' Click "Add scenarios" to append one.'}
|
feat(m6): missions + snapshot CRUD, membership visibility, status state machine
Adds the mission layer that materialises template snapshots, plus the SPA
list / 3-step wizard / detail page.
Backend:
- app/services/missions.py — create_mission snapshots scenarios, tests, MITRE
tags in a 4-query write; list/get apply a non-admin membership filter that
collapses to 404 (no existence leak); status state machine enforces
draft → in_progress → completed → archived with archived as a sink; the
non-admin creator is auto-added as role_hint='red' to retain visibility.
- app/api/missions.py — 8 endpoints (list, get, create, update, add
scenarios, set members, transition, soft-delete) with strict pydantic
schemas. The transition endpoint splits the perm gate manually so
archive requires mission.archive while other targets use mission.update.
- app/api/users.py — new GET /users/roster returning (id, email,
display_name) only, gated by user.read OR mission.create OR
mission.update — lets non-admin wizard users see assignable peers
without exposing the admin /users payload.
- app/api/diag.py — /diag/reset truncates the mission_* tables before the
template tables because the source_*_template_id FKs are ON DELETE SET
NULL, which is cheaper to short-circuit by removing the children first.
Frontend:
- lib/missions.ts — typed client, queryKey factory, status accent map.
- pages/MissionsListPage.tsx — list cards with status accent + filters
(q, client, status).
- pages/MissionsCreatePage.tsx — 3-step wizard (meta → scenarios → members)
with member roster fed by /users/roster.
- pages/MissionDetailPage.tsx — header + transition buttons (legal next
states only) + Tests/Members/Synthesis/Export tabs.
- Routes + nav entry (visible to anyone with mission.read or admin).
Tests:
- backend/tests/test_missions.py — 22 pytest covering snapshot fidelity,
MITRE propagation, membership visibility, transition state machine,
perm gating, member set replace, append scenarios, soft-delete, partial
update, inverted-date rejection.
- e2e/tests/m6-missions.spec.ts — 5 Playwright (snapshot freezing, non-admin
visibility, status transitions + 409, SPA wizard end-to-end, list filter).
Docs:
- CHANGELOG, tasks/testing-m6.md, tasks/lessons.md (snapshot tradeoffs,
membership=404 pattern, /diag/reset order, auto-creator add).
- README + tasks/todo.md updated.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 15:07:32 +02:00
|
|
|
</p>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="flex flex-col gap-4" data-testid="mission-scenarios">
|
|
|
|
|
{m.scenarios.map((sc) => (
|
|
|
|
|
<div
|
|
|
|
|
key={sc.id}
|
|
|
|
|
className="rounded-md border border-border bg-bg-card p-3"
|
|
|
|
|
data-testid={`mission-scenario-${sc.id}`}
|
|
|
|
|
>
|
|
|
|
|
<div className="flex items-center gap-2 mb-2">
|
|
|
|
|
<Tag accent="cyan">#{sc.position + 1}</Tag>
|
|
|
|
|
<p className="font-mono text-xs text-text-bright">
|
|
|
|
|
{sc.snapshot_name}
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
{sc.snapshot_description && (
|
|
|
|
|
<p className="mb-2 font-mono text-2xs text-text-dim">
|
|
|
|
|
{sc.snapshot_description}
|
|
|
|
|
</p>
|
|
|
|
|
)}
|
|
|
|
|
<table className="w-full font-mono text-2xs">
|
|
|
|
|
<thead>
|
|
|
|
|
<tr className="text-text-dim uppercase tracking-wider2">
|
|
|
|
|
<th className="text-left py-1">#</th>
|
|
|
|
|
<th className="text-left py-1">Test</th>
|
|
|
|
|
<th className="text-left py-1">MITRE</th>
|
|
|
|
|
<th className="text-left py-1">OPSEC</th>
|
|
|
|
|
<th className="text-left py-1">State</th>
|
|
|
|
|
</tr>
|
|
|
|
|
</thead>
|
|
|
|
|
<tbody>
|
|
|
|
|
{sc.tests.map((t) => (
|
|
|
|
|
<tr
|
|
|
|
|
key={t.id}
|
feat(m7): per-test execution — red/blue zones, evidence pipeline, activity poll
DoD M7 (spec §F5 + §F6 + §F8 + tasks/todo.md M7) covered end-to-end:
Backend
- New migration `91a4e7c6d2f3` adds `mission_tests.last_actor_id` (FK users
ON DELETE SET NULL) and `ix_mission_tests_updated_at` for the polling query.
- `detection_levels`: 4 default rows seeded at boot, `GET /detection-levels`
read-only (CRUD lands in M8).
- `mission_tests` service + `missions` API extension:
- `GET /missions/{id}/tests/{test_id}` — full detail incl. evidence list
- `PUT /missions/{id}/tests/{test_id}` — patch red/blue fields with per-field
perm classification (`mission.write_red_fields` vs `mission.write_blue_fields`)
- `POST /missions/{id}/tests/{test_id}/transition` — pending↔skipped/blocked
and pending→executed→reviewed_by_blue (+ undo paths), side-aware perm gate
that fires *before* idempotency, `executed_at` auto-stamped on the way in
- `GET /missions/{id}/activity?since=<ISO>` — drives the 15 s polling badge
- `evidence` service + top-level `/evidence/<id>` API:
- Streaming upload, SHA256 chunk-by-chunk, 25 MB cap, ext+MIME whitelist
- Content-addressed storage at ${EVIDENCE_DIR}/<mission>/<test>/<sha256><ext>
- Atomic `os.replace`, hex-validated SHA path component, root-dir guard
- Membership-aware (404 on miss/forbidden, no existence leak)
- `/diag/reset` now wipes ${EVIDENCE_DIR}/* in test mode (symlink-safe) and
re-seeds detection levels as a safety net.
Frontend
- `lib/missions.ts` — M7 types + queryKey factory + state-machine matrix.
- `pages/MissionTestPage.tsx` — two-zone layout: red border (command, output,
comment, mark-executed + override toggle) and cyan border (detection-level
select, comment, drag-and-drop evidence dropzone). Last-touched badge polls
/activity every 15 s, gated on document.visibilityState. Per-field disable
based on the user's red/blue perms (server stays the arbiter).
- `pages/MissionDetailPage.tsx` — test rows link to the new per-test page.
- `App.tsx` — registers /missions/:id/tests/:testId behind RequireAuth.
- `HomePage.tsx` — hero + roadmap card bumped to M7; next is M8.
Tests
- `backend/tests/test_mission_tests.py` — 27 pytest tests (red/blue field
gating, state-machine matrix incl. idempotent-side enforcement, executed_at
override, 24/26 MB upload + SHA256, MIME/ext whitelist, soft-delete hide,
activity polling with URL-encoded `since`, membership 404 vs admin bypass,
cross-mission evidence access).
- `e2e/tests/m7-execution.spec.ts` — 5 Playwright tests against the live stack
(red-only/blue-only API gating, mark-executed + reviewed_by_blue side
enforcement, 24 MB/26 MB upload + SHA256 round-trip, SPA per-test page save
+ transition, non-member 404 message). afterAll restores stable admin and
re-syncs MITRE.
Docs
- CHANGELOG.md: M7 section + post-M7 review-pass subsection.
- README.md: status, feature blurb, roadmap, testing-m7 link.
- tasks/testing-m7.md: manual + automated procedure with transition matrix
and perm-gating table.
- tasks/lessons.md: M7 retrospectives (LogRecord `created` trap, URL-encoded
query timestamps, perm-before-flush, atomic move, polling visibility gate).
Test count: 133 pytest / 49 Playwright, all green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 08:16:48 +02:00
|
|
|
className="border-t border-border/40 hover:bg-bg-base/60"
|
feat(m6): missions + snapshot CRUD, membership visibility, status state machine
Adds the mission layer that materialises template snapshots, plus the SPA
list / 3-step wizard / detail page.
Backend:
- app/services/missions.py — create_mission snapshots scenarios, tests, MITRE
tags in a 4-query write; list/get apply a non-admin membership filter that
collapses to 404 (no existence leak); status state machine enforces
draft → in_progress → completed → archived with archived as a sink; the
non-admin creator is auto-added as role_hint='red' to retain visibility.
- app/api/missions.py — 8 endpoints (list, get, create, update, add
scenarios, set members, transition, soft-delete) with strict pydantic
schemas. The transition endpoint splits the perm gate manually so
archive requires mission.archive while other targets use mission.update.
- app/api/users.py — new GET /users/roster returning (id, email,
display_name) only, gated by user.read OR mission.create OR
mission.update — lets non-admin wizard users see assignable peers
without exposing the admin /users payload.
- app/api/diag.py — /diag/reset truncates the mission_* tables before the
template tables because the source_*_template_id FKs are ON DELETE SET
NULL, which is cheaper to short-circuit by removing the children first.
Frontend:
- lib/missions.ts — typed client, queryKey factory, status accent map.
- pages/MissionsListPage.tsx — list cards with status accent + filters
(q, client, status).
- pages/MissionsCreatePage.tsx — 3-step wizard (meta → scenarios → members)
with member roster fed by /users/roster.
- pages/MissionDetailPage.tsx — header + transition buttons (legal next
states only) + Tests/Members/Synthesis/Export tabs.
- Routes + nav entry (visible to anyone with mission.read or admin).
Tests:
- backend/tests/test_missions.py — 22 pytest covering snapshot fidelity,
MITRE propagation, membership visibility, transition state machine,
perm gating, member set replace, append scenarios, soft-delete, partial
update, inverted-date rejection.
- e2e/tests/m6-missions.spec.ts — 5 Playwright (snapshot freezing, non-admin
visibility, status transitions + 409, SPA wizard end-to-end, list filter).
Docs:
- CHANGELOG, tasks/testing-m6.md, tasks/lessons.md (snapshot tradeoffs,
membership=404 pattern, /diag/reset order, auto-creator add).
- README + tasks/todo.md updated.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 15:07:32 +02:00
|
|
|
data-testid={`mission-test-${t.id}`}
|
|
|
|
|
>
|
|
|
|
|
<td className="py-1 text-text-dim">{t.position + 1}</td>
|
feat(m7): per-test execution — red/blue zones, evidence pipeline, activity poll
DoD M7 (spec §F5 + §F6 + §F8 + tasks/todo.md M7) covered end-to-end:
Backend
- New migration `91a4e7c6d2f3` adds `mission_tests.last_actor_id` (FK users
ON DELETE SET NULL) and `ix_mission_tests_updated_at` for the polling query.
- `detection_levels`: 4 default rows seeded at boot, `GET /detection-levels`
read-only (CRUD lands in M8).
- `mission_tests` service + `missions` API extension:
- `GET /missions/{id}/tests/{test_id}` — full detail incl. evidence list
- `PUT /missions/{id}/tests/{test_id}` — patch red/blue fields with per-field
perm classification (`mission.write_red_fields` vs `mission.write_blue_fields`)
- `POST /missions/{id}/tests/{test_id}/transition` — pending↔skipped/blocked
and pending→executed→reviewed_by_blue (+ undo paths), side-aware perm gate
that fires *before* idempotency, `executed_at` auto-stamped on the way in
- `GET /missions/{id}/activity?since=<ISO>` — drives the 15 s polling badge
- `evidence` service + top-level `/evidence/<id>` API:
- Streaming upload, SHA256 chunk-by-chunk, 25 MB cap, ext+MIME whitelist
- Content-addressed storage at ${EVIDENCE_DIR}/<mission>/<test>/<sha256><ext>
- Atomic `os.replace`, hex-validated SHA path component, root-dir guard
- Membership-aware (404 on miss/forbidden, no existence leak)
- `/diag/reset` now wipes ${EVIDENCE_DIR}/* in test mode (symlink-safe) and
re-seeds detection levels as a safety net.
Frontend
- `lib/missions.ts` — M7 types + queryKey factory + state-machine matrix.
- `pages/MissionTestPage.tsx` — two-zone layout: red border (command, output,
comment, mark-executed + override toggle) and cyan border (detection-level
select, comment, drag-and-drop evidence dropzone). Last-touched badge polls
/activity every 15 s, gated on document.visibilityState. Per-field disable
based on the user's red/blue perms (server stays the arbiter).
- `pages/MissionDetailPage.tsx` — test rows link to the new per-test page.
- `App.tsx` — registers /missions/:id/tests/:testId behind RequireAuth.
- `HomePage.tsx` — hero + roadmap card bumped to M7; next is M8.
Tests
- `backend/tests/test_mission_tests.py` — 27 pytest tests (red/blue field
gating, state-machine matrix incl. idempotent-side enforcement, executed_at
override, 24/26 MB upload + SHA256, MIME/ext whitelist, soft-delete hide,
activity polling with URL-encoded `since`, membership 404 vs admin bypass,
cross-mission evidence access).
- `e2e/tests/m7-execution.spec.ts` — 5 Playwright tests against the live stack
(red-only/blue-only API gating, mark-executed + reviewed_by_blue side
enforcement, 24 MB/26 MB upload + SHA256 round-trip, SPA per-test page save
+ transition, non-member 404 message). afterAll restores stable admin and
re-syncs MITRE.
Docs
- CHANGELOG.md: M7 section + post-M7 review-pass subsection.
- README.md: status, feature blurb, roadmap, testing-m7 link.
- tasks/testing-m7.md: manual + automated procedure with transition matrix
and perm-gating table.
- tasks/lessons.md: M7 retrospectives (LogRecord `created` trap, URL-encoded
query timestamps, perm-before-flush, atomic move, polling visibility gate).
Test count: 133 pytest / 49 Playwright, all green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 08:16:48 +02:00
|
|
|
<td className="py-1 text-text-bright">
|
|
|
|
|
<Link
|
|
|
|
|
to={`/missions/${m.id}/tests/${t.id}`}
|
|
|
|
|
className="hover:underline"
|
|
|
|
|
data-testid={`mission-test-link-${t.id}`}
|
|
|
|
|
>
|
|
|
|
|
{t.snapshot_name}
|
|
|
|
|
</Link>
|
|
|
|
|
</td>
|
feat(m6): missions + snapshot CRUD, membership visibility, status state machine
Adds the mission layer that materialises template snapshots, plus the SPA
list / 3-step wizard / detail page.
Backend:
- app/services/missions.py — create_mission snapshots scenarios, tests, MITRE
tags in a 4-query write; list/get apply a non-admin membership filter that
collapses to 404 (no existence leak); status state machine enforces
draft → in_progress → completed → archived with archived as a sink; the
non-admin creator is auto-added as role_hint='red' to retain visibility.
- app/api/missions.py — 8 endpoints (list, get, create, update, add
scenarios, set members, transition, soft-delete) with strict pydantic
schemas. The transition endpoint splits the perm gate manually so
archive requires mission.archive while other targets use mission.update.
- app/api/users.py — new GET /users/roster returning (id, email,
display_name) only, gated by user.read OR mission.create OR
mission.update — lets non-admin wizard users see assignable peers
without exposing the admin /users payload.
- app/api/diag.py — /diag/reset truncates the mission_* tables before the
template tables because the source_*_template_id FKs are ON DELETE SET
NULL, which is cheaper to short-circuit by removing the children first.
Frontend:
- lib/missions.ts — typed client, queryKey factory, status accent map.
- pages/MissionsListPage.tsx — list cards with status accent + filters
(q, client, status).
- pages/MissionsCreatePage.tsx — 3-step wizard (meta → scenarios → members)
with member roster fed by /users/roster.
- pages/MissionDetailPage.tsx — header + transition buttons (legal next
states only) + Tests/Members/Synthesis/Export tabs.
- Routes + nav entry (visible to anyone with mission.read or admin).
Tests:
- backend/tests/test_missions.py — 22 pytest covering snapshot fidelity,
MITRE propagation, membership visibility, transition state machine,
perm gating, member set replace, append scenarios, soft-delete, partial
update, inverted-date rejection.
- e2e/tests/m6-missions.spec.ts — 5 Playwright (snapshot freezing, non-admin
visibility, status transitions + 409, SPA wizard end-to-end, list filter).
Docs:
- CHANGELOG, tasks/testing-m6.md, tasks/lessons.md (snapshot tradeoffs,
membership=404 pattern, /diag/reset order, auto-creator add).
- README + tasks/todo.md updated.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 15:07:32 +02:00
|
|
|
<td className="py-1">
|
|
|
|
|
<div className="flex flex-wrap gap-1">
|
|
|
|
|
{t.mitre_tags.map((tag) => (
|
|
|
|
|
<Tag
|
|
|
|
|
accent="cyan"
|
|
|
|
|
key={`${tag.kind}-${tag.external_id}`}
|
|
|
|
|
>
|
|
|
|
|
{tag.external_id}
|
|
|
|
|
</Tag>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</td>
|
|
|
|
|
<td className="py-1 text-text">
|
|
|
|
|
{t.snapshot_opsec_level}
|
|
|
|
|
</td>
|
|
|
|
|
<td className="py-1">
|
|
|
|
|
<Tag
|
|
|
|
|
accent={
|
|
|
|
|
t.state === 'pending'
|
|
|
|
|
? 'teal'
|
|
|
|
|
: t.state === 'executed'
|
|
|
|
|
? 'orange'
|
|
|
|
|
: t.state === 'reviewed_by_blue'
|
|
|
|
|
? 'green'
|
|
|
|
|
: 'rose'
|
|
|
|
|
}
|
|
|
|
|
>
|
|
|
|
|
{t.state}
|
|
|
|
|
</Tag>
|
|
|
|
|
</td>
|
|
|
|
|
</tr>
|
|
|
|
|
))}
|
|
|
|
|
</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</Card>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{tab === 'members' && (
|
|
|
|
|
<Card>
|
fix(m6): mission detail page can now edit metadata, append scenarios, edit members
The M6 SPA shipped the create wizard but the detail page was read-only —
even though the backend already exposed PUT /missions/{id}, POST
/missions/{id}/scenarios, and PUT /missions/{id}/members. So once a
mission was created you couldn't fix a typo in the client name, add a
scenario you forgot, or change member assignments without curl.
Added three modals on the detail page, gated by `is_admin ||
mission.update`:
- Edit metadata (header button, 3xl modal): name + client + dates +
markdown description, same validation as the wizard step 1.
- Add scenarios (Tests tab): scenario picker matching wizard step 2,
calls POST /missions/{id}/scenarios which appends snapshots at
current_max_position + 1.
- Edit members (Members tab): roster + red/blue toggles, calls
PUT /missions/{id}/members (full-set replace), pre-populated with
the current member set.
The detail page now imports useAuth so `canEdit` is computed once and
shared between the three buttons.
E2E: new "detail page edits metadata, appends scenarios, edits members"
spec exercises the three modals end-to-end. M6 e2e count is now 6.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 07:37:06 +02:00
|
|
|
<div className="flex items-baseline justify-between flex-wrap gap-2 mb-3">
|
|
|
|
|
<p className="font-mono text-2xs text-text-dim">
|
|
|
|
|
Members see this mission and (for reds) can author red-side fields
|
|
|
|
|
on its tests in M7+.
|
|
|
|
|
</p>
|
|
|
|
|
{canEdit && (
|
|
|
|
|
<Button
|
|
|
|
|
accent="cyan"
|
|
|
|
|
onClick={() => setEditMembers(true)}
|
|
|
|
|
data-testid="mission-edit-members"
|
|
|
|
|
>
|
|
|
|
|
Edit members
|
|
|
|
|
</Button>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
feat(m6): missions + snapshot CRUD, membership visibility, status state machine
Adds the mission layer that materialises template snapshots, plus the SPA
list / 3-step wizard / detail page.
Backend:
- app/services/missions.py — create_mission snapshots scenarios, tests, MITRE
tags in a 4-query write; list/get apply a non-admin membership filter that
collapses to 404 (no existence leak); status state machine enforces
draft → in_progress → completed → archived with archived as a sink; the
non-admin creator is auto-added as role_hint='red' to retain visibility.
- app/api/missions.py — 8 endpoints (list, get, create, update, add
scenarios, set members, transition, soft-delete) with strict pydantic
schemas. The transition endpoint splits the perm gate manually so
archive requires mission.archive while other targets use mission.update.
- app/api/users.py — new GET /users/roster returning (id, email,
display_name) only, gated by user.read OR mission.create OR
mission.update — lets non-admin wizard users see assignable peers
without exposing the admin /users payload.
- app/api/diag.py — /diag/reset truncates the mission_* tables before the
template tables because the source_*_template_id FKs are ON DELETE SET
NULL, which is cheaper to short-circuit by removing the children first.
Frontend:
- lib/missions.ts — typed client, queryKey factory, status accent map.
- pages/MissionsListPage.tsx — list cards with status accent + filters
(q, client, status).
- pages/MissionsCreatePage.tsx — 3-step wizard (meta → scenarios → members)
with member roster fed by /users/roster.
- pages/MissionDetailPage.tsx — header + transition buttons (legal next
states only) + Tests/Members/Synthesis/Export tabs.
- Routes + nav entry (visible to anyone with mission.read or admin).
Tests:
- backend/tests/test_missions.py — 22 pytest covering snapshot fidelity,
MITRE propagation, membership visibility, transition state machine,
perm gating, member set replace, append scenarios, soft-delete, partial
update, inverted-date rejection.
- e2e/tests/m6-missions.spec.ts — 5 Playwright (snapshot freezing, non-admin
visibility, status transitions + 409, SPA wizard end-to-end, list filter).
Docs:
- CHANGELOG, tasks/testing-m6.md, tasks/lessons.md (snapshot tradeoffs,
membership=404 pattern, /diag/reset order, auto-creator add).
- README + tasks/todo.md updated.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 15:07:32 +02:00
|
|
|
{m.members.length === 0 ? (
|
|
|
|
|
<p className="font-mono text-xs text-text-dim">
|
fix(m6): mission detail page can now edit metadata, append scenarios, edit members
The M6 SPA shipped the create wizard but the detail page was read-only —
even though the backend already exposed PUT /missions/{id}, POST
/missions/{id}/scenarios, and PUT /missions/{id}/members. So once a
mission was created you couldn't fix a typo in the client name, add a
scenario you forgot, or change member assignments without curl.
Added three modals on the detail page, gated by `is_admin ||
mission.update`:
- Edit metadata (header button, 3xl modal): name + client + dates +
markdown description, same validation as the wizard step 1.
- Add scenarios (Tests tab): scenario picker matching wizard step 2,
calls POST /missions/{id}/scenarios which appends snapshots at
current_max_position + 1.
- Edit members (Members tab): roster + red/blue toggles, calls
PUT /missions/{id}/members (full-set replace), pre-populated with
the current member set.
The detail page now imports useAuth so `canEdit` is computed once and
shared between the three buttons.
E2E: new "detail page edits metadata, appends scenarios, edits members"
spec exercises the three modals end-to-end. M6 e2e count is now 6.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 07:37:06 +02:00
|
|
|
No members assigned.
|
|
|
|
|
{canEdit && ' Click "Edit members" to add some.'}
|
feat(m6): missions + snapshot CRUD, membership visibility, status state machine
Adds the mission layer that materialises template snapshots, plus the SPA
list / 3-step wizard / detail page.
Backend:
- app/services/missions.py — create_mission snapshots scenarios, tests, MITRE
tags in a 4-query write; list/get apply a non-admin membership filter that
collapses to 404 (no existence leak); status state machine enforces
draft → in_progress → completed → archived with archived as a sink; the
non-admin creator is auto-added as role_hint='red' to retain visibility.
- app/api/missions.py — 8 endpoints (list, get, create, update, add
scenarios, set members, transition, soft-delete) with strict pydantic
schemas. The transition endpoint splits the perm gate manually so
archive requires mission.archive while other targets use mission.update.
- app/api/users.py — new GET /users/roster returning (id, email,
display_name) only, gated by user.read OR mission.create OR
mission.update — lets non-admin wizard users see assignable peers
without exposing the admin /users payload.
- app/api/diag.py — /diag/reset truncates the mission_* tables before the
template tables because the source_*_template_id FKs are ON DELETE SET
NULL, which is cheaper to short-circuit by removing the children first.
Frontend:
- lib/missions.ts — typed client, queryKey factory, status accent map.
- pages/MissionsListPage.tsx — list cards with status accent + filters
(q, client, status).
- pages/MissionsCreatePage.tsx — 3-step wizard (meta → scenarios → members)
with member roster fed by /users/roster.
- pages/MissionDetailPage.tsx — header + transition buttons (legal next
states only) + Tests/Members/Synthesis/Export tabs.
- Routes + nav entry (visible to anyone with mission.read or admin).
Tests:
- backend/tests/test_missions.py — 22 pytest covering snapshot fidelity,
MITRE propagation, membership visibility, transition state machine,
perm gating, member set replace, append scenarios, soft-delete, partial
update, inverted-date rejection.
- e2e/tests/m6-missions.spec.ts — 5 Playwright (snapshot freezing, non-admin
visibility, status transitions + 409, SPA wizard end-to-end, list filter).
Docs:
- CHANGELOG, tasks/testing-m6.md, tasks/lessons.md (snapshot tradeoffs,
membership=404 pattern, /diag/reset order, auto-creator add).
- README + tasks/todo.md updated.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 15:07:32 +02:00
|
|
|
</p>
|
|
|
|
|
) : (
|
|
|
|
|
<ul className="flex flex-col gap-2" data-testid="mission-members">
|
|
|
|
|
{m.members.map((mb) => (
|
|
|
|
|
<li
|
|
|
|
|
key={mb.user_id}
|
|
|
|
|
className="flex items-center justify-between rounded-md border border-border bg-bg-card p-3"
|
|
|
|
|
data-testid={`mission-member-${mb.user_id}`}
|
|
|
|
|
>
|
|
|
|
|
<div>
|
|
|
|
|
<p className="font-mono text-xs text-text-bright">
|
|
|
|
|
{mb.user_display_name ?? mb.user_email}
|
|
|
|
|
</p>
|
|
|
|
|
{mb.user_display_name && (
|
|
|
|
|
<p className="font-mono text-2xs text-text-dim">
|
|
|
|
|
{mb.user_email}
|
|
|
|
|
</p>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
<Tag accent={mb.role_hint === 'red' ? 'red' : 'cyan'}>
|
|
|
|
|
{mb.role_hint}
|
|
|
|
|
</Tag>
|
|
|
|
|
</li>
|
|
|
|
|
))}
|
|
|
|
|
</ul>
|
|
|
|
|
)}
|
|
|
|
|
</Card>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{tab === 'synthesis' && (
|
|
|
|
|
<Card>
|
|
|
|
|
<p className="font-mono text-xs text-text-dim">
|
|
|
|
|
Reveal.js slide synthesis lands in M10.
|
|
|
|
|
</p>
|
|
|
|
|
</Card>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{tab === 'export' && (
|
|
|
|
|
<Card>
|
|
|
|
|
<p className="font-mono text-xs text-text-dim">
|
|
|
|
|
JSON / CSV exports land in M11.
|
|
|
|
|
</p>
|
|
|
|
|
</Card>
|
|
|
|
|
)}
|
fix(m6): mission detail page can now edit metadata, append scenarios, edit members
The M6 SPA shipped the create wizard but the detail page was read-only —
even though the backend already exposed PUT /missions/{id}, POST
/missions/{id}/scenarios, and PUT /missions/{id}/members. So once a
mission was created you couldn't fix a typo in the client name, add a
scenario you forgot, or change member assignments without curl.
Added three modals on the detail page, gated by `is_admin ||
mission.update`:
- Edit metadata (header button, 3xl modal): name + client + dates +
markdown description, same validation as the wizard step 1.
- Add scenarios (Tests tab): scenario picker matching wizard step 2,
calls POST /missions/{id}/scenarios which appends snapshots at
current_max_position + 1.
- Edit members (Members tab): roster + red/blue toggles, calls
PUT /missions/{id}/members (full-set replace), pre-populated with
the current member set.
The detail page now imports useAuth so `canEdit` is computed once and
shared between the three buttons.
E2E: new "detail page edits metadata, appends scenarios, edits members"
spec exercises the three modals end-to-end. M6 e2e count is now 6.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 07:37:06 +02:00
|
|
|
|
|
|
|
|
<MetaEditModal mission={m} open={editMeta} onClose={() => setEditMeta(false)} />
|
|
|
|
|
<AddScenariosModal
|
|
|
|
|
mission={m}
|
|
|
|
|
open={addScenarios}
|
|
|
|
|
onClose={() => setAddScenarios(false)}
|
|
|
|
|
/>
|
|
|
|
|
<EditMembersModal
|
|
|
|
|
mission={m}
|
|
|
|
|
open={editMembers}
|
|
|
|
|
onClose={() => setEditMembers(false)}
|
|
|
|
|
/>
|
feat(m6): missions + snapshot CRUD, membership visibility, status state machine
Adds the mission layer that materialises template snapshots, plus the SPA
list / 3-step wizard / detail page.
Backend:
- app/services/missions.py — create_mission snapshots scenarios, tests, MITRE
tags in a 4-query write; list/get apply a non-admin membership filter that
collapses to 404 (no existence leak); status state machine enforces
draft → in_progress → completed → archived with archived as a sink; the
non-admin creator is auto-added as role_hint='red' to retain visibility.
- app/api/missions.py — 8 endpoints (list, get, create, update, add
scenarios, set members, transition, soft-delete) with strict pydantic
schemas. The transition endpoint splits the perm gate manually so
archive requires mission.archive while other targets use mission.update.
- app/api/users.py — new GET /users/roster returning (id, email,
display_name) only, gated by user.read OR mission.create OR
mission.update — lets non-admin wizard users see assignable peers
without exposing the admin /users payload.
- app/api/diag.py — /diag/reset truncates the mission_* tables before the
template tables because the source_*_template_id FKs are ON DELETE SET
NULL, which is cheaper to short-circuit by removing the children first.
Frontend:
- lib/missions.ts — typed client, queryKey factory, status accent map.
- pages/MissionsListPage.tsx — list cards with status accent + filters
(q, client, status).
- pages/MissionsCreatePage.tsx — 3-step wizard (meta → scenarios → members)
with member roster fed by /users/roster.
- pages/MissionDetailPage.tsx — header + transition buttons (legal next
states only) + Tests/Members/Synthesis/Export tabs.
- Routes + nav entry (visible to anyone with mission.read or admin).
Tests:
- backend/tests/test_missions.py — 22 pytest covering snapshot fidelity,
MITRE propagation, membership visibility, transition state machine,
perm gating, member set replace, append scenarios, soft-delete, partial
update, inverted-date rejection.
- e2e/tests/m6-missions.spec.ts — 5 Playwright (snapshot freezing, non-admin
visibility, status transitions + 409, SPA wizard end-to-end, list filter).
Docs:
- CHANGELOG, tasks/testing-m6.md, tasks/lessons.md (snapshot tradeoffs,
membership=404 pattern, /diag/reset order, auto-creator add).
- README + tasks/todo.md updated.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 15:07:32 +02:00
|
|
|
</section>
|
|
|
|
|
);
|
|
|
|
|
}
|