diff --git a/admin-compliance/app/api/sdk/v1/cra/checks/[checkId]/run/route.ts b/admin-compliance/app/api/sdk/v1/cra/checks/[checkId]/run/route.ts new file mode 100644 index 00000000..69804e9e --- /dev/null +++ b/admin-compliance/app/api/sdk/v1/cra/checks/[checkId]/run/route.ts @@ -0,0 +1,23 @@ +import { NextRequest, NextResponse } from 'next/server' + +const BACKEND_URL = process.env.BACKEND_URL || 'http://backend-compliance:8002' + +export async function POST(request: NextRequest, ctx: { params: Promise<{ checkId: string }> }) { + const { checkId } = await ctx.params + const tenantId = request.headers.get('x-tenant-id') || '00000000-0000-0000-0000-000000000001' + const body = await request.text() + try { + const resp = await fetch(`${BACKEND_URL}/api/v1/cra/projects/checks/${checkId}/run`, { + method: 'POST', + headers: { 'X-Tenant-ID': tenantId, 'Content-Type': 'application/json' }, + body: body || '{}', + }) + const text = await resp.text() + return new NextResponse(text, { + status: resp.status, + headers: { 'Content-Type': resp.headers.get('Content-Type') || 'application/json' }, + }) + } catch (err) { + return NextResponse.json({ error: 'Backend unreachable', details: String(err) }, { status: 502 }) + } +} diff --git a/admin-compliance/app/api/sdk/v1/cra/projects/[id]/backlog/route.ts b/admin-compliance/app/api/sdk/v1/cra/projects/[id]/backlog/route.ts new file mode 100644 index 00000000..e41486df --- /dev/null +++ b/admin-compliance/app/api/sdk/v1/cra/projects/[id]/backlog/route.ts @@ -0,0 +1,20 @@ +import { NextRequest, NextResponse } from 'next/server' + +const BACKEND_URL = process.env.BACKEND_URL || 'http://backend-compliance:8002' + +export async function GET(request: NextRequest, ctx: { params: Promise<{ id: string }> }) { + const { id } = await ctx.params + const tenantId = request.headers.get('x-tenant-id') || '00000000-0000-0000-0000-000000000001' + try { + const resp = await fetch(`${BACKEND_URL}/api/v1/cra/projects/${id}/backlog`, { + headers: { 'X-Tenant-ID': tenantId }, + }) + const text = await resp.text() + return new NextResponse(text, { + status: resp.status, + headers: { 'Content-Type': resp.headers.get('Content-Type') || 'application/json' }, + }) + } catch (err) { + return NextResponse.json({ error: 'Backend unreachable', details: String(err) }, { status: 502 }) + } +} diff --git a/admin-compliance/app/api/sdk/v1/cra/projects/[id]/checks/route.ts b/admin-compliance/app/api/sdk/v1/cra/projects/[id]/checks/route.ts new file mode 100644 index 00000000..e11ed699 --- /dev/null +++ b/admin-compliance/app/api/sdk/v1/cra/projects/[id]/checks/route.ts @@ -0,0 +1,41 @@ +import { NextRequest, NextResponse } from 'next/server' + +const BACKEND_URL = process.env.BACKEND_URL || 'http://backend-compliance:8002' + +function tenant(req: NextRequest) { + return req.headers.get('x-tenant-id') || '00000000-0000-0000-0000-000000000001' +} + +export async function GET(request: NextRequest, ctx: { params: Promise<{ id: string }> }) { + const { id } = await ctx.params + try { + const resp = await fetch(`${BACKEND_URL}/api/v1/cra/projects/${id}/checks`, { + headers: { 'X-Tenant-ID': tenant(request) }, + }) + const text = await resp.text() + return new NextResponse(text, { + status: resp.status, + headers: { 'Content-Type': resp.headers.get('Content-Type') || 'application/json' }, + }) + } catch (err) { + return NextResponse.json({ error: 'Backend unreachable', details: String(err) }, { status: 502 }) + } +} + +/** POST /checks (no body) -> backend /checks/init creates default checks */ +export async function POST(request: NextRequest, ctx: { params: Promise<{ id: string }> }) { + const { id } = await ctx.params + try { + const resp = await fetch(`${BACKEND_URL}/api/v1/cra/projects/${id}/checks/init`, { + method: 'POST', + headers: { 'X-Tenant-ID': tenant(request) }, + }) + const text = await resp.text() + return new NextResponse(text, { + status: resp.status, + headers: { 'Content-Type': resp.headers.get('Content-Type') || 'application/json' }, + }) + } catch (err) { + return NextResponse.json({ error: 'Backend unreachable', details: String(err) }, { status: 502 }) + } +} diff --git a/admin-compliance/app/api/sdk/v1/cra/projects/[id]/path-select/route.ts b/admin-compliance/app/api/sdk/v1/cra/projects/[id]/path-select/route.ts new file mode 100644 index 00000000..71374c49 --- /dev/null +++ b/admin-compliance/app/api/sdk/v1/cra/projects/[id]/path-select/route.ts @@ -0,0 +1,29 @@ +import { NextRequest, NextResponse } from 'next/server' + +const BACKEND_URL = process.env.BACKEND_URL || 'http://backend-compliance:8002' + +export async function POST(request: NextRequest, ctx: { params: Promise<{ id: string }> }) { + const { id } = await ctx.params + const tenantId = request.headers.get('x-tenant-id') || '00000000-0000-0000-0000-000000000001' + const body = await request.text() + try { + const resp = await fetch(`${BACKEND_URL}/api/v1/cra/projects/${id}/path-select`, { + method: 'POST', + headers: { + 'X-Tenant-ID': tenantId, + 'Content-Type': 'application/json', + }, + body, + }) + const text = await resp.text() + return new NextResponse(text, { + status: resp.status, + headers: { 'Content-Type': resp.headers.get('Content-Type') || 'application/json' }, + }) + } catch (err) { + return NextResponse.json( + { error: 'Backend unreachable', details: String(err) }, + { status: 502 } + ) + } +} diff --git a/admin-compliance/app/api/sdk/v1/cra/projects/[id]/requirements/route.ts b/admin-compliance/app/api/sdk/v1/cra/projects/[id]/requirements/route.ts new file mode 100644 index 00000000..e03fba70 --- /dev/null +++ b/admin-compliance/app/api/sdk/v1/cra/projects/[id]/requirements/route.ts @@ -0,0 +1,20 @@ +import { NextRequest, NextResponse } from 'next/server' + +const BACKEND_URL = process.env.BACKEND_URL || 'http://backend-compliance:8002' + +export async function GET(request: NextRequest, ctx: { params: Promise<{ id: string }> }) { + const { id } = await ctx.params + const tenantId = request.headers.get('x-tenant-id') || '00000000-0000-0000-0000-000000000001' + try { + const resp = await fetch(`${BACKEND_URL}/api/v1/cra/projects/${id}/requirements`, { + headers: { 'X-Tenant-ID': tenantId }, + }) + const text = await resp.text() + return new NextResponse(text, { + status: resp.status, + headers: { 'Content-Type': resp.headers.get('Content-Type') || 'application/json' }, + }) + } catch (err) { + return NextResponse.json({ error: 'Backend unreachable', details: String(err) }, { status: 502 }) + } +} diff --git a/admin-compliance/app/api/sdk/v1/cra/projects/[id]/route.ts b/admin-compliance/app/api/sdk/v1/cra/projects/[id]/route.ts new file mode 100644 index 00000000..1bfb78eb --- /dev/null +++ b/admin-compliance/app/api/sdk/v1/cra/projects/[id]/route.ts @@ -0,0 +1,45 @@ +import { NextRequest, NextResponse } from 'next/server' + +const BACKEND_URL = process.env.BACKEND_URL || 'http://backend-compliance:8002' + +function tenantHeader(request: NextRequest): string { + return request.headers.get('x-tenant-id') || '00000000-0000-0000-0000-000000000001' +} + +async function proxy(request: NextRequest, method: string, id: string, body?: string) { + const tenantId = tenantHeader(request) + const init: RequestInit = { + method, + headers: { 'X-Tenant-ID': tenantId, 'Content-Type': 'application/json' }, + } + if (body !== undefined) init.body = body + try { + const resp = await fetch(`${BACKEND_URL}/api/v1/cra/projects/${id}`, init) + const text = await resp.text() + return new NextResponse(text, { + status: resp.status, + headers: { 'Content-Type': resp.headers.get('Content-Type') || 'application/json' }, + }) + } catch (err) { + return NextResponse.json( + { error: 'Backend unreachable', details: String(err) }, + { status: 502 } + ) + } +} + +export async function GET(request: NextRequest, ctx: { params: Promise<{ id: string }> }) { + const { id } = await ctx.params + return proxy(request, 'GET', id) +} + +export async function PATCH(request: NextRequest, ctx: { params: Promise<{ id: string }> }) { + const { id } = await ctx.params + const body = await request.text() + return proxy(request, 'PATCH', id, body) +} + +export async function DELETE(request: NextRequest, ctx: { params: Promise<{ id: string }> }) { + const { id } = await ctx.params + return proxy(request, 'DELETE', id) +} diff --git a/admin-compliance/app/api/sdk/v1/cra/projects/[id]/sbom/route.ts b/admin-compliance/app/api/sdk/v1/cra/projects/[id]/sbom/route.ts new file mode 100644 index 00000000..767c331d --- /dev/null +++ b/admin-compliance/app/api/sdk/v1/cra/projects/[id]/sbom/route.ts @@ -0,0 +1,48 @@ +import { NextRequest, NextResponse } from 'next/server' + +const BACKEND_URL = process.env.BACKEND_URL || 'http://backend-compliance:8002' + +function tenant(req: NextRequest) { + return req.headers.get('x-tenant-id') || '00000000-0000-0000-0000-000000000001' +} + +/** GET /sbom -> List uploads. We map this to the backend /sboms endpoint. */ +export async function GET(request: NextRequest, ctx: { params: Promise<{ id: string }> }) { + const { id } = await ctx.params + try { + const resp = await fetch(`${BACKEND_URL}/api/v1/cra/projects/${id}/sboms`, { + headers: { 'X-Tenant-ID': tenant(request) }, + }) + const text = await resp.text() + return new NextResponse(text, { + status: resp.status, + headers: { 'Content-Type': resp.headers.get('Content-Type') || 'application/json' }, + }) + } catch (err) { + return NextResponse.json({ error: 'Backend unreachable', details: String(err) }, { status: 502 }) + } +} + +/** POST /sbom -> multipart upload to backend /sbom/upload */ +export async function POST(request: NextRequest, ctx: { params: Promise<{ id: string }> }) { + const { id } = await ctx.params + try { + const formData = await request.formData() + const upstreamForm = new FormData() + for (const [key, value] of formData.entries()) { + upstreamForm.append(key, value) + } + const resp = await fetch(`${BACKEND_URL}/api/v1/cra/projects/${id}/sbom/upload`, { + method: 'POST', + headers: { 'X-Tenant-ID': tenant(request) }, + body: upstreamForm as unknown as BodyInit, + }) + const text = await resp.text() + return new NextResponse(text, { + status: resp.status, + headers: { 'Content-Type': resp.headers.get('Content-Type') || 'application/json' }, + }) + } catch (err) { + return NextResponse.json({ error: 'Backend unreachable', details: String(err) }, { status: 502 }) + } +} diff --git a/admin-compliance/app/api/sdk/v1/cra/projects/[id]/scope-check/route.ts b/admin-compliance/app/api/sdk/v1/cra/projects/[id]/scope-check/route.ts new file mode 100644 index 00000000..44c5dc03 --- /dev/null +++ b/admin-compliance/app/api/sdk/v1/cra/projects/[id]/scope-check/route.ts @@ -0,0 +1,24 @@ +import { NextRequest, NextResponse } from 'next/server' + +const BACKEND_URL = process.env.BACKEND_URL || 'http://backend-compliance:8002' + +export async function POST(request: NextRequest, ctx: { params: Promise<{ id: string }> }) { + const { id } = await ctx.params + const tenantId = request.headers.get('x-tenant-id') || '00000000-0000-0000-0000-000000000001' + try { + const resp = await fetch(`${BACKEND_URL}/api/v1/cra/projects/${id}/scope-check`, { + method: 'POST', + headers: { 'X-Tenant-ID': tenantId }, + }) + const text = await resp.text() + return new NextResponse(text, { + status: resp.status, + headers: { 'Content-Type': resp.headers.get('Content-Type') || 'application/json' }, + }) + } catch (err) { + return NextResponse.json( + { error: 'Backend unreachable', details: String(err) }, + { status: 502 } + ) + } +} diff --git a/admin-compliance/app/api/sdk/v1/cra/projects/route.ts b/admin-compliance/app/api/sdk/v1/cra/projects/route.ts new file mode 100644 index 00000000..fcb19ab2 --- /dev/null +++ b/admin-compliance/app/api/sdk/v1/cra/projects/route.ts @@ -0,0 +1,56 @@ +import { NextRequest, NextResponse } from 'next/server' + +const BACKEND_URL = process.env.BACKEND_URL || 'http://backend-compliance:8002' + +function tenantHeader(request: NextRequest): string { + return request.headers.get('x-tenant-id') || '00000000-0000-0000-0000-000000000001' +} + +/** GET /api/sdk/v1/cra/projects -> Backend list */ +export async function GET(request: NextRequest) { + const tenantId = tenantHeader(request) + const { searchParams } = new URL(request.url) + const qs = searchParams.toString() + try { + const resp = await fetch( + `${BACKEND_URL}/api/v1/cra/projects${qs ? `?${qs}` : ''}`, + { headers: { 'X-Tenant-ID': tenantId } } + ) + const body = await resp.text() + return new NextResponse(body, { + status: resp.status, + headers: { 'Content-Type': resp.headers.get('Content-Type') || 'application/json' }, + }) + } catch (err) { + return NextResponse.json( + { error: 'Backend unreachable', details: String(err) }, + { status: 502 } + ) + } +} + +/** POST /api/sdk/v1/cra/projects -> Backend create */ +export async function POST(request: NextRequest) { + const tenantId = tenantHeader(request) + const body = await request.text() + try { + const resp = await fetch(`${BACKEND_URL}/api/v1/cra/projects`, { + method: 'POST', + headers: { + 'X-Tenant-ID': tenantId, + 'Content-Type': 'application/json', + }, + body, + }) + const text = await resp.text() + return new NextResponse(text, { + status: resp.status, + headers: { 'Content-Type': resp.headers.get('Content-Type') || 'application/json' }, + }) + } catch (err) { + return NextResponse.json( + { error: 'Backend unreachable', details: String(err) }, + { status: 502 } + ) + } +} diff --git a/admin-compliance/app/sdk/cra/[projectId]/backlog/page.tsx b/admin-compliance/app/sdk/cra/[projectId]/backlog/page.tsx new file mode 100644 index 00000000..118b162c --- /dev/null +++ b/admin-compliance/app/sdk/cra/[projectId]/backlog/page.tsx @@ -0,0 +1,155 @@ +'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(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

