Fix: Remove broken getKlausurApiUrl and clean up empty lines
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-school (push) Successful in 42s
CI / test-go-edu-search (push) Successful in 34s
CI / test-python-klausur (push) Failing after 2m51s
CI / test-python-agent-core (push) Successful in 21s
CI / test-nodejs-website (push) Successful in 29s

sed replacement left orphaned hostname references in story page
and empty lines in getApiBase functions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-04-24 16:02:04 +02:00
parent b07f802c24
commit 9ba420fa91
150 changed files with 30231 additions and 32053 deletions

View File

@@ -0,0 +1,168 @@
'use client'
import type { DockerStats, ContainerInfo, ContainerFilter } from '../types'
import { getStateColor } from './helpers'
interface DeploymentsTabProps {
dockerStats: DockerStats | null
filteredContainers: ContainerInfo[]
containerFilter: ContainerFilter
setContainerFilter: (f: ContainerFilter) => void
actionLoading: string | null
containerAction: (containerId: string, action: 'start' | 'stop' | 'restart') => Promise<void>
loadContainerData: () => Promise<void>
}
export function DeploymentsTab({
dockerStats,
filteredContainers,
containerFilter,
setContainerFilter,
actionLoading,
containerAction,
loadContainerData,
}: DeploymentsTabProps) {
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h3 className="text-lg font-semibold text-slate-800">Docker Container</h3>
{dockerStats && (
<p className="text-sm text-slate-600">
{dockerStats.running_containers} laufend, {dockerStats.stopped_containers} gestoppt, {dockerStats.total_containers} gesamt
</p>
)}
</div>
<div className="flex items-center gap-2">
<select
value={containerFilter}
onChange={(e) => setContainerFilter(e.target.value as ContainerFilter)}
className="px-3 py-1.5 text-sm border border-slate-300 rounded-lg bg-white"
>
<option value="all">Alle</option>
<option value="running">Laufend</option>
<option value="stopped">Gestoppt</option>
</select>
<button
onClick={loadContainerData}
className="px-3 py-1.5 text-sm border border-slate-300 text-slate-700 rounded-lg hover:bg-slate-50"
>
Aktualisieren
</button>
</div>
</div>
{/* Container List */}
{filteredContainers.length === 0 ? (
<div className="text-center py-8 text-slate-500">Keine Container gefunden</div>
) : (
<div className="space-y-3">
{filteredContainers.map((container) => (
<ContainerCard
key={container.id}
container={container}
actionLoading={actionLoading}
containerAction={containerAction}
/>
))}
</div>
)}
</div>
)
}
// ============================================================================
// Container Card Sub-component
// ============================================================================
function ContainerCard({
container,
actionLoading,
containerAction,
}: {
container: ContainerInfo
actionLoading: string | null
containerAction: (containerId: string, action: 'start' | 'stop' | 'restart') => Promise<void>
}) {
return (
<div
className={`border rounded-xl p-4 transition-colors ${
container.state === 'running'
? 'border-green-200 bg-green-50/30'
: 'border-slate-200 bg-slate-50/50'
}`}
>
<div className="flex items-start justify-between gap-4">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<span className="font-semibold text-slate-900 truncate">{container.name}</span>
<span className={`px-2 py-0.5 text-xs font-medium rounded-full ${getStateColor(container.state)}`}>
{container.state}
</span>
</div>
<div className="text-sm text-slate-500 mb-2">
<span className="font-mono">{container.image}</span>
{container.ports.length > 0 && (
<span className="ml-2 text-slate-400">
| {container.ports.slice(0, 2).join(', ')}
{container.ports.length > 2 && ` +${container.ports.length - 2}`}
</span>
)}
</div>
{container.state === 'running' && (
<div className="flex flex-wrap gap-4 text-sm">
<div className="flex items-center gap-1">
<span className="text-slate-500">CPU:</span>
<span className={`font-medium ${container.cpu_percent > 80 ? 'text-red-600' : 'text-slate-700'}`}>
{container.cpu_percent.toFixed(1)}%
</span>
</div>
<div className="flex items-center gap-1">
<span className="text-slate-500">RAM:</span>
<span className={`font-medium ${container.memory_percent > 80 ? 'text-red-600' : 'text-slate-700'}`}>
{container.memory_usage}
</span>
<span className="text-slate-400">({container.memory_percent.toFixed(1)}%)</span>
</div>
<div className="flex items-center gap-1">
<span className="text-slate-500">Net:</span>
<span className="text-slate-700">{container.network_rx} / {container.network_tx}</span>
</div>
</div>
)}
</div>
<div className="flex items-center gap-2 flex-shrink-0">
{container.state === 'running' ? (
<>
<button
onClick={() => containerAction(container.id, 'restart')}
disabled={actionLoading !== null}
className="px-3 py-1.5 text-sm bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 transition-colors"
>
{actionLoading === `${container.id}-restart` ? '...' : 'Restart'}
</button>
<button
onClick={() => containerAction(container.id, 'stop')}
disabled={actionLoading !== null}
className="px-3 py-1.5 text-sm bg-red-600 text-white rounded-lg hover:bg-red-700 disabled:opacity-50 transition-colors"
>
{actionLoading === `${container.id}-stop` ? '...' : 'Stop'}
</button>
</>
) : (
<button
onClick={() => containerAction(container.id, 'start')}
disabled={actionLoading !== null}
className="px-3 py-1.5 text-sm bg-green-600 text-white rounded-lg hover:bg-green-700 disabled:opacity-50 transition-colors"
>
{actionLoading === `${container.id}-start` ? '...' : 'Start'}
</button>
)}
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,278 @@
'use client'
import type { PipelineStatus, PipelineRun, SystemStats, DockerStats, WoodpeckerStatus, TabType } from '../types'
import { ProgressBar } from './helpers'
interface OverviewTabProps {
pipelineStatus: PipelineStatus | null
pipelineHistory: PipelineRun[]
systemStats: SystemStats | null
dockerStats: DockerStats | null
woodpeckerStatus: WoodpeckerStatus | null
triggeringWoodpecker: boolean
triggerWoodpeckerPipeline: () => Promise<void>
setActiveTab: (tab: TabType) => void
}
export function OverviewTab({
pipelineStatus,
pipelineHistory,
systemStats,
dockerStats,
woodpeckerStatus,
triggeringWoodpecker,
triggerWoodpeckerPipeline,
setActiveTab,
}: OverviewTabProps) {
return (
<div className="space-y-6">
{/* Woodpecker CI Status - Prominent */}
<WoodpeckerOverviewCard
woodpeckerStatus={woodpeckerStatus}
triggeringWoodpecker={triggeringWoodpecker}
triggerWoodpeckerPipeline={triggerWoodpeckerPipeline}
setActiveTab={setActiveTab}
/>
{/* Status Cards */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div className={`p-4 rounded-lg ${pipelineStatus?.gitea_connected ? 'bg-green-50' : 'bg-yellow-50'}`}>
<div className="flex items-center gap-2 mb-2">
<span className={`w-3 h-3 rounded-full ${pipelineStatus?.gitea_connected ? 'bg-green-500' : 'bg-yellow-500'}`}></span>
<span className="text-sm font-medium">Gitea Status</span>
</div>
<p className={`text-lg font-bold ${pipelineStatus?.gitea_connected ? 'text-green-700' : 'text-yellow-700'}`}>
{pipelineStatus?.gitea_connected ? 'Verbunden' : 'Nicht verbunden'}
</p>
<p className="text-xs text-slate-500">http://macmini:3003</p>
</div>
<div className="bg-blue-50 p-4 rounded-lg">
<div className="flex items-center gap-2 mb-2">
<svg className="w-4 h-4 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
</svg>
<span className="text-sm font-medium">Pipeline Runs</span>
</div>
<p className="text-lg font-bold text-blue-700">{pipelineStatus?.total_runs || 0}</p>
<p className="text-xs text-slate-500">{pipelineStatus?.successful_runs || 0} erfolgreich</p>
</div>
<div className="bg-purple-50 p-4 rounded-lg">
<div className="flex items-center gap-2 mb-2">
<svg className="w-4 h-4 text-purple-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2" />
</svg>
<span className="text-sm font-medium">Container</span>
</div>
<p className="text-lg font-bold text-purple-700">{dockerStats?.running_containers || 0}</p>
<p className="text-xs text-slate-500">von {dockerStats?.total_containers || 0} laufend</p>
</div>
<div className="bg-slate-50 p-4 rounded-lg">
<div className="flex items-center gap-2 mb-2">
<svg className="w-4 h-4 text-slate-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span className="text-sm font-medium">Letztes Update</span>
</div>
<p className="text-lg font-bold text-slate-700">
{pipelineStatus?.last_sbom_update ? new Date(pipelineStatus.last_sbom_update).toLocaleDateString('de-DE') : 'Nie'}
</p>
<p className="text-xs text-slate-500">
{pipelineStatus?.last_sbom_update ? new Date(pipelineStatus.last_sbom_update).toLocaleTimeString('de-DE') : '-'}
</p>
</div>
</div>
{/* System Resources */}
{systemStats && (
<div className="bg-slate-50 rounded-lg p-4">
<h3 className="font-medium text-slate-800 mb-4 flex items-center gap-2">
<svg className="w-5 h-5 text-slate-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z" />
</svg>
Server Ressourcen ({systemStats.hostname})
</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="bg-white rounded-lg p-3">
<div className="flex justify-between mb-2">
<span className="text-sm text-slate-600">CPU</span>
<span className={`font-bold ${systemStats.cpu.usage_percent > 80 ? 'text-red-600' : 'text-slate-900'}`}>
{systemStats.cpu.usage_percent.toFixed(1)}%
</span>
</div>
<ProgressBar percent={systemStats.cpu.usage_percent} />
</div>
<div className="bg-white rounded-lg p-3">
<div className="flex justify-between mb-2">
<span className="text-sm text-slate-600">RAM</span>
<span className={`font-bold ${systemStats.memory.usage_percent > 80 ? 'text-red-600' : 'text-slate-900'}`}>
{systemStats.memory.usage_percent.toFixed(1)}%
</span>
</div>
<ProgressBar percent={systemStats.memory.usage_percent} color="purple" />
</div>
<div className="bg-white rounded-lg p-3">
<div className="flex justify-between mb-2">
<span className="text-sm text-slate-600">Disk</span>
<span className={`font-bold ${systemStats.disk.usage_percent > 80 ? 'text-red-600' : 'text-slate-900'}`}>
{systemStats.disk.usage_percent.toFixed(1)}%
</span>
</div>
<ProgressBar percent={systemStats.disk.usage_percent} color="green" />
</div>
</div>
</div>
)}
{/* Recent Pipeline Runs */}
{pipelineHistory.length > 0 && (
<div className="bg-slate-50 rounded-lg p-4">
<h3 className="font-medium text-slate-800 mb-3">Letzte Pipeline Runs</h3>
<div className="space-y-2">
{pipelineHistory.slice(0, 5).map((run) => (
<div key={run.id} className="flex items-center justify-between bg-white p-3 rounded-lg">
<div className="flex items-center gap-3">
<span className={`w-2 h-2 rounded-full ${
run.status === 'success' ? 'bg-green-500' :
run.status === 'failed' ? 'bg-red-500' :
run.status === 'running' ? 'bg-yellow-500 animate-pulse' : 'bg-slate-400'
}`}></span>
<div>
<p className="text-sm font-medium text-slate-800">{run.workflow || 'SBOM Pipeline'}</p>
<p className="text-xs text-slate-500">{run.branch} - {run.commit_sha.substring(0, 8)}</p>
</div>
</div>
<div className="text-right">
<p className={`text-sm font-medium ${
run.status === 'success' ? 'text-green-600' :
run.status === 'failed' ? 'text-red-600' :
run.status === 'running' ? 'text-yellow-600' : 'text-slate-600'
}`}>
{run.status === 'success' ? 'Erfolgreich' :
run.status === 'failed' ? 'Fehlgeschlagen' :
run.status === 'running' ? 'Laeuft...' : run.status}
</p>
<p className="text-xs text-slate-500">
{new Date(run.started_at).toLocaleString('de-DE')}
</p>
</div>
</div>
))}
</div>
</div>
)}
</div>
)
}
// ============================================================================
// Woodpecker Overview Card (sub-component)
// ============================================================================
function WoodpeckerOverviewCard({
woodpeckerStatus,
triggeringWoodpecker,
triggerWoodpeckerPipeline,
setActiveTab,
}: {
woodpeckerStatus: WoodpeckerStatus | null
triggeringWoodpecker: boolean
triggerWoodpeckerPipeline: () => Promise<void>
setActiveTab: (tab: TabType) => void
}) {
const latestPipeline = woodpeckerStatus?.pipelines?.[0]
const isOnline = woodpeckerStatus?.status === 'online'
const latestStatus = latestPipeline?.status
const borderClass = isOnline
? latestStatus === 'success'
? 'border-green-300 bg-green-50'
: latestStatus === 'failure' || latestStatus === 'error'
? 'border-red-300 bg-red-50'
: latestStatus === 'running'
? 'border-blue-300 bg-blue-50'
: 'border-slate-300 bg-slate-50'
: 'border-red-300 bg-red-50'
const iconBgClass = isOnline
? latestStatus === 'success'
? 'bg-green-100'
: latestStatus === 'failure' || latestStatus === 'error'
? 'bg-red-100'
: 'bg-blue-100'
: 'bg-red-100'
return (
<div className={`p-4 rounded-xl border-2 ${borderClass}`}>
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<div className={`p-3 rounded-lg ${iconBgClass}`}>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
</div>
<div>
<div className="flex items-center gap-2">
<h3 className="font-semibold text-slate-900">Woodpecker CI</h3>
<span className={`px-2 py-0.5 text-xs font-medium rounded-full ${
isOnline ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
}`}>
{isOnline ? 'Online' : 'Offline'}
</span>
</div>
{latestPipeline && (
<p className="text-sm text-slate-600 mt-1">
Pipeline #{latestPipeline.number}: {' '}
<span className={`font-medium ${
latestStatus === 'success' ? 'text-green-600' :
latestStatus === 'failure' || latestStatus === 'error' ? 'text-red-600' :
latestStatus === 'running' ? 'text-blue-600' : 'text-slate-600'
}`}>
{latestStatus}
</span>
{' '}auf {latestPipeline.branch}
</p>
)}
</div>
</div>
<div className="flex items-center gap-2">
<button
onClick={() => setActiveTab('woodpecker')}
className="px-3 py-1.5 text-sm border border-slate-300 text-slate-700 rounded-lg hover:bg-white"
>
Details
</button>
<button
onClick={triggerWoodpeckerPipeline}
disabled={triggeringWoodpecker}
className="px-3 py-1.5 text-sm bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 flex items-center gap-1"
>
{triggeringWoodpecker ? (
<div className="animate-spin rounded-full h-3 w-3 border-b-2 border-white" />
) : (
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
</svg>
)}
Starten
</button>
</div>
</div>
{/* Failed steps preview */}
{latestPipeline?.steps?.some(s => s.state === 'failure') && (
<div className="mt-3 pt-3 border-t border-red-200">
<p className="text-xs font-medium text-red-700 mb-2">Fehlgeschlagene Steps:</p>
<div className="flex flex-wrap gap-2">
{latestPipeline.steps.filter(s => s.state === 'failure').map((step, i) => (
<span key={i} className="px-2 py-1 bg-red-100 text-red-700 text-xs rounded">
{step.name}
</span>
))}
</div>
</div>
)}
</div>
)
}

View File

@@ -0,0 +1,145 @@
'use client'
import type { PipelineRun } from '../types'
interface PipelinesTabProps {
pipelineHistory: PipelineRun[]
triggeringPipeline: boolean
triggerPipeline: () => Promise<void>
}
export function PipelinesTab({
pipelineHistory,
triggeringPipeline,
triggerPipeline,
}: PipelinesTabProps) {
return (
<div className="space-y-6">
{/* Pipeline Controls */}
<div className="flex items-center justify-between">
<div>
<h3 className="text-lg font-semibold text-slate-800">Gitea Actions Pipelines</h3>
<p className="text-sm text-slate-600">Workflows werden bei Push auf main/develop automatisch ausgefuehrt</p>
</div>
<button
onClick={triggerPipeline}
disabled={triggeringPipeline}
className="px-4 py-2 bg-orange-600 text-white rounded-lg font-medium hover:bg-orange-700 disabled:opacity-50 transition-colors flex items-center gap-2"
>
{triggeringPipeline ? (
<>
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
Laeuft...
</>
) : (
<>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
Pipeline starten
</>
)}
</button>
</div>
{/* Available Pipelines */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="bg-green-50 border border-green-200 rounded-lg p-4">
<div className="flex items-center gap-2 mb-2">
<span className="w-2 h-2 rounded-full bg-green-500"></span>
<span className="font-medium text-green-800">SBOM Pipeline</span>
</div>
<p className="text-sm text-green-700 mb-2">Generiert Software Bill of Materials</p>
<p className="text-xs text-green-600">5 Jobs: generate, scan, license, upload, summary</p>
</div>
<div className="bg-slate-50 border border-slate-200 rounded-lg p-4 opacity-60">
<div className="flex items-center gap-2 mb-2">
<span className="w-2 h-2 rounded-full bg-slate-400"></span>
<span className="font-medium text-slate-600">Test Pipeline</span>
</div>
<p className="text-sm text-slate-500 mb-2">Unit & Integration Tests</p>
<p className="text-xs text-slate-400">Geplant</p>
</div>
<div className="bg-slate-50 border border-slate-200 rounded-lg p-4 opacity-60">
<div className="flex items-center gap-2 mb-2">
<span className="w-2 h-2 rounded-full bg-slate-400"></span>
<span className="font-medium text-slate-600">Security Pipeline</span>
</div>
<p className="text-sm text-slate-500 mb-2">SAST, SCA, Secrets Scan</p>
<p className="text-xs text-slate-400">Geplant</p>
</div>
</div>
{/* Pipeline History */}
<div className="bg-slate-50 rounded-lg p-4">
<h4 className="font-medium text-slate-800 mb-4">Pipeline Historie</h4>
{pipelineHistory.length === 0 ? (
<div className="text-center py-8 text-slate-500">
Keine Pipeline-Runs vorhanden. Starten Sie die erste Pipeline!
</div>
) : (
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="border-b border-slate-200">
<th className="text-left py-2 px-3 text-xs font-semibold text-slate-500 uppercase">Status</th>
<th className="text-left py-2 px-3 text-xs font-semibold text-slate-500 uppercase">Workflow</th>
<th className="text-left py-2 px-3 text-xs font-semibold text-slate-500 uppercase">Branch</th>
<th className="text-left py-2 px-3 text-xs font-semibold text-slate-500 uppercase">Commit</th>
<th className="text-left py-2 px-3 text-xs font-semibold text-slate-500 uppercase">Gestartet</th>
<th className="text-left py-2 px-3 text-xs font-semibold text-slate-500 uppercase">Dauer</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{pipelineHistory.map((run) => (
<tr key={run.id} className="hover:bg-white">
<td className="py-2 px-3">
<span className={`inline-flex items-center gap-1 px-2 py-1 rounded-full text-xs font-medium ${
run.status === 'success' ? 'bg-green-100 text-green-800' :
run.status === 'failed' ? 'bg-red-100 text-red-800' :
run.status === 'running' ? 'bg-yellow-100 text-yellow-800' : 'bg-slate-100 text-slate-600'
}`}>
<span className={`w-1.5 h-1.5 rounded-full ${
run.status === 'success' ? 'bg-green-500' :
run.status === 'failed' ? 'bg-red-500' :
run.status === 'running' ? 'bg-yellow-500 animate-pulse' : 'bg-slate-400'
}`}></span>
{run.status}
</span>
</td>
<td className="py-2 px-3 text-sm text-slate-900">{run.workflow || 'SBOM Pipeline'}</td>
<td className="py-2 px-3 text-sm text-slate-600">{run.branch}</td>
<td className="py-2 px-3 text-sm font-mono text-slate-500">{run.commit_sha.substring(0, 8)}</td>
<td className="py-2 px-3 text-sm text-slate-500">{new Date(run.started_at).toLocaleString('de-DE')}</td>
<td className="py-2 px-3 text-sm text-slate-500">
{run.duration_seconds ? `${run.duration_seconds}s` : '-'}
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
{/* Pipeline Architecture */}
<div className="bg-slate-50 rounded-lg p-4">
<h4 className="font-medium text-slate-800 mb-3">SBOM Pipeline Architektur</h4>
<pre className="bg-slate-800 text-slate-100 p-4 rounded-lg overflow-x-auto text-sm">
{`Gitea Actions Pipeline (.gitea/workflows/sbom.yaml)
|
+-- 1. generate-sbom -> Syft generiert CycloneDX SBOM
|
+-- 2. vulnerability-scan -> Grype scannt auf CVEs
|
+-- 3. license-check -> Prueft GPL/AGPL Lizenzen
|
+-- 4. upload-dashboard -> POST /api/v1/security/sbom/upload
|
+-- 5. summary -> Job Summary generieren`}
</pre>
</div>
</div>
)
}

View File

@@ -0,0 +1,286 @@
'use client'
export function SchedulerTab() {
return (
<div className="space-y-6">
{/* Status Overview */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<StatusCard
icon={<ClockIcon />}
title="launchd Job"
description="Taeglich um 07:00 Uhr automatisch"
/>
<StatusCard
icon={<TerminalIcon />}
title="Git Hook"
description="Quick Tests bei voice-service Aenderungen"
/>
<StatusCard
icon={<BellIcon />}
title="Benachrichtigungen"
description="Desktop-Alerts bei Fehlern aktiviert"
/>
</div>
{/* Quick Actions */}
<div className="bg-slate-50 rounded-lg p-4">
<h3 className="font-medium text-slate-800 mb-4">Quick Actions (BQAS)</h3>
<div className="flex flex-wrap gap-3">
<a
href="/ai/test-quality"
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 flex items-center gap-2"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
Test Dashboard oeffnen
</a>
<span className="text-sm text-slate-500 self-center">
Starte Tests direkt im BQAS Dashboard
</span>
</div>
</div>
{/* GitHub Actions vs Local - Comparison */}
<ComparisonTable />
{/* Configuration Details */}
<ConfigurationDetails />
{/* Detailed Explanation */}
<DetailedExplanation />
</div>
)
}
// ============================================================================
// Sub-components
// ============================================================================
function StatusCard({ icon, title, description }: { icon: React.ReactNode; title: string; description: string }) {
return (
<div className="rounded-xl border p-5 bg-emerald-100 border-emerald-200 text-emerald-700">
<div className="flex items-start gap-4">
<div className="flex-shrink-0">
{icon}
</div>
<div className="flex-1">
<div className="flex items-center gap-2">
<h4 className="font-semibold">{title}</h4>
<span className="w-2 h-2 rounded-full bg-emerald-500" />
</div>
<p className="text-sm mt-1 opacity-80">{description}</p>
</div>
</div>
</div>
)
}
function ComparisonTable() {
return (
<div className="bg-slate-50 rounded-lg p-4">
<h3 className="font-medium text-slate-800 mb-4">GitHub Actions Alternative</h3>
<p className="text-slate-600 mb-4">
Der lokale BQAS Scheduler ersetzt GitHub Actions und bietet DSGVO-konforme, vollstaendig lokale Test-Ausfuehrung.
</p>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-slate-200 bg-white">
<th className="text-left py-3 px-4 font-medium text-slate-700">Feature</th>
<th className="text-center py-3 px-4 font-medium text-slate-700">GitHub Actions</th>
<th className="text-center py-3 px-4 font-medium text-slate-700">Lokaler Scheduler</th>
</tr>
</thead>
<tbody>
<ComparisonRow
feature="Taegliche Tests (07:00)"
github={<span className="text-slate-600">schedule: cron</span>}
local={<Badge color="emerald">macOS launchd</Badge>}
/>
<ComparisonRow
feature="Push-basierte Tests"
github={<span className="text-slate-600">on: push</span>}
local={<Badge color="emerald">Git post-commit Hook</Badge>}
/>
<ComparisonRow
feature="PR-basierte Tests"
github={<Badge color="emerald">on: pull_request</Badge>}
local={<Badge color="amber">Nicht moeglich</Badge>}
/>
<ComparisonRow
feature="DSGVO-Konformitaet"
github={<Badge color="amber">Daten bei GitHub (US)</Badge>}
local={<Badge color="emerald">100% lokal</Badge>}
/>
<ComparisonRow
feature="Offline-Faehig"
github={<Badge color="red">Nein</Badge>}
local={<Badge color="emerald">Ja</Badge>}
isLast
/>
</tbody>
</table>
</div>
</div>
)
}
function ComparisonRow({
feature,
github,
local,
isLast = false,
}: {
feature: string
github: React.ReactNode
local: React.ReactNode
isLast?: boolean
}) {
return (
<tr className={isLast ? '' : 'border-b border-slate-100'}>
<td className="py-3 px-4 text-slate-600">{feature}</td>
<td className="py-3 px-4 text-center">{github}</td>
<td className="py-3 px-4 text-center">{local}</td>
</tr>
)
}
function Badge({ color, children }: { color: 'emerald' | 'amber' | 'red'; children: React.ReactNode }) {
const colorClasses = {
emerald: 'bg-emerald-100 text-emerald-700',
amber: 'bg-amber-100 text-amber-700',
red: 'bg-red-100 text-red-700',
}
return (
<span className={`px-2 py-1 rounded text-xs font-medium ${colorClasses[color]}`}>
{children}
</span>
)
}
function ConfigurationDetails() {
return (
<div className="bg-slate-50 rounded-lg p-4">
<h3 className="font-medium text-slate-800 mb-4">Konfiguration</h3>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* launchd Configuration */}
<div>
<h4 className="font-medium text-slate-700 mb-3">launchd Job</h4>
<div className="bg-slate-900 rounded-lg p-4 font-mono text-sm text-slate-100 overflow-x-auto">
<pre>{`# ~/Library/LaunchAgents/com.breakpilot.bqas.plist
Label: com.breakpilot.bqas
Schedule: 07:00 taeglich
Script: /voice-service/scripts/run_bqas.sh
Logs: /var/log/bqas/`}</pre>
</div>
</div>
{/* Environment Variables */}
<div>
<h4 className="font-medium text-slate-700 mb-3">Umgebungsvariablen</h4>
<div className="space-y-2 text-sm">
<EnvVar name="BQAS_SERVICE_URL" value="http://localhost:8091" />
<EnvVar name="BQAS_REGRESSION_THRESHOLD" value="0.1" />
<EnvVar name="BQAS_NOTIFY_DESKTOP" value="true" isActive />
<EnvVar name="BQAS_NOTIFY_SLACK" value="false" />
</div>
</div>
</div>
</div>
)
}
function EnvVar({ name, value, isActive }: { name: string; value: string; isActive?: boolean }) {
return (
<div className="flex justify-between p-2 bg-white rounded">
<span className="font-mono text-slate-600">{name}</span>
<span className={isActive ? 'text-emerald-600 font-medium' : value === 'false' ? 'text-slate-400' : 'text-slate-900'}>
{value}
</span>
</div>
)
}
function DetailedExplanation() {
return (
<div className="bg-gradient-to-r from-blue-50 to-indigo-50 rounded-xl border border-blue-200 p-6">
<h3 className="text-lg font-semibold text-slate-900 mb-4 flex items-center gap-2">
<svg className="w-5 h-5 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
Detaillierte Erklaerung
</h3>
<div className="prose prose-sm max-w-none text-slate-700">
<h4 className="text-base font-semibold mt-4 mb-2">Warum ein lokaler Scheduler?</h4>
<p className="mb-4">
Der lokale BQAS Scheduler wurde entwickelt, um die gleiche Funktionalitaet wie GitHub Actions zu bieten,
aber mit dem entscheidenden Vorteil, dass <strong>alle Daten zu 100% auf dem lokalen Mac Mini verbleiben</strong>.
Dies ist besonders wichtig fuer DSGVO-Konformitaet, da keine Schuelerdaten oder Testergebnisse an externe Server uebertragen werden.
</p>
<h4 className="text-base font-semibold mt-4 mb-2">Komponenten</h4>
<ul className="list-disc list-inside space-y-2 mb-4">
<li>
<strong>run_bqas.sh</strong> - Hauptscript das pytest ausfuehrt, Regression-Checks macht und Benachrichtigungen versendet
</li>
<li>
<strong>launchd Job</strong> - macOS-nativer Scheduler der das Script taeglich um 07:00 Uhr startet
</li>
<li>
<strong>Git Hook</strong> - post-commit Hook der bei Aenderungen im voice-service automatisch Quick-Tests startet
</li>
<li>
<strong>Notifier</strong> - Python-Modul das Desktop-, Slack- und E-Mail-Benachrichtigungen versendet
</li>
</ul>
<h4 className="text-base font-semibold mt-4 mb-2">Installation</h4>
<div className="bg-slate-900 rounded-lg p-3 font-mono text-sm text-slate-100 mb-4">
<code>./voice-service/scripts/install_bqas_scheduler.sh install</code>
</div>
<h4 className="text-base font-semibold mt-4 mb-2">Vorteile gegenueber GitHub Actions</h4>
<ul className="list-disc list-inside space-y-1">
<li>100% DSGVO-konform - alle Daten bleiben lokal</li>
<li>Keine Internet-Abhaengigkeit - funktioniert auch offline</li>
<li>Keine GitHub-Kosten fuer private Repositories</li>
<li>Schnellere Ausfuehrung ohne Cloud-Overhead</li>
<li>Volle Kontrolle ueber Scheduling und Benachrichtigungen</li>
</ul>
</div>
</div>
)
}
// ============================================================================
// SVG Icons
// ============================================================================
function ClockIcon() {
return (
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
)
}
function TerminalIcon() {
return (
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
)
}
function BellIcon() {
return (
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" />
</svg>
)
}

View File

@@ -0,0 +1,166 @@
'use client'
import type { PipelineStatus } from '../types'
interface SetupTabProps {
pipelineStatus: PipelineStatus | null
}
export function SetupTab({ pipelineStatus }: SetupTabProps) {
return (
<div className="space-y-6">
<div>
<h3 className="text-lg font-semibold text-slate-800 mb-2">Erstkonfiguration - Gitea CI/CD</h3>
<p className="text-slate-600">
Anleitung zur Einrichtung der CI/CD Pipeline mit Gitea Actions auf dem Mac Mini Server.
</p>
</div>
{/* Gitea Server Info */}
<div className="bg-blue-50 p-4 rounded-lg">
<h4 className="font-medium text-blue-800 mb-3 flex items-center gap-2">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2" />
</svg>
Gitea Server
</h4>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="bg-white p-3 rounded-lg">
<p className="text-sm text-slate-500">Web-URL</p>
<p className="font-mono text-blue-700">http://macmini:3003</p>
</div>
<div className="bg-white p-3 rounded-lg">
<p className="text-sm text-slate-500">SSH</p>
<p className="font-mono text-blue-700">macmini:2222</p>
</div>
<div className="bg-white p-3 rounded-lg">
<p className="text-sm text-slate-500">Status</p>
<p className={`font-medium ${pipelineStatus?.gitea_connected ? 'text-green-600' : 'text-yellow-600'}`}>
{pipelineStatus?.gitea_connected ? 'Verbunden' : 'Konfiguration erforderlich'}
</p>
</div>
</div>
</div>
{/* Implementierte Komponenten */}
<div className="bg-slate-50 p-4 rounded-lg">
<h4 className="font-medium text-slate-800 mb-3">Implementierte Komponenten</h4>
<div className="overflow-x-auto">
<table className="min-w-full text-sm">
<thead>
<tr className="border-b border-slate-200">
<th className="text-left py-2 px-3 font-medium text-slate-600">Komponente</th>
<th className="text-left py-2 px-3 font-medium text-slate-600">Pfad</th>
<th className="text-left py-2 px-3 font-medium text-slate-600">Beschreibung</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
<tr>
<td className="py-2 px-3 font-medium">Gitea Service</td>
<td className="py-2 px-3"><code className="bg-slate-200 px-1 rounded text-xs">docker-compose.yml</code></td>
<td className="py-2 px-3 text-slate-600">Gitea 1.22 mit Actions enabled</td>
</tr>
<tr>
<td className="py-2 px-3 font-medium">Gitea Runner</td>
<td className="py-2 px-3"><code className="bg-slate-200 px-1 rounded text-xs">docker-compose.yml</code></td>
<td className="py-2 px-3 text-slate-600">act_runner fuer Job-Ausfuehrung</td>
</tr>
<tr>
<td className="py-2 px-3 font-medium">SBOM Workflow</td>
<td className="py-2 px-3"><code className="bg-slate-200 px-1 rounded text-xs">.gitea/workflows/sbom.yaml</code></td>
<td className="py-2 px-3 text-slate-600">5 Jobs: generate, scan, license, upload, summary</td>
</tr>
<tr>
<td className="py-2 px-3 font-medium">Backend API</td>
<td className="py-2 px-3"><code className="bg-slate-200 px-1 rounded text-xs">backend/security_api.py</code></td>
<td className="py-2 px-3 text-slate-600">SBOM Upload, Pipeline Status, History</td>
</tr>
<tr>
<td className="py-2 px-3 font-medium">Runner Config</td>
<td className="py-2 px-3"><code className="bg-slate-200 px-1 rounded text-xs">gitea/runner-config.yaml</code></td>
<td className="py-2 px-3 text-slate-600">Labels: ubuntu-latest, self-hosted</td>
</tr>
</tbody>
</table>
</div>
</div>
{/* Setup Steps */}
<div className="bg-orange-50 p-4 rounded-lg">
<h4 className="font-medium text-orange-800 mb-3 flex items-center gap-2">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01" />
</svg>
Setup-Schritte
</h4>
<div className="space-y-3">
<div className="bg-white p-3 rounded-lg">
<h5 className="font-medium text-slate-800 mb-1">1. Gitea oeffnen</h5>
<code className="text-sm bg-slate-100 px-2 py-1 rounded">http://macmini:3003</code>
</div>
<div className="bg-white p-3 rounded-lg">
<h5 className="font-medium text-slate-800 mb-1">2. Admin-Account erstellen</h5>
<p className="text-sm text-slate-600">Username: admin, Email: admin@breakpilot.de</p>
</div>
<div className="bg-white p-3 rounded-lg">
<h5 className="font-medium text-slate-800 mb-1">3. Repository erstellen</h5>
<p className="text-sm text-slate-600">Name: breakpilot-pwa, Visibility: Private</p>
</div>
<div className="bg-white p-3 rounded-lg">
<h5 className="font-medium text-slate-800 mb-1">4. Actions aktivieren</h5>
<p className="text-sm text-slate-600">Repository Settings &rarr; Actions &rarr; Enable Repository Actions</p>
</div>
<div className="bg-white p-3 rounded-lg">
<h5 className="font-medium text-slate-800 mb-1">5. Runner Token erstellen & starten</h5>
<pre className="text-xs bg-slate-100 p-2 rounded mt-1 overflow-x-auto">
{`export GITEA_RUNNER_TOKEN=<token>
docker compose up -d gitea-runner`}
</pre>
</div>
<div className="bg-white p-3 rounded-lg">
<h5 className="font-medium text-slate-800 mb-1">6. Repository pushen</h5>
<pre className="text-xs bg-slate-100 p-2 rounded mt-1 overflow-x-auto">
{`git remote add gitea http://macmini:3003/admin/breakpilot-pwa.git
git push gitea main`}
</pre>
</div>
</div>
</div>
{/* Quick Links */}
<div className="bg-purple-50 p-4 rounded-lg">
<h4 className="font-medium text-purple-800 mb-3">Quick Links</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
<a
href="http://macmini:3003"
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-between bg-white p-3 rounded-lg hover:bg-purple-100 transition-colors"
>
<div>
<p className="font-medium text-purple-800">Gitea</p>
<p className="text-xs text-slate-500">Git Server & CI/CD</p>
</div>
<svg className="w-5 h-5 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
</svg>
</a>
<a
href="http://macmini:3003/admin/breakpilot-pwa/actions"
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-between bg-white p-3 rounded-lg hover:bg-purple-100 transition-colors"
>
<div>
<p className="font-medium text-purple-800">Pipeline Actions</p>
<p className="text-xs text-slate-500">Workflow Runs</p>
</div>
<svg className="w-5 h-5 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
</svg>
</a>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,325 @@
'use client'
import type { WoodpeckerStatus, WoodpeckerPipeline } from '../types'
interface WoodpeckerTabProps {
woodpeckerStatus: WoodpeckerStatus | null
triggeringWoodpecker: boolean
triggerWoodpeckerPipeline: () => Promise<void>
}
export function WoodpeckerTab({
woodpeckerStatus,
triggeringWoodpecker,
triggerWoodpeckerPipeline,
}: WoodpeckerTabProps) {
return (
<div className="space-y-6">
{/* Woodpecker Status Header */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<h3 className="text-lg font-semibold text-slate-800">Woodpecker CI Pipeline</h3>
<span className={`flex items-center gap-1.5 px-2 py-1 rounded-full text-xs font-medium ${
woodpeckerStatus?.status === 'online'
? 'bg-green-100 text-green-800'
: 'bg-red-100 text-red-800'
}`}>
<span className={`w-2 h-2 rounded-full ${
woodpeckerStatus?.status === 'online' ? 'bg-green-500' : 'bg-red-500'
}`} />
{woodpeckerStatus?.status === 'online' ? 'Online' : 'Offline'}
</span>
</div>
<div className="flex items-center gap-2">
<a
href="http://macmini:8090"
target="_blank"
rel="noopener noreferrer"
className="px-3 py-2 text-sm border border-slate-300 text-slate-700 rounded-lg hover:bg-slate-50 flex items-center gap-2"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
</svg>
Woodpecker UI
</a>
<button
onClick={triggerWoodpeckerPipeline}
disabled={triggeringWoodpecker}
className="px-4 py-2 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 disabled:opacity-50 transition-colors flex items-center gap-2"
>
{triggeringWoodpecker ? (
<>
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white" />
Startet...
</>
) : (
<>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
Pipeline starten
</>
)}
</button>
</div>
</div>
{/* Pipeline Stats */}
<WoodpeckerStats pipelines={woodpeckerStatus?.pipelines || []} />
{/* Pipeline List */}
{woodpeckerStatus?.pipelines && woodpeckerStatus.pipelines.length > 0 ? (
<div className="bg-slate-50 rounded-lg p-4">
<h4 className="font-medium text-slate-800 mb-4">Pipeline Historie</h4>
<div className="space-y-3">
{woodpeckerStatus.pipelines.map((pipeline) => (
<PipelineCard key={pipeline.id} pipeline={pipeline} />
))}
</div>
</div>
) : (
<div className="bg-slate-50 rounded-lg p-8 text-center">
<svg className="w-12 h-12 text-slate-300 mx-auto mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
</svg>
<p className="text-slate-500">Keine Pipelines gefunden</p>
<p className="text-sm text-slate-400 mt-1">Starte eine neue Pipeline oder pruefe die Woodpecker-Konfiguration</p>
</div>
)}
{/* Pipeline Configuration Info */}
<div className="bg-slate-50 rounded-lg p-4">
<h4 className="font-medium text-slate-800 mb-3">Pipeline Konfiguration</h4>
<pre className="bg-slate-800 text-slate-100 p-4 rounded-lg overflow-x-auto text-sm">
{`Woodpecker CI Pipeline (.woodpecker/main.yml)
|
+-- 1. go-lint -> Go Linting (PR only)
+-- 2. python-lint -> Python Linting (PR only)
+-- 3. secrets-scan -> GitLeaks Secrets Scan
|
+-- 4. test-go-consent -> Go Unit Tests
+-- 5. test-go-billing -> Billing Service Tests
+-- 6. test-go-school -> School Service Tests
+-- 7. test-python -> Python Backend Tests
|
+-- 8. build-images -> Docker Image Build
+-- 9. generate-sbom -> SBOM Generation (Syft)
+-- 10. vuln-scan -> Vulnerability Scan (Grype)
+-- 11. container-scan -> Container Scan (Trivy)
|
+-- 12. sign-images -> Cosign Image Signing
+-- 13. attest-sbom -> SBOM Attestation
+-- 14. provenance -> SLSA Provenance
|
+-- 15. deploy-prod -> Production Deployment`}
</pre>
</div>
{/* Workflow Anleitung */}
<WorkflowGuide />
</div>
)
}
// ============================================================================
// Sub-components
// ============================================================================
function WoodpeckerStats({ pipelines }: { pipelines: WoodpeckerPipeline[] }) {
return (
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div className="bg-blue-50 p-4 rounded-lg">
<div className="flex items-center gap-2 mb-2">
<svg className="w-4 h-4 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
</svg>
<span className="text-sm font-medium">Gesamt</span>
</div>
<p className="text-2xl font-bold text-blue-700">{pipelines.length}</p>
</div>
<div className="bg-green-50 p-4 rounded-lg">
<div className="flex items-center gap-2 mb-2">
<svg className="w-4 h-4 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
<span className="text-sm font-medium">Erfolgreich</span>
</div>
<p className="text-2xl font-bold text-green-700">
{pipelines.filter(p => p.status === 'success').length}
</p>
</div>
<div className="bg-red-50 p-4 rounded-lg">
<div className="flex items-center gap-2 mb-2">
<svg className="w-4 h-4 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
<span className="text-sm font-medium">Fehlgeschlagen</span>
</div>
<p className="text-2xl font-bold text-red-700">
{pipelines.filter(p => p.status === 'failure' || p.status === 'error').length}
</p>
</div>
<div className="bg-yellow-50 p-4 rounded-lg">
<div className="flex items-center gap-2 mb-2">
<svg className="w-4 h-4 text-yellow-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span className="text-sm font-medium">Laufend</span>
</div>
<p className="text-2xl font-bold text-yellow-700">
{pipelines.filter(p => p.status === 'running' || p.status === 'pending').length}
</p>
</div>
</div>
)
}
function PipelineCard({ pipeline }: { pipeline: WoodpeckerPipeline }) {
const borderClass =
pipeline.status === 'success'
? 'border-green-200 bg-green-50/30'
: pipeline.status === 'failure' || pipeline.status === 'error'
? 'border-red-200 bg-red-50/30'
: pipeline.status === 'running'
? 'border-blue-200 bg-blue-50/30'
: 'border-slate-200 bg-white'
return (
<div className={`border rounded-xl p-4 transition-colors ${borderClass}`}>
<div className="flex items-start justify-between gap-4">
<div className="flex-1">
<div className="flex items-center gap-2 mb-2">
<span className={`w-3 h-3 rounded-full ${
pipeline.status === 'success' ? 'bg-green-500' :
pipeline.status === 'failure' || pipeline.status === 'error' ? 'bg-red-500' :
pipeline.status === 'running' ? 'bg-blue-500 animate-pulse' : 'bg-slate-400'
}`} />
<span className="font-semibold text-slate-900">Pipeline #{pipeline.number}</span>
<span className={`px-2 py-0.5 text-xs font-medium rounded-full ${
pipeline.status === 'success' ? 'bg-green-100 text-green-800' :
pipeline.status === 'failure' || pipeline.status === 'error' ? 'bg-red-100 text-red-800' :
pipeline.status === 'running' ? 'bg-blue-100 text-blue-800' :
'bg-slate-100 text-slate-600'
}`}>
{pipeline.status}
</span>
</div>
<div className="text-sm text-slate-600 mb-2">
<span className="font-mono">{pipeline.branch}</span>
<span className="mx-2 text-slate-400"></span>
<span className="font-mono text-slate-500">{pipeline.commit}</span>
<span className="mx-2 text-slate-400"></span>
<span>{pipeline.event}</span>
</div>
{pipeline.message && (
<p className="text-sm text-slate-500 mb-2 truncate max-w-xl">{pipeline.message}</p>
)}
{/* Steps Progress */}
{pipeline.steps && pipeline.steps.length > 0 && (
<div className="mt-3">
<div className="flex gap-1 mb-2">
{pipeline.steps.map((step, i) => (
<div
key={i}
className={`h-2 flex-1 rounded-full ${
step.state === 'success' ? 'bg-green-500' :
step.state === 'failure' ? 'bg-red-500' :
step.state === 'running' ? 'bg-blue-500 animate-pulse' :
step.state === 'skipped' ? 'bg-slate-200' : 'bg-slate-300'
}`}
title={`${step.name}: ${step.state}`}
/>
))}
</div>
<div className="flex flex-wrap gap-2 text-xs">
{pipeline.steps.map((step, i) => (
<span
key={i}
className={`px-2 py-1 rounded ${
step.state === 'success' ? 'bg-green-100 text-green-700' :
step.state === 'failure' ? 'bg-red-100 text-red-700' :
step.state === 'running' ? 'bg-blue-100 text-blue-700' :
'bg-slate-100 text-slate-600'
}`}
>
{step.name}
</span>
))}
</div>
</div>
)}
{/* Errors */}
{pipeline.errors && pipeline.errors.length > 0 && (
<div className="mt-3 p-3 bg-red-50 border border-red-200 rounded-lg">
<h5 className="text-sm font-medium text-red-800 mb-1">Fehler:</h5>
<ul className="text-xs text-red-700 space-y-1">
{pipeline.errors.map((err, i) => (
<li key={i} className="font-mono">{err}</li>
))}
</ul>
</div>
)}
</div>
<div className="text-right text-sm text-slate-500">
<p>{new Date(pipeline.created * 1000).toLocaleDateString('de-DE')}</p>
<p className="text-xs">{new Date(pipeline.created * 1000).toLocaleTimeString('de-DE')}</p>
{pipeline.started && pipeline.finished && (
<p className="text-xs mt-1">
Dauer: {Math.round((pipeline.finished - pipeline.started) / 60)}m
</p>
)}
</div>
</div>
</div>
)
}
function WorkflowGuide() {
return (
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<h4 className="font-medium text-blue-800 mb-3 flex items-center gap-2">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
Workflow-Anleitung
</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
<div>
<h5 className="font-medium text-blue-700 mb-2">Automatisch (bei jedem Push/PR):</h5>
<ul className="space-y-1 text-blue-600">
<li>- <strong>Linting</strong> - Code-Qualitaet pruefen (nur PRs)</li>
<li>- <strong>Unit Tests</strong> - Go & Python Tests</li>
<li>- <strong>Test-Dashboard</strong> - Ergebnisse werden gesendet</li>
<li>- <strong>Backlog</strong> - Fehlgeschlagene Tests werden erfasst</li>
</ul>
</div>
<div>
<h5 className="font-medium text-blue-700 mb-2">Manuell (Button oder Tag):</h5>
<ul className="space-y-1 text-blue-600">
<li>- <strong>Docker Builds</strong> - Container erstellen</li>
<li>- <strong>SBOM/Scans</strong> - Sicherheitsanalyse</li>
<li>- <strong>Deployment</strong> - In Produktion deployen</li>
<li>- <strong>Pipeline starten</strong> - Diesen Button verwenden</li>
</ul>
</div>
</div>
<div className="mt-4 pt-3 border-t border-blue-200">
<h5 className="font-medium text-blue-700 mb-2">Setup: API Token konfigurieren</h5>
<p className="text-blue-600 text-sm">
Um Pipelines ueber das Dashboard zu starten, muss ein <strong>WOODPECKER_TOKEN</strong> konfiguriert werden:
</p>
<ol className="mt-2 space-y-1 text-blue-600 text-sm list-decimal list-inside">
<li>Woodpecker UI oeffnen: <a href="http://macmini:8090" target="_blank" rel="noopener noreferrer" className="underline hover:text-blue-800">http://macmini:8090</a></li>
<li>Mit Gitea-Account einloggen</li>
<li>Klick auf Profil &rarr; <strong>User Settings</strong> &rarr; <strong>Personal Access Tokens</strong></li>
<li>Neues Token erstellen und in <code className="bg-blue-100 px-1 rounded">.env</code> eintragen: <code className="bg-blue-100 px-1 rounded">WOODPECKER_TOKEN=...</code></li>
<li>Container neu starten: <code className="bg-blue-100 px-1 rounded">docker compose up -d admin-v2</code></li>
</ol>
</div>
</div>
)
}

View File

@@ -0,0 +1,42 @@
// ============================================================================
// CI/CD Dashboard - Shared Helper Components & Utilities
// ============================================================================
export function ProgressBar({ percent, color = 'blue' }: { percent: number; color?: string }) {
const getColor = () => {
if (percent > 90) return 'bg-red-500'
if (percent > 70) return 'bg-yellow-500'
if (color === 'green') return 'bg-green-500'
if (color === 'purple') return 'bg-purple-500'
return 'bg-blue-500'
}
return (
<div className="w-full bg-slate-200 rounded-full h-2">
<div
className={`h-2 rounded-full transition-all duration-300 ${getColor()}`}
style={{ width: `${Math.min(percent, 100)}%` }}
/>
</div>
)
}
export function formatUptime(seconds: number): string {
const days = Math.floor(seconds / 86400)
const hours = Math.floor((seconds % 86400) / 3600)
const minutes = Math.floor((seconds % 3600) / 60)
if (days > 0) return `${days}d ${hours}h ${minutes}m`
if (hours > 0) return `${hours}h ${minutes}m`
return `${minutes}m`
}
export function getStateColor(state: string): string {
switch (state) {
case 'running': return 'bg-green-100 text-green-800'
case 'exited':
case 'dead': return 'bg-red-100 text-red-800'
case 'paused': return 'bg-yellow-100 text-yellow-800'
case 'restarting': return 'bg-blue-100 text-blue-800'
default: return 'bg-slate-100 text-slate-600'
}
}