feat(iace): show ESAW evidence panel in risk view (B1)

The Risikobewertung page only mentioned the data sources as static prose.
Add a collapsible "Datenquellen & Evidenz" panel sourced from
/iace/risk-data-sources: the real Eurostat ESAW 2023 contact-mode shares
per mode, with license + ready-to-print attribution, and the note that
tiers anchor the ordering while values stay GT-calibrated.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-06-11 14:11:15 +02:00
parent 3c6deac1c5
commit b40edd6d33
3 changed files with 111 additions and 0 deletions
@@ -0,0 +1,59 @@
'use client'
import { useState } from 'react'
import { RiskDataSources as RiskDataSourcesData } from '../_hooks/useRiskDataSources'
/**
* Collapsible evidence panel: the real public-statistics figures (Eurostat ESAW
* 2023) that anchor the W/S tiers, with license + ready-to-print attribution.
* Confidence-aware tonality — informs the source, does not alarm.
*/
export function RiskDataSources({ data }: { data: RiskDataSourcesData }) {
const [open, setOpen] = useState(false)
if (!data.evidence?.length) return null
return (
<div className="rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800">
<button
onClick={() => setOpen(!open)}
className="w-full flex items-center justify-between px-4 py-3 text-left"
>
<span className="text-sm font-medium text-gray-800 dark:text-gray-200">
Datenquellen &amp; Evidenz <span className="text-gray-400 font-normal">({data.evidence.length} belegte Kontaktmodi)</span>
</span>
<span className="text-gray-400 text-xs">{open ? '▲' : '▼'}</span>
</button>
{open && (
<div className="px-4 pb-4 space-y-3">
<p className="text-xs text-gray-500 dark:text-gray-400">{data.note}</p>
<div className="overflow-x-auto">
<table className="w-full text-xs">
<thead>
<tr className="text-gray-500 border-b border-gray-200 dark:border-gray-700 text-left">
<th className="py-1.5 pr-3">Kontaktmodus</th>
<th className="py-1.5 pr-3">Belegte Quote</th>
<th className="py-1.5 pr-3">Quelle</th>
<th className="py-1.5">Lizenz</th>
</tr>
</thead>
<tbody>
{data.evidence.map((e) => (
<tr key={e.mode} className="border-b border-gray-100 dark:border-gray-700/50 align-top">
<td className="py-1.5 pr-3 text-gray-700 dark:text-gray-300 font-medium">{e.label}</td>
<td className="py-1.5 pr-3 text-gray-600 dark:text-gray-300 tabular-nums">{e.stat}</td>
<td className="py-1.5 pr-3 text-gray-500">{e.source}</td>
<td className="py-1.5 text-gray-500">{e.license}</td>
</tr>
))}
</tbody>
</table>
</div>
<p className="text-[10px] text-gray-400">
{data.evidence[0]?.attribution} · Tiers verankern die Quoten-<em>Ordnung</em>; die Werte sind an
BreakPilot-Ground-Truth kalibriert (keine Norm-Tabelle reproduziert).
</p>
</div>
)}
</div>
)
}
@@ -0,0 +1,47 @@
'use client'
import { useEffect, useState } from 'react'
export interface RiskEvidence {
mode: string
label: string
stat: string
source: string
license: string
attribution: string
retrieved: string
}
export interface RiskDataSources {
note: string
evidence: RiskEvidence[]
}
/**
* Loads the license-tagged public-statistics evidence behind the risk-frequency
* anchors (Eurostat ESAW hsw_ph3_08, 2023). Global, not per project — so an
* auditor can see WHERE the W/S tiers come from and the source is cited.
*/
export function useRiskDataSources() {
const [data, setData] = useState<RiskDataSources | null>(null)
useEffect(() => {
let cancelled = false
async function load() {
try {
const res = await fetch('/api/sdk/v1/iace/risk-data-sources')
if (!res.ok) return
const json = (await res.json()) as RiskDataSources
if (!cancelled) setData(json)
} catch (err) {
console.error('Failed to load risk data sources:', err)
}
}
load()
return () => {
cancelled = true
}
}, [])
return { data }
}
@@ -3,14 +3,17 @@
import { useParams } from 'next/navigation' import { useParams } from 'next/navigation'
import { useRiskAssessment } from './_hooks/useRiskAssessment' import { useRiskAssessment } from './_hooks/useRiskAssessment'
import { useRiskMatrix } from './_hooks/useRiskMatrix' import { useRiskMatrix } from './_hooks/useRiskMatrix'
import { useRiskDataSources } from './_hooks/useRiskDataSources'
import { RiskModelCard } from './_components/RiskModelCard' import { RiskModelCard } from './_components/RiskModelCard'
import { RiskMatrix } from './_components/RiskMatrix' import { RiskMatrix } from './_components/RiskMatrix'
import { RiskDataSources } from './_components/RiskDataSources'
export default function RisikobewertungPage() { export default function RisikobewertungPage() {
const params = useParams<{ projectId: string }>() const params = useParams<{ projectId: string }>()
const projectId = params.projectId const projectId = params.projectId
const { hazards, suggestions, loading } = useRiskAssessment(projectId) const { hazards, suggestions, loading } = useRiskAssessment(projectId)
const { data: matrix } = useRiskMatrix(projectId) const { data: matrix } = useRiskMatrix(projectId)
const { data: dataSources } = useRiskDataSources()
return ( return (
<div className="space-y-6"> <div className="space-y-6">
@@ -26,6 +29,8 @@ export default function RisikobewertungPage() {
{matrix && matrix.total > 0 && <RiskMatrix data={matrix} />} {matrix && matrix.total > 0 && <RiskMatrix data={matrix} />}
{dataSources && <RiskDataSources data={dataSources} />}
{loading && ( {loading && (
<div className="text-sm text-gray-500 dark:text-gray-400">Lade Gefaehrdungen</div> <div className="text-sm text-gray-500 dark:text-gray-400">Lade Gefaehrdungen</div>
)} )}