Laedt...

+ if (error) return

{error}

+ if (!data) return null + + return ( +
+
+
+ + ← Zurueck zum Projekt + +

Prioritaeten-Backlog

+

+ Sortiert nach Severity × Deadline-Druck × Effort. Was du heute tust, was naechsten Sprint, was vor 11.12.2027. +

+
+ + {/* Deadline-Banner */} +
+ {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 ( +
+
{d.date}
+
{d.label}
+
+ {isPast ? 'bereits abgelaufen' : `noch ${days} Tage`} +
+
+ ) + })} +
+ + {/* Backlog */} +
+ + + + + + + + + + + + + + {data.items.map(item => ( + + + + + + + + + + ))} + +
RangAnforderungSeverityScoreAufwandMassnahmeAktion
{item.rank} +
{item.title}
+
{item.category} · {item.annex_anchor}
+
{item.priority_score}{item.effort_days} PT + {item.mapped_measure_names.length > 0 ? ( +
+ {item.mapped_measure_names.map(m => ( +
+ {m.id}: {m.name.length > 50 ? m.name.slice(0, 50) + '...' : m.name} +
+ ))} +
+ ) : ( + + )} +
+ +
+
+ +

+ Tage bis CE-Marking-Pflicht (11.12.2027): {data.days_to_ce_deadline} +

