mission canvas: security-scan + benchmark trigger buttons
Fills the frontend gap after slices 7 + 8 shipped the backend runners
without triggers. Each phase card in the Phases tab now grows an
action row when the mission is running or completed:
- security_scan phase → "Run scan" button (POSTs /security-scan)
- benchmark phase → "Baseline" + "After" buttons, iteration auto-
derived from the current benchmark_snapshots count
Findings from security_scan already surface in the Tasks tab via
the task-card parser (external_id = cargo_audit:RUSTSEC-... etc);
the tool prefix in the badge is enough to distinguish sources
without a separate grouped view.
Closes tasks #17 + #18.
This commit is contained in:
@@ -15,6 +15,8 @@ import {
|
|||||||
getMission,
|
getMission,
|
||||||
refineMission,
|
refineMission,
|
||||||
setMissionStatus,
|
setMissionStatus,
|
||||||
|
triggerBenchmark,
|
||||||
|
triggerSecurityScan,
|
||||||
type MissionDetail,
|
type MissionDetail,
|
||||||
type MissionStatus,
|
type MissionStatus,
|
||||||
type PhaseKind,
|
type PhaseKind,
|
||||||
@@ -80,6 +82,7 @@ export function MissionCanvas({
|
|||||||
const [tab, setTab] = useState<Tab>("overview");
|
const [tab, setTab] = useState<Tab>("overview");
|
||||||
const [launching, setLaunching] = useState(false);
|
const [launching, setLaunching] = useState(false);
|
||||||
const [refining, setRefining] = useState(false);
|
const [refining, setRefining] = useState(false);
|
||||||
|
const [phaseBusy, setPhaseBusy] = useState<string | null>(null);
|
||||||
|
|
||||||
const load = useCallback(async () => {
|
const load = useCallback(async () => {
|
||||||
if (!selectedId) {
|
if (!selectedId) {
|
||||||
@@ -118,6 +121,45 @@ export function MissionCanvas({
|
|||||||
}
|
}
|
||||||
}, [mission, onChanged, load]);
|
}, [mission, onChanged, load]);
|
||||||
|
|
||||||
|
const runSecurityScan = useCallback(
|
||||||
|
async (phaseId: string) => {
|
||||||
|
if (!mission) return;
|
||||||
|
setPhaseBusy(`sec:${phaseId}`);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
await triggerSecurityScan(mission.id, phaseId);
|
||||||
|
await load();
|
||||||
|
} catch (e) {
|
||||||
|
setError(e instanceof Error ? e.message : "security scan failed");
|
||||||
|
} finally {
|
||||||
|
setPhaseBusy(null);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[mission, load],
|
||||||
|
);
|
||||||
|
|
||||||
|
const runBenchmark = useCallback(
|
||||||
|
async (phaseId: string, slot: "baseline" | "after") => {
|
||||||
|
if (!mission) return;
|
||||||
|
setPhaseBusy(`bench:${phaseId}:${slot}`);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
await triggerBenchmark(
|
||||||
|
mission.id,
|
||||||
|
phaseId,
|
||||||
|
slot,
|
||||||
|
slot === "after" ? Math.max(1, mission.benchmarks.length) : undefined,
|
||||||
|
);
|
||||||
|
await load();
|
||||||
|
} catch (e) {
|
||||||
|
setError(e instanceof Error ? e.message : "benchmark failed");
|
||||||
|
} finally {
|
||||||
|
setPhaseBusy(null);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[mission, load],
|
||||||
|
);
|
||||||
|
|
||||||
const launch = useCallback(async () => {
|
const launch = useCallback(async () => {
|
||||||
if (!mission) return;
|
if (!mission) return;
|
||||||
setLaunching(true);
|
setLaunching(true);
|
||||||
@@ -398,6 +440,53 @@ export function MissionCanvas({
|
|||||||
: ""}
|
: ""}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
{mission.status === "running" || mission.status === "completed" ? (
|
||||||
|
<div style={{ display: "flex", gap: 6, marginTop: 6 }}>
|
||||||
|
{p.kind === "security_scan" && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => runSecurityScan(p.id)}
|
||||||
|
disabled={phaseBusy === `sec:${p.id}`}
|
||||||
|
style={{
|
||||||
|
...secondaryBtn,
|
||||||
|
opacity: phaseBusy === `sec:${p.id}` ? 0.5 : 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{phaseBusy === `sec:${p.id}` ? "Scanning…" : "Run scan"}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{p.kind === "benchmark" && (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => runBenchmark(p.id, "baseline")}
|
||||||
|
disabled={phaseBusy === `bench:${p.id}:baseline`}
|
||||||
|
style={{
|
||||||
|
...secondaryBtn,
|
||||||
|
opacity:
|
||||||
|
phaseBusy === `bench:${p.id}:baseline` ? 0.5 : 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{phaseBusy === `bench:${p.id}:baseline`
|
||||||
|
? "Running…"
|
||||||
|
: "Baseline"}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => runBenchmark(p.id, "after")}
|
||||||
|
disabled={phaseBusy === `bench:${p.id}:after`}
|
||||||
|
style={{
|
||||||
|
...secondaryBtn,
|
||||||
|
opacity:
|
||||||
|
phaseBusy === `bench:${p.id}:after` ? 0.5 : 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{phaseBusy === `bench:${p.id}:after` ? "Running…" : "After"}
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user