diff --git a/app/console/attack-pipeline/page.tsx b/app/console/attack-pipeline/page.tsx index 7b6dbb9..f2b37fc 100644 --- a/app/console/attack-pipeline/page.tsx +++ b/app/console/attack-pipeline/page.tsx @@ -135,7 +135,6 @@ function ScoreGauge({ score }: { score: number }) { const r = 54; const cx = 64; const cy = 64; - // Start at 180° (left), sweep clockwise to angle const toRad = (deg: number) => (deg * Math.PI) / 180; const sx = cx + r * Math.cos(toRad(180)); const sy = cy + r * Math.sin(toRad(180)); @@ -175,7 +174,7 @@ function CurlCopy({ cmd }: { cmd: string }) { function FindingRow({ f }: { f: Finding }) { const [open, setOpen] = useState(false); - if (f.result === "pass") return null; // only show failures + if (f.result === "pass") return null; return (
{/* Live terminal */} - {(scanning || logs.length > 0) && ( + {(scanning || logLines.length > 0) && (
@@ -531,8 +541,9 @@ export default function AttackPipelinePage() { {scanning && }
- {logs.map((line, i) => ( -
{line}
+ {/* FIX #39: stable key per log line instead of array index */} + {logLines.map((line) => ( +
{line.text}
))}
@@ -565,6 +576,7 @@ export default function AttackPipelinePage() { { label: "Passed", count: report.summary.passed, color: "text-green-600 dark:text-green-400", bg: "bg-green-50 dark:bg-green-500/10 border-green-200 dark:border-green-500/20" }, { label: "Total", count: report.findings.length, color: "text-gray-700 dark:text-zinc-300", bg: "bg-gray-50 dark:bg-zinc-800 border-gray-200 dark:border-zinc-700" }, ].map(({ label, count, color, bg }) => ( + // FIX #39: `label` is a unique static string — safe as key

{label}

{count}

@@ -585,7 +597,8 @@ export default function AttackPipelinePage() {

Key Findings

    {report.aiAnalysis.criticalFindings.map((f, i) => ( -
  • + // FIX #39: deterministic key combining content slug + position +
  • {f}
  • ))} @@ -597,7 +610,8 @@ export default function AttackPipelinePage() {

    Recommendations

      {report.aiAnalysis.recommendations.map((r, i) => ( -
    • + // FIX #39: deterministic key combining content slug + position +
    • {r}
    • ))} @@ -611,6 +625,7 @@ export default function AttackPipelinePage() {
      {(["findings", "vibetest", "coverage", "performance"] as const).map((tab) => ( + // FIX #39: tab name is a stable unique string — fine as key
      {open ? : } + {/* FIX #39: Finding.id is a proper unique identifier from the API */} {open && group.map((f) => )}
      ); @@ -670,13 +687,15 @@ export default function AttackPipelinePage() { {["Route", "Test Case", "Status", "Evidence"].map((h) => ( + // FIX #39: header label is unique — fine as key {h} ))} {report.vibetest.map((v, i) => ( - + // FIX #39: deterministic key: route + checkType + position + {v.route} {v.testCase} @@ -694,8 +713,9 @@ export default function AttackPipelinePage() { {/* Coverage tab */} {activeTab === "coverage" && (
      - {(report.coverage ?? []).map((c, i) => ( -
      ( + // FIX #39: route is the natural unique identifier for coverage entries +
      {["Route", "Avg", "P95", "Req/s", "Success", "Status"].map((h) => ( + // FIX #39: header label is unique — fine as key {h} ))} - {report.performance.routes.map((r, i) => ( - + {report.performance.routes.map((r) => ( + // FIX #39: route is unique per performance entry — use as key + {r.route} {r.avgLatency}ms {r.p95Latency}ms @@ -770,6 +792,7 @@ export default function AttackPipelinePage() {
      )} + {/* FIX #39: run.id is the stable DB identifier — already correct */} {runs.map((run) => (
      ); -} +} \ No newline at end of file