feat(cra): Management-Fortschritts-Ansicht (Ticket-Status-Readback)

Liest den Lebenszyklus jedes Befunds (status + tracker_issue_url) aus dem
Scanner zurück und rollt ihn zu einem Management-Bild auf: % erledigt,
4-Phasen (offen/in Arbeit/erledigt/ausgeschlossen), offenes Restrisiko nach
Schweregrad, Fortschritt je CRA-Anforderung und eine Aufgaben-/Ticket-Tabelle
mit Jira-Link. Neuer Endpoint GET/POST /api/v1/cra/progress (dünn → Service
cra_progress, rein deterministisch, kein /assess-Schema-Drift). Frontend:
ProgressView in Ebene 1 (CRACyberView), live je Scanner-Repo, sonst Demo-Status.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-06-16 10:10:45 +02:00
parent 7a4f086151
commit 9e9d780902
8 changed files with 541 additions and 2 deletions
@@ -0,0 +1,97 @@
'use client'
import { useEffect, useState } from 'react'
import { CRAFinding } from './useCRADemo'
// Management progress: reads each finding's lifecycle (status + Jira ticket) back
// from the scanner and rolls it up. Live for a linked scanner repo; for the demo
// (no repo) it POSTs the demo findings with a sample status mix so the layout is
// illustrative. Mirrors the backend compliance/services/cra_progress.py phases.
export interface ProgressTheme {
finding_id: string
title: string
requirement: string
requirement_title: string
risk_level: string
phase: string
phase_label: string
status: string
tracker_url: string
has_ticket: boolean
location: string
updated_at: string
}
export interface ProgressReq {
req_id: string
title: string
total: number
offen: number
in_arbeit: number
erledigt: number
ausgeschlossen: number
phase: string
phase_label: string
completion_pct: number
open_risk: string
}
export interface CRAProgress {
total: number
actionable: number
completion_pct: number
by_phase: Record<string, number>
by_status: Record<string, number>
by_risk_open: Record<string, number>
open_count: number
requirements: ProgressReq[]
themes: ProgressTheme[]
source?: { scanner: boolean; pulled: number; repo_id?: string }
}
// Demo status mix (no real repo): one fixed, two ticketed/in-progress, rest open.
const DEMO_STATUS: Record<string, { status: string; tracker?: string }> = {
'KH-CY-6': { status: 'resolved' },
'KH-CY-4': { status: 'triaged', tracker: 'https://jira.example.com/browse/BP-204' },
'KH-CY-3': { status: 'triaged', tracker: 'https://jira.example.com/browse/BP-203' },
}
export function useCRAProgress(scannerRepo: string, demoFindings: CRAFinding[]) {
const [progress, setProgress] = useState<CRAProgress | null>(null)
const [live, setLive] = useState(false)
useEffect(() => {
let cancelled = false
;(async () => {
if (scannerRepo) {
try {
const r = await fetch(`/api/v1/cra/progress?repo_id=${encodeURIComponent(scannerRepo)}`)
if (r.ok) {
const j = await r.json()
if (!cancelled) { setProgress(j); setLive(true) }
return
}
} catch { /* fall through to demo */ }
}
try {
const findings = demoFindings.map((f) => ({
id: f.id, cwe: f.cwe, severity: f.scanner_severity, title: f.title,
status: DEMO_STATUS[f.id]?.status || 'open',
tracker_issue_url: DEMO_STATUS[f.id]?.tracker || null,
}))
const r = await fetch('/api/v1/cra/progress', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ findings }),
})
if (r.ok) {
const j = await r.json()
if (!cancelled) { setProgress(j); setLive(false) }
}
} catch { /* leave null → view hides */ }
})()
return () => { cancelled = true }
}, [scannerRepo, demoFindings])
return { progress, live }
}