+
+
+ ) +} diff --git a/admin-compliance/app/sdk/cra/[projectId]/checks/page.tsx b/admin-compliance/app/sdk/cra/[projectId]/checks/page.tsx new file mode 100644 index 00000000..2737ad46 --- /dev/null +++ b/admin-compliance/app/sdk/cra/[projectId]/checks/page.tsx @@ -0,0 +1,195 @@ +'use client' + +import React, { useEffect, useState, useCallback, use } from 'react' + +interface CheckItem { + id: string + check_code: string + title: string + description: string + check_type: string + target_url: string | null + linked_req_ids: string[] + last_run_at: string | null + is_active: boolean + latest_result: { status: string; message: string; ran_at: string } | null +} + +interface ChecksResponse { + project_id: string + total: number + items: CheckItem[] +} + +const STATUS_STYLE: Record = { + pass: 'bg-green-100 text-green-800', + fail: 'bg-red-100 text-red-800', + manual_review_required: 'bg-yellow-100 text-yellow-800', +} + +export default function ChecksPage({ + params, +}: { + params: Promise<{ projectId: string }> +}) { + const { projectId } = use(params) + const [data, setData] = useState(null) + const [loading, setLoading] = useState(true) + const [error, setError] = useState('') + const [running, setRunning] = useState(null) + const [urlInputs, setUrlInputs] = useState>({}) + + const tenant = '00000000-0000-0000-0000-000000000001' + + const load = useCallback(async () => { + try { + const res = await fetch(`/api/sdk/v1/cra/projects/${projectId}/checks`, { + headers: { 'X-Tenant-ID': tenant }, + }) + if (!res.ok) throw new Error(await res.text()) + const json: ChecksResponse = await res.json() + setData(json) + const u: Record = {} + for (const c of json.items) u[c.id] = c.target_url || '' + setUrlInputs(u) + } catch (e) { + setError(e instanceof Error ? e.message : 'Fehler beim Laden') + } finally { + setLoading(false) + } + }, [projectId]) + + useEffect(() => { load() }, [load]) + + const initChecks = async () => { + setError('') + try { + const res = await fetch(`/api/sdk/v1/cra/projects/${projectId}/checks`, { + method: 'POST', + headers: { 'X-Tenant-ID': tenant }, + }) + if (!res.ok) throw new Error(await res.text()) + await load() + } catch (e) { + setError(e instanceof Error ? e.message : 'Init fehlgeschlagen') + } + } + + const runCheck = async (checkId: string) => { + setRunning(checkId) + setError('') + try { + const res = await fetch(`/api/sdk/v1/cra/checks/${checkId}/run`, { + method: 'POST', + headers: { 'X-Tenant-ID': tenant, 'Content-Type': 'application/json' }, + body: JSON.stringify({ target_url: urlInputs[checkId] || null }), + }) + if (!res.ok) throw new Error(await res.text()) + await load() + } catch (e) { + setError(e instanceof Error ? e.message : 'Run fehlgeschlagen') + } finally { + setRunning(null) + } + } + + if (loading) return

Laedt...

+ + return ( +
+
+
+ + ← Zurueck zum Projekt + +

Automatisierte Checks

+

+ CRA-typische Online-Pruefungen: security.txt, Update-Policy, TLS-Konfiguration, Vuln-Disclosure. +

+
+ + {error && ( +
+
{error}
+ +
+ )} + + {data && data.items.length === 0 && ( +
+

Noch keine Checks fuer dieses Projekt konfiguriert.

+ +
+ )} + + {data && data.items.length > 0 && ( +
+ {data.items.map(c => ( +
+
+
+
+

{c.title}

+ {c.check_code} +
+

{c.description}

+ {c.linked_req_ids.length > 0 && ( +
+ {c.linked_req_ids.map(r => ( + {r} + ))} +
+ )} +
+ {c.latest_result && ( + + {c.latest_result.status} + + )} +
+ + {(c.check_type === 'url_probe' || c.check_type === 'tls_probe' || c.check_type === 'manual_review') && ( +
+ setUrlInputs({ ...urlInputs, [c.id]: e.target.value })} + className="flex-1 px-3 py-1.5 border border-gray-300 rounded text-sm focus:ring-2 focus:ring-red-500" + /> + +
+ )} + + {c.latest_result && ( +
+ {c.latest_result.message} +
+ Geprueft: {new Date(c.latest_result.ran_at).toLocaleString('de-DE')} +
+
+ )} +
+ ))} +
+ )} + +
+ Hinweis: Aktuell implementiert: cra_security_txt (HTTP) und cra_tls_cert_check (TLS-Handshake). + Andere Check-Typen sind als manual_review_required markiert — der Pruefer beantwortet sie manuell. +
+
+
+ ) +} diff --git a/admin-compliance/app/sdk/cra/[projectId]/intake/page.tsx b/admin-compliance/app/sdk/cra/[projectId]/intake/page.tsx new file mode 100644 index 00000000..6dfd0a07 --- /dev/null +++ b/admin-compliance/app/sdk/cra/[projectId]/intake/page.tsx @@ -0,0 +1,240 @@ +'use client' + +import React, { useEffect, useState, useCallback, use } from 'react' +import { useRouter } from 'next/navigation' + +const LANGUAGES = [ + { value: '', label: '— bitte waehlen —' }, + { value: 'js', label: 'JavaScript / TypeScript' }, + { value: 'python', label: 'Python' }, + { value: 'go', label: 'Go' }, + { value: 'rust', label: 'Rust' }, + { value: 'java', label: 'Java / Kotlin' }, + { value: 'csharp', label: 'C# / .NET' }, + { value: 'cpp', label: 'C / C++' }, + { value: 'swift', label: 'Swift' }, + { value: 'mixed', label: 'Mehrere Sprachen' }, + { value: 'other', label: 'Andere' }, +] + +interface CRAProject { + id: string + name: string + description: string + repo_url: string | null + primary_language: string | null + has_firmware: boolean + connected_to_internet: boolean + has_software_updates: boolean + processes_personal_data: boolean + is_critical_infra_supplier: boolean + intended_use: string +} + +export default function IntakePage({ + params, +}: { + params: Promise<{ projectId: string }> +}) { + const { projectId } = use(params) + const router = useRouter() + + const [loading, setLoading] = useState(true) + const [saving, setSaving] = useState(false) + const [error, setError] = useState('') + + const [name, setName] = useState('') + const [description, setDescription] = useState('') + const [repoUrl, setRepoUrl] = useState('') + const [primaryLanguage, setPrimaryLanguage] = useState('') + const [hasFirmware, setHasFirmware] = useState(false) + const [connectedInternet, setConnectedInternet] = useState(false) + const [hasUpdates, setHasUpdates] = useState(false) + const [processesPersonal, setProcessesPersonal] = useState(false) + const [isCriticalInfra, setIsCriticalInfra] = useState(false) + const [intendedUse, setIntendedUse] = useState('') + + const tenant = '00000000-0000-0000-0000-000000000001' + + const load = useCallback(async () => { + try { + const res = await fetch(`/api/sdk/v1/cra/projects/${projectId}`, { + headers: { 'X-Tenant-ID': tenant }, + }) + if (!res.ok) throw new Error(await res.text()) + const p: CRAProject = await res.json() + setName(p.name) + setDescription(p.description || '') + setRepoUrl(p.repo_url || '') + setPrimaryLanguage(p.primary_language || '') + setHasFirmware(p.has_firmware) + setConnectedInternet(p.connected_to_internet) + setHasUpdates(p.has_software_updates) + setProcessesPersonal(p.processes_personal_data) + setIsCriticalInfra(p.is_critical_infra_supplier) + setIntendedUse(p.intended_use || '') + } catch (e) { + setError(e instanceof Error ? e.message : 'Fehler beim Laden') + } finally { + setLoading(false) + } + }, [projectId]) + + useEffect(() => { load() }, [load]) + + const save = async () => { + setSaving(true) + setError('') + try { + const res = await fetch(`/api/sdk/v1/cra/projects/${projectId}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json', 'X-Tenant-ID': tenant }, + body: JSON.stringify({ + name, + description, + repo_url: repoUrl || null, + primary_language: primaryLanguage || null, + has_firmware: hasFirmware, + connected_to_internet: connectedInternet, + has_software_updates: hasUpdates, + processes_personal_data: processesPersonal, + is_critical_infra_supplier: isCriticalInfra, + intended_use: intendedUse, + status: 'scoped', + }), + }) + if (!res.ok) throw new Error(await res.text()) + router.push(`/sdk/cra/${projectId}/scope`) + } catch (e) { + setError(e instanceof Error ? e.message : 'Speichern fehlgeschlagen') + } finally { + setSaving(false) + } + } + + if (loading) return

Laedt...

+ + return ( +
+
+
+ + ← Zurueck zum Projekt + +

Intake — Software-Profil

+

+ Schritt 1 von 3 — Beschreibe Software, Firmware und Connectivity. Daraus leiten wir die CRA-Klassifikation ab. +

+
+ + {error && ( +
{error}
+ )} + +
+
+ + setName(e.target.value)} + className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-red-500" + placeholder="z.B. SmartHome Gateway v3" + /> +
+ +
+ +