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>
326 lines
15 KiB
TypeScript
326 lines
15 KiB
TypeScript
'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 → <strong>User Settings</strong> → <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>
|
|
)
|
|
}
|