import { useState } from "react"; import { useParams, useNavigate } from "react-router-dom"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { Loader2, AlertCircle, ArrowLeft, Play, CheckCircle, Target, Plus, Trash2, Zap, Calendar, Clock, Repeat, History, } from "lucide-react"; import { getCampaign, activateCampaign, completeCampaign, removeTestFromCampaign, scheduleCampaign, getCampaignHistory, type Campaign, type CampaignHistoryEntry, } from "../api/campaigns"; import { useAuth } from "../context/AuthContext"; import CampaignTimeline from "../components/CampaignTimeline"; const statusColors: Record = { draft: "bg-gray-800/50 text-gray-400 border-gray-600/30", active: "bg-cyan-900/50 text-cyan-400 border-cyan-500/30", completed: "bg-green-900/50 text-green-400 border-green-500/30", archived: "bg-gray-800/50 text-gray-500 border-gray-700/30", }; const typeLabels: Record = { custom: "Custom", apt_emulation: "APT Emulation", kill_chain: "Kill Chain", compliance: "Compliance", }; const testStateColors: 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", blue_evaluating: "bg-indigo-900/50 text-indigo-400 border-indigo-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", }; export default function CampaignDetailPage() { const { campaignId } = useParams<{ campaignId: string }>(); const navigate = useNavigate(); const queryClient = useQueryClient(); const { user } = useAuth(); const [toast, setToast] = useState<{ message: string; type: "success" | "error" } | null>(null); const showToast = (message: string, type: "success" | "error") => { setToast({ message, type }); setTimeout(() => setToast(null), 5000); }; const role = user?.role ?? ""; const canManage = role === "admin" || role === "red_tech"; const canComplete = role === "admin" || role === "red_lead"; const { data: campaign, isLoading, error, } = useQuery({ queryKey: ["campaign", campaignId], queryFn: () => getCampaign(campaignId!), enabled: !!campaignId, }); const activateMutation = useMutation({ mutationFn: () => activateCampaign(campaignId!), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["campaign", campaignId] }); showToast("Campaign activated", "success"); }, onError: (err: Error) => showToast(err.message, "error"), }); const completeMutation = useMutation({ mutationFn: () => completeCampaign(campaignId!), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["campaign", campaignId] }); showToast("Campaign completed", "success"); }, onError: (err: Error) => showToast(err.message, "error"), }); const removeMutation = useMutation({ mutationFn: (campaignTestId: string) => removeTestFromCampaign(campaignId!, campaignTestId), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["campaign", campaignId] }); showToast("Test removed from campaign", "success"); }, onError: (err: Error) => showToast(err.message, "error"), }); const scheduleMutation = useMutation({ mutationFn: (payload: { is_recurring: boolean; recurrence_pattern?: string; next_run_at?: string }) => scheduleCampaign(campaignId!, payload), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["campaign", campaignId] }); showToast("Schedule updated", "success"); }, onError: (err: Error) => showToast(err.message, "error"), }); const { data: historyData } = useQuery({ queryKey: ["campaign-history", campaignId], queryFn: () => getCampaignHistory(campaignId!), enabled: !!campaignId && !!campaign?.is_recurring, }); const [schedRecurring, setSchedRecurring] = useState(false); const [schedPattern, setSchedPattern] = useState("monthly"); const [schedNextRun, setSchedNextRun] = useState(""); // Sync scheduling state from campaign when loaded useState(() => { if (campaign) { setSchedRecurring(campaign.is_recurring || false); setSchedPattern(campaign.recurrence_pattern || "monthly"); setSchedNextRun(campaign.next_run_at ? campaign.next_run_at.slice(0, 16) : ""); } }); const handleScheduleSave = () => { if (schedRecurring) { scheduleMutation.mutate({ is_recurring: true, recurrence_pattern: schedPattern, next_run_at: schedNextRun || undefined, }); } else { scheduleMutation.mutate({ is_recurring: false }); } }; const formatDate = (dateStr: string | null) => { if (!dateStr) return "\u2014"; return new Date(dateStr).toLocaleDateString("en-US", { year: "numeric", month: "short", day: "numeric", hour: "2-digit", minute: "2-digit", }); }; if (isLoading) { return (
); } if (error || !campaign) { return (

Failed to load campaign

); } const progress = campaign.progress; return (
{/* Back button */} {/* Header */}

{campaign.name}

{campaign.status} {typeLabels[campaign.type] || campaign.type}
{campaign.description && (

{campaign.description}

)}
{campaign.threat_actor_name && ( )} Created {formatDate(campaign.created_at)} {campaign.completed_at && ( Completed {formatDate(campaign.completed_at)} )}
{/* Actions */}
{canManage && campaign.status === "draft" && ( )} {canComplete && campaign.status === "active" && ( )}
{/* Progress Panel */}

Progress

{progress.completion_pct}% complete
{progress.by_state?.validated || 0} / {progress.total} tests
{/* State breakdown */} {progress.total > 0 && (
{Object.entries(progress.by_state || {}).map(([state, count]) => (
{state.replace(/_/g, " ")}: {count}
))}
)}
{/* Kill Chain Timeline */}

Kill Chain Timeline

{campaign.tags && campaign.tags.length > 0 && (
{campaign.tags.map((tag, i) => ( {tag} ))}
)}
{/* Scheduling Panel */} {(canManage || campaign.is_recurring) && (

Scheduling

{campaign.next_run_at && ( Next run: {formatDate(campaign.next_run_at)} )}
{canManage && (
{/* Recurring toggle */} {schedRecurring && (
setSchedNextRun(e.target.value)} className="w-full rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-sm text-white focus:border-cyan-500 focus:outline-none" />
)}
)} {!canManage && campaign.is_recurring && (
This campaign runs {campaign.recurrence_pattern}. {campaign.last_run_at && ( Last run: {formatDate(campaign.last_run_at)} )}
)}
)} {/* Execution History */} {campaign.is_recurring && historyData && historyData.items.length > 0 && (

Execution History ({historyData.items.length})

{historyData.items.map((entry: CampaignHistoryEntry) => ( navigate(`/campaigns/${entry.id}`)} > ))}
Date Name Tests Progress Status
{formatDate(entry.created_at)} {entry.name} {entry.test_count}
{entry.completion_pct}%
{entry.status}
)} {/* Tests Table */}

Tests ({campaign.tests.length})

{canManage && campaign.status === "draft" && ( )}
{campaign.tests.length > 0 ? (
{campaign.tests .sort((a, b) => a.order_index - b.order_index) .map((ct) => ( ))}
# Technique Test Name Phase State Platform Actions
{ct.order_index + 1} {ct.technique_mitre_id || "\u2014"} {ct.phase?.replace(/_/g, " ") || "\u2014"} {(ct.test_state || "draft").replace(/_/g, " ")} {ct.platform || "\u2014"}
{canManage && (campaign.status === "draft" || campaign.status === "active") && ( )}
) : (

No tests in this campaign yet.

)}
{/* Toast notification */} {toast && (
{toast.message}
)}
); }