From 20075305a53446b3ac6209fb0d47a5165cd30247 Mon Sep 17 00:00:00 2001 From: kitos Date: Fri, 29 May 2026 08:58:32 +0200 Subject: [PATCH] feat(review-queue): MITRE update review queue for leads - New /techniques/review-queue page: lists all techniques flagged for review after a MITRE ATT&CK sync, grouped by tactic. Leads and admins can mark each one reviewed inline without leaving the page. - Sidebar: 'Review Queue' link (admin/red_lead/blue_lead only) with an amber badge showing the live pending count. - TechniqueDetailPage: amber banner when review_required=true explaining what happened and who can act; 'Mark as Reviewed' button now amber coloured for visual distinction. 'Leads only' chip shown for blue_tech. Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/App.tsx | 2 + frontend/src/components/Sidebar.tsx | 38 +++- frontend/src/pages/ReviewQueuePage.tsx | 227 +++++++++++++++++++++ frontend/src/pages/TechniqueDetailPage.tsx | 27 ++- 4 files changed, 287 insertions(+), 7 deletions(-) create mode 100644 frontend/src/pages/ReviewQueuePage.tsx diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 78ef499..b277095 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -19,6 +19,7 @@ const TestCreatePage = React.lazy(() => import("./pages/TestCreatePage")); const TestDetailPage = React.lazy(() => import("./pages/TestDetailPage")); const TestCatalogPage = React.lazy(() => import("./pages/TestCatalogPage")); const ValidatedTestsPage = React.lazy(() => import("./pages/ValidatedTestsPage")); +const ReviewQueuePage = React.lazy(() => import("./pages/ReviewQueuePage")); const ReportsPage = React.lazy(() => import("./pages/ReportsPage")); const SystemPage = React.lazy(() => import("./pages/SystemPage")); const UsersPage = React.lazy(() => import("./pages/UsersPage")); @@ -52,6 +53,7 @@ export default function App() { }>} /> }>} /> + }>} /> {/* ── Executive Dashboard (leads + admin) ──────────────── */} `flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-colors ${ isActive @@ -112,8 +117,13 @@ function SidebarLink({ item }: { item: NavItem }) { }` } > - - {item.label} + + {item.label} + {badge !== undefined && badge > 0 && ( + + {badge} + + )} ); } @@ -123,10 +133,22 @@ export default function Sidebar() { const role = user?.role ?? ""; const isAdmin = role === "admin"; + const canSeeReviewQueue = + isAdmin || role === "red_lead" || role === "blue_lead"; + + // Fetch review queue count for the badge (only for roles that can see it) + const { data: reviewQueue } = useQuery({ + queryKey: ["techniques", "review-queue"], + queryFn: () => getTechniques({ review_required: true }), + enabled: canSeeReviewQueue, + staleTime: 5 * 60 * 1000, // 5 min — don't hammer the API + }); + const reviewCount = reviewQueue?.length ?? 0; + /** Returns true when the current user is allowed to see `item`. */ const canSee = (item: NavItem) => { - if (!item.roles) return true; // no restriction - if (isAdmin) return true; // admin sees everything + if (!item.roles) return true; + if (isAdmin) return true; return item.roles.includes(role); }; @@ -143,7 +165,11 @@ export default function Sidebar() { {/* Main nav */}