Files
breakpilot-compliance/admin-compliance/app/sdk/cra/[projectId]/backlog/page.tsx
T
Benjamin Admin 1cf5de1d45 feat(cra): CRA Compliance module Phase 1+2+3 (intake, scope, path, requirements, backlog, sbom, checks)
Phase 1 — Intake + Scope + Path:
- Migration 119: compliance_cra_projects table (intake + classification + path + status state machine)
- Backend service cra_routes.py: CRUD + scope-check + path-select
- Deterministic Annex III/IV classifier (verbatim mapping from migration 059 wiki)
- Path validation per classification (CRITICAL → notified_body mandatory)
- Frontend: project list, dashboard, 3-step wizard (intake/scope/path)
- Sidebar entry under "CRA Compliance" (red)

Phase 2 — Annex I Requirements + Priorisierungs-Backlog:
- cra_annex_i_data.py: 40 Annex-I requirements (8 categories), 9 measures (M540-M548), 3 CRA deadlines
- Endpoints: /requirements (40 items), /backlog (priority-sorted with deadline pressure)
- Frontend: requirements table with filters + expandable details, backlog with deadline banner + score-ranked table
- Dashboard KPI cards (Critical count, days to CE deadline, etc.) + top-10 backlog snippet

Phase 3 — SBOM Upload + Automated Checks:
- Migration 120: compliance_cra_sboms (versioned uploads, CycloneDX + SPDX)
- SBOM endpoints: POST /sbom/upload (format detection, summary extraction), GET /sboms
- Checks reuse compliance_evidence_checks: init creates 6 default CRA checks, run executes
- Real implementations: cra_security_txt (HTTP + Contact: line) and cra_tls_cert_check (TLS handshake)
- Frontend: SBOM file upload + version list, Checks page with per-check URL input + Run button

Backend-Reuse: gap_projects (intake pre-population), compliance_evidence_checks/_check_results.
Tenant scoping via existing X-Tenant-ID header pattern.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 17:56:52 +02:00

156 lines
6.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client'
import React, { useEffect, useState, useCallback, use } from 'react'
import { SeverityBadge } from '../../_components/SeverityBadge'
interface BacklogItem {
rank: number
req_id: string
title: string
category: string
severity: string
annex_anchor: string
description: string
effort_days: number
mapped_measure_names: { id: string; name: string }[]
status: string
priority_score: number
}
interface BacklogResponse {
project_id: string
classification: string | null
days_to_ce_deadline: number
deadlines: { date: string; label: string }[]
total: number
items: BacklogItem[]
}
export default function BacklogPage({
params,
}: {
params: Promise<{ projectId: string }>
}) {
const { projectId } = use(params)
const [data, setData] = useState<BacklogResponse | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState('')
const load = useCallback(async () => {
try {
const res = await fetch(`/api/sdk/v1/cra/projects/${projectId}/backlog`, {
headers: { 'X-Tenant-ID': '00000000-0000-0000-0000-000000000001' },
})
if (!res.ok) throw new Error(await res.text())
setData(await res.json())
} catch (e) {
setError(e instanceof Error ? e.message : 'Fehler beim Laden')
} finally {
setLoading(false)
}
}, [projectId])
useEffect(() => { load() }, [load])
if (loading) return <div className="min-h-screen bg-gray-50 p-8"><p className="text-gray-500">Laedt...</p></div>
if (error) return <div className="min-h-screen bg-gray-50 p-8"><p className="text-red-600">{error}</p></div>
if (!data) return null
return (
<div className="min-h-screen bg-gray-50 py-8">
<div className="max-w-6xl mx-auto px-4">
<div className="mb-6">
<a href={`/sdk/cra/${projectId}`} className="text-sm text-blue-600 hover:underline">
&larr; Zurueck zum Projekt
</a>
<h1 className="text-2xl font-bold text-gray-900 mt-2">Prioritaeten-Backlog</h1>
<p className="text-sm text-gray-600 mt-1">
Sortiert nach Severity × Deadline-Druck × Effort. Was du heute tust, was naechsten Sprint, was vor 11.12.2027.
</p>
</div>
{/* Deadline-Banner */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-3 mb-6">
{data.deadlines.map(d => {
const days = Math.max(0, Math.round((new Date(d.date).getTime() - Date.now()) / 86400000))
const isPast = new Date(d.date).getTime() < Date.now()
return (
<div
key={d.date}
className={`rounded-xl border p-4 ${
isPast ? 'bg-gray-100 border-gray-200' :
days < 90 ? 'bg-red-50 border-red-200' :
days < 365 ? 'bg-orange-50 border-orange-200' :
'bg-blue-50 border-blue-200'
}`}
>
<div className="text-xs text-gray-500">{d.date}</div>
<div className="font-semibold text-gray-900 text-sm mt-0.5">{d.label}</div>
<div className={`text-xs mt-1 ${isPast ? 'text-gray-500' : 'text-gray-700'}`}>
{isPast ? 'bereits abgelaufen' : `noch ${days} Tage`}
</div>
</div>
)
})}
</div>
{/* Backlog */}
<div className="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
<table className="w-full">
<thead className="bg-gray-50 border-b border-gray-200">
<tr>
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">Rang</th>
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">Anforderung</th>
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">Severity</th>
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">Score</th>
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">Aufwand</th>
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">Massnahme</th>
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">Aktion</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-100">
{data.items.map(item => (
<tr key={item.req_id} className="hover:bg-gray-50">
<td className="px-3 py-3 text-sm font-bold text-gray-700">{item.rank}</td>
<td className="px-3 py-3">
<div className="text-sm font-medium text-gray-900">{item.title}</div>
<div className="text-xs text-gray-500">{item.category} · {item.annex_anchor}</div>
</td>
<td className="px-3 py-3"><SeverityBadge value={item.severity} /></td>
<td className="px-3 py-3 text-sm font-mono text-gray-700">{item.priority_score}</td>
<td className="px-3 py-3 text-sm text-gray-600">{item.effort_days} PT</td>
<td className="px-3 py-3 text-xs text-gray-600">
{item.mapped_measure_names.length > 0 ? (
<div className="space-y-0.5">
{item.mapped_measure_names.map(m => (
<div key={m.id} title={m.name}>
<span className="font-mono text-gray-400">{m.id}:</span> {m.name.length > 50 ? m.name.slice(0, 50) + '...' : m.name}
</div>
))}
</div>
) : (
<span className="text-gray-400"></span>
)}
</td>
<td className="px-3 py-3">
<button
className="px-2 py-1 text-xs bg-purple-100 text-purple-800 rounded hover:bg-purple-200"
onClick={() => alert(`Jira-Export fuer ${item.req_id} — Phase-4-Feature`)}
>
Jira
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
<p className="text-xs text-gray-500 mt-4 text-center">
Tage bis CE-Marking-Pflicht (11.12.2027): <span className="font-semibold">{data.days_to_ce_deadline}</span>
</p>
</div>
</div>
)
}