import { useState, useMemo } from "react"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { useNavigate } from "react-router-dom"; import { Loader2, AlertCircle, Plus, Filter, ListChecks, Clock, CheckCircle, XCircle, Eye, Play, Shield, Search, ChevronUp, ChevronDown, ChevronsUpDown, Timer, AlertTriangle, Lightbulb, } from "lucide-react"; import { getTests, type TestListFilters } from "../api/tests"; import { getProcedureSuggestions, approveProcedureSuggestion, rejectProcedureSuggestion, } from "../api/procedure-suggestions"; import type { Test, TestState } from "../types/models"; import { useAuth } from "../context/AuthContext"; /* ── Badge colour map ──────────────────────────────────────────────── */ const testStateBadgeColors: Record = { draft: "bg-gray-800/50 text-gray-400 border-gray-600/30", red_executing: "bg-orange-900/50 text-orange-400 border-orange-500/30", red_review: "bg-amber-900/50 text-amber-400 border-amber-500/30", blue_evaluating: "bg-indigo-900/50 text-indigo-400 border-indigo-500/30", blue_review: "bg-purple-900/50 text-purple-400 border-purple-500/30", in_review: "bg-blue-900/50 text-blue-400 border-blue-500/30", validated: "bg-green-900/50 text-green-400 border-green-500/30", rejected: "bg-red-900/50 text-red-400 border-red-500/30", disputed: "bg-amber-900/50 text-amber-400 border-amber-500/30", }; const testStateLabels: Record = { draft: "Draft", red_executing: "Red Executing", red_review: "Red Review", blue_evaluating: "Blue Evaluating", blue_review: "Blue Review", in_review: "In Review", validated: "Validated", rejected: "Rejected", disputed: "Disputed", }; const ALL_STATES: TestState[] = [ "draft", "red_executing", "red_review", "blue_evaluating", "blue_review", "in_review", "validated", "rejected", "disputed", ]; /* ── Helper: which team "owns" the current state ────────────────────── */ function currentTeamForState(state: TestState): string { switch (state) { case "draft": case "red_executing": return "Red Team"; case "red_review": return "Red Lead"; case "blue_evaluating": return "Blue Team"; case "blue_review": return "Blue Lead"; case "in_review": return "Managers"; case "validated": return "-"; case "rejected": return "Red Team"; default: return "-"; } } /* ── Helper: most recent activity timestamp on a test ──────────────── */ // updated_at column does not exist in the DB — derive from available fields function lastActivityDate(t: Test): string | null { const candidates = [ t.blue_validated_at, t.red_validated_at, (t as any).blue_work_started_at, (t as any).blue_started_at, (t as any).red_started_at, t.created_at, ].filter(Boolean) as string[]; if (!candidates.length) return null; return candidates.reduce((latest, d) => new Date(d) > new Date(latest) ? d : latest ); } /* ── Helper: elapsed time since a date ─────────────────────────────── */ function formatElapsed(dateStr: string | null | undefined): string { if (!dateStr) return "—"; const utc = dateStr.endsWith("Z") || dateStr.includes("+") ? dateStr : dateStr + "Z"; const ms = Date.now() - new Date(utc).getTime(); const minutes = Math.floor(ms / 60000); if (minutes < 1) return "<1m"; if (minutes < 60) return `${minutes}m`; const hours = Math.floor(minutes / 60); if (hours < 24) return `${hours}h ${minutes % 60}m`; const days = Math.floor(hours / 24); return `${days}d ${hours % 24}h`; } /* ── Shared sort key type ────────────────────────────────────────────── */ type SortKey = | "name" | "technique" | "state" | "team" | "platform" | "created_at" | "updated_at" | "waiting_time"; /* ── Component ──────────────────────────────────────────────────────── */ const isTechRole = (role?: string) => role === "red_tech" || role === "blue_tech"; export default function TestsPage() { const navigate = useNavigate(); const { user } = useAuth(); // Only leads can create tests — admin administers the site, not test content. const canCreate = user?.role === "red_lead" || user?.role === "blue_lead"; const techRole = isTechRole(user?.role); // ── Filter state ────────────────────────────────────────────────── const [stateFilter, setStateFilter] = useState(""); const [platformFilter, setPlatformFilter] = useState(""); const [searchText, setSearchText] = useState(""); const [showMyTasks, setShowMyTasks] = useState(false); const isReviewLead = user?.role === "red_lead" || user?.role === "blue_lead"; const reviewQueueActive = isReviewLead && !showMyTasks; // ── Sort state ──────────────────────────────────────────────────── const [sortKey, setSortKey] = useState("created_at"); const [sortDir, setSortDir] = useState<"asc" | "desc">("desc"); const handleSort = (key: SortKey) => { if (sortKey === key) { setSortDir((d) => (d === "asc" ? "desc" : "asc")); } else { setSortKey(key); setSortDir(key === "waiting_time" ? "asc" : "asc"); } }; // Build API filters const filters = useMemo(() => { const f: TestListFilters = { limit: 200 }; if (showMyTasks && user) { switch (user.role) { case "red_tech": f.created_by = user.id; if (stateFilter) f.state = stateFilter as TestState; break; case "blue_tech": f.state = "blue_evaluating"; break; case "red_lead": f.pending_validation_side = "red"; break; case "blue_lead": f.pending_validation_side = "blue"; break; default: if (stateFilter) f.state = stateFilter as TestState; break; } } else { if (stateFilter) f.state = stateFilter as TestState; } if (platformFilter) f.platform = platformFilter; return f; }, [stateFilter, platformFilter, showMyTasks, user]); const { data: allTests, isLoading, error, } = useQuery({ queryKey: ["tests", filters], queryFn: () => getTests(filters), }); // Client-side filtering const tests = useMemo(() => { if (!allTests) return []; let filtered = allTests; // Red tech "my tasks" — client-side filter for draft + red_executing if (showMyTasks && user?.role === "red_tech" && !stateFilter) { filtered = filtered.filter( (t) => t.state === "draft" || t.state === "red_executing" ); } // Exclude validated from this table — they live in /tests/validated if (stateFilter !== "validated") { filtered = filtered.filter((t) => t.state !== "validated"); } // Search text if (searchText.trim()) { const q = searchText.toLowerCase(); filtered = filtered.filter( (t) => t.name.toLowerCase().includes(q) || (t.technique_mitre_id && t.technique_mitre_id.toLowerCase().includes(q)) || (t.technique_name && t.technique_name.toLowerCase().includes(q)) ); } // Sort filtered = [...filtered].sort((a, b) => { let av = ""; let bv = ""; switch (sortKey) { case "name": av = a.name.toLowerCase(); bv = b.name.toLowerCase(); break; case "technique": av = (a.technique_mitre_id || "").toLowerCase(); bv = (b.technique_mitre_id || "").toLowerCase(); break; case "state": av = a.state; bv = b.state; break; case "team": av = currentTeamForState(a.state); bv = currentTeamForState(b.state); break; case "platform": av = (a.platform || "").toLowerCase(); bv = (b.platform || "").toLowerCase(); break; case "created_at": av = a.created_at || ""; bv = b.created_at || ""; break; case "updated_at": av = lastActivityDate(a) || ""; bv = lastActivityDate(b) || ""; break; case "waiting_time": { const aIsBlue = a.state === "blue_evaluating"; const bIsBlue = b.state === "blue_evaluating"; if (aIsBlue && bIsBlue) { // Older blue_started_at = waited longer = sort first in asc av = (a as any).blue_started_at || a.created_at || ""; bv = (b as any).blue_started_at || b.created_at || ""; } else { if (aIsBlue && !bIsBlue) return sortDir === "asc" ? -1 : 1; if (!aIsBlue && bIsBlue) return sortDir === "asc" ? 1 : -1; av = (a as any).blue_started_at || a.created_at || ""; bv = (b as any).blue_started_at || b.created_at || ""; } break; } } const cmp = av < bv ? -1 : av > bv ? 1 : 0; return sortDir === "asc" ? cmp : -cmp; }); return filtered; }, [allTests, searchText, showMyTasks, user, stateFilter, sortKey, sortDir]); // ── State counters ──────────────────────────────────────────────── const { data: allTestsUnfiltered } = useQuery({ queryKey: ["tests", "unfiltered-counts"], queryFn: () => getTests({ limit: 200 }), }); const globalCounts = useMemo(() => { const counts: Record = {}; for (const s of ALL_STATES) counts[s] = 0; if (allTestsUnfiltered) { for (const t of allTestsUnfiltered) { counts[t.state] = (counts[t.state] || 0) + 1; } } return counts; }, [allTestsUnfiltered]); const totalTests = allTestsUnfiltered?.length || 0; // ── Two-queue split for tech roles ──────────────────────────────── const { availableTests, myAssignedTests } = useMemo(() => { if (!techRole || !user || !allTests) { return { availableTests: [] as typeof tests, myAssignedTests: [] as typeof tests }; } let searchFiltered = allTests.filter((t) => t.state !== "validated"); if (searchText.trim()) { const q = searchText.toLowerCase(); searchFiltered = searchFiltered.filter( (t) => t.name.toLowerCase().includes(q) || (t.technique_mitre_id && t.technique_mitre_id.toLowerCase().includes(q)) || (t.technique_name && t.technique_name.toLowerCase().includes(q)) ); } if (platformFilter) { searchFiltered = searchFiltered.filter((t) => t.platform?.toLowerCase().includes(platformFilter.toLowerCase()) ); } if (user.role === "red_tech") { const available = searchFiltered.filter( (t) => (t.state === "draft" || t.state === "red_executing") && t.red_tech_assignee === null ); const mine = searchFiltered.filter((t) => t.red_tech_assignee === user.id); return { availableTests: available, myAssignedTests: mine }; } if (user.role === "blue_tech") { const available = searchFiltered.filter( (t) => t.state === "blue_evaluating" && t.blue_tech_assignee === null && t.blue_work_started_at === null ); const mine = searchFiltered.filter((t) => t.blue_tech_assignee === user.id); return { availableTests: available, myAssignedTests: mine }; } return { availableTests: [] as typeof tests, myAssignedTests: [] as typeof tests }; }, [techRole, user, allTests, searchText, platformFilter]); // ── Two-queue split for lead review gate (red_review / blue_review) ─ // The auto load-balancer always assigns a reviewer immediately, so // "available" here means "assigned to another lead" — visible so a // lead can pick up a peer's review if they can't get to it. // // Also includes cross-validation (in_review) tests still awaiting this // lead's vote — previously only surfaced by toggling "My Tasks", which // meant a lead landing on this page by default never saw them here at // all despite this being "the review queue" as far as they're concerned. const { availableReviews, myAssignedReviews, pendingMyValidation } = useMemo(() => { if (!isReviewLead || !user || !allTests || showMyTasks) { return { availableReviews: [] as typeof tests, myAssignedReviews: [] as typeof tests, pendingMyValidation: [] as typeof tests, }; } let searchFiltered = allTests; if (searchText.trim()) { const q = searchText.toLowerCase(); searchFiltered = searchFiltered.filter( (t) => t.name.toLowerCase().includes(q) || (t.technique_mitre_id && t.technique_mitre_id.toLowerCase().includes(q)) || (t.technique_name && t.technique_name.toLowerCase().includes(q)) ); } if (platformFilter) { searchFiltered = searchFiltered.filter((t) => t.platform?.toLowerCase().includes(platformFilter.toLowerCase()) ); } const reviewState = user.role === "red_lead" ? "red_review" : "blue_review"; const reviewerField = user.role === "red_lead" ? "red_reviewer_assignee" : "blue_reviewer_assignee"; const inState = searchFiltered.filter((t) => t.state === reviewState); const available = inState.filter((t) => t[reviewerField] !== user.id); const mine = inState.filter((t) => t[reviewerField] === user.id); const validationField = user.role === "red_lead" ? "red_validation_status" : "blue_validation_status"; const pendingValidation = searchFiltered.filter( (t) => t.state === "in_review" && !t[validationField] ); return { availableReviews: available, myAssignedReviews: mine, pendingMyValidation: pendingValidation }; }, [isReviewLead, user, allTests, searchText, platformFilter, showMyTasks]); // ── Disputed tests — always fetched independently of the my-tasks // toggle/state filter so this alert never silently disappears just // because the lead is looking at a different view. ────────────────── const { data: disputedTests = [] } = useQuery({ queryKey: ["tests", "disputed-queue"], queryFn: () => getTests({ state: "disputed", limit: 200 }), enabled: isReviewLead, }); // ── Formatting helpers ───────────────────────────────────────────── const formatDate = (dateStr: string | null | undefined) => { if (!dateStr) return "-"; const utc = dateStr.endsWith("Z") || dateStr.includes("+") ? dateStr : dateStr + "Z"; return new Date(utc).toLocaleDateString("es-ES", { year: "numeric", month: "short", day: "numeric", }); }; // ── My tasks label ──────────────────────────────────────────────── const myTasksLabel = useMemo(() => { if (!user) return "My Tasks"; switch (user.role) { case "red_tech": return "My Tests (Draft / Executing)"; case "blue_tech": return "Pending Blue Evaluation"; case "red_lead": return "Pending Red Validation"; case "blue_lead": return "Pending Blue Validation"; default: return "My Tasks"; } }, [user]); // ── Render ───────────────────────────────────────────────────────── if (isLoading) { return (
); } if (error) { return (

Failed to load tests

); } const mainTableColumns: { key: SortKey; label: string; cls: string }[] = [ { key: "name", label: "Name", cls: "pr-4" }, { key: "technique", label: "Technique", cls: "px-4" }, { key: "state", label: "State", cls: "px-4" }, { key: "team", label: "Current Team", cls: "px-4" }, { key: "platform", label: "Platform", cls: "px-4" }, { key: "waiting_time", label: "Waiting", cls: "px-4" }, { key: "created_at", label: "Created", cls: "px-4" }, { key: "updated_at", label: "Updated", cls: "px-4" }, ]; return (
{/* Header */}

Tests

Security tests for technique validation — Red/Blue workflow

{canCreate && ( )}
{/* ── State Counter Cards ───────────────────────────────────────── */}
{ALL_STATES.map((state) => { const icons: Record = { draft: , red_executing: , red_review: , blue_evaluating: , blue_review: , in_review: , validated: , rejected: , disputed: , }; const colorMap: Record = { draft: "text-gray-400", red_executing: "text-orange-400", red_review: "text-amber-400", blue_evaluating: "text-indigo-400", blue_review: "text-purple-400", in_review: "text-blue-400", validated: "text-green-400", rejected: "text-red-400", disputed: "text-amber-400", }; return ( ); })}
{/* ── Filters Bar ───────────────────────────────────────────────── */}
{/* My tasks toggle — hidden for tech roles (two-queue view below); for leads this switches AWAY from the review two-queue view to the in_review dual-validation stage, a distinct responsibility. */} {user?.role !== "admin" && user?.role !== "viewer" && !techRole && ( )} {/* State filter */}
{/* Platform filter */} setPlatformFilter(e.target.value)} placeholder="Platform..." className="rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-sm text-gray-300 placeholder-gray-500 focus:border-cyan-500 focus:outline-none w-32" /> {/* Search */}
setSearchText(e.target.value)} placeholder="Search by name or technique..." className="w-full rounded-lg border border-gray-700 bg-gray-800 pl-9 pr-3 py-2 text-sm text-gray-300 placeholder-gray-500 focus:border-cyan-500 focus:outline-none" />
{/* Clear filters */} {(stateFilter || platformFilter || searchText || showMyTasks) && ( )}
{/* Active filter summary */} {(stateFilter || showMyTasks) && (
Showing: {showMyTasks && ( {myTasksLabel} )} {stateFilter && ( {testStateLabels[stateFilter as TestState]} )} ({tests.length} of {totalTests} tests)
)}
{/* ── Disputed tests — most urgent, always on top when present ──── */} {isReviewLead && disputedTests.length > 0 && (

Disputed

{disputedTests.length}
Leads disagree on the outcome — needs resolution
)} {/* ── Tests Table / Two-Queue View ─────────────────────────────── */} {techRole ? ( /* Two-section queue for red_tech and blue_tech */
{/* Available Tests */}

Available Tests

{availableTests.length}
Unassigned — pick one to claim it
{/* My Assigned Tests */}

My Assigned Tests

{myAssignedTests.length}
Tests assigned to you
) : reviewQueueActive ? ( /* Two-section review queue for red_lead/blue_lead */
{/* Available to Review */}

Available to Review

{availableReviews.length}
Assigned to other leads — pick one up if needed
{/* My Assigned Reviews */}

My Assigned Reviews

{myAssignedReviews.length}
Reviews assigned to you
{/* Awaiting My Validation — cross-validation stage (in_review), distinct from the red_review/blue_review gate above: both leads act independently here, so there's no "assigned to someone else" bucket, just "have I voted yet or not". */}

Awaiting My Validation

{pendingMyValidation.length}
Cross-validation — both leads vote independently
) : ( /* Normal single-table view for admin, viewer, and leads viewing "My Tasks" */

{showMyTasks ? myTasksLabel : "All Tests"}

{tests.length} tests
)} {/* ── Procedure suggestions for leads ─────────────────────────── GET /procedure-suggestions is already scoped server-side to the caller's own team (red_lead sees only red, blue_lead only blue), so no client-side filtering is needed here. */} {isReviewLead && } {/* ── Team queue overview for leads ───────────────────────────── red_lead/blue_lead never hit the plain "All Tests" branch above — they're always routed into the review two-queue view or their own "My Tasks" — so they had no way to see every test and who's working it, which is what they need to spot a stuck ticket or an overloaded operator. Always shown at the end, independent of the my-tasks toggle above, with assignees visible on both sides. */} {isReviewLead && (

All Tests — Team Queue Overview

{tests.length}
Every test, with who's assigned on each side
)}
); } /* ── Procedure suggestions panel (leads only) ─────────────────────── */ function ProcedureSuggestionsPanel() { const queryClient = useQueryClient(); const { data: suggestions = [] } = useQuery({ queryKey: ["procedure-suggestions"], queryFn: getProcedureSuggestions, }); const invalidate = () => queryClient.invalidateQueries({ queryKey: ["procedure-suggestions"] }); const approveMutation = useMutation({ mutationFn: approveProcedureSuggestion, onSuccess: invalidate }); const rejectMutation = useMutation({ mutationFn: rejectProcedureSuggestion, onSuccess: invalidate }); const isBusy = approveMutation.isPending || rejectMutation.isPending; if (suggestions.length === 0) return null; return (

Procedure Suggestions

{suggestions.length} Commands extracted from submitted rounds, proposed as template updates
{suggestions.map((s) => (

{s.template_name || "Unknown template"}

{s.template_current_text && (

Current

                  {s.template_current_text}
                
)}

Suggested

                {s.suggested_text}
              
))}
); } /* ── Shared test table component ────────────────────────────────────── */ function TestTable({ tests, columns, sortKey, sortDir, handleSort, navigate, formatDate, emptyMessage, showAssignees = false, }: { tests: Test[]; columns: { key: SortKey; label: string; cls: string }[]; sortKey: SortKey; sortDir: "asc" | "desc"; handleSort: (key: SortKey) => void; navigate: (path: string) => void; formatDate: (d: string | null | undefined) => string; emptyMessage: string; /** Adds Red/Blue assignee columns — for the leads' team queue overview, * where seeing who's working each side is the whole point. */ showAssignees?: boolean; }) { return (
{columns.map(({ key, label, cls }) => ( ))} {showAssignees && ( <> )} {tests.map((test: Test) => ( navigate(`/tests/${test.id}`)} > {/* Waiting time — how long since Red submitted to Blue */} {showAssignees && ( <> )} ))}
handleSort(key)} > {key === "waiting_time" && ( )} {label} {sortKey === key ? ( sortDir === "asc" ? ( ) : ( ) ) : ( )} Red Assignee Blue AssigneeAction
{test.name} {test.technique_mitre_id ? (
{test.technique_mitre_id} {test.technique_name}
) : ( - )}
{test.state === "blue_evaluating" && !test.blue_work_started_at ? "Queued Blue Team" : testStateLabels[test.state]} {currentTeamForState(test.state)} {test.platform || "-"} {test.state === "blue_evaluating" ? ( {formatElapsed(test.blue_started_at)} ) : ( )} {formatDate(test.created_at)} {formatDate(lastActivityDate(test))} {test.red_tech_assignee_username || "-"} {test.blue_tech_assignee_username || "-"}
{tests.length === 0 && (
{emptyMessage}
)}
); }