feat: Add DevSecOps tools, Woodpecker proxy, Vault persistent storage, pitch-deck annex slides
All checks were successful
CI / test-bqas (push) Successful in 32s
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-consent (push) Successful in 46s
CI / test-python-voice (push) Successful in 38s
All checks were successful
CI / test-bqas (push) Successful in 32s
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-consent (push) Successful in 46s
CI / test-python-voice (push) Successful in 38s
- Install Gitleaks, Trivy, Grype, Syft, Semgrep, Bandit in backend-core Dockerfile - Add Woodpecker SQLite proxy API (fallback without API token) - Mount woodpecker_data volume read-only to backend-core - Add backend proxy fallback in admin-core Woodpecker route - Add Vault file-based persistent storage (config.hcl, init-vault.sh) - Auto-init, unseal and root-token persistence for Vault - Add 6 pitch-deck annex slides (Assumptions, Architecture, GTM, Regulatory, Engineering, AI Pipeline) - Dynamic margin/amortization KPIs in BusinessModelSlide - Market sources modal with citations in MarketSlide - Redesign nginx landing page to 3-column layout (Lehrer/Compliance/Core) - Extend MkDocs nav with Services and SDK documentation sections - Add SDK Protection architecture doc Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@ import { NextRequest, NextResponse } from 'next/server'
|
||||
// Woodpecker API configuration
|
||||
const WOODPECKER_URL = process.env.WOODPECKER_URL || 'http://woodpecker-server:8000'
|
||||
const WOODPECKER_TOKEN = process.env.WOODPECKER_TOKEN || ''
|
||||
const BACKEND_URL = process.env.BACKEND_URL || 'http://backend-core:8000'
|
||||
|
||||
export interface PipelineStep {
|
||||
name: string
|
||||
@@ -25,6 +26,7 @@ export interface Pipeline {
|
||||
finished: number
|
||||
steps: PipelineStep[]
|
||||
errors?: string[]
|
||||
repo_name?: string
|
||||
}
|
||||
|
||||
export interface WoodpeckerStatusResponse {
|
||||
@@ -34,82 +36,129 @@ export interface WoodpeckerStatusResponse {
|
||||
error?: string
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const searchParams = request.nextUrl.searchParams
|
||||
const repoId = searchParams.get('repo') || '1'
|
||||
const limit = parseInt(searchParams.get('limit') || '10')
|
||||
async function fetchFromBackendProxy(repoId: string, limit: number): Promise<WoodpeckerStatusResponse> {
|
||||
// Use backend-core proxy that reads Woodpecker sqlite DB directly
|
||||
const url = `${BACKEND_URL}/api/v1/woodpecker/pipelines?repo=${repoId}&limit=${limit}`
|
||||
const response = await fetch(url, { cache: 'no-store' })
|
||||
|
||||
try {
|
||||
// Fetch pipelines from Woodpecker API
|
||||
const response = await fetch(
|
||||
`${WOODPECKER_URL}/api/repos/${repoId}/pipelines?per_page=${limit}`,
|
||||
{
|
||||
headers: {
|
||||
'Authorization': `Bearer ${WOODPECKER_TOKEN}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
cache: 'no-store',
|
||||
}
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
return NextResponse.json({
|
||||
status: 'offline',
|
||||
pipelines: [],
|
||||
lastUpdate: new Date().toISOString(),
|
||||
error: `Woodpecker API nicht erreichbar (${response.status})`
|
||||
} as WoodpeckerStatusResponse)
|
||||
if (!response.ok) {
|
||||
return {
|
||||
status: 'offline',
|
||||
pipelines: [],
|
||||
lastUpdate: new Date().toISOString(),
|
||||
error: `Backend Woodpecker Proxy Fehler (${response.status})`
|
||||
}
|
||||
}
|
||||
|
||||
const rawPipelines = await response.json()
|
||||
const data = await response.json()
|
||||
return {
|
||||
status: data.status || 'online',
|
||||
pipelines: (data.pipelines || []).map((p: any) => ({
|
||||
id: p.id,
|
||||
number: p.number,
|
||||
status: p.status,
|
||||
event: p.event,
|
||||
branch: p.branch || 'main',
|
||||
commit: p.commit || '',
|
||||
message: p.message || '',
|
||||
author: p.author || '',
|
||||
created: p.created,
|
||||
started: p.started,
|
||||
finished: p.finished,
|
||||
repo_name: p.repo_name,
|
||||
steps: (p.steps || []).map((s: any) => ({
|
||||
name: s.name,
|
||||
state: s.state,
|
||||
exit_code: s.exit_code || 0,
|
||||
error: s.error
|
||||
})),
|
||||
})),
|
||||
lastUpdate: data.lastUpdate || new Date().toISOString(),
|
||||
}
|
||||
}
|
||||
|
||||
// Transform pipelines to our format
|
||||
const pipelines: Pipeline[] = rawPipelines.map((p: any) => {
|
||||
// Extract errors from workflows/steps
|
||||
const errors: string[] = []
|
||||
const steps: PipelineStep[] = []
|
||||
async function fetchFromWoodpeckerAPI(repoId: string, limit: number): Promise<WoodpeckerStatusResponse> {
|
||||
const response = await fetch(
|
||||
`${WOODPECKER_URL}/api/repos/${repoId}/pipelines?per_page=${limit}`,
|
||||
{
|
||||
headers: {
|
||||
'Authorization': `Bearer ${WOODPECKER_TOKEN}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
cache: 'no-store',
|
||||
}
|
||||
)
|
||||
|
||||
if (p.workflows) {
|
||||
for (const workflow of p.workflows) {
|
||||
if (workflow.children) {
|
||||
for (const child of workflow.children) {
|
||||
steps.push({
|
||||
name: child.name,
|
||||
state: child.state,
|
||||
exit_code: child.exit_code,
|
||||
error: child.error
|
||||
})
|
||||
if (child.state === 'failure' && child.error) {
|
||||
errors.push(`${child.name}: ${child.error}`)
|
||||
}
|
||||
if (!response.ok) {
|
||||
return {
|
||||
status: 'offline',
|
||||
pipelines: [],
|
||||
lastUpdate: new Date().toISOString(),
|
||||
error: `Woodpecker API nicht erreichbar (${response.status})`
|
||||
}
|
||||
}
|
||||
|
||||
const rawPipelines = await response.json()
|
||||
|
||||
const pipelines: Pipeline[] = rawPipelines.map((p: any) => {
|
||||
const errors: string[] = []
|
||||
const steps: PipelineStep[] = []
|
||||
|
||||
if (p.workflows) {
|
||||
for (const workflow of p.workflows) {
|
||||
if (workflow.children) {
|
||||
for (const child of workflow.children) {
|
||||
steps.push({
|
||||
name: child.name,
|
||||
state: child.state,
|
||||
exit_code: child.exit_code,
|
||||
error: child.error
|
||||
})
|
||||
if (child.state === 'failure' && child.error) {
|
||||
errors.push(`${child.name}: ${child.error}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: p.id,
|
||||
number: p.number,
|
||||
status: p.status,
|
||||
event: p.event,
|
||||
branch: p.branch,
|
||||
commit: p.commit?.substring(0, 7) || '',
|
||||
message: p.message || '',
|
||||
author: p.author,
|
||||
created: p.created,
|
||||
started: p.started,
|
||||
finished: p.finished,
|
||||
steps,
|
||||
errors: errors.length > 0 ? errors : undefined
|
||||
}
|
||||
})
|
||||
return {
|
||||
id: p.id,
|
||||
number: p.number,
|
||||
status: p.status,
|
||||
event: p.event,
|
||||
branch: p.branch,
|
||||
commit: p.commit?.substring(0, 7) || '',
|
||||
message: p.message || '',
|
||||
author: p.author,
|
||||
created: p.created,
|
||||
started: p.started,
|
||||
finished: p.finished,
|
||||
steps,
|
||||
errors: errors.length > 0 ? errors : undefined
|
||||
}
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
status: 'online',
|
||||
pipelines,
|
||||
lastUpdate: new Date().toISOString()
|
||||
} as WoodpeckerStatusResponse)
|
||||
return {
|
||||
status: 'online',
|
||||
pipelines,
|
||||
lastUpdate: new Date().toISOString()
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const searchParams = request.nextUrl.searchParams
|
||||
const repoId = searchParams.get('repo') || '0'
|
||||
const limit = parseInt(searchParams.get('limit') || '10')
|
||||
|
||||
try {
|
||||
// If WOODPECKER_TOKEN is set, use the Woodpecker API directly
|
||||
// Otherwise, use the backend proxy that reads the sqlite DB
|
||||
if (WOODPECKER_TOKEN) {
|
||||
return NextResponse.json(await fetchFromWoodpeckerAPI(repoId, limit))
|
||||
} else {
|
||||
return NextResponse.json(await fetchFromBackendProxy(repoId, limit))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Woodpecker API error:', error)
|
||||
return NextResponse.json({
|
||||
@@ -127,6 +176,13 @@ export async function POST(request: NextRequest) {
|
||||
const body = await request.json()
|
||||
const { repoId = '1', branch = 'main' } = body
|
||||
|
||||
if (!WOODPECKER_TOKEN) {
|
||||
return NextResponse.json(
|
||||
{ error: 'WOODPECKER_TOKEN nicht konfiguriert - Pipeline-Start nicht moeglich' },
|
||||
{ status: 503 }
|
||||
)
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
`${WOODPECKER_URL}/api/repos/${repoId}/pipelines`,
|
||||
{
|
||||
@@ -178,6 +234,13 @@ export async function PUT(request: NextRequest) {
|
||||
)
|
||||
}
|
||||
|
||||
if (!WOODPECKER_TOKEN) {
|
||||
return NextResponse.json(
|
||||
{ error: 'WOODPECKER_TOKEN nicht konfiguriert' },
|
||||
{ status: 503 }
|
||||
)
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
`${WOODPECKER_URL}/api/repos/${repoId}/pipelines/${pipelineNumber}/logs/${stepId}`,
|
||||
{
|
||||
|
||||
@@ -18,7 +18,8 @@ COPY requirements.txt .
|
||||
RUN python -m venv /opt/venv
|
||||
ENV PATH="/opt/venv/bin:$PATH"
|
||||
RUN pip install --no-cache-dir --upgrade pip && \
|
||||
pip install --no-cache-dir -r requirements.txt
|
||||
pip install --no-cache-dir -r requirements.txt && \
|
||||
pip install --no-cache-dir semgrep bandit
|
||||
|
||||
# ---------- Runtime stage ----------
|
||||
FROM python:3.12-slim-bookworm
|
||||
@@ -38,8 +39,26 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
libgl1 \
|
||||
libglib2.0-0 \
|
||||
curl \
|
||||
git \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install DevSecOps tools (gitleaks, trivy, grype, syft)
|
||||
ARG TARGETARCH=arm64
|
||||
RUN set -eux; \
|
||||
# Gitleaks
|
||||
GITLEAKS_VERSION=8.21.2; \
|
||||
if [ "$TARGETARCH" = "arm64" ]; then GITLEAKS_ARCH=arm64; else GITLEAKS_ARCH=x64; fi; \
|
||||
curl -sSfL "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_${GITLEAKS_ARCH}.tar.gz" \
|
||||
| tar xz -C /usr/local/bin gitleaks; \
|
||||
# Trivy
|
||||
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin; \
|
||||
# Grype
|
||||
curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin; \
|
||||
# Syft
|
||||
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin; \
|
||||
# Verify
|
||||
gitleaks version && trivy --version && grype version && syft version
|
||||
|
||||
# Copy virtualenv from builder
|
||||
COPY --from=builder /opt/venv /opt/venv
|
||||
ENV PATH="/opt/venv/bin:$PATH"
|
||||
|
||||
@@ -25,6 +25,7 @@ from email_template_api import (
|
||||
)
|
||||
from system_api import router as system_router
|
||||
from security_api import router as security_router
|
||||
from woodpecker_proxy_api import router as woodpecker_router
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Middleware imports
|
||||
@@ -105,6 +106,7 @@ app.include_router(system_router) # already has paths defined in r
|
||||
|
||||
# Security / DevSecOps dashboard
|
||||
app.include_router(security_router, prefix="/api")
|
||||
app.include_router(woodpecker_router, prefix="/api")
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Startup / Shutdown events
|
||||
|
||||
@@ -34,6 +34,9 @@ BACKEND_DIR = Path(__file__).parent
|
||||
REPORTS_DIR = BACKEND_DIR / "security-reports"
|
||||
SCRIPTS_DIR = BACKEND_DIR / "scripts"
|
||||
|
||||
# Projekt-Root fuer Security-Scans
|
||||
PROJECT_ROOT = BACKEND_DIR
|
||||
|
||||
# Sicherstellen, dass das Reports-Verzeichnis existiert
|
||||
try:
|
||||
REPORTS_DIR.mkdir(exist_ok=True)
|
||||
|
||||
133
backend-core/woodpecker_proxy_api.py
Normal file
133
backend-core/woodpecker_proxy_api.py
Normal file
@@ -0,0 +1,133 @@
|
||||
"""
|
||||
Woodpecker CI Proxy API
|
||||
|
||||
Liest Pipeline-Daten direkt aus der Woodpecker SQLite-Datenbank.
|
||||
Wird als Fallback verwendet, wenn kein WOODPECKER_TOKEN konfiguriert ist.
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
from fastapi import APIRouter, Query
|
||||
|
||||
router = APIRouter(prefix="/v1/woodpecker", tags=["Woodpecker CI"])
|
||||
|
||||
WOODPECKER_DB = Path("/woodpecker-data/woodpecker.sqlite")
|
||||
|
||||
|
||||
def get_db():
|
||||
if not WOODPECKER_DB.exists():
|
||||
return None
|
||||
conn = sqlite3.connect(f"file:{WOODPECKER_DB}?mode=ro", uri=True)
|
||||
conn.row_factory = sqlite3.Row
|
||||
return conn
|
||||
|
||||
|
||||
@router.get("/status")
|
||||
async def get_status():
|
||||
conn = get_db()
|
||||
if not conn:
|
||||
return {"status": "offline", "error": "Woodpecker DB nicht gefunden"}
|
||||
|
||||
try:
|
||||
repos = [dict(r) for r in conn.execute(
|
||||
"SELECT id, name, full_name, active FROM repos ORDER BY id"
|
||||
).fetchall()]
|
||||
|
||||
total_pipelines = conn.execute("SELECT COUNT(*) FROM pipelines").fetchone()[0]
|
||||
success = conn.execute("SELECT COUNT(*) FROM pipelines WHERE status='success'").fetchone()[0]
|
||||
failure = conn.execute("SELECT COUNT(*) FROM pipelines WHERE status='failure'").fetchone()[0]
|
||||
|
||||
latest = conn.execute("SELECT MAX(created) FROM pipelines").fetchone()[0]
|
||||
|
||||
return {
|
||||
"status": "online",
|
||||
"repos": repos,
|
||||
"stats": {
|
||||
"total_pipelines": total_pipelines,
|
||||
"success": success,
|
||||
"failure": failure,
|
||||
"success_rate": round(success / total_pipelines * 100, 1) if total_pipelines > 0 else 0,
|
||||
},
|
||||
"last_activity": datetime.fromtimestamp(latest).isoformat() if latest else None,
|
||||
}
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
@router.get("/pipelines")
|
||||
async def get_pipelines(
|
||||
repo: int = Query(default=0, description="Repo ID (0 = alle)"),
|
||||
limit: int = Query(default=10, ge=1, le=100),
|
||||
):
|
||||
conn = get_db()
|
||||
if not conn:
|
||||
return {"status": "offline", "pipelines": [], "lastUpdate": datetime.now().isoformat()}
|
||||
|
||||
try:
|
||||
base_sql = """SELECT p.id, p.repo_id, p.number, p.status, p.event, p.branch,
|
||||
p."commit", p.message, p.author, p.created, p.started, p.finished,
|
||||
r.name as repo_name
|
||||
FROM pipelines p
|
||||
JOIN repos r ON r.id = p.repo_id"""
|
||||
|
||||
if repo > 0:
|
||||
rows = conn.execute(
|
||||
base_sql + " WHERE p.repo_id = ? ORDER BY p.id DESC LIMIT ?",
|
||||
(repo, limit)
|
||||
).fetchall()
|
||||
else:
|
||||
rows = conn.execute(
|
||||
base_sql + " ORDER BY p.id DESC LIMIT ?",
|
||||
(limit,)
|
||||
).fetchall()
|
||||
|
||||
pipelines = []
|
||||
for r in rows:
|
||||
p = dict(r)
|
||||
|
||||
# Get steps directly (steps.pipeline_id links to pipelines.id)
|
||||
steps = [dict(s) for s in conn.execute(
|
||||
"""SELECT s.name, s.state, s.exit_code, s.error
|
||||
FROM steps s
|
||||
WHERE s.pipeline_id = ?
|
||||
ORDER BY s.pid""",
|
||||
(p["id"],)
|
||||
).fetchall()]
|
||||
|
||||
p["steps"] = steps
|
||||
p["commit"] = (p.get("commit") or "")[:7]
|
||||
msg = p.get("message") or ""
|
||||
p["message"] = msg.split("\n")[0][:100]
|
||||
pipelines.append(p)
|
||||
|
||||
return {
|
||||
"status": "online",
|
||||
"pipelines": pipelines,
|
||||
"lastUpdate": datetime.now().isoformat(),
|
||||
}
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
@router.get("/repos")
|
||||
async def get_repos():
|
||||
conn = get_db()
|
||||
if not conn:
|
||||
return []
|
||||
|
||||
try:
|
||||
repos = []
|
||||
for r in conn.execute("SELECT id, name, full_name, active FROM repos ORDER BY id").fetchall():
|
||||
repo = dict(r)
|
||||
latest = conn.execute(
|
||||
'SELECT status, created FROM pipelines WHERE repo_id = ? ORDER BY id DESC LIMIT 1',
|
||||
(repo["id"],)
|
||||
).fetchone()
|
||||
if latest:
|
||||
repo["last_status"] = latest["status"]
|
||||
repo["last_activity"] = datetime.fromtimestamp(latest["created"]).isoformat()
|
||||
repos.append(repo)
|
||||
return repos
|
||||
finally:
|
||||
conn.close()
|
||||
@@ -268,6 +268,7 @@ services:
|
||||
expose:
|
||||
- "8000"
|
||||
volumes:
|
||||
- woodpecker_data:/woodpecker-data:ro
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
environment:
|
||||
DATABASE_URL: postgresql+asyncpg://${POSTGRES_USER:-breakpilot}:${POSTGRES_PASSWORD:-breakpilot123}@postgres:5432/${POSTGRES_DB:-breakpilot_db}?options=-csearch_path%3Dcore,public
|
||||
|
||||
317
docs-src/architecture/sdk-protection.md
Normal file
317
docs-src/architecture/sdk-protection.md
Normal file
@@ -0,0 +1,317 @@
|
||||
# SDK Protection Middleware
|
||||
|
||||
## 1. Worum geht es?
|
||||
|
||||
Die SDK Protection Middleware schuetzt die Compliance-SDK-Endpunkte vor einer bestimmten Art von Angriff: der **systematischen Enumeration**. Was bedeutet das?
|
||||
|
||||
> *Ein Wettbewerber registriert sich als zahlender Kunde und laesst ein Skript langsam und verteilt alle TOM-Controls, alle Pruefaspekte und alle Assessment-Kriterien abfragen. Aus den Ergebnissen rekonstruiert er die gesamte Compliance-Framework-Logik.*
|
||||
|
||||
Der klassische Rate Limiter (100 Requests/Minute) hilft hier nicht, weil ein cleverer Angreifer langsam vorgeht -- vielleicht nur 20 Anfragen pro Minute, dafuer systematisch und ueber Stunden. Die SDK Protection erkennt solche Muster und reagiert darauf.
|
||||
|
||||
!!! info "Kern-Designprinzip"
|
||||
**Normale Nutzer merken nichts.** Ein Lehrer, der im TOM-Modul arbeitet, greift typischerweise auf 3-5 Kategorien zu und wiederholt Anfragen an gleiche Endpunkte. Ein Angreifer durchlaeuft dagegen 40+ Kategorien in alphabetischer Reihenfolge. Genau diesen Unterschied erkennt die Middleware.
|
||||
|
||||
---
|
||||
|
||||
## 2. Wie funktioniert der Schutz?
|
||||
|
||||
Die Middleware nutzt ein **Anomaly-Score-System**. Jeder Benutzer hat einen Score, der bei 0 beginnt. Verschiedene verdaechtige Verhaltensweisen erhoehen den Score. Ueber die Zeit sinkt er wieder ab. Je hoeher der Score, desto staerker wird der Benutzer gebremst.
|
||||
|
||||
Man kann es sich wie eine Ampel vorstellen:
|
||||
|
||||
| Score | Ampel | Wirkung | Beispiel |
|
||||
|-------|-------|---------|----------|
|
||||
| 0-29 | Gruen | Keine Einschraenkung | Normaler Nutzer |
|
||||
| 30-59 | Gelb | 1-3 Sekunden Verzoegerung | Leicht auffaelliges Muster |
|
||||
| 60-84 | Orange | 5-10 Sekunden Verzoegerung, reduzierte Details | Deutlich verdaechtiges Verhalten |
|
||||
| 85+ | Rot | Zugriff blockiert (HTTP 429) | Sehr wahrscheinlich automatisierter Angriff |
|
||||
|
||||
### Score-Zerfall
|
||||
|
||||
Der Score sinkt automatisch: Alle 5 Minuten wird er mit dem Faktor 0,95 multipliziert. Ein Score von 60 faellt also innerhalb einer Stunde auf etwa 30 -- wenn kein neues verdaechtiges Verhalten hinzukommt.
|
||||
|
||||
---
|
||||
|
||||
## 3. Was wird erkannt?
|
||||
|
||||
Die Middleware erkennt fuenf verschiedene Anomalie-Muster:
|
||||
|
||||
### 3.1 Hohe Kategorie-Diversitaet
|
||||
|
||||
**Was:** Ein Benutzer greift innerhalb einer Stunde auf mehr als 40 verschiedene SDK-Kategorien zu.
|
||||
|
||||
**Warum verdaechtig:** Ein normaler Nutzer arbeitet in der Regel mit 3-10 Kategorien. Wer systematisch alle durchlaeuft, sammelt vermutlich Daten.
|
||||
|
||||
**Score-Erhoehung:** +15
|
||||
|
||||
```
|
||||
Normal: tom/access-control → tom/access-control → tom/encryption → tom/encryption
|
||||
(3 verschiedene Kategorien in einer Stunde)
|
||||
|
||||
Verdaechtig: tom/access-control → tom/encryption → tom/pseudonymization → tom/integrity
|
||||
→ tom/availability → tom/resilience → dsfa/threshold → dsfa/necessity → ...
|
||||
(40+ verschiedene Kategorien in einer Stunde)
|
||||
```
|
||||
|
||||
### 3.2 Burst-Erkennung
|
||||
|
||||
**Was:** Ein Benutzer sendet mehr als 15 Anfragen an die gleiche Kategorie innerhalb von 2 Minuten.
|
||||
|
||||
**Warum verdaechtig:** Selbst ein eifriger Nutzer klickt nicht 15-mal pro Minute auf denselben Endpunkt. Das deutet auf automatisiertes Scraping hin.
|
||||
|
||||
**Score-Erhoehung:** +20
|
||||
|
||||
### 3.3 Sequentielle Enumeration
|
||||
|
||||
**Was:** Die letzten 10 aufgerufenen Kategorien sind zu mindestens 70% in alphabetischer oder numerischer Reihenfolge.
|
||||
|
||||
**Warum verdaechtig:** Menschen springen zwischen Kategorien -- sie arbeiten thematisch, nicht alphabetisch. Ein Skript dagegen iteriert oft ueber eine sortierte Liste.
|
||||
|
||||
**Score-Erhoehung:** +25
|
||||
|
||||
```
|
||||
Verdaechtig: assessment_general → compliance_general → controls_general
|
||||
→ dsfa_measures → dsfa_necessity → dsfa_residual → dsfa_risks
|
||||
→ dsfa_threshold → eh_general → namespace_general
|
||||
(alphabetisch sortiert = Skript-Verhalten)
|
||||
```
|
||||
|
||||
### 3.4 Ungewoehnliche Uhrzeiten
|
||||
|
||||
**Was:** Anfragen zwischen 0:00 und 5:00 Uhr UTC.
|
||||
|
||||
**Warum verdaechtig:** Lehrer arbeiten tagsüber. Wer um 3 Uhr morgens SDK-Endpunkte abfragt, ist wahrscheinlich ein automatisierter Prozess.
|
||||
|
||||
**Score-Erhoehung:** +10
|
||||
|
||||
### 3.5 Multi-Tenant-Zugriff
|
||||
|
||||
**Was:** Ein Benutzer greift innerhalb einer Stunde auf mehr als 3 verschiedene Mandanten (Tenants) zu.
|
||||
|
||||
**Warum verdaechtig:** Ein normaler Nutzer gehoert zu einem Mandanten. Wer mehrere durchprobiert, koennte versuchen, mandantenuebergreifend Daten zu sammeln.
|
||||
|
||||
**Score-Erhoehung:** +15
|
||||
|
||||
---
|
||||
|
||||
## 4. Quota-System (Mengenbegrenzung)
|
||||
|
||||
Zusaetzlich zum Anomaly-Score gibt es klassische Mengenbegrenzungen in vier Zeitfenstern:
|
||||
|
||||
| Tier | pro Minute | pro Stunde | pro Tag | pro Monat |
|
||||
|------|-----------|-----------|---------|-----------|
|
||||
| **Free** | 30 | 500 | 3.000 | 50.000 |
|
||||
| **Standard** | 60 | 1.500 | 10.000 | 200.000 |
|
||||
| **Enterprise** | 120 | 5.000 | 50.000 | 1.000.000 |
|
||||
|
||||
Wenn ein Limit in irgendeinem Zeitfenster ueberschritten wird, erhaelt der Nutzer sofort HTTP 429 -- unabhaengig vom Anomaly-Score.
|
||||
|
||||
---
|
||||
|
||||
## 5. Architektur
|
||||
|
||||
### Datenfluss eines SDK-Requests
|
||||
|
||||
```
|
||||
Request kommt an
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Ist der Pfad geschuetzt? │
|
||||
│ (/api/sdk/*, /api/v1/tom/*, /api/v1/dsfa/*, ...) │
|
||||
│ Nein → direkt weiterleiten │
|
||||
└──────────────┬──────────────────────────────────────────────┘
|
||||
│ Ja
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ User + Tier + Kategorie extrahieren │
|
||||
│ (aus Session, API-Key oder X-SDK-Tier Header) │
|
||||
└──────────────┬──────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Multi-Window Quota pruefen │
|
||||
│ (Minute / Stunde / Tag / Monat) │
|
||||
│ Ueberschritten → HTTP 429 zurueck │
|
||||
└──────────────┬──────────────────────────────────────────────┘
|
||||
│ OK
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Anomaly-Score laden (aus Valkey) │
|
||||
│ Zeitbasierten Zerfall anwenden (×0,95 alle 5 min) │
|
||||
└──────────────┬──────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Anomalie-Detektoren ausfuehren: │
|
||||
│ ├── Diversity-Tracking (+15 wenn >40 Kategorien/h) │
|
||||
│ ├── Burst-Detection (+20 wenn >15 gleiche/2min) │
|
||||
│ ├── Sequential-Enumeration (+25 wenn sortiert) │
|
||||
│ ├── Unusual-Hours (+10 wenn 0-5 Uhr UTC) │
|
||||
│ └── Multi-Tenant (+15 wenn >3 Tenants/h) │
|
||||
└──────────────┬──────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Throttle-Level bestimmen │
|
||||
│ Level 3 (Score ≥85) → HTTP 429 │
|
||||
│ Level 2 (Score ≥60) → 5-10s Delay + reduzierte Details │
|
||||
│ Level 1 (Score ≥30) → 1-3s Delay │
|
||||
│ Level 0 → keine Einschraenkung │
|
||||
└──────────────┬──────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Request weiterleiten │
|
||||
│ Response-Headers setzen: │
|
||||
│ ├── X-SDK-Quota-Remaining-Minute/Hour │
|
||||
│ ├── X-SDK-Throttle-Level │
|
||||
│ ├── X-SDK-Detail-Reduced (bei Level ≥2) │
|
||||
│ └── X-BP-Trace (HMAC-Watermark) │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Valkey-Datenstrukturen
|
||||
|
||||
Die Middleware speichert alle Tracking-Daten in Valkey (Redis-Fork). Wenn Valkey nicht erreichbar ist, wird automatisch auf eine In-Memory-Implementierung zurueckgefallen.
|
||||
|
||||
| Zweck | Valkey-Typ | Key-Muster | TTL |
|
||||
|-------|-----------|------------|-----|
|
||||
| Quota pro Zeitfenster | Sorted Set | `sdk_protect:quota:{user}:{window}` | Fenster + 10s |
|
||||
| Kategorie-Diversitaet | Set | `sdk_protect:diversity:{user}:{stunde}` | 3660s |
|
||||
| Burst-Tracking | Sorted Set | `sdk_protect:burst:{user}:{kategorie}` | 130s |
|
||||
| Sequenz-Tracking | List | `sdk_protect:seq:{user}` | 310s |
|
||||
| Anomaly-Score | Hash | `sdk_protect:score:{user}` | 86400s |
|
||||
| Tenant-Tracking | Set | `sdk_protect:tenants:{user}:{stunde}` | 3660s |
|
||||
|
||||
### Watermarking
|
||||
|
||||
Jede Antwort enthaelt einen `X-BP-Trace` Header mit einem HMAC-basierten Fingerabdruck. Damit kann nachtraeglich nachgewiesen werden, welcher Benutzer wann welche Daten abgerufen hat -- ohne dass der Benutzer den Trace veraendern kann.
|
||||
|
||||
---
|
||||
|
||||
## 6. Geschuetzte Endpunkte
|
||||
|
||||
Die Middleware schuetzt alle Pfade, die SDK- und Compliance-relevante Daten liefern:
|
||||
|
||||
| Pfad-Prefix | Bereich |
|
||||
|-------------|---------|
|
||||
| `/api/sdk/*` | SDK-Hauptendpunkte |
|
||||
| `/api/compliance/*` | Compliance-Bewertungen |
|
||||
| `/api/v1/tom/*` | Technisch-organisatorische Massnahmen |
|
||||
| `/api/v1/dsfa/*` | Datenschutz-Folgenabschaetzung |
|
||||
| `/api/v1/vvt/*` | Verarbeitungsverzeichnis |
|
||||
| `/api/v1/controls/*` | Controls und Massnahmen |
|
||||
| `/api/v1/assessment/*` | Assessment-Bewertungen |
|
||||
| `/api/v1/eh/*` | Erwartungshorizonte |
|
||||
| `/api/v1/namespace/*` | Namespace-Verwaltung |
|
||||
|
||||
Nicht geschuetzt sind `/health`, `/metrics` und `/api/health`.
|
||||
|
||||
---
|
||||
|
||||
## 7. Admin-Verwaltung
|
||||
|
||||
Ueber das Admin-Dashboard koennen Anomaly-Scores eingesehen und verwaltet werden:
|
||||
|
||||
| Endpoint | Methode | Beschreibung |
|
||||
|----------|---------|--------------|
|
||||
| `/api/admin/middleware/sdk-protection/scores` | GET | Aktuelle Anomaly-Scores aller Benutzer |
|
||||
| `/api/admin/middleware/sdk-protection/stats` | GET | Statistik: Benutzer pro Throttle-Level |
|
||||
| `/api/admin/middleware/sdk-protection/reset-score/{user_id}` | POST | Score eines Benutzers zuruecksetzen |
|
||||
| `/api/admin/middleware/sdk-protection/tiers` | GET | Tier-Konfigurationen anzeigen |
|
||||
| `/api/admin/middleware/sdk-protection/tiers/{name}` | PUT | Tier-Limits aendern |
|
||||
|
||||
---
|
||||
|
||||
## 8. Dateien und Quellcode
|
||||
|
||||
| Datei | Beschreibung |
|
||||
|-------|--------------|
|
||||
| `backend/middleware/sdk_protection.py` | Kern-Middleware (~460 Zeilen) |
|
||||
| `backend/middleware/__init__.py` | Export der Middleware-Klassen |
|
||||
| `backend/main.py` | Registrierung im FastAPI-Stack |
|
||||
| `backend/middleware_admin_api.py` | Admin-API-Endpoints |
|
||||
| `backend/migrations/add_sdk_protection_tables.sql` | Datenbank-Migration |
|
||||
| `backend/tests/test_middleware.py` | 14 Tests fuer alle Erkennungsmechanismen |
|
||||
|
||||
---
|
||||
|
||||
## 9. Datenbank-Tabellen
|
||||
|
||||
### sdk_anomaly_scores
|
||||
|
||||
Speichert Snapshots der Anomaly-Scores fuer Audit und Analyse.
|
||||
|
||||
| Spalte | Typ | Beschreibung |
|
||||
|--------|-----|--------------|
|
||||
| `id` | UUID | Primaerschluessel |
|
||||
| `user_id` | VARCHAR(255) | Benutzer-Identifikation |
|
||||
| `score` | DECIMAL(5,2) | Aktueller Anomaly-Score |
|
||||
| `throttle_level` | SMALLINT | Aktueller Throttle-Level (0-3) |
|
||||
| `triggered_rules` | JSONB | Welche Regeln ausgeloest wurden |
|
||||
| `endpoint_diversity_count` | INT | Anzahl verschiedener Kategorien |
|
||||
| `request_count_1h` | INT | Anfragen in der letzten Stunde |
|
||||
| `snapshot_at` | TIMESTAMPTZ | Zeitpunkt des Snapshots |
|
||||
|
||||
### sdk_protection_tiers
|
||||
|
||||
Konfigurierbare Quota-Tiers, editierbar ueber die Admin-API.
|
||||
|
||||
| Spalte | Typ | Beschreibung |
|
||||
|--------|-----|--------------|
|
||||
| `tier_name` | VARCHAR(50) | Name des Tiers (free, standard, enterprise) |
|
||||
| `quota_per_minute` | INT | Maximale Anfragen pro Minute |
|
||||
| `quota_per_hour` | INT | Maximale Anfragen pro Stunde |
|
||||
| `quota_per_day` | INT | Maximale Anfragen pro Tag |
|
||||
| `quota_per_month` | INT | Maximale Anfragen pro Monat |
|
||||
| `diversity_threshold` | INT | Max verschiedene Kategorien pro Stunde |
|
||||
| `burst_threshold` | INT | Max gleiche Kategorie in 2 Minuten |
|
||||
|
||||
---
|
||||
|
||||
## 10. Konfiguration
|
||||
|
||||
Die Middleware wird in `main.py` registriert:
|
||||
|
||||
```python
|
||||
from middleware import SDKProtectionMiddleware
|
||||
|
||||
app.add_middleware(SDKProtectionMiddleware)
|
||||
```
|
||||
|
||||
Alle Parameter koennen ueber die `SDKProtectionConfig` Dataclass angepasst werden. Die wichtigsten Umgebungsvariablen:
|
||||
|
||||
| Variable | Default | Beschreibung |
|
||||
|----------|---------|--------------|
|
||||
| `VALKEY_URL` | `redis://localhost:6379` | Verbindung zur Valkey-Instanz |
|
||||
| `SDK_WATERMARK_SECRET` | (generiert) | HMAC-Secret fuer Watermarks |
|
||||
|
||||
---
|
||||
|
||||
## 11. Tests
|
||||
|
||||
Die Middleware wird durch 14 automatisierte Tests abgedeckt:
|
||||
|
||||
```bash
|
||||
# Alle SDK Protection Tests ausfuehren
|
||||
docker compose run --rm --no-deps backend \
|
||||
python -m pytest tests/test_middleware.py -v -k sdk
|
||||
```
|
||||
|
||||
| Test | Prueft |
|
||||
|------|--------|
|
||||
| `test_allows_normal_request` | Normaler Request wird durchgelassen |
|
||||
| `test_blocks_after_quota_exceeded` | 429 bei Quota-Ueberschreitung |
|
||||
| `test_diversity_tracking_increments_score` | Viele Kategorien erhoehen den Score |
|
||||
| `test_burst_detection` | Schnelle gleiche Anfragen erhoehen den Score |
|
||||
| `test_sequential_enumeration_detection` | Alphabetische Muster werden erkannt |
|
||||
| `test_progressive_throttling_level_1` | Delay bei Score >= 30 |
|
||||
| `test_progressive_throttling_level_3_blocks` | Block bei Score >= 85 |
|
||||
| `test_score_decay_over_time` | Score sinkt ueber die Zeit |
|
||||
| `test_skips_non_protected_paths` | Nicht-SDK-Pfade bleiben frei |
|
||||
| `test_watermark_header_present` | X-BP-Trace Header vorhanden |
|
||||
| `test_fallback_to_inmemory` | Funktioniert ohne Valkey |
|
||||
| `test_no_user_passes_through` | Anonyme Requests passieren |
|
||||
| `test_category_extraction` | Korrekte Kategorie-Zuordnung |
|
||||
| `test_quota_headers_present` | Response-Headers vorhanden |
|
||||
47
mkdocs.yml
47
mkdocs.yml
@@ -1,5 +1,5 @@
|
||||
site_name: BreakPilot Core - Dokumentation
|
||||
site_url: https://macmini:8009
|
||||
site_name: Breakpilot Dokumentation
|
||||
site_url: http://macmini:8009
|
||||
docs_dir: docs-src
|
||||
site_dir: docs-site
|
||||
|
||||
@@ -9,16 +9,14 @@ theme:
|
||||
palette:
|
||||
- scheme: default
|
||||
primary: teal
|
||||
accent: teal
|
||||
toggle:
|
||||
icon: material/brightness-7
|
||||
name: Dark Mode
|
||||
name: Dark Mode aktivieren
|
||||
- scheme: slate
|
||||
primary: teal
|
||||
accent: teal
|
||||
toggle:
|
||||
icon: material/brightness-4
|
||||
name: Light Mode
|
||||
name: Light Mode aktivieren
|
||||
features:
|
||||
- search.highlight
|
||||
- search.suggest
|
||||
@@ -27,6 +25,7 @@ theme:
|
||||
- navigation.expand
|
||||
- navigation.top
|
||||
- content.code.copy
|
||||
- content.tabs.link
|
||||
- toc.follow
|
||||
|
||||
plugins:
|
||||
@@ -53,18 +52,48 @@ markdown_extensions:
|
||||
- toc:
|
||||
permalink: true
|
||||
|
||||
extra:
|
||||
social:
|
||||
- icon: fontawesome/brands/github
|
||||
link: http://macmini:3003/breakpilot/breakpilot-pwa
|
||||
|
||||
nav:
|
||||
- Start: index.md
|
||||
- Erste Schritte:
|
||||
- Einrichtung: getting-started/environment-setup.md
|
||||
- Umgebung einrichten: getting-started/environment-setup.md
|
||||
- Mac Mini Setup: getting-started/mac-mini-setup.md
|
||||
- Architektur:
|
||||
- System-Architektur: architecture/system-architecture.md
|
||||
- Systemuebersicht: architecture/system-architecture.md
|
||||
- Auth-System: architecture/auth-system.md
|
||||
- Mail & RBAC: architecture/mail-rbac-architecture.md
|
||||
- Mail-RBAC: architecture/mail-rbac-architecture.md
|
||||
- Multi-Agent: architecture/multi-agent.md
|
||||
- Secrets Management: architecture/secrets-management.md
|
||||
- DevSecOps: architecture/devsecops.md
|
||||
- SDK Protection: architecture/sdk-protection.md
|
||||
- Environments: architecture/environments.md
|
||||
- Zeugnis-System: architecture/zeugnis-system.md
|
||||
- Services:
|
||||
- KI-Daten-Pipeline:
|
||||
- Uebersicht: services/ki-daten-pipeline/index.md
|
||||
- Architektur: services/ki-daten-pipeline/architecture.md
|
||||
- Klausur-Service:
|
||||
- Uebersicht: services/klausur-service/index.md
|
||||
- BYOEH Systemerklaerung: services/klausur-service/byoeh-system-erklaerung.md
|
||||
- BYOEH Architektur: services/klausur-service/BYOEH-Architecture.md
|
||||
- BYOEH Developer Guide: services/klausur-service/BYOEH-Developer-Guide.md
|
||||
- NiBiS Pipeline: services/klausur-service/NiBiS-Ingestion-Pipeline.md
|
||||
- OCR Labeling: services/klausur-service/OCR-Labeling-Spec.md
|
||||
- OCR Compare: services/klausur-service/OCR-Compare.md
|
||||
- RAG Admin: services/klausur-service/RAG-Admin-Spec.md
|
||||
- Worksheet Editor: services/klausur-service/Worksheet-Editor-Architecture.md
|
||||
- Voice-Service: services/voice-service/index.md
|
||||
- Agent-Core: services/agent-core/index.md
|
||||
- AI-Compliance-SDK:
|
||||
- Uebersicht: services/ai-compliance-sdk/index.md
|
||||
- Architektur: services/ai-compliance-sdk/ARCHITECTURE.md
|
||||
- Developer Guide: services/ai-compliance-sdk/DEVELOPER.md
|
||||
- Auditor Dokumentation: services/ai-compliance-sdk/AUDITOR_DOCUMENTATION.md
|
||||
- SBOM: services/ai-compliance-sdk/SBOM.md
|
||||
- API:
|
||||
- Backend API: api/backend-api.md
|
||||
- Entwicklung:
|
||||
|
||||
@@ -34,6 +34,13 @@
|
||||
--docs-core: #14b8a6;
|
||||
--docs-lehrer: #0ea5e9;
|
||||
--docs-compliance: #8b5cf6;
|
||||
|
||||
--pitch-500: #f59e0b;
|
||||
--pitch-400: #fbbf24;
|
||||
--pitch-600: #d97706;
|
||||
--pitch-bg: rgba(245, 158, 11, 0.08);
|
||||
--pitch-bg-hover: rgba(245, 158, 11, 0.14);
|
||||
--pitch-border: rgba(245, 158, 11, 0.25);
|
||||
}
|
||||
|
||||
/* ── Light Theme ── */
|
||||
@@ -95,6 +102,9 @@
|
||||
--core-bg: rgba(100, 116, 139, 0.1);
|
||||
--core-bg-hover: rgba(100, 116, 139, 0.18);
|
||||
--core-border: rgba(148, 163, 184, 0.15);
|
||||
--pitch-bg: rgba(245, 158, 11, 0.1);
|
||||
--pitch-bg-hover: rgba(245, 158, 11, 0.18);
|
||||
--pitch-border: rgba(251, 191, 36, 0.2);
|
||||
}
|
||||
|
||||
body {
|
||||
@@ -176,6 +186,57 @@
|
||||
margin: 0 auto 2.5rem;
|
||||
}
|
||||
|
||||
.columns-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
gap: 1.5rem;
|
||||
max-width: 1100px;
|
||||
margin: 0 auto 2.5rem;
|
||||
}
|
||||
|
||||
.column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.column-header {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
padding: 0.4rem 0.75rem;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.column-header-lehrer {
|
||||
color: var(--lehrer-500);
|
||||
background: var(--lehrer-bg);
|
||||
}
|
||||
|
||||
.column-header-compliance {
|
||||
color: var(--compliance-500);
|
||||
background: var(--compliance-bg);
|
||||
}
|
||||
|
||||
.column-header-core {
|
||||
color: var(--core-400);
|
||||
background: var(--core-bg);
|
||||
}
|
||||
|
||||
.column .card {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.columns-layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Cards ── */
|
||||
.card {
|
||||
border-radius: 14px;
|
||||
@@ -214,6 +275,12 @@
|
||||
}
|
||||
.card-core:hover { background: var(--core-bg-hover); }
|
||||
|
||||
.card-pitch {
|
||||
border-color: var(--pitch-border);
|
||||
background: var(--pitch-bg);
|
||||
}
|
||||
.card-pitch:hover { background: var(--pitch-bg-hover); }
|
||||
|
||||
.card-icon {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
@@ -258,6 +325,7 @@
|
||||
.stripe-docs-core { background: var(--docs-core); }
|
||||
.stripe-docs-lehrer { background: var(--docs-lehrer); }
|
||||
.stripe-docs-compliance { background: var(--docs-compliance); }
|
||||
.stripe-pitch { background: var(--pitch-500); }
|
||||
|
||||
.divider {
|
||||
max-width: 1100px;
|
||||
@@ -555,116 +623,130 @@
|
||||
<div style="max-width: 1100px; margin: 0 auto;">
|
||||
<div class="section-title">Projekte</div>
|
||||
</div>
|
||||
<div class="grid">
|
||||
<div class="columns-layout">
|
||||
|
||||
<a class="card card-core" href="https://macmini:3008/dashboard">
|
||||
<div class="stripe stripe-core"></div>
|
||||
<div class="card-body">
|
||||
<h3>Admin Core</h3>
|
||||
<p>Infrastruktur, Services, Monitoring</p>
|
||||
<div class="url">macmini:3008/dashboard</div>
|
||||
</div>
|
||||
</a>
|
||||
<!-- ── Lehrer (links) ── -->
|
||||
<div class="column">
|
||||
<div class="column-header column-header-lehrer">Lehrer</div>
|
||||
|
||||
<a class="card card-lehrer" href="https://macmini:3002/dashboard">
|
||||
<div class="stripe stripe-lehrer"></div>
|
||||
<div class="card-body">
|
||||
<h3>Admin Lehrer</h3>
|
||||
<p>Verwaltung, AI Tools, Klausuren</p>
|
||||
<div class="url">macmini:3002</div>
|
||||
</div>
|
||||
</a>
|
||||
<a class="card card-lehrer" href="https://macmini:3002/dashboard">
|
||||
<div class="stripe stripe-lehrer"></div>
|
||||
<div class="card-body">
|
||||
<h3>Admin Lehrer</h3>
|
||||
<p>Verwaltung, AI Tools, Klausuren</p>
|
||||
<div class="url">macmini:3002</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a class="card card-compliance" href="https://macmini:3007/sdk">
|
||||
<div class="stripe stripe-compliance"></div>
|
||||
<div class="card-body">
|
||||
<h3>Compliance SDK</h3>
|
||||
<p>DSGVO, Audit, GRC — Alle SDK-Module</p>
|
||||
<div class="url">macmini:3007/sdk</div>
|
||||
</div>
|
||||
</a>
|
||||
<a class="card card-lehrer" href="https://macmini">
|
||||
<div class="stripe stripe-lehrer"></div>
|
||||
<div class="card-body">
|
||||
<h3>Studio v2</h3>
|
||||
<p>Lehrer- und Schueler-Interface</p>
|
||||
<div class="url">macmini</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a class="card card-lehrer" href="https://macmini">
|
||||
<div class="stripe stripe-lehrer"></div>
|
||||
<div class="card-body">
|
||||
<h3>Studio v2</h3>
|
||||
<p>Lehrer- und Schueler-Interface</p>
|
||||
<div class="url">macmini</div>
|
||||
</div>
|
||||
</a>
|
||||
<a class="card card-lehrer" href="https://macmini:3000">
|
||||
<div class="stripe stripe-lehrer"></div>
|
||||
<div class="card-body">
|
||||
<h3>Website</h3>
|
||||
<p>Oeffentliche BreakPilot Website</p>
|
||||
<div class="url">macmini:3000</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a class="card card-compliance" href="https://macmini:3007/dashboard">
|
||||
<div class="stripe stripe-compliance"></div>
|
||||
<div class="card-body">
|
||||
<h3>Compliance Dashboard</h3>
|
||||
<p>Kataloge, Statistiken, Verwaltung</p>
|
||||
<div class="url">macmini:3007/dashboard</div>
|
||||
</div>
|
||||
</a>
|
||||
<a class="card card-lehrer" href="http://macmini:8010/">
|
||||
<div class="stripe stripe-docs-lehrer"></div>
|
||||
<div class="card-body">
|
||||
<h3>Dokumentation</h3>
|
||||
<p>Klausur, Voice, Agent-Core, Studio</p>
|
||||
<div class="url">macmini:8010</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<a class="card card-lehrer" href="https://macmini:3000">
|
||||
<div class="stripe stripe-lehrer"></div>
|
||||
<div class="card-body">
|
||||
<h3>Website</h3>
|
||||
<p>Oeffentliche BreakPilot Website</p>
|
||||
<div class="url">macmini:3000</div>
|
||||
</div>
|
||||
</a>
|
||||
<!-- ── Compliance (mitte) ── -->
|
||||
<div class="column">
|
||||
<div class="column-header column-header-compliance">Compliance</div>
|
||||
|
||||
<a class="card card-compliance" href="https://macmini:3007/dashboard">
|
||||
<div class="stripe stripe-compliance"></div>
|
||||
<div class="card-body">
|
||||
<h3>Katalogverwaltung</h3>
|
||||
<p>SDK-Kataloge & Auswahltabellen</p>
|
||||
<div class="url">macmini:3007/dashboard</div>
|
||||
</div>
|
||||
</a>
|
||||
<a class="card card-compliance" href="https://macmini:3007/sdk">
|
||||
<div class="stripe stripe-compliance"></div>
|
||||
<div class="card-body">
|
||||
<h3>Compliance SDK</h3>
|
||||
<p>DSGVO, Audit, GRC — Alle SDK-Module</p>
|
||||
<div class="url">macmini:3007/sdk</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a class="card card-compliance" href="https://macmini:3010/compliance-hub/">
|
||||
<div class="stripe stripe-compliance"></div>
|
||||
<div class="card-body">
|
||||
<h3>Comply Website</h3>
|
||||
<p>Marketing-Website fuer den KI Compliance Hub</p>
|
||||
<div class="url">macmini:3010/compliance-hub</div>
|
||||
</div>
|
||||
</a>
|
||||
<a class="card card-compliance" href="https://macmini:3007/dashboard">
|
||||
<div class="stripe stripe-compliance"></div>
|
||||
<div class="card-body">
|
||||
<h3>Compliance Dashboard</h3>
|
||||
<p>Kataloge, Statistiken, Verwaltung</p>
|
||||
<div class="url">macmini:3007/dashboard</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
</div>
|
||||
<a class="card card-compliance" href="https://macmini:3007/dashboard">
|
||||
<div class="stripe stripe-compliance"></div>
|
||||
<div class="card-body">
|
||||
<h3>Katalogverwaltung</h3>
|
||||
<p>SDK-Kataloge & Auswahltabellen</p>
|
||||
<div class="url">macmini:3007/dashboard</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<hr class="divider">
|
||||
<a class="card card-compliance" href="https://macmini:3010/compliance-hub/">
|
||||
<div class="stripe stripe-compliance"></div>
|
||||
<div class="card-body">
|
||||
<h3>Comply Website</h3>
|
||||
<p>Marketing-Website fuer den KI Compliance Hub</p>
|
||||
<div class="url">macmini:3010/compliance-hub</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<!-- ── Dokumentation ── -->
|
||||
<div style="max-width: 1100px; margin: 0 auto;">
|
||||
<div class="section-title">Dokumentation</div>
|
||||
</div>
|
||||
<div class="grid">
|
||||
<a class="card card-pitch" href="http://macmini:3012">
|
||||
<div class="stripe stripe-pitch"></div>
|
||||
<div class="card-body">
|
||||
<h3>Pitch Deck</h3>
|
||||
<p>Interaktives Investor Pitch Deck — ComplAI</p>
|
||||
<div class="url">macmini:3012</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a class="card card-core" href="http://macmini:8009/">
|
||||
<div class="stripe stripe-docs-core"></div>
|
||||
<div class="card-body">
|
||||
<h3>Core Dokumentation</h3>
|
||||
<p>Architektur, Auth, DevSecOps, RAG</p>
|
||||
<div class="url">macmini:8009</div>
|
||||
</div>
|
||||
</a>
|
||||
<a class="card card-compliance" href="http://macmini:8011/">
|
||||
<div class="stripe stripe-docs-compliance"></div>
|
||||
<div class="card-body">
|
||||
<h3>Dokumentation</h3>
|
||||
<p>AI-SDK, Auditor-Doku, SBOM</p>
|
||||
<div class="url">macmini:8011</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<a class="card card-lehrer" href="http://macmini:8010/">
|
||||
<div class="stripe stripe-docs-lehrer"></div>
|
||||
<div class="card-body">
|
||||
<h3>Lehrer Dokumentation</h3>
|
||||
<p>Klausur, Voice, Agent-Core, Studio</p>
|
||||
<div class="url">macmini:8010</div>
|
||||
</div>
|
||||
</a>
|
||||
<!-- ── Core (rechts) ── -->
|
||||
<div class="column">
|
||||
<div class="column-header column-header-core">Core</div>
|
||||
|
||||
<a class="card card-compliance" href="http://macmini:8011/">
|
||||
<div class="stripe stripe-docs-compliance"></div>
|
||||
<div class="card-body">
|
||||
<h3>Compliance Dokumentation</h3>
|
||||
<p>AI-SDK, Auditor-Doku, SBOM</p>
|
||||
<div class="url">macmini:8011</div>
|
||||
</div>
|
||||
</a>
|
||||
<a class="card card-core" href="https://macmini:3008/dashboard">
|
||||
<div class="stripe stripe-core"></div>
|
||||
<div class="card-body">
|
||||
<h3>Admin Core</h3>
|
||||
<p>Infrastruktur, Services, Monitoring</p>
|
||||
<div class="url">macmini:3008/dashboard</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a class="card card-core" href="http://macmini:8009/">
|
||||
<div class="stripe stripe-docs-core"></div>
|
||||
<div class="card-body">
|
||||
<h3>Dokumentation</h3>
|
||||
<p>Architektur, Auth, DevSecOps, RAG</p>
|
||||
<div class="url">macmini:8009</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
@@ -28,6 +28,12 @@ import TeamSlide from './slides/TeamSlide'
|
||||
import FinancialsSlide from './slides/FinancialsSlide'
|
||||
import TheAskSlide from './slides/TheAskSlide'
|
||||
import AIQASlide from './slides/AIQASlide'
|
||||
import AssumptionsSlide from './slides/AssumptionsSlide'
|
||||
import ArchitectureSlide from './slides/ArchitectureSlide'
|
||||
import GTMSlide from './slides/GTMSlide'
|
||||
import RegulatorySlide from './slides/RegulatorySlide'
|
||||
import EngineeringSlide from './slides/EngineeringSlide'
|
||||
import AIPipelineSlide from './slides/AIPipelineSlide'
|
||||
|
||||
interface PitchDeckProps {
|
||||
lang: Language
|
||||
@@ -91,7 +97,7 @@ export default function PitchDeck({ lang, onToggleLanguage }: PitchDeckProps) {
|
||||
|
||||
switch (nav.currentSlide) {
|
||||
case 'cover':
|
||||
return <CoverSlide lang={lang} onNext={nav.nextSlide} />
|
||||
return <CoverSlide lang={lang} onNext={nav.nextSlide} funding={data.funding} />
|
||||
case 'problem':
|
||||
return <ProblemSlide lang={lang} />
|
||||
case 'solution':
|
||||
@@ -116,6 +122,18 @@ export default function PitchDeck({ lang, onToggleLanguage }: PitchDeckProps) {
|
||||
return <TheAskSlide lang={lang} funding={data.funding} />
|
||||
case 'ai-qa':
|
||||
return <AIQASlide lang={lang} />
|
||||
case 'annex-assumptions':
|
||||
return <AssumptionsSlide lang={lang} />
|
||||
case 'annex-architecture':
|
||||
return <ArchitectureSlide lang={lang} />
|
||||
case 'annex-gtm':
|
||||
return <GTMSlide lang={lang} />
|
||||
case 'annex-regulatory':
|
||||
return <RegulatorySlide lang={lang} />
|
||||
case 'annex-engineering':
|
||||
return <EngineeringSlide lang={lang} />
|
||||
case 'annex-aipipeline':
|
||||
return <AIPipelineSlide lang={lang} />
|
||||
default:
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -42,9 +42,9 @@ export default function SlideContainer({ children, slideKey, direction }: SlideC
|
||||
opacity: { duration: 0.3 },
|
||||
scale: { duration: 0.3 },
|
||||
}}
|
||||
className="absolute inset-0 flex items-center justify-center overflow-y-auto"
|
||||
className="absolute inset-0 flex justify-center overflow-y-auto"
|
||||
>
|
||||
<div className="w-full max-w-6xl mx-auto px-6 py-12 md:py-16">
|
||||
<div className="w-full max-w-6xl mx-auto px-6 py-12 md:py-16 my-auto">
|
||||
{children}
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
329
pitch-deck/components/slides/AIPipelineSlide.tsx
Normal file
329
pitch-deck/components/slides/AIPipelineSlide.tsx
Normal file
@@ -0,0 +1,329 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { Language } from '@/lib/types'
|
||||
import { t } from '@/lib/i18n'
|
||||
import GradientText from '../ui/GradientText'
|
||||
import FadeInView from '../ui/FadeInView'
|
||||
import GlassCard from '../ui/GlassCard'
|
||||
import {
|
||||
Brain,
|
||||
Search,
|
||||
Database,
|
||||
FileText,
|
||||
Bot,
|
||||
Zap,
|
||||
Layers,
|
||||
ArrowRight,
|
||||
Activity,
|
||||
Shield,
|
||||
Cpu,
|
||||
MessageSquare,
|
||||
Eye,
|
||||
Gauge,
|
||||
Network,
|
||||
Sparkles,
|
||||
} from 'lucide-react'
|
||||
|
||||
interface AIPipelineSlideProps {
|
||||
lang: Language
|
||||
}
|
||||
|
||||
type PipelineTab = 'rag' | 'agents' | 'quality'
|
||||
|
||||
export default function AIPipelineSlide({ lang }: AIPipelineSlideProps) {
|
||||
const i = t(lang)
|
||||
const de = lang === 'de'
|
||||
const [activeTab, setActiveTab] = useState<PipelineTab>('rag')
|
||||
|
||||
const heroStats = [
|
||||
{ value: '19', label: de ? 'Indexierte Verordnungen' : 'Indexed Regulations', sub: 'DSGVO · AI Act · NIS2 · CRA · ePrivacy · ...', color: 'text-indigo-400' },
|
||||
{ value: '7', label: de ? 'Autonome Agenten' : 'Autonomous Agents', sub: de ? 'SOUL-basiert · Orchestriert' : 'SOUL-based · Orchestrated', color: 'text-purple-400' },
|
||||
{ value: '5', label: de ? 'KI-Modelle lokal' : 'Local AI Models', sub: 'Llama 3.2 · Qwen 2.5 · BGE-M3 · TrOCR · CrossEncoder', color: 'text-emerald-400' },
|
||||
{ value: '97', label: de ? 'Golden-Suite Tests' : 'Golden Suite Tests', sub: de ? 'Automatische Qualitaetssicherung' : 'Automatic Quality Assurance', color: 'text-amber-400' },
|
||||
]
|
||||
|
||||
const tabs: { id: PipelineTab; label: string; icon: typeof Brain }[] = [
|
||||
{ id: 'rag', label: de ? 'RAG-Pipeline' : 'RAG Pipeline', icon: Search },
|
||||
{ id: 'agents', label: de ? 'Multi-Agent-System' : 'Multi-Agent System', icon: Bot },
|
||||
{ id: 'quality', label: de ? 'Qualitaetssicherung' : 'Quality Assurance', icon: Gauge },
|
||||
]
|
||||
|
||||
// RAG Pipeline content
|
||||
const ragPipelineSteps = [
|
||||
{
|
||||
icon: FileText,
|
||||
color: 'text-blue-400',
|
||||
bg: 'bg-blue-500/10 border-blue-500/20',
|
||||
title: de ? '1. Ingestion' : '1. Ingestion',
|
||||
items: de
|
||||
? ['PDF-Upload, URL-Crawling, API-Import', 'Automatische Spracherkennung (DE/EN)', 'Semantisches Chunking (rekursiv, 512 Tokens)', 'Metadaten-Extraktion (Verordnung, Artikel, Absatz)']
|
||||
: ['PDF upload, URL crawling, API import', 'Automatic language detection (DE/EN)', 'Semantic chunking (recursive, 512 tokens)', 'Metadata extraction (regulation, article, paragraph)'],
|
||||
},
|
||||
{
|
||||
icon: Cpu,
|
||||
color: 'text-purple-400',
|
||||
bg: 'bg-purple-500/10 border-purple-500/20',
|
||||
title: de ? '2. Embedding' : '2. Embedding',
|
||||
items: de
|
||||
? ['BGE-M3 Multilingual Embeddings (lokal)', 'CrossEncoder Re-Ranking (lokal)', 'HyDE: Hypothetical Document Embeddings', 'Lazy Model Loading (Speicher-optimiert)']
|
||||
: ['BGE-M3 multilingual embeddings (local)', 'CrossEncoder re-ranking (local)', 'HyDE: Hypothetical Document Embeddings', 'Lazy model loading (memory-optimized)'],
|
||||
},
|
||||
{
|
||||
icon: Database,
|
||||
color: 'text-emerald-400',
|
||||
bg: 'bg-emerald-500/10 border-emerald-500/20',
|
||||
title: de ? '3. Vektorspeicher' : '3. Vector Store',
|
||||
items: de
|
||||
? ['Qdrant Vector DB (Self-hosted)', '5 Collections: Legal Corpus, DSFA, Compliance, Dokumente, Agenten-Wissen', 'MinIO Object Storage fuer Quelldokumente', 'Automatische Re-Indexierung bei Updates']
|
||||
: ['Qdrant Vector DB (self-hosted)', '5 Collections: Legal Corpus, DSFA, Compliance, Documents, Agent Knowledge', 'MinIO object storage for source documents', 'Automatic re-indexing on updates'],
|
||||
},
|
||||
{
|
||||
icon: Search,
|
||||
color: 'text-indigo-400',
|
||||
bg: 'bg-indigo-500/10 border-indigo-500/20',
|
||||
title: de ? '4. Hybrid Search' : '4. Hybrid Search',
|
||||
items: de
|
||||
? ['Dense Retrieval (70%) + BM25 Keyword (30%)', 'Deutsche Komposita-Zerlegung', 'Cross-Encoder Re-Ranking der Top-K Ergebnisse', 'Quellen-Attribution mit Artikel-Referenz']
|
||||
: ['Dense retrieval (70%) + BM25 keyword (30%)', 'German compound word decomposition', 'Cross-encoder re-ranking of top-K results', 'Source attribution with article reference'],
|
||||
},
|
||||
]
|
||||
|
||||
// Multi-Agent System content
|
||||
const agents = [
|
||||
{ name: de ? 'Compliance-Berater' : 'Compliance Advisor', soul: 'compliance-advisor.soul.md', desc: de ? 'Beantwortet Compliance-Fragen mit RAG-Kontext' : 'Answers compliance questions with RAG context', color: 'text-indigo-400' },
|
||||
{ name: de ? 'Audit-Agent' : 'Audit Agent', soul: 'quality-judge.soul.md', desc: de ? 'Prueft Dokumente gegen regulatorische Anforderungen' : 'Checks documents against regulatory requirements', color: 'text-emerald-400' },
|
||||
{ name: de ? 'Dokument-Agent' : 'Drafting Agent', soul: 'drafting-agent.soul.md', desc: de ? 'Erstellt Compliance-Dokumente und Policies' : 'Creates compliance documents and policies', color: 'text-purple-400' },
|
||||
{ name: 'Orchestrator', soul: 'orchestrator.soul.md', desc: de ? 'Task-Routing und Koordination aller Agenten' : 'Task routing and coordination of all agents', color: 'text-amber-400' },
|
||||
{ name: de ? 'Alert-Agent' : 'Alert Agent', soul: 'alert-agent.soul.md', desc: de ? 'Monitoring, Fristen und Benachrichtigungen' : 'Monitoring, deadlines and notifications', color: 'text-red-400' },
|
||||
{ name: de ? 'Tutor-Agent' : 'Tutor Agent', soul: 'tutor-agent.soul.md', desc: de ? 'Interaktive Compliance-Schulungen' : 'Interactive compliance training', color: 'text-blue-400' },
|
||||
]
|
||||
|
||||
const agentInfra = [
|
||||
{ icon: MessageSquare, label: 'SOUL Files', desc: de ? 'Deklarative Agenten-Persoenlichkeit in Markdown' : 'Declarative agent personality in Markdown' },
|
||||
{ icon: Brain, label: 'Shared Brain', desc: de ? 'Gemeinsamer Wissensspeicher + Langzeitgedaechtnis' : 'Shared knowledge store + long-term memory' },
|
||||
{ icon: Network, label: 'Message Bus', desc: de ? 'Valkey/Redis · Pub/Sub · Task Queue' : 'Valkey/Redis · Pub/Sub · Task Queue' },
|
||||
{ icon: Activity, label: 'Session Manager', desc: de ? 'Heartbeat · Checkpoints · Recovery' : 'Heartbeat · Checkpoints · Recovery' },
|
||||
]
|
||||
|
||||
// Quality Assurance content
|
||||
const qaFeatures = [
|
||||
{
|
||||
icon: Shield,
|
||||
color: 'text-emerald-400',
|
||||
title: de ? 'BQAS — Quality Assurance System' : 'BQAS — Quality Assurance System',
|
||||
items: de
|
||||
? ['97 Golden-Suite-Referenztests fuer Regressionserkennung', 'Synthetische Testgenerierung per LLM', 'RAG-Retrieval-Accuracy und Correction-Tests', 'Precision, Recall, F1 Tracking ueber alle Releases']
|
||||
: ['97 golden suite reference tests for regression detection', 'Synthetic test generation via LLM', 'RAG retrieval accuracy and correction tests', 'Precision, recall, F1 tracking across all releases'],
|
||||
},
|
||||
{
|
||||
icon: Eye,
|
||||
color: 'text-indigo-400',
|
||||
title: de ? 'LLM Evaluation & Vergleich' : 'LLM Evaluation & Comparison',
|
||||
items: de
|
||||
? ['Side-by-Side-Vergleich: Ollama lokal vs. OpenAI vs. Claude', 'Latenz-, Token- und Qualitaets-Metriken pro Provider', 'Automatischer Fallback bei Provider-Ausfall', 'Self-RAG: Selbstreflektierende Antwortvalidierung']
|
||||
: ['Side-by-side comparison: Ollama local vs. OpenAI vs. Claude', 'Latency, token and quality metrics per provider', 'Automatic fallback on provider failure', 'Self-RAG: Self-reflective answer validation'],
|
||||
},
|
||||
{
|
||||
icon: Sparkles,
|
||||
color: 'text-purple-400',
|
||||
title: de ? 'Document Intelligence' : 'Document Intelligence',
|
||||
items: de
|
||||
? ['TrOCR Handschrifterkennung mit LoRA Fine-Tuning', 'Multi-OCR-Pipeline: 5 Methoden parallel (Vision LLM, Tesseract, OpenCV, ...)', 'Labeling-Interface fuer Trainingsdaten-Erstellung (DSGVO-konform, lokal)', 'OpenCV Document Reconstruction (Deskew, Dewarp, Binarisierung)']
|
||||
: ['TrOCR handwriting recognition with LoRA fine-tuning', 'Multi-OCR pipeline: 5 methods in parallel (Vision LLM, Tesseract, OpenCV, ...)', 'Labeling interface for training data creation (GDPR-compliant, local)', 'OpenCV document reconstruction (deskew, dewarp, binarization)'],
|
||||
},
|
||||
{
|
||||
icon: Zap,
|
||||
color: 'text-amber-400',
|
||||
title: de ? 'GPU & Training' : 'GPU & Training',
|
||||
items: de
|
||||
? ['Lokales Training auf Apple Silicon (M4 Max, 64 GB unified)', 'vast.ai Integration fuer Cloud-GPU bei Bedarf', 'LoRA/QLoRA Fine-Tuning mit konfigurierbaren Hyperparametern', 'SSE-Streaming fuer Echtzeit-Trainingsmetriken (Loss, Accuracy, F1)']
|
||||
: ['Local training on Apple Silicon (M4 Max, 64 GB unified)', 'vast.ai integration for cloud GPU on demand', 'LoRA/QLoRA fine-tuning with configurable hyperparameters', 'SSE streaming for real-time training metrics (loss, accuracy, F1)'],
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div>
|
||||
<FadeInView className="text-center mb-5">
|
||||
<p className="text-xs font-mono text-indigo-400/60 uppercase tracking-widest mb-2">
|
||||
{de ? 'Anhang' : 'Appendix'}
|
||||
</p>
|
||||
<h2 className="text-4xl md:text-5xl font-bold mb-2">
|
||||
<GradientText>{i.annex.aipipeline.title}</GradientText>
|
||||
</h2>
|
||||
<p className="text-lg text-white/50 max-w-2xl mx-auto">{i.annex.aipipeline.subtitle}</p>
|
||||
</FadeInView>
|
||||
|
||||
{/* Hero Stats */}
|
||||
<FadeInView delay={0.1}>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-2 mb-4">
|
||||
{heroStats.map((stat, idx) => (
|
||||
<div key={idx} className="border border-white/[0.08] rounded-xl p-2.5 bg-white/[0.03] text-center">
|
||||
<p className={`text-2xl font-black tracking-tight ${stat.color}`}>{stat.value}</p>
|
||||
<p className="text-[11px] font-semibold text-white/70">{stat.label}</p>
|
||||
<p className="text-[9px] text-white/30 mt-0.5 leading-tight">{stat.sub}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</FadeInView>
|
||||
|
||||
{/* Tab Navigation */}
|
||||
<FadeInView delay={0.15}>
|
||||
<div className="flex items-center justify-center gap-2 mb-4">
|
||||
{tabs.map((tab) => {
|
||||
const Icon = tab.icon
|
||||
return (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => setActiveTab(tab.id)}
|
||||
className={`flex items-center gap-2 px-4 py-2 rounded-xl text-sm transition-all
|
||||
${activeTab === tab.id
|
||||
? 'bg-indigo-500/20 text-indigo-300 border border-indigo-500/30'
|
||||
: 'bg-white/[0.04] text-white/40 border border-transparent hover:text-white/60 hover:bg-white/[0.06]'
|
||||
}`}
|
||||
>
|
||||
<Icon className="w-4 h-4" />
|
||||
{tab.label}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</FadeInView>
|
||||
|
||||
{/* Tab Content */}
|
||||
<FadeInView delay={0.2} key={activeTab}>
|
||||
{activeTab === 'rag' && (
|
||||
<div>
|
||||
{/* Pipeline Flow Visualization */}
|
||||
<div className="flex items-center justify-center gap-1 mb-4 flex-wrap">
|
||||
{[
|
||||
{ icon: FileText, label: de ? 'Dokumente' : 'Documents' },
|
||||
{ icon: Layers, label: 'Chunking' },
|
||||
{ icon: Cpu, label: 'BGE-M3' },
|
||||
{ icon: Database, label: 'Qdrant' },
|
||||
{ icon: Search, label: 'Hybrid Search' },
|
||||
{ icon: Brain, label: 'LLM' },
|
||||
].map((step, idx, arr) => (
|
||||
<div key={idx} className="flex items-center gap-1">
|
||||
<div className="flex items-center gap-1 px-2 py-1 rounded-lg bg-white/[0.05] border border-white/[0.08]">
|
||||
<step.icon className="w-3 h-3 text-indigo-400" />
|
||||
<span className="text-[10px] text-white/50">{step.label}</span>
|
||||
</div>
|
||||
{idx < arr.length - 1 && <ArrowRight className="w-3 h-3 text-white/20" />}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{/* Pipeline Steps */}
|
||||
<div className="grid md:grid-cols-2 gap-3">
|
||||
{ragPipelineSteps.map((step, idx) => {
|
||||
const Icon = step.icon
|
||||
return (
|
||||
<div key={idx} className={`border rounded-xl p-3 ${step.bg}`}>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Icon className={`w-4 h-4 ${step.color}`} />
|
||||
<h3 className="text-xs font-bold text-white">{step.title}</h3>
|
||||
</div>
|
||||
<ul className="space-y-1">
|
||||
{step.items.map((item, iidx) => (
|
||||
<li key={iidx} className="flex items-start gap-1.5 text-[11px] text-white/50">
|
||||
<span className={`w-1 h-1 rounded-full mt-1.5 ${step.color} bg-current shrink-0`} />
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'agents' && (
|
||||
<div className="grid md:grid-cols-12 gap-4">
|
||||
{/* Agent List */}
|
||||
<div className="md:col-span-7">
|
||||
<GlassCard hover={false} className="p-4">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Bot className="w-4 h-4 text-white/40" />
|
||||
<p className="text-xs font-semibold text-white/40 uppercase tracking-wider">
|
||||
{de ? 'Agenten-Fleet' : 'Agent Fleet'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{agents.map((agent, idx) => (
|
||||
<div key={idx} className="p-2 rounded-lg bg-white/[0.03] border border-white/5">
|
||||
<div className="flex items-center gap-1.5 mb-1">
|
||||
<div className={`w-1.5 h-1.5 rounded-full ${agent.color} bg-current`} />
|
||||
<p className="text-xs font-bold text-white/80">{agent.name}</p>
|
||||
</div>
|
||||
<p className="text-[10px] text-white/40 leading-tight">{agent.desc}</p>
|
||||
<p className="text-[9px] font-mono text-white/20 mt-1">{agent.soul}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</GlassCard>
|
||||
</div>
|
||||
{/* Agent Infrastructure */}
|
||||
<div className="md:col-span-5">
|
||||
<GlassCard hover={false} className="p-4 h-full">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Network className="w-4 h-4 text-white/40" />
|
||||
<p className="text-xs font-semibold text-white/40 uppercase tracking-wider">
|
||||
{de ? 'Infrastruktur' : 'Infrastructure'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-2.5">
|
||||
{agentInfra.map((inf, idx) => {
|
||||
const Icon = inf.icon
|
||||
return (
|
||||
<div key={idx} className="flex items-start gap-2.5">
|
||||
<div className="w-7 h-7 rounded-lg bg-purple-500/10 border border-purple-500/20 flex items-center justify-center shrink-0">
|
||||
<Icon className="w-3.5 h-3.5 text-purple-400" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs font-semibold text-white/70">{inf.label}</p>
|
||||
<p className="text-[10px] text-white/40">{inf.desc}</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<div className="mt-3 pt-3 border-t border-white/5">
|
||||
<p className="text-[10px] text-white/20">
|
||||
{de
|
||||
? 'Alle Agenten laufen lokal · Kein API-Schluessel erforderlich · DSGVO-konform'
|
||||
: 'All agents run locally · No API key required · GDPR-compliant'}
|
||||
</p>
|
||||
</div>
|
||||
</GlassCard>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'quality' && (
|
||||
<div className="grid md:grid-cols-2 gap-3">
|
||||
{qaFeatures.map((feat, idx) => {
|
||||
const Icon = feat.icon
|
||||
return (
|
||||
<GlassCard key={idx} hover={false} className="p-3">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Icon className={`w-4 h-4 ${feat.color}`} />
|
||||
<h3 className="text-xs font-bold text-white">{feat.title}</h3>
|
||||
</div>
|
||||
<ul className="space-y-1">
|
||||
{feat.items.map((item, iidx) => (
|
||||
<li key={iidx} className="flex items-start gap-1.5 text-[11px] text-white/50">
|
||||
<span className={`w-1 h-1 rounded-full mt-1.5 ${feat.color} bg-current shrink-0`} />
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</GlassCard>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</FadeInView>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
130
pitch-deck/components/slides/ArchitectureSlide.tsx
Normal file
130
pitch-deck/components/slides/ArchitectureSlide.tsx
Normal file
@@ -0,0 +1,130 @@
|
||||
'use client'
|
||||
|
||||
import { Language } from '@/lib/types'
|
||||
import { t } from '@/lib/i18n'
|
||||
import GradientText from '../ui/GradientText'
|
||||
import FadeInView from '../ui/FadeInView'
|
||||
import GlassCard from '../ui/GlassCard'
|
||||
import { Server, Cpu, Shield, Database, Globe, Lock, Layers, Workflow } from 'lucide-react'
|
||||
|
||||
interface ArchitectureSlideProps {
|
||||
lang: Language
|
||||
}
|
||||
|
||||
export default function ArchitectureSlide({ lang }: ArchitectureSlideProps) {
|
||||
const i = t(lang)
|
||||
const de = lang === 'de'
|
||||
|
||||
const layers = [
|
||||
{
|
||||
icon: Server,
|
||||
color: 'text-indigo-400',
|
||||
bg: 'bg-indigo-500/10 border-indigo-500/20',
|
||||
title: de ? 'Hardware-Schicht' : 'Hardware Layer',
|
||||
items: [
|
||||
{ label: 'ComplAI Mini', desc: 'Mac Mini M4 · 16 GB · Llama 3.2 3B' },
|
||||
{ label: 'ComplAI Studio', desc: 'Mac Studio M4 Max · 64 GB · Qwen 2.5 32B' },
|
||||
{ label: 'ComplAI Cloud', desc: de ? 'Managed GPU-Cluster · Multi-Model' : 'Managed GPU Cluster · Multi-Model' },
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: Cpu,
|
||||
color: 'text-purple-400',
|
||||
bg: 'bg-purple-500/10 border-purple-500/20',
|
||||
title: de ? 'KI-Engine' : 'AI Engine',
|
||||
items: [
|
||||
{ label: 'Ollama Runtime', desc: de ? 'Lokale LLM-Inferenz, GPU-optimiert' : 'Local LLM inference, GPU-optimized' },
|
||||
{ label: 'RAG Pipeline', desc: de ? 'Vektorsuche mit Compliance-Wissensbasis' : 'Vector search with compliance knowledge base' },
|
||||
{ label: 'Agent Framework', desc: de ? 'Autonome Compliance-Agenten (Audit, Monitoring, Reporting)' : 'Autonomous compliance agents (Audit, Monitoring, Reporting)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: Shield,
|
||||
color: 'text-emerald-400',
|
||||
bg: 'bg-emerald-500/10 border-emerald-500/20',
|
||||
title: de ? 'Compliance-Module' : 'Compliance Modules',
|
||||
items: [
|
||||
{ label: 'DSGVO Engine', desc: de ? 'VVT, DSFA, Betroffenenrechte, Loeschkonzept' : 'RoPA, DPIA, Data Subject Rights, Deletion Concept' },
|
||||
{ label: 'AI Act Module', desc: de ? 'Risikoklassifizierung, Konformitaetsbewertung, Dokumentation' : 'Risk Classification, Conformity Assessment, Documentation' },
|
||||
{ label: 'NIS2 Module', desc: de ? 'Cybersecurity-Policies, Incident Response, Meldewege' : 'Cybersecurity Policies, Incident Response, Reporting Chains' },
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: Layers,
|
||||
color: 'text-blue-400',
|
||||
bg: 'bg-blue-500/10 border-blue-500/20',
|
||||
title: de ? 'Plattform-Services' : 'Platform Services',
|
||||
items: [
|
||||
{ label: de ? 'Admin-Dashboard' : 'Admin Dashboard', desc: 'Next.js · ' + (de ? 'Mandantenfaehig · Rollenbasiert' : 'Multi-Tenant · Role-Based') },
|
||||
{ label: 'SDK API', desc: 'Go/Gin · REST · ' + (de ? 'Tenant-isoliert' : 'Tenant-Isolated') },
|
||||
{ label: 'DevSecOps Suite', desc: 'Semgrep · Trivy · Gitleaks · CycloneDX SBOM' },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const securityFeatures = [
|
||||
{ icon: Lock, label: de ? 'Zero-Trust Architektur' : 'Zero-Trust Architecture' },
|
||||
{ icon: Database, label: de ? 'Daten verlassen nie das Unternehmen' : 'Data Never Leaves the Company' },
|
||||
{ icon: Globe, label: de ? 'Kein Cloud-Abhaengigkeit' : 'No Cloud Dependency' },
|
||||
{ icon: Workflow, label: de ? 'Air-Gap faehig' : 'Air-Gap Capable' },
|
||||
]
|
||||
|
||||
return (
|
||||
<div>
|
||||
<FadeInView className="text-center mb-8">
|
||||
<p className="text-xs font-mono text-indigo-400/60 uppercase tracking-widest mb-2">
|
||||
{de ? 'Anhang' : 'Appendix'}
|
||||
</p>
|
||||
<h2 className="text-4xl md:text-5xl font-bold mb-3">
|
||||
<GradientText>{i.annex.architecture.title}</GradientText>
|
||||
</h2>
|
||||
<p className="text-lg text-white/50 max-w-2xl mx-auto">{i.annex.architecture.subtitle}</p>
|
||||
</FadeInView>
|
||||
|
||||
{/* Architecture Layers */}
|
||||
<div className="grid md:grid-cols-2 gap-4 mb-6">
|
||||
{layers.map((layer, idx) => {
|
||||
const Icon = layer.icon
|
||||
return (
|
||||
<FadeInView key={idx} delay={0.2 + idx * 0.1}>
|
||||
<div className={`border rounded-xl p-4 ${layer.bg}`}>
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Icon className={`w-5 h-5 ${layer.color}`} />
|
||||
<h3 className="text-sm font-bold text-white">{layer.title}</h3>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{layer.items.map((item, iidx) => (
|
||||
<div key={iidx} className="flex items-start gap-2">
|
||||
<div className={`w-1.5 h-1.5 rounded-full mt-1.5 ${layer.color} bg-current opacity-50`} />
|
||||
<div>
|
||||
<span className="text-xs font-semibold text-white/80">{item.label}</span>
|
||||
<span className="text-xs text-white/40 ml-2">{item.desc}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</FadeInView>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Security Bar */}
|
||||
<FadeInView delay={0.6}>
|
||||
<GlassCard hover={false} className="p-4">
|
||||
<div className="flex items-center justify-center gap-8 flex-wrap">
|
||||
{securityFeatures.map((feat, idx) => {
|
||||
const Icon = feat.icon
|
||||
return (
|
||||
<div key={idx} className="flex items-center gap-2">
|
||||
<Icon className="w-4 h-4 text-emerald-400" />
|
||||
<span className="text-xs text-white/60">{feat.label}</span>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</GlassCard>
|
||||
</FadeInView>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
198
pitch-deck/components/slides/AssumptionsSlide.tsx
Normal file
198
pitch-deck/components/slides/AssumptionsSlide.tsx
Normal file
@@ -0,0 +1,198 @@
|
||||
'use client'
|
||||
|
||||
import { Language } from '@/lib/types'
|
||||
import { useFinancialModel } from '@/lib/hooks/useFinancialModel'
|
||||
import { t } from '@/lib/i18n'
|
||||
import GradientText from '../ui/GradientText'
|
||||
import FadeInView from '../ui/FadeInView'
|
||||
import GlassCard from '../ui/GlassCard'
|
||||
import { SlidersHorizontal, TrendingUp, TrendingDown, Minus } from 'lucide-react'
|
||||
|
||||
interface AssumptionsSlideProps {
|
||||
lang: Language
|
||||
}
|
||||
|
||||
interface SensitivityResult {
|
||||
label: string
|
||||
base: string
|
||||
bull: string
|
||||
bear: string
|
||||
}
|
||||
|
||||
export default function AssumptionsSlide({ lang }: AssumptionsSlideProps) {
|
||||
const i = t(lang)
|
||||
const fm = useFinancialModel()
|
||||
const de = lang === 'de'
|
||||
|
||||
const baseScenario = fm.scenarios.find(s => s.name === 'Base Case')
|
||||
const bullScenario = fm.scenarios.find(s => s.name === 'Bull Case')
|
||||
const bearScenario = fm.scenarios.find(s => s.name === 'Bear Case')
|
||||
|
||||
function getVal(scenario: typeof baseScenario, key: string): string {
|
||||
if (!scenario) return '-'
|
||||
const a = scenario.assumptions.find(a => a.key === key)
|
||||
if (!a) return '-'
|
||||
const v = a.value
|
||||
if (typeof v === 'number') return String(v)
|
||||
return String(v)
|
||||
}
|
||||
|
||||
const rows: SensitivityResult[] = [
|
||||
{
|
||||
label: de ? 'Monatliches Wachstum' : 'Monthly Growth Rate',
|
||||
base: getVal(baseScenario, 'monthly_growth_rate') + '%',
|
||||
bull: getVal(bullScenario, 'monthly_growth_rate') + '%',
|
||||
bear: getVal(bearScenario, 'monthly_growth_rate') + '%',
|
||||
},
|
||||
{
|
||||
label: de ? 'Monatliche Churn Rate' : 'Monthly Churn Rate',
|
||||
base: getVal(baseScenario, 'churn_rate_monthly') + '%',
|
||||
bull: getVal(bullScenario, 'churn_rate_monthly') + '%',
|
||||
bear: getVal(bearScenario, 'churn_rate_monthly') + '%',
|
||||
},
|
||||
{
|
||||
label: de ? 'Startkunden' : 'Initial Customers',
|
||||
base: getVal(baseScenario, 'initial_customers'),
|
||||
bull: getVal(bullScenario, 'initial_customers'),
|
||||
bear: getVal(bearScenario, 'initial_customers'),
|
||||
},
|
||||
{
|
||||
label: 'ARPU Mini',
|
||||
base: getVal(baseScenario, 'arpu_mini') + ' EUR',
|
||||
bull: getVal(bullScenario, 'arpu_mini') + ' EUR',
|
||||
bear: getVal(bearScenario, 'arpu_mini') + ' EUR',
|
||||
},
|
||||
{
|
||||
label: 'ARPU Studio',
|
||||
base: getVal(baseScenario, 'arpu_studio') + ' EUR',
|
||||
bull: getVal(bullScenario, 'arpu_studio') + ' EUR',
|
||||
bear: getVal(bearScenario, 'arpu_studio') + ' EUR',
|
||||
},
|
||||
{
|
||||
label: 'ARPU Cloud',
|
||||
base: getVal(baseScenario, 'arpu_cloud') + ' EUR',
|
||||
bull: getVal(bullScenario, 'arpu_cloud') + ' EUR',
|
||||
bear: getVal(bearScenario, 'arpu_cloud') + ' EUR',
|
||||
},
|
||||
{
|
||||
label: 'CAC',
|
||||
base: getVal(baseScenario, 'cac') + ' EUR',
|
||||
bull: getVal(bullScenario, 'cac') + ' EUR',
|
||||
bear: getVal(bearScenario, 'cac') + ' EUR',
|
||||
},
|
||||
{
|
||||
label: de ? 'Produktmix Mini/Studio/Cloud' : 'Product Mix Mini/Studio/Cloud',
|
||||
base: `${getVal(baseScenario, 'product_mix_mini')}/${getVal(baseScenario, 'product_mix_studio')}/${getVal(baseScenario, 'product_mix_cloud')}`,
|
||||
bull: `${getVal(bullScenario, 'product_mix_mini')}/${getVal(bullScenario, 'product_mix_studio')}/${getVal(bullScenario, 'product_mix_cloud')}`,
|
||||
bear: `${getVal(bearScenario, 'product_mix_mini')}/${getVal(bearScenario, 'product_mix_studio')}/${getVal(bearScenario, 'product_mix_cloud')}`,
|
||||
},
|
||||
{
|
||||
label: de ? 'Marketing / Monat' : 'Marketing / Month',
|
||||
base: getVal(baseScenario, 'marketing_monthly') + ' EUR',
|
||||
bull: getVal(bullScenario, 'marketing_monthly') + ' EUR',
|
||||
bear: getVal(bearScenario, 'marketing_monthly') + ' EUR',
|
||||
},
|
||||
]
|
||||
|
||||
// Summary KPIs from computed results
|
||||
const baseSummary = fm.results.get(baseScenario?.id || '')?.summary
|
||||
const bullSummary = fm.results.get(bullScenario?.id || '')?.summary
|
||||
const bearSummary = fm.results.get(bearScenario?.id || '')?.summary
|
||||
|
||||
return (
|
||||
<div>
|
||||
<FadeInView className="text-center mb-8">
|
||||
<p className="text-xs font-mono text-indigo-400/60 uppercase tracking-widest mb-2">
|
||||
{de ? 'Anhang' : 'Appendix'}
|
||||
</p>
|
||||
<h2 className="text-4xl md:text-5xl font-bold mb-3">
|
||||
<GradientText>{i.annex.assumptions.title}</GradientText>
|
||||
</h2>
|
||||
<p className="text-lg text-white/50 max-w-2xl mx-auto">{i.annex.assumptions.subtitle}</p>
|
||||
</FadeInView>
|
||||
|
||||
{/* Sensitivity Table */}
|
||||
<FadeInView delay={0.2}>
|
||||
<GlassCard hover={false} className="p-5 mb-6 overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-white/10">
|
||||
<th className="text-left py-2 pr-4 text-white/40 font-medium min-w-[200px]">
|
||||
{de ? 'Annahme' : 'Assumption'}
|
||||
</th>
|
||||
<th className="text-center py-2 px-3 min-w-[100px]">
|
||||
<span className="flex items-center justify-center gap-1 text-red-400 font-medium">
|
||||
<TrendingDown className="w-3 h-3" /> Bear
|
||||
</span>
|
||||
</th>
|
||||
<th className="text-center py-2 px-3 min-w-[100px]">
|
||||
<span className="flex items-center justify-center gap-1 text-indigo-400 font-medium">
|
||||
<Minus className="w-3 h-3" /> Base
|
||||
</span>
|
||||
</th>
|
||||
<th className="text-center py-2 px-3 min-w-[100px]">
|
||||
<span className="flex items-center justify-center gap-1 text-emerald-400 font-medium">
|
||||
<TrendingUp className="w-3 h-3" /> Bull
|
||||
</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.map((row, idx) => (
|
||||
<tr key={idx} className={idx % 2 === 0 ? 'bg-white/[0.02]' : ''}>
|
||||
<td className="py-2 pr-4 text-white/60">{row.label}</td>
|
||||
<td className="py-2 px-3 text-center font-mono text-red-400/70">{row.bear}</td>
|
||||
<td className="py-2 px-3 text-center font-mono text-white font-semibold">{row.base}</td>
|
||||
<td className="py-2 px-3 text-center font-mono text-emerald-400/70">{row.bull}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</GlassCard>
|
||||
</FadeInView>
|
||||
|
||||
{/* Outcome Summary */}
|
||||
{baseSummary && (
|
||||
<div className="grid md:grid-cols-3 gap-4">
|
||||
{[
|
||||
{ label: 'Bear', summary: bearSummary, color: 'text-red-400', bg: 'bg-red-500/5 border-red-500/10' },
|
||||
{ label: 'Base', summary: baseSummary, color: 'text-indigo-400', bg: 'bg-indigo-500/5 border-indigo-500/10' },
|
||||
{ label: 'Bull', summary: bullSummary, color: 'text-emerald-400', bg: 'bg-emerald-500/5 border-emerald-500/10' },
|
||||
].map((s, idx) => (
|
||||
<FadeInView key={s.label} delay={0.4 + idx * 0.1}>
|
||||
<div className={`border rounded-xl p-4 ${s.bg}`}>
|
||||
<p className={`text-sm font-bold ${s.color} mb-3`}>{s.label} Case</p>
|
||||
<div className="space-y-2 text-xs">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-white/40">ARR 2030</span>
|
||||
<span className="text-white font-mono">
|
||||
{s.summary ? `${(s.summary.final_arr / 1_000_000).toFixed(1)}M` : '-'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-white/40">{de ? 'Kunden 2030' : 'Customers 2030'}</span>
|
||||
<span className="text-white font-mono">
|
||||
{s.summary?.final_customers?.toLocaleString('de-DE') || '-'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-white/40">Break-Even</span>
|
||||
<span className="text-white font-mono">
|
||||
{s.summary?.break_even_month ? `${de ? 'Monat' : 'Month'} ${s.summary.break_even_month}` : (de ? 'Nicht erreicht' : 'Not reached')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-white/40">LTV/CAC</span>
|
||||
<span className="text-white font-mono">
|
||||
{s.summary?.final_ltv_cac ? `${s.summary.final_ltv_cac.toFixed(1)}x` : '-'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</FadeInView>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -13,8 +13,48 @@ interface BusinessModelSlideProps {
|
||||
products: PitchProduct[]
|
||||
}
|
||||
|
||||
const AMORT_MONTHS = 24
|
||||
|
||||
function computeKPIs(products: PitchProduct[]) {
|
||||
if (!products.length) return { weightedMarginDuring: 0, weightedMarginAfter: 0, amortMonths: AMORT_MONTHS }
|
||||
|
||||
// Compute weighted margin based on product mix (equal weight per product as proxy)
|
||||
const n = products.length
|
||||
let sumMarginDuring = 0
|
||||
let sumMarginAfter = 0
|
||||
let maxAmortMonths = 0
|
||||
|
||||
for (const p of products) {
|
||||
const price = p.monthly_price_eur
|
||||
if (price <= 0) continue
|
||||
const amort = p.hardware_cost_eur > 0 ? p.hardware_cost_eur / AMORT_MONTHS : 0
|
||||
const opex = p.operating_cost_eur > 0 ? p.operating_cost_eur : 0
|
||||
|
||||
// Margin during amortization
|
||||
const marginDuring = (price - amort - opex) / price
|
||||
sumMarginDuring += marginDuring
|
||||
|
||||
// Margin after amortization (no more HW cost)
|
||||
const marginAfter = (price - opex) / price
|
||||
sumMarginAfter += marginAfter
|
||||
|
||||
// Payback period in months
|
||||
if (p.hardware_cost_eur > 0 && price - opex > 0) {
|
||||
const payback = Math.ceil(p.hardware_cost_eur / (price - opex))
|
||||
if (payback > maxAmortMonths) maxAmortMonths = payback
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
weightedMarginDuring: Math.round((sumMarginDuring / n) * 100),
|
||||
weightedMarginAfter: Math.round((sumMarginAfter / n) * 100),
|
||||
amortMonths: maxAmortMonths || AMORT_MONTHS,
|
||||
}
|
||||
}
|
||||
|
||||
export default function BusinessModelSlide({ lang, products }: BusinessModelSlideProps) {
|
||||
const i = t(lang)
|
||||
const { weightedMarginDuring, weightedMarginAfter, amortMonths } = computeKPIs(products)
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -25,7 +65,7 @@ export default function BusinessModelSlide({ lang, products }: BusinessModelSlid
|
||||
<p className="text-lg text-white/50 max-w-2xl mx-auto">{i.businessModel.subtitle}</p>
|
||||
</FadeInView>
|
||||
|
||||
{/* Key Metrics */}
|
||||
{/* Key Metrics — dynamisch berechnet */}
|
||||
<div className="grid md:grid-cols-3 gap-4 mb-8">
|
||||
<GlassCard delay={0.2} className="text-center">
|
||||
<Repeat className="w-6 h-6 text-indigo-400 mx-auto mb-2" />
|
||||
@@ -36,14 +76,18 @@ export default function BusinessModelSlide({ lang, products }: BusinessModelSlid
|
||||
<GlassCard delay={0.3} className="text-center">
|
||||
<DollarSign className="w-6 h-6 text-green-400 mx-auto mb-2" />
|
||||
<p className="text-sm text-white/50 mb-1">{i.businessModel.margin}</p>
|
||||
<p className="text-2xl font-bold text-white">>70%</p>
|
||||
<p className="text-xs text-white/30">{lang === 'de' ? 'nach Amortisation' : 'post amortization'}</p>
|
||||
<p className="text-2xl font-bold text-white">>{weightedMarginAfter}%</p>
|
||||
<p className="text-xs text-white/30">
|
||||
{lang === 'de' ? 'nach Amortisation' : 'post amortization'}
|
||||
{' · '}
|
||||
{weightedMarginDuring}% {lang === 'de' ? 'waehrend' : 'during'}
|
||||
</p>
|
||||
</GlassCard>
|
||||
<GlassCard delay={0.4} className="text-center">
|
||||
<TrendingUp className="w-6 h-6 text-purple-400 mx-auto mb-2" />
|
||||
<p className="text-sm text-white/50 mb-1">{i.businessModel.amortization}</p>
|
||||
<p className="text-2xl font-bold text-white">24 {i.businessModel.months}</p>
|
||||
<p className="text-xs text-white/30">{lang === 'de' ? 'Hardware-Amortisation' : 'Hardware Amortization'}</p>
|
||||
<p className="text-2xl font-bold text-white">{amortMonths} {i.businessModel.months}</p>
|
||||
<p className="text-xs text-white/30">{lang === 'de' ? 'max. Hardware-Amortisation' : 'max. Hardware Amortization'}</p>
|
||||
</GlassCard>
|
||||
</div>
|
||||
|
||||
@@ -52,7 +96,7 @@ export default function BusinessModelSlide({ lang, products }: BusinessModelSlid
|
||||
<h3 className="text-lg font-semibold mb-4 text-white/70">{i.businessModel.unitEconomics}</h3>
|
||||
<div className="grid md:grid-cols-3 gap-4">
|
||||
{products.map((p, idx) => {
|
||||
const amort = p.hardware_cost_eur > 0 ? Math.round(p.hardware_cost_eur / 24) : 0
|
||||
const amort = p.hardware_cost_eur > 0 ? Math.round(p.hardware_cost_eur / AMORT_MONTHS) : 0
|
||||
const monthlyMargin = p.monthly_price_eur - amort - (p.operating_cost_eur > 0 ? p.operating_cost_eur : 0)
|
||||
const marginPct = Math.round((monthlyMargin / p.monthly_price_eur) * 100)
|
||||
|
||||
@@ -79,7 +123,7 @@ export default function BusinessModelSlide({ lang, products }: BusinessModelSlid
|
||||
{p.operating_cost_eur > 0 && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-white/50">{i.businessModel.operatingCost}</span>
|
||||
<span className="text-white/70">-{p.operating_cost_eur.toLocaleString('de-DE')} EUR/Mo</span>
|
||||
<span className="text-white/70">-{p.operating_cost_eur} EUR/Mo</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="border-t border-white/10 pt-2 flex justify-between">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import { motion } from 'framer-motion'
|
||||
import { Language } from '@/lib/types'
|
||||
import { Language, PitchFunding } from '@/lib/types'
|
||||
import { t } from '@/lib/i18n'
|
||||
import { ArrowRight } from 'lucide-react'
|
||||
import GradientText from '../ui/GradientText'
|
||||
@@ -10,11 +10,36 @@ import BrandName from '../ui/BrandName'
|
||||
interface CoverSlideProps {
|
||||
lang: Language
|
||||
onNext: () => void
|
||||
funding?: PitchFunding
|
||||
}
|
||||
|
||||
export default function CoverSlide({ lang, onNext }: CoverSlideProps) {
|
||||
function formatRoundLabel(funding: PitchFunding | undefined): string {
|
||||
if (!funding) return 'Pre-Seed'
|
||||
// Extract a short round label from round_name
|
||||
const name = funding.round_name || ''
|
||||
if (name.toLowerCase().includes('seed')) return 'Pre-Seed'
|
||||
if (name.toLowerCase().includes('series a')) return 'Series A'
|
||||
return 'Pre-Seed'
|
||||
}
|
||||
|
||||
function formatQuarter(dateStr: string | undefined): string {
|
||||
if (!dateStr) return ''
|
||||
try {
|
||||
const d = new Date(dateStr)
|
||||
const quarter = Math.ceil((d.getMonth() + 1) / 3)
|
||||
return `Q${quarter} ${d.getFullYear()}`
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
export default function CoverSlide({ lang, onNext, funding }: CoverSlideProps) {
|
||||
const i = t(lang)
|
||||
|
||||
const roundLabel = formatRoundLabel(funding)
|
||||
const quarter = formatQuarter(funding?.target_date)
|
||||
const subtitle = quarter ? `${roundLabel} · ${quarter}` : roundLabel
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center text-center min-h-[70vh]">
|
||||
{/* Logo / Brand */}
|
||||
@@ -63,14 +88,14 @@ export default function CoverSlide({ lang, onNext }: CoverSlideProps) {
|
||||
{i.cover.tagline}
|
||||
</motion.p>
|
||||
|
||||
{/* Subtitle */}
|
||||
{/* Subtitle — dynamisch aus Funding */}
|
||||
<motion.p
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.5, delay: 0.8 }}
|
||||
className="text-sm text-white/30 font-mono tracking-wider mb-12"
|
||||
>
|
||||
{i.cover.subtitle}
|
||||
{subtitle}
|
||||
</motion.p>
|
||||
|
||||
{/* CTA */}
|
||||
|
||||
274
pitch-deck/components/slides/EngineeringSlide.tsx
Normal file
274
pitch-deck/components/slides/EngineeringSlide.tsx
Normal file
@@ -0,0 +1,274 @@
|
||||
'use client'
|
||||
|
||||
import { Language } from '@/lib/types'
|
||||
import { t } from '@/lib/i18n'
|
||||
import GradientText from '../ui/GradientText'
|
||||
import FadeInView from '../ui/FadeInView'
|
||||
import GlassCard from '../ui/GlassCard'
|
||||
import {
|
||||
Code2,
|
||||
Container,
|
||||
GitBranch,
|
||||
Layers,
|
||||
ShieldCheck,
|
||||
Terminal,
|
||||
Cpu,
|
||||
Database,
|
||||
Braces,
|
||||
FileCode2,
|
||||
Server,
|
||||
Workflow,
|
||||
} from 'lucide-react'
|
||||
|
||||
interface EngineeringSlideProps {
|
||||
lang: Language
|
||||
}
|
||||
|
||||
export default function EngineeringSlide({ lang }: EngineeringSlideProps) {
|
||||
const i = t(lang)
|
||||
const de = lang === 'de'
|
||||
|
||||
const heroStats = [
|
||||
{
|
||||
value: '691K',
|
||||
label: de ? 'Zeilen Code' : 'Lines of Code',
|
||||
sub: 'Go · Python · TypeScript',
|
||||
color: 'text-indigo-400',
|
||||
borderColor: 'border-indigo-500/30',
|
||||
},
|
||||
{
|
||||
value: '45',
|
||||
label: de ? 'Docker Container' : 'Docker Containers',
|
||||
sub: de ? 'Produktiv auf einem Mac Studio' : 'Production on one Mac Studio',
|
||||
color: 'text-emerald-400',
|
||||
borderColor: 'border-emerald-500/30',
|
||||
},
|
||||
{
|
||||
value: '27',
|
||||
label: de ? 'Microservices' : 'Microservices',
|
||||
sub: '10 Go · 9 Python · 8 Next.js',
|
||||
color: 'text-purple-400',
|
||||
borderColor: 'border-purple-500/30',
|
||||
},
|
||||
{
|
||||
value: '37',
|
||||
label: 'Dockerfiles',
|
||||
sub: de ? 'Vollstaendig containerisiert' : 'Fully containerized',
|
||||
color: 'text-amber-400',
|
||||
borderColor: 'border-amber-500/30',
|
||||
},
|
||||
]
|
||||
|
||||
const languageBreakdown = [
|
||||
{ lang: 'TypeScript / TSX', pct: 58, loc: '403K', color: 'bg-blue-500', icon: Braces },
|
||||
{ lang: 'Python', pct: 23, loc: '160K', color: 'bg-yellow-500', icon: Terminal },
|
||||
{ lang: 'Go', pct: 18, loc: '127K', color: 'bg-cyan-500', icon: Code2 },
|
||||
]
|
||||
|
||||
const devopsStack = [
|
||||
{
|
||||
icon: GitBranch,
|
||||
label: 'Gitea',
|
||||
desc: de ? 'Self-hosted Git · 4 Repos · Code Review' : 'Self-hosted Git · 4 Repos · Code Review',
|
||||
},
|
||||
{
|
||||
icon: Workflow,
|
||||
label: 'Woodpecker CI',
|
||||
desc: de ? 'Self-hosted CI/CD · Lint · Test · Build · Deploy' : 'Self-hosted CI/CD · Lint · Test · Build · Deploy',
|
||||
},
|
||||
{
|
||||
icon: Container,
|
||||
label: 'Docker Compose',
|
||||
desc: de ? '66 Service-Definitionen · Multi-Stage Builds' : '66 Service Definitions · Multi-Stage Builds',
|
||||
},
|
||||
{
|
||||
icon: ShieldCheck,
|
||||
label: 'DevSecOps',
|
||||
desc: 'Semgrep · Trivy · Gitleaks · CycloneDX SBOM',
|
||||
},
|
||||
{
|
||||
icon: Database,
|
||||
label: 'HashiCorp Vault',
|
||||
desc: de ? 'Secrets Management · Auto-Rotation · PKI' : 'Secrets Management · Auto-Rotation · PKI',
|
||||
},
|
||||
{
|
||||
icon: Server,
|
||||
label: de ? 'Infrastruktur' : 'Infrastructure',
|
||||
desc: 'Nginx · PostgreSQL · Qdrant · MinIO · Valkey',
|
||||
},
|
||||
]
|
||||
|
||||
const serviceArchitecture = [
|
||||
{
|
||||
project: 'breakpilot-core',
|
||||
color: 'text-indigo-400',
|
||||
dotColor: 'bg-indigo-400',
|
||||
services: de
|
||||
? ['Admin Dashboard', 'Consent Service (Go)', 'Billing Service (Go)', 'RAG Pipeline', 'Embedding Service', 'Voice Service', 'Pitch Deck', 'Nginx Reverse Proxy']
|
||||
: ['Admin Dashboard', 'Consent Service (Go)', 'Billing Service (Go)', 'RAG Pipeline', 'Embedding Service', 'Voice Service', 'Pitch Deck', 'Nginx Reverse Proxy'],
|
||||
},
|
||||
{
|
||||
project: 'breakpilot-lehrer',
|
||||
color: 'text-purple-400',
|
||||
dotColor: 'bg-purple-400',
|
||||
services: de
|
||||
? ['Lehrer Dashboard', 'Studio v2', 'Website', 'Klausur Service', 'School Service (Go)', 'Edu Search (Go)']
|
||||
: ['Teacher Dashboard', 'Studio v2', 'Website', 'Exam Service', 'School Service (Go)', 'Edu Search (Go)'],
|
||||
},
|
||||
{
|
||||
project: 'breakpilot-compliance',
|
||||
color: 'text-emerald-400',
|
||||
dotColor: 'bg-emerald-400',
|
||||
services: de
|
||||
? ['Compliance Dashboard', 'Developer Portal', 'AI SDK (Go)', 'Document Crawler', 'DSMS Gateway', 'Security Scanner (Go)']
|
||||
: ['Compliance Dashboard', 'Developer Portal', 'AI SDK (Go)', 'Document Crawler', 'DSMS Gateway', 'Security Scanner (Go)'],
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div>
|
||||
<FadeInView className="text-center mb-6">
|
||||
<p className="text-xs font-mono text-indigo-400/60 uppercase tracking-widest mb-2">
|
||||
{de ? 'Anhang' : 'Appendix'}
|
||||
</p>
|
||||
<h2 className="text-4xl md:text-5xl font-bold mb-2">
|
||||
<GradientText>{i.annex.engineering.title}</GradientText>
|
||||
</h2>
|
||||
<p className="text-lg text-white/50 max-w-2xl mx-auto">{i.annex.engineering.subtitle}</p>
|
||||
</FadeInView>
|
||||
|
||||
{/* Hero Stats */}
|
||||
<FadeInView delay={0.1}>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 mb-5">
|
||||
{heroStats.map((stat, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className={`border rounded-xl p-3 bg-white/[0.03] text-center ${stat.borderColor}`}
|
||||
>
|
||||
<p className={`text-3xl font-black tracking-tight ${stat.color}`}>{stat.value}</p>
|
||||
<p className="text-xs font-semibold text-white/70 mt-0.5">{stat.label}</p>
|
||||
<p className="text-[10px] text-white/30 mt-0.5">{stat.sub}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</FadeInView>
|
||||
|
||||
<div className="grid md:grid-cols-12 gap-4">
|
||||
{/* Left Column: Language Breakdown + Service Map */}
|
||||
<div className="md:col-span-5 space-y-4">
|
||||
{/* Language Breakdown */}
|
||||
<FadeInView delay={0.2}>
|
||||
<GlassCard hover={false} className="p-4">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<FileCode2 className="w-4 h-4 text-white/40" />
|
||||
<p className="text-xs font-semibold text-white/40 uppercase tracking-wider">
|
||||
{de ? 'Sprachen-Mix' : 'Language Mix'}
|
||||
</p>
|
||||
</div>
|
||||
{/* Stacked bar */}
|
||||
<div className="flex rounded-full overflow-hidden h-3 mb-3">
|
||||
{languageBreakdown.map((l, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className={`${l.color} transition-all`}
|
||||
style={{ width: `${l.pct}%` }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
{languageBreakdown.map((l, idx) => {
|
||||
const Icon = l.icon
|
||||
return (
|
||||
<div key={idx} className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className={`w-2 h-2 rounded-full ${l.color}`} />
|
||||
<Icon className="w-3 h-3 text-white/40" />
|
||||
<span className="text-xs text-white/60">{l.lang}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs font-mono text-white/40">{l.loc}</span>
|
||||
<span className="text-xs font-bold text-white/70">{l.pct}%</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</GlassCard>
|
||||
</FadeInView>
|
||||
|
||||
{/* Service Map */}
|
||||
<FadeInView delay={0.3}>
|
||||
<GlassCard hover={false} className="p-4">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Layers className="w-4 h-4 text-white/40" />
|
||||
<p className="text-xs font-semibold text-white/40 uppercase tracking-wider">
|
||||
{de ? 'Service-Architektur' : 'Service Architecture'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{serviceArchitecture.map((proj, idx) => (
|
||||
<div key={idx}>
|
||||
<p className={`text-[10px] font-bold uppercase tracking-wider ${proj.color} mb-1`}>
|
||||
{proj.project}
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{proj.services.map((svc, sidx) => (
|
||||
<span
|
||||
key={sidx}
|
||||
className="text-[10px] px-1.5 py-0.5 rounded bg-white/[0.05] text-white/50 border border-white/[0.06]"
|
||||
>
|
||||
{svc}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</GlassCard>
|
||||
</FadeInView>
|
||||
</div>
|
||||
|
||||
{/* Right Column: DevOps Stack */}
|
||||
<div className="md:col-span-7">
|
||||
<FadeInView delay={0.25}>
|
||||
<GlassCard hover={false} className="p-4 h-full">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Cpu className="w-4 h-4 text-white/40" />
|
||||
<p className="text-xs font-semibold text-white/40 uppercase tracking-wider">
|
||||
{de ? 'DevOps & Toolchain' : 'DevOps & Toolchain'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{devopsStack.map((tool, idx) => {
|
||||
const Icon = tool.icon
|
||||
return (
|
||||
<div
|
||||
key={idx}
|
||||
className="flex items-start gap-3 p-2.5 rounded-lg bg-white/[0.03] border border-white/5"
|
||||
>
|
||||
<div className="w-8 h-8 rounded-lg bg-indigo-500/10 border border-indigo-500/20 flex items-center justify-center shrink-0">
|
||||
<Icon className="w-4 h-4 text-indigo-400" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-white/80">{tool.label}</p>
|
||||
<p className="text-xs text-white/40 mt-0.5">{tool.desc}</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
{/* Footer note */}
|
||||
<div className="mt-3 pt-3 border-t border-white/5">
|
||||
<p className="text-[10px] text-white/20 text-center">
|
||||
{de
|
||||
? '100% Self-Hosted · Kein externer Cloud-Anbieter · Vollstaendige Kontrolle ueber Code und Daten'
|
||||
: '100% Self-Hosted · No External Cloud Provider · Full Control Over Code and Data'}
|
||||
</p>
|
||||
</div>
|
||||
</GlassCard>
|
||||
</FadeInView>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
141
pitch-deck/components/slides/GTMSlide.tsx
Normal file
141
pitch-deck/components/slides/GTMSlide.tsx
Normal file
@@ -0,0 +1,141 @@
|
||||
'use client'
|
||||
|
||||
import { Language } from '@/lib/types'
|
||||
import { t } from '@/lib/i18n'
|
||||
import GradientText from '../ui/GradientText'
|
||||
import FadeInView from '../ui/FadeInView'
|
||||
import GlassCard from '../ui/GlassCard'
|
||||
import { Target, Users, Handshake, Megaphone, Building2, GraduationCap } from 'lucide-react'
|
||||
|
||||
interface GTMSlideProps {
|
||||
lang: Language
|
||||
}
|
||||
|
||||
export default function GTMSlide({ lang }: GTMSlideProps) {
|
||||
const i = t(lang)
|
||||
const de = lang === 'de'
|
||||
|
||||
const phases = [
|
||||
{
|
||||
phase: de ? 'Phase 1: Pilot (2026)' : 'Phase 1: Pilot (2026)',
|
||||
color: 'border-indigo-500/30 bg-indigo-500/5',
|
||||
textColor: 'text-indigo-400',
|
||||
items: [
|
||||
de ? 'Direktvertrieb an 5-20 KMU in DACH' : 'Direct sales to 5-20 SMEs in DACH',
|
||||
de ? 'Fokus: Gesundheitswesen, Finanzdienstleister, Rechtsanwaelte' : 'Focus: Healthcare, Financial Services, Law Firms',
|
||||
de ? 'Persoenliches Onboarding, White-Glove-Service' : 'Personal onboarding, white-glove service',
|
||||
de ? 'Case Studies und Referenzkunden aufbauen' : 'Build case studies and reference customers',
|
||||
],
|
||||
},
|
||||
{
|
||||
phase: de ? 'Phase 2: Skalierung (2027)' : 'Phase 2: Scale (2027)',
|
||||
color: 'border-purple-500/30 bg-purple-500/5',
|
||||
textColor: 'text-purple-400',
|
||||
items: [
|
||||
de ? 'Channel-Partnerschaften mit IT-Systemhaeusern' : 'Channel partnerships with IT system integrators',
|
||||
de ? 'IHK- und Handwerkskammer-Kooperationen' : 'Chamber of Commerce & Industry partnerships',
|
||||
de ? 'Content Marketing: Compliance-Webinare, Whitepaper' : 'Content marketing: Compliance webinars, whitepapers',
|
||||
de ? 'Zielkunden: 50-200 in regulierten Branchen' : 'Target: 50-200 customers in regulated industries',
|
||||
],
|
||||
},
|
||||
{
|
||||
phase: de ? 'Phase 3: Expansion (2028+)' : 'Phase 3: Expansion (2028+)',
|
||||
color: 'border-emerald-500/30 bg-emerald-500/5',
|
||||
textColor: 'text-emerald-400',
|
||||
items: [
|
||||
de ? 'Cloud-Tier fuer groessere Unternehmen (50-500 MA)' : 'Cloud tier for larger companies (50-500 employees)',
|
||||
de ? 'EU-Expansion: Oesterreich, Schweiz, Benelux, Nordics' : 'EU expansion: Austria, Switzerland, Benelux, Nordics',
|
||||
de ? 'OEM/Whitelabel fuer Steuerberater und Wirtschaftspruefer' : 'OEM/whitelabel for tax advisors and auditors',
|
||||
de ? 'Self-Service-Onboarding und PLG-Motion' : 'Self-service onboarding and PLG motion',
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const channels = [
|
||||
{ icon: Target, label: de ? 'Direktvertrieb' : 'Direct Sales', pct: '40%', desc: de ? 'Outbound + Inbound, 2 AEs ab 2027' : 'Outbound + Inbound, 2 AEs from 2027' },
|
||||
{ icon: Handshake, label: de ? 'Channel-Partner' : 'Channel Partners', pct: '30%', desc: de ? 'IT-Haendler, Systemhaeuser, MSPs' : 'IT resellers, system integrators, MSPs' },
|
||||
{ icon: Megaphone, label: de ? 'Content & Events' : 'Content & Events', pct: '20%', desc: de ? 'Webinare, Messen (it-sa), SEO' : 'Webinars, trade shows (it-sa), SEO' },
|
||||
{ icon: Users, label: de ? 'Empfehlungen' : 'Referrals', pct: '10%', desc: de ? 'Bestandskunden-Empfehlungsprogramm' : 'Customer referral program' },
|
||||
]
|
||||
|
||||
const idealCustomer = [
|
||||
{ icon: Building2, label: de ? '10-250 Mitarbeiter' : '10-250 Employees' },
|
||||
{ icon: GraduationCap, label: de ? 'Regulierte Branche (Gesundheit, Finanzen, Energie, KRITIS)' : 'Regulated Industry (Healthcare, Finance, Energy, Critical Infrastructure)' },
|
||||
{ icon: Target, label: de ? 'Kein interner Compliance-Officer oder DSB' : 'No Internal Compliance Officer or DPO' },
|
||||
]
|
||||
|
||||
return (
|
||||
<div>
|
||||
<FadeInView className="text-center mb-8">
|
||||
<p className="text-xs font-mono text-indigo-400/60 uppercase tracking-widest mb-2">
|
||||
{de ? 'Anhang' : 'Appendix'}
|
||||
</p>
|
||||
<h2 className="text-4xl md:text-5xl font-bold mb-3">
|
||||
<GradientText>{i.annex.gtm.title}</GradientText>
|
||||
</h2>
|
||||
<p className="text-lg text-white/50 max-w-2xl mx-auto">{i.annex.gtm.subtitle}</p>
|
||||
</FadeInView>
|
||||
|
||||
{/* ICP */}
|
||||
<FadeInView delay={0.15}>
|
||||
<GlassCard hover={false} className="p-4 mb-6">
|
||||
<p className="text-xs font-semibold text-white/40 uppercase tracking-wider mb-3">
|
||||
{de ? 'Ideales Kundenprofil (ICP)' : 'Ideal Customer Profile (ICP)'}
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-4">
|
||||
{idealCustomer.map((ic, idx) => {
|
||||
const Icon = ic.icon
|
||||
return (
|
||||
<div key={idx} className="flex items-center gap-2">
|
||||
<Icon className="w-4 h-4 text-indigo-400" />
|
||||
<span className="text-sm text-white/70">{ic.label}</span>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</GlassCard>
|
||||
</FadeInView>
|
||||
|
||||
{/* Phases */}
|
||||
<div className="grid md:grid-cols-3 gap-4 mb-6">
|
||||
{phases.map((phase, idx) => (
|
||||
<FadeInView key={idx} delay={0.2 + idx * 0.1}>
|
||||
<div className={`border rounded-xl p-4 h-full ${phase.color}`}>
|
||||
<p className={`text-sm font-bold ${phase.textColor} mb-3`}>{phase.phase}</p>
|
||||
<ul className="space-y-2">
|
||||
{phase.items.map((item, iidx) => (
|
||||
<li key={iidx} className="flex items-start gap-2 text-xs text-white/60">
|
||||
<span className={`w-1 h-1 rounded-full mt-1.5 ${phase.textColor} bg-current`} />
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</FadeInView>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Channel Mix */}
|
||||
<FadeInView delay={0.5}>
|
||||
<GlassCard hover={false} className="p-4">
|
||||
<p className="text-xs font-semibold text-white/40 uppercase tracking-wider mb-3">
|
||||
{de ? 'Vertriebskanalmix (Ziel 2028)' : 'Channel Mix (Target 2028)'}
|
||||
</p>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||
{channels.map((ch, idx) => {
|
||||
const Icon = ch.icon
|
||||
return (
|
||||
<div key={idx} className="text-center">
|
||||
<Icon className="w-5 h-5 text-indigo-400 mx-auto mb-1" />
|
||||
<p className="text-lg font-bold text-white">{ch.pct}</p>
|
||||
<p className="text-xs font-semibold text-white/60 mb-0.5">{ch.label}</p>
|
||||
<p className="text-[10px] text-white/30">{ch.desc}</p>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</GlassCard>
|
||||
</FadeInView>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
'use client'
|
||||
|
||||
import { motion } from 'framer-motion'
|
||||
import { useState } from 'react'
|
||||
import { motion, AnimatePresence } from 'framer-motion'
|
||||
import { Language, PitchMarket } from '@/lib/types'
|
||||
import { t, formatEur } from '@/lib/i18n'
|
||||
import { t } from '@/lib/i18n'
|
||||
import { ExternalLink, X, TrendingUp } from 'lucide-react'
|
||||
import GradientText from '../ui/GradientText'
|
||||
import FadeInView from '../ui/FadeInView'
|
||||
import AnimatedCounter from '../ui/AnimatedCounter'
|
||||
@@ -12,14 +14,133 @@ interface MarketSlideProps {
|
||||
market: PitchMarket[]
|
||||
}
|
||||
|
||||
interface MarketSourceInfo {
|
||||
name: string
|
||||
url: string
|
||||
date: string
|
||||
excerpt_de: string
|
||||
excerpt_en: string
|
||||
}
|
||||
|
||||
// Quellenangaben fuer die Marktzahlen
|
||||
const marketSources: Record<string, MarketSourceInfo[]> = {
|
||||
TAM: [
|
||||
{
|
||||
name: 'Grand View Research — GRC Market Report',
|
||||
url: 'https://www.grandviewresearch.com/industry-analysis/governance-risk-management-compliance-market',
|
||||
date: '2024',
|
||||
excerpt_de: 'Der globale GRC-Software-Markt wurde 2023 auf rund 11,8 Mrd. USD bewertet und soll bis 2030 mit einer CAGR von 14,3% auf ca. 35 Mrd. USD wachsen. Compliance-Management ist das am schnellsten wachsende Segment.',
|
||||
excerpt_en: 'The global GRC software market was valued at approximately USD 11.8B in 2023 and is projected to grow at a CAGR of 14.3% to reach ~USD 35B by 2030. Compliance management is the fastest-growing segment.',
|
||||
},
|
||||
],
|
||||
SAM: [
|
||||
{
|
||||
name: 'Statista / IDC — European Compliance Software',
|
||||
url: 'https://www.statista.com/outlook/tmo/software/enterprise-software/compliance-software/europe',
|
||||
date: '2024',
|
||||
excerpt_de: 'Der europaeische Compliance-Software-Markt wird auf ca. 4,2 Mrd. EUR geschaetzt, wobei die DACH-Region (Deutschland, Oesterreich, Schweiz) mit rund 2,1 Mrd. EUR etwa die Haelfte ausmacht. Der Markt waechst mit 18% p.a. — getrieben durch DSGVO, NIS2 und den AI Act.',
|
||||
excerpt_en: 'The European compliance software market is estimated at approx. EUR 4.2B, with the DACH region (Germany, Austria, Switzerland) accounting for roughly EUR 2.1B. The market is growing at 18% p.a. — driven by GDPR, NIS2, and the AI Act.',
|
||||
},
|
||||
],
|
||||
SOM: [
|
||||
{
|
||||
name: 'Eigene Analyse auf Basis von Destatis und KfW-Mittelstandspanel',
|
||||
url: 'https://www.destatis.de/DE/Themen/Branchen-Unternehmen/Unternehmen/Kleine-Unternehmen-Mittlere-Unternehmen/_inhalt.html',
|
||||
date: '2024-2025',
|
||||
excerpt_de: 'In Deutschland gibt es ca. 3,5 Mio. KMU (Destatis). Davon sind geschaetzt 150.000-200.000 in regulierten Branchen (Gesundheit, Finanzen, Energie, KRITIS) mit erhoehtem Compliance-Bedarf. Bei einem durchschnittlichen Jahresumsatz von 900-1.200 EUR pro Kunde ergibt sich ein adressierbarer Markt von ca. 180 Mio. EUR fuer Self-Hosted-Compliance-Loesungen.',
|
||||
excerpt_en: 'Germany has approx. 3.5M SMEs (Destatis). Of these, an estimated 150,000-200,000 operate in regulated industries (healthcare, finance, energy, critical infrastructure) with elevated compliance needs. At an average annual revenue of EUR 900-1,200 per customer, this yields an addressable market of approx. EUR 180M for self-hosted compliance solutions.',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const sizes = [280, 200, 130]
|
||||
const colors = ['border-indigo-500/30 bg-indigo-500/5', 'border-purple-500/30 bg-purple-500/5', 'border-blue-500/30 bg-blue-500/5']
|
||||
const textColors = ['text-indigo-400', 'text-purple-400', 'text-blue-400']
|
||||
|
||||
function SourceModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
segment,
|
||||
lang,
|
||||
}: {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
segment: string
|
||||
lang: Language
|
||||
}) {
|
||||
if (!isOpen) return null
|
||||
const sources = marketSources[segment] || []
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{isOpen && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="fixed inset-0 z-50 flex items-center justify-center p-4"
|
||||
onClick={onClose}
|
||||
>
|
||||
<div className="absolute inset-0 bg-black/60 backdrop-blur-sm" />
|
||||
<motion.div
|
||||
initial={{ scale: 0.9, opacity: 0 }}
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
exit={{ scale: 0.9, opacity: 0 }}
|
||||
className="relative bg-slate-900/95 border border-white/10 rounded-2xl p-6 max-w-2xl w-full max-h-[80vh] overflow-y-auto"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="absolute top-4 right-4 p-1 text-white/40 hover:text-white/80 transition-colors"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
|
||||
<h3 className="text-xl font-bold text-white mb-1">{segment}</h3>
|
||||
<p className="text-sm text-white/40 mb-6">
|
||||
{lang === 'de' ? 'Quellenangaben' : 'Sources'}
|
||||
</p>
|
||||
|
||||
<div className="space-y-4">
|
||||
{sources.map((src, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className="bg-white/[0.05] border border-white/10 rounded-xl p-4"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3 mb-2">
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-white">{src.name}</p>
|
||||
<p className="text-xs text-white/30">{src.date}</p>
|
||||
</div>
|
||||
<a
|
||||
href={src.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex-shrink-0 p-1.5 text-indigo-400 hover:text-indigo-300 hover:bg-indigo-500/10 rounded-lg transition-colors"
|
||||
title={lang === 'de' ? 'Quelle oeffnen' : 'Open source'}
|
||||
>
|
||||
<ExternalLink className="w-4 h-4" />
|
||||
</a>
|
||||
</div>
|
||||
<p className="text-sm text-white/60 leading-relaxed">
|
||||
{lang === 'de' ? src.excerpt_de : src.excerpt_en}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
)
|
||||
}
|
||||
|
||||
export default function MarketSlide({ lang, market }: MarketSlideProps) {
|
||||
const i = t(lang)
|
||||
const labels = [i.market.tamLabel, i.market.samLabel, i.market.somLabel]
|
||||
const segments = [i.market.tam, i.market.sam, i.market.som]
|
||||
const segmentKeys = ['TAM', 'SAM', 'SOM']
|
||||
const [activeModal, setActiveModal] = useState<string | null>(null)
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -56,36 +177,67 @@ export default function MarketSlide({ lang, market }: MarketSlideProps) {
|
||||
|
||||
{/* Labels */}
|
||||
<div className="space-y-6">
|
||||
{market.map((m, idx) => (
|
||||
<motion.div
|
||||
key={m.id}
|
||||
initial={{ opacity: 0, x: 20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: 0.5 + idx * 0.15 }}
|
||||
className="flex items-center gap-4"
|
||||
>
|
||||
<div className={`w-3 h-3 rounded-full ${textColors[idx]} bg-current`} />
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`text-sm font-bold ${textColors[idx]}`}>{segments[idx]}</span>
|
||||
<span className="text-xs text-white/30">{labels[idx]}</span>
|
||||
{market.map((m, idx) => {
|
||||
const segKey = segmentKeys[idx] || m.market_segment
|
||||
const sourceCount = marketSources[segKey]?.length || 0
|
||||
return (
|
||||
<motion.div
|
||||
key={m.id}
|
||||
initial={{ opacity: 0, x: 20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: 0.5 + idx * 0.15 }}
|
||||
className="group cursor-pointer"
|
||||
onClick={() => setActiveModal(segKey)}
|
||||
>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className={`w-3 h-3 rounded-full ${textColors[idx]} bg-current`} />
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`text-sm font-bold ${textColors[idx]}`}>{segments[idx]}</span>
|
||||
<span className="text-xs text-white/30">{labels[idx]}</span>
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-white">
|
||||
<AnimatedCounter
|
||||
target={m.value_eur / 1_000_000_000}
|
||||
suffix={lang === 'de' ? ' Mrd. EUR' : 'B EUR'}
|
||||
decimals={1}
|
||||
duration={1500}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 text-xs">
|
||||
{m.growth_rate_pct > 0 && (
|
||||
<span className="flex items-center gap-1 text-emerald-400">
|
||||
<TrendingUp className="w-3 h-3" />
|
||||
{m.growth_rate_pct}% p.a.
|
||||
</span>
|
||||
)}
|
||||
<span className="text-white/40">
|
||||
{i.market.source}: {m.source}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-[10px] text-indigo-400/60 group-hover:text-indigo-400 transition-colors mt-0.5">
|
||||
{sourceCount} {lang === 'de' ? (sourceCount === 1 ? 'Quelle' : 'Quellen') : (sourceCount === 1 ? 'source' : 'sources')}
|
||||
{' · '}
|
||||
{lang === 'de' ? 'Klicken fuer Details' : 'Click for details'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-white">
|
||||
<AnimatedCounter
|
||||
target={m.value_eur / 1_000_000_000}
|
||||
suffix={lang === 'de' ? ' Mrd. EUR' : 'B EUR'}
|
||||
decimals={1}
|
||||
duration={1500}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-xs text-white/40">
|
||||
{i.market.growth}: {m.growth_rate_pct}% · {i.market.source}: {m.source}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</motion.div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Source Modals */}
|
||||
{segmentKeys.map((seg) => (
|
||||
<SourceModal
|
||||
key={seg}
|
||||
isOpen={activeModal === seg}
|
||||
onClose={() => setActiveModal(null)}
|
||||
segment={seg}
|
||||
lang={lang}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
'use client'
|
||||
|
||||
import { motion } from 'framer-motion'
|
||||
import { useState } from 'react'
|
||||
import { motion, AnimatePresence } from 'framer-motion'
|
||||
import { Language } from '@/lib/types'
|
||||
import { t } from '@/lib/i18n'
|
||||
import { AlertTriangle, Scale, Shield } from 'lucide-react'
|
||||
import { AlertTriangle, Scale, Shield, ExternalLink, X } from 'lucide-react'
|
||||
import GlassCard from '../ui/GlassCard'
|
||||
import GradientText from '../ui/GradientText'
|
||||
import FadeInView from '../ui/FadeInView'
|
||||
@@ -12,10 +13,150 @@ interface ProblemSlideProps {
|
||||
lang: Language
|
||||
}
|
||||
|
||||
interface SourceInfo {
|
||||
name: string
|
||||
url: string
|
||||
date: string
|
||||
excerpt_de: string
|
||||
excerpt_en: string
|
||||
}
|
||||
|
||||
interface ProblemCardData {
|
||||
sources: SourceInfo[]
|
||||
}
|
||||
|
||||
// Quellenangaben fuer jede Behauptung
|
||||
const cardSources: ProblemCardData[] = [
|
||||
{
|
||||
// DSGVO: 4.1 Mrd EUR Bussgelder, 83% KMU
|
||||
sources: [
|
||||
{
|
||||
name: 'GDPR Enforcement Tracker (CMS Law)',
|
||||
url: 'https://www.enforcementtracker.com/',
|
||||
date: '2025',
|
||||
excerpt_de: 'Der GDPR Enforcement Tracker dokumentiert alle oeffentlich bekannten DSGVO-Bussgelder in der EU. Kumuliert belaufen sich die Bussgelder auf ueber 4,1 Mrd. EUR seit Inkrafttreten der DSGVO im Mai 2018.',
|
||||
excerpt_en: 'The GDPR Enforcement Tracker documents all publicly known GDPR fines across the EU. Cumulative fines exceed EUR 4.1 billion since the GDPR took effect in May 2018.',
|
||||
},
|
||||
{
|
||||
name: 'DIHK Digitalisierungsumfrage 2024',
|
||||
url: 'https://www.dihk.de/de/themen-und-positionen/wirtschaft-digital/digitalisierung',
|
||||
date: '2024',
|
||||
excerpt_de: 'Laut der DIHK-Digitalisierungsumfrage 2024 geben 83% der befragten KMU an, die DSGVO-Anforderungen nicht vollstaendig umgesetzt zu haben. Hauptgruende sind mangelnde Ressourcen, fehlendes Know-how und die Komplexitaet der Vorschriften.',
|
||||
excerpt_en: 'According to the DIHK Digitization Survey 2024, 83% of surveyed SMEs report not having fully implemented GDPR requirements. Main reasons cited are lack of resources, missing expertise, and regulatory complexity.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
// AI Act: August 2025
|
||||
sources: [
|
||||
{
|
||||
name: 'EU AI Act — Verordnung (EU) 2024/1689',
|
||||
url: 'https://eur-lex.europa.eu/eli/reg/2024/1689',
|
||||
date: '2024-08-01',
|
||||
excerpt_de: 'Die EU-KI-Verordnung (AI Act) trat am 1. August 2024 in Kraft. Ab dem 2. August 2025 gelten die Verbote fuer KI-Systeme mit unannehmbarem Risiko (Art. 5) sowie die Verpflichtungen fuer Anbieter von KI-Modellen mit allgemeinem Verwendungszweck (Art. 51-56). Ab August 2026 gelten die Anforderungen fuer Hochrisiko-KI-Systeme.',
|
||||
excerpt_en: 'The EU AI Act (Regulation 2024/1689) entered into force on August 1, 2024. From August 2, 2025, prohibitions on unacceptable-risk AI systems (Art. 5) and obligations for general-purpose AI model providers (Art. 51-56) apply. Requirements for high-risk AI systems apply from August 2026.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
// NIS2: 30.000+ Unternehmen
|
||||
sources: [
|
||||
{
|
||||
name: 'BSI — NIS-2-Umsetzungs- und Cybersicherheitsstaerkungsgesetz',
|
||||
url: 'https://www.bsi.bund.de/DE/Themen/Regulierte-Wirtschaft/NIS-2-regulierte-Unternehmen/nis-2-regulierte-unternehmen_node.html',
|
||||
date: '2025',
|
||||
excerpt_de: 'Das NIS-2-Umsetzungsgesetz (NIS2UmsuCG) erweitert den Kreis der regulierten Unternehmen in Deutschland erheblich. Nach Schaetzungen des BSI und des BMI sind kuenftig mehr als 30.000 Unternehmen und Einrichtungen von den neuen Cybersicherheitsanforderungen betroffen — gegenueber bisher ca. 4.500 unter der NIS-1-Richtlinie.',
|
||||
excerpt_en: 'The NIS-2 Implementation Act significantly expands the scope of regulated entities in Germany. According to BSI and BMI estimates, more than 30,000 companies and institutions will be affected by the new cybersecurity requirements — compared to approximately 4,500 under NIS-1.',
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const icons = [AlertTriangle, Scale, Shield]
|
||||
|
||||
function SourceModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
cardIndex,
|
||||
lang,
|
||||
cardTitle,
|
||||
}: {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
cardIndex: number
|
||||
lang: Language
|
||||
cardTitle: string
|
||||
}) {
|
||||
if (!isOpen) return null
|
||||
const sources = cardSources[cardIndex]?.sources || []
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{isOpen && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="fixed inset-0 z-50 flex items-center justify-center p-4"
|
||||
onClick={onClose}
|
||||
>
|
||||
<div className="absolute inset-0 bg-black/60 backdrop-blur-sm" />
|
||||
<motion.div
|
||||
initial={{ scale: 0.9, opacity: 0 }}
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
exit={{ scale: 0.9, opacity: 0 }}
|
||||
className="relative bg-slate-900/95 border border-white/10 rounded-2xl p-6 max-w-2xl w-full max-h-[80vh] overflow-y-auto"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="absolute top-4 right-4 p-1 text-white/40 hover:text-white/80 transition-colors"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
|
||||
<h3 className="text-xl font-bold text-white mb-1">{cardTitle}</h3>
|
||||
<p className="text-sm text-white/40 mb-6">
|
||||
{lang === 'de' ? 'Quellenangaben' : 'Sources'}
|
||||
</p>
|
||||
|
||||
<div className="space-y-4">
|
||||
{sources.map((src, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className="bg-white/[0.05] border border-white/10 rounded-xl p-4"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3 mb-2">
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-white">{src.name}</p>
|
||||
<p className="text-xs text-white/30">{src.date}</p>
|
||||
</div>
|
||||
<a
|
||||
href={src.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex-shrink-0 p-1.5 text-indigo-400 hover:text-indigo-300 hover:bg-indigo-500/10 rounded-lg transition-colors"
|
||||
title={lang === 'de' ? 'Quelle oeffnen' : 'Open source'}
|
||||
>
|
||||
<ExternalLink className="w-4 h-4" />
|
||||
</a>
|
||||
</div>
|
||||
<p className="text-sm text-white/60 leading-relaxed">
|
||||
{lang === 'de' ? src.excerpt_de : src.excerpt_en}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
)
|
||||
}
|
||||
|
||||
export default function ProblemSlide({ lang }: ProblemSlideProps) {
|
||||
const i = t(lang)
|
||||
const [activeModal, setActiveModal] = useState<number | null>(null)
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -29,14 +170,25 @@ export default function ProblemSlide({ lang }: ProblemSlideProps) {
|
||||
<div className="grid md:grid-cols-3 gap-6 mb-12">
|
||||
{i.problem.cards.map((card, idx) => {
|
||||
const Icon = icons[idx]
|
||||
const sourceCount = cardSources[idx]?.sources.length || 0
|
||||
return (
|
||||
<GlassCard key={idx} delay={0.2 + idx * 0.15} className="text-center">
|
||||
<GlassCard
|
||||
key={idx}
|
||||
delay={0.2 + idx * 0.15}
|
||||
className="text-center cursor-pointer group"
|
||||
onClick={() => setActiveModal(idx)}
|
||||
>
|
||||
<div className="w-12 h-12 mx-auto mb-4 rounded-xl bg-red-500/10 flex items-center justify-center">
|
||||
<Icon className="w-6 h-6 text-red-400" />
|
||||
</div>
|
||||
<h3 className="text-lg font-bold mb-2 text-white">{card.title}</h3>
|
||||
<p className="text-3xl font-bold text-red-400 mb-3">{card.stat}</p>
|
||||
<p className="text-sm text-white/50 leading-relaxed">{card.desc}</p>
|
||||
<p className="text-sm text-white/50 leading-relaxed mb-3">{card.desc}</p>
|
||||
<p className="text-[10px] text-indigo-400/60 group-hover:text-indigo-400 transition-colors">
|
||||
{sourceCount} {lang === 'de' ? (sourceCount === 1 ? 'Quelle' : 'Quellen') : (sourceCount === 1 ? 'source' : 'sources')}
|
||||
{' · '}
|
||||
{lang === 'de' ? 'Klicken fuer Details' : 'Click for details'}
|
||||
</p>
|
||||
</GlassCard>
|
||||
)
|
||||
})}
|
||||
@@ -49,6 +201,18 @@ export default function ProblemSlide({ lang }: ProblemSlideProps) {
|
||||
</p>
|
||||
</blockquote>
|
||||
</FadeInView>
|
||||
|
||||
{/* Source Modals */}
|
||||
{i.problem.cards.map((card, idx) => (
|
||||
<SourceModal
|
||||
key={idx}
|
||||
isOpen={activeModal === idx}
|
||||
onClose={() => setActiveModal(null)}
|
||||
cardIndex={idx}
|
||||
lang={lang}
|
||||
cardTitle={card.title}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
271
pitch-deck/components/slides/RegulatorySlide.tsx
Normal file
271
pitch-deck/components/slides/RegulatorySlide.tsx
Normal file
@@ -0,0 +1,271 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { Language } from '@/lib/types'
|
||||
import { t } from '@/lib/i18n'
|
||||
import GradientText from '../ui/GradientText'
|
||||
import FadeInView from '../ui/FadeInView'
|
||||
import GlassCard from '../ui/GlassCard'
|
||||
import { Shield, Scale, Wifi, Calendar, AlertTriangle, CheckCircle2, Clock } from 'lucide-react'
|
||||
|
||||
interface RegulatorySlideProps {
|
||||
lang: Language
|
||||
}
|
||||
|
||||
type RegTab = 'dsgvo' | 'aiact' | 'nis2'
|
||||
|
||||
export default function RegulatorySlide({ lang }: RegulatorySlideProps) {
|
||||
const i = t(lang)
|
||||
const de = lang === 'de'
|
||||
const [activeTab, setActiveTab] = useState<RegTab>('dsgvo')
|
||||
|
||||
const tabs: { id: RegTab; label: string; icon: typeof Shield }[] = [
|
||||
{ id: 'dsgvo', label: de ? 'DSGVO / GDPR' : 'GDPR', icon: Shield },
|
||||
{ id: 'aiact', label: 'AI Act', icon: Scale },
|
||||
{ id: 'nis2', label: 'NIS2', icon: Wifi },
|
||||
]
|
||||
|
||||
const regulations: Record<RegTab, {
|
||||
fullName: string
|
||||
status: string
|
||||
statusColor: string
|
||||
statusIcon: typeof CheckCircle2
|
||||
deadline: string
|
||||
affectedCompanies: string
|
||||
keyRequirements: string[]
|
||||
fines: string
|
||||
howWeHelp: string[]
|
||||
}> = {
|
||||
dsgvo: {
|
||||
fullName: de ? 'Datenschutz-Grundverordnung (EU 2016/679)' : 'General Data Protection Regulation (EU 2016/679)',
|
||||
status: de ? 'In Kraft seit Mai 2018' : 'In effect since May 2018',
|
||||
statusColor: 'text-emerald-400',
|
||||
statusIcon: CheckCircle2,
|
||||
deadline: de ? 'Bereits anzuwenden' : 'Already applicable',
|
||||
affectedCompanies: de ? 'Alle Unternehmen die personenbezogene Daten verarbeiten' : 'All companies processing personal data',
|
||||
keyRequirements: de
|
||||
? [
|
||||
'Verzeichnis von Verarbeitungstaetigkeiten (VVT)',
|
||||
'Datenschutz-Folgenabschaetzung (DSFA)',
|
||||
'Technische und organisatorische Massnahmen (TOM)',
|
||||
'Betroffenenrechte (Auskunft, Loeschung, Portabilitaet)',
|
||||
'Auftragsverarbeitungsvertraege (AVV)',
|
||||
'Datenschutzbeauftragter (ab 20 MA)',
|
||||
'Meldepflicht bei Datenpannen (72h)',
|
||||
]
|
||||
: [
|
||||
'Records of Processing Activities (RoPA)',
|
||||
'Data Protection Impact Assessment (DPIA)',
|
||||
'Technical & Organizational Measures (TOMs)',
|
||||
'Data Subject Rights (Access, Erasure, Portability)',
|
||||
'Data Processing Agreements (DPA)',
|
||||
'Data Protection Officer (from 20 employees)',
|
||||
'Breach Notification (72h)',
|
||||
],
|
||||
fines: de ? 'Bis zu 20 Mio. EUR oder 4% des weltweiten Jahresumsatzes' : 'Up to EUR 20M or 4% of global annual revenue',
|
||||
howWeHelp: de
|
||||
? [
|
||||
'Automatische VVT-Erstellung aus Unternehmensdaten',
|
||||
'KI-gestuetzte DSFA-Durchfuehrung',
|
||||
'TOM-Generator mit Branchenvorlagen',
|
||||
'Self-Service-Portal fuer Betroffenenanfragen',
|
||||
'Automatische Dokumentation und Audit-Trail',
|
||||
]
|
||||
: [
|
||||
'Automatic RoPA generation from company data',
|
||||
'AI-powered DPIA execution',
|
||||
'TOM generator with industry templates',
|
||||
'Self-service portal for data subject requests',
|
||||
'Automatic documentation and audit trail',
|
||||
],
|
||||
},
|
||||
aiact: {
|
||||
fullName: de ? 'KI-Verordnung (EU 2024/1689)' : 'AI Act (EU 2024/1689)',
|
||||
status: de ? 'Schrittweise ab Aug 2025' : 'Phased from Aug 2025',
|
||||
statusColor: 'text-amber-400',
|
||||
statusIcon: Clock,
|
||||
deadline: de ? 'Aug 2025: Verbote · Aug 2026: Hochrisiko · Aug 2027: Vollstaendig' : 'Aug 2025: Prohibitions · Aug 2026: High-Risk · Aug 2027: Full',
|
||||
affectedCompanies: de ? 'Anbieter und Betreiber von KI-Systemen in der EU' : 'Providers and deployers of AI systems in the EU',
|
||||
keyRequirements: de
|
||||
? [
|
||||
'Risikoklassifizierung aller KI-Systeme (Art. 6)',
|
||||
'Konformitaetsbewertung fuer Hochrisiko-KI (Art. 43)',
|
||||
'Technische Dokumentation und Transparenz (Art. 11-13)',
|
||||
'Menschliche Aufsicht (Art. 14)',
|
||||
'Registrierung in EU-Datenbank (Art. 49)',
|
||||
'GPAI-Modell-Pflichten (Art. 51-56)',
|
||||
'Grundrechte-Folgenabschaetzung (Art. 27)',
|
||||
]
|
||||
: [
|
||||
'Risk classification of all AI systems (Art. 6)',
|
||||
'Conformity assessment for high-risk AI (Art. 43)',
|
||||
'Technical documentation and transparency (Art. 11-13)',
|
||||
'Human oversight (Art. 14)',
|
||||
'Registration in EU database (Art. 49)',
|
||||
'GPAI model obligations (Art. 51-56)',
|
||||
'Fundamental rights impact assessment (Art. 27)',
|
||||
],
|
||||
fines: de ? 'Bis zu 35 Mio. EUR oder 7% des weltweiten Jahresumsatzes' : 'Up to EUR 35M or 7% of global annual revenue',
|
||||
howWeHelp: de
|
||||
? [
|
||||
'Automatische Risikoklassifizierung von KI-Systemen',
|
||||
'Konformitaets-Checklisten mit KI-Unterstuetzung',
|
||||
'Technische Dokumentation per Template-Engine',
|
||||
'Audit-Vorbereitung fuer Hochrisiko-Systeme',
|
||||
'Monitoring von Rechtsaenderungen',
|
||||
]
|
||||
: [
|
||||
'Automatic AI system risk classification',
|
||||
'Conformity checklists with AI assistance',
|
||||
'Technical documentation via template engine',
|
||||
'Audit preparation for high-risk systems',
|
||||
'Regulatory change monitoring',
|
||||
],
|
||||
},
|
||||
nis2: {
|
||||
fullName: de ? 'NIS-2-Richtlinie (EU 2022/2555)' : 'NIS2 Directive (EU 2022/2555)',
|
||||
status: de ? 'Umsetzung in nationales Recht laeuft' : 'National transposition in progress',
|
||||
statusColor: 'text-amber-400',
|
||||
statusIcon: Clock,
|
||||
deadline: de ? 'NIS2UmsuCG: voraussichtlich 2025/2026' : 'NIS2 Implementation Act: expected 2025/2026',
|
||||
affectedCompanies: de ? '30.000+ Unternehmen in DE (Energie, Transport, Gesundheit, Digital, KRITIS)' : '30,000+ companies in DE (Energy, Transport, Healthcare, Digital, Critical Infrastructure)',
|
||||
keyRequirements: de
|
||||
? [
|
||||
'Risikomanagement-Massnahmen (Art. 21)',
|
||||
'Incident-Meldepflichten: 24h Fruehwarnung, 72h Bericht (Art. 23)',
|
||||
'Business Continuity und Krisenmanagement',
|
||||
'Supply-Chain-Security (Lieferkettenrisiken)',
|
||||
'Geschaeftsleiterhaftung (persoenliche Haftung)',
|
||||
'Registrierung beim BSI',
|
||||
'Regelmaessige Audits und Nachweise',
|
||||
]
|
||||
: [
|
||||
'Risk management measures (Art. 21)',
|
||||
'Incident reporting: 24h early warning, 72h report (Art. 23)',
|
||||
'Business continuity and crisis management',
|
||||
'Supply chain security',
|
||||
'Management liability (personal liability)',
|
||||
'Registration with national authority (BSI)',
|
||||
'Regular audits and evidence',
|
||||
],
|
||||
fines: de ? 'Bis zu 10 Mio. EUR oder 2% des weltweiten Jahresumsatzes' : 'Up to EUR 10M or 2% of global annual revenue',
|
||||
howWeHelp: de
|
||||
? [
|
||||
'Cybersecurity-Policy-Generator nach BSI-Grundschutz',
|
||||
'Incident-Response-Plaene mit KI-Unterstuetzung',
|
||||
'Supply-Chain-Risikoanalyse',
|
||||
'Automatische Audit-Dokumentation',
|
||||
'NIS2-Readiness-Assessment',
|
||||
]
|
||||
: [
|
||||
'Cybersecurity policy generator based on BSI standards',
|
||||
'AI-assisted incident response plans',
|
||||
'Supply chain risk analysis',
|
||||
'Automatic audit documentation',
|
||||
'NIS2 readiness assessment',
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
const reg = regulations[activeTab]
|
||||
|
||||
return (
|
||||
<div>
|
||||
<FadeInView className="text-center mb-8">
|
||||
<p className="text-xs font-mono text-indigo-400/60 uppercase tracking-widest mb-2">
|
||||
{de ? 'Anhang' : 'Appendix'}
|
||||
</p>
|
||||
<h2 className="text-4xl md:text-5xl font-bold mb-3">
|
||||
<GradientText>{i.annex.regulatory.title}</GradientText>
|
||||
</h2>
|
||||
<p className="text-lg text-white/50 max-w-2xl mx-auto">{i.annex.regulatory.subtitle}</p>
|
||||
</FadeInView>
|
||||
|
||||
{/* Tab Navigation */}
|
||||
<FadeInView delay={0.15}>
|
||||
<div className="flex items-center justify-center gap-2 mb-6">
|
||||
{tabs.map((tab) => {
|
||||
const Icon = tab.icon
|
||||
return (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => setActiveTab(tab.id)}
|
||||
className={`flex items-center gap-2 px-4 py-2 rounded-xl text-sm transition-all
|
||||
${activeTab === tab.id
|
||||
? 'bg-indigo-500/20 text-indigo-300 border border-indigo-500/30'
|
||||
: 'bg-white/[0.04] text-white/40 border border-transparent hover:text-white/60 hover:bg-white/[0.06]'
|
||||
}`}
|
||||
>
|
||||
<Icon className="w-4 h-4" />
|
||||
{tab.label}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</FadeInView>
|
||||
|
||||
{/* Content */}
|
||||
<FadeInView delay={0.2} key={activeTab}>
|
||||
<div className="grid md:grid-cols-12 gap-4">
|
||||
{/* Left: Overview */}
|
||||
<div className="md:col-span-5 space-y-4">
|
||||
<GlassCard hover={false} className="p-4">
|
||||
<h3 className="text-sm font-bold text-white mb-1">{reg.fullName}</h3>
|
||||
<div className="space-y-2 mt-3 text-xs">
|
||||
<div className="flex items-center gap-2">
|
||||
<reg.statusIcon className={`w-4 h-4 ${reg.statusColor}`} />
|
||||
<span className="text-white/60">{reg.status}</span>
|
||||
</div>
|
||||
<div className="flex items-start gap-2">
|
||||
<Calendar className="w-4 h-4 text-white/30 mt-0.5" />
|
||||
<span className="text-white/60">{reg.deadline}</span>
|
||||
</div>
|
||||
<div className="flex items-start gap-2">
|
||||
<AlertTriangle className="w-4 h-4 text-red-400 mt-0.5" />
|
||||
<span className="text-red-400/80">{reg.fines}</span>
|
||||
</div>
|
||||
</div>
|
||||
</GlassCard>
|
||||
|
||||
<GlassCard hover={false} className="p-4">
|
||||
<p className="text-xs font-semibold text-emerald-400 uppercase tracking-wider mb-2">
|
||||
{de ? 'Wie ComplAI hilft' : 'How ComplAI Helps'}
|
||||
</p>
|
||||
<ul className="space-y-1.5">
|
||||
{reg.howWeHelp.map((item, idx) => (
|
||||
<li key={idx} className="flex items-start gap-2 text-xs text-white/60">
|
||||
<CheckCircle2 className="w-3 h-3 text-emerald-400 mt-0.5 shrink-0" />
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</GlassCard>
|
||||
</div>
|
||||
|
||||
{/* Right: Requirements */}
|
||||
<div className="md:col-span-7">
|
||||
<GlassCard hover={false} className="p-4 h-full">
|
||||
<p className="text-xs font-semibold text-white/40 uppercase tracking-wider mb-3">
|
||||
{de ? 'Kernanforderungen' : 'Key Requirements'}
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
{reg.keyRequirements.map((req, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className="flex items-start gap-3 p-2 rounded-lg bg-white/[0.03] border border-white/5"
|
||||
>
|
||||
<span className="text-xs font-mono text-indigo-400/60 mt-0.5 shrink-0 w-4 text-right">{idx + 1}</span>
|
||||
<span className="text-xs text-white/70">{req}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-[10px] text-white/20 mt-3">
|
||||
{de ? 'Betroffene Unternehmen' : 'Affected companies'}: {reg.affectedCompanies}
|
||||
</p>
|
||||
</GlassCard>
|
||||
</div>
|
||||
</div>
|
||||
</FadeInView>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -3,9 +3,10 @@
|
||||
import { motion } from 'framer-motion'
|
||||
import { Language, PitchTeamMember } from '@/lib/types'
|
||||
import { t } from '@/lib/i18n'
|
||||
import { User } from 'lucide-react'
|
||||
import { User, Linkedin } from 'lucide-react'
|
||||
import GradientText from '../ui/GradientText'
|
||||
import FadeInView from '../ui/FadeInView'
|
||||
import Image from 'next/image'
|
||||
|
||||
interface TeamSlideProps {
|
||||
lang: Language
|
||||
@@ -34,14 +35,39 @@ export default function TeamSlide({ lang, team }: TeamSlideProps) {
|
||||
className="bg-white/[0.08] backdrop-blur-xl border border-white/10 rounded-3xl p-8"
|
||||
>
|
||||
<div className="flex items-start gap-5">
|
||||
{/* Avatar */}
|
||||
<div className="w-20 h-20 rounded-2xl bg-gradient-to-br from-indigo-500 to-purple-600
|
||||
flex items-center justify-center shrink-0 shadow-lg">
|
||||
<User className="w-10 h-10 text-white" />
|
||||
</div>
|
||||
{/* Avatar — Foto oder Fallback */}
|
||||
{member.photo_url ? (
|
||||
<div className="w-20 h-20 rounded-2xl overflow-hidden shrink-0 shadow-lg">
|
||||
<Image
|
||||
src={member.photo_url}
|
||||
alt={member.name}
|
||||
width={80}
|
||||
height={80}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-20 h-20 rounded-2xl bg-gradient-to-br from-indigo-500 to-purple-600
|
||||
flex items-center justify-center shrink-0 shadow-lg">
|
||||
<User className="w-10 h-10 text-white" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex-1">
|
||||
<h3 className="text-xl font-bold text-white mb-1">{member.name}</h3>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<h3 className="text-xl font-bold text-white">{member.name}</h3>
|
||||
{member.linkedin_url && (
|
||||
<a
|
||||
href={member.linkedin_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="p-1 text-white/30 hover:text-[#0A66C2] transition-colors"
|
||||
title="LinkedIn"
|
||||
>
|
||||
<Linkedin className="w-4 h-4" />
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-indigo-400 text-sm font-medium mb-3">
|
||||
{lang === 'de' ? member.role_de : member.role_en}
|
||||
</p>
|
||||
|
||||
@@ -17,9 +17,32 @@ interface TheAskSlideProps {
|
||||
|
||||
const COLORS = ['#6366f1', '#a78bfa', '#60a5fa', '#34d399', '#fbbf24']
|
||||
|
||||
function formatFundingAmount(amount: number): { target: number; suffix: string } {
|
||||
if (amount >= 1_000_000) {
|
||||
return { target: Math.round(amount / 100_000) / 10, suffix: ' Mio.' }
|
||||
}
|
||||
if (amount >= 1_000) {
|
||||
return { target: Math.round(amount / 1_000), suffix: 'k' }
|
||||
}
|
||||
return { target: amount, suffix: '' }
|
||||
}
|
||||
|
||||
function formatTargetDate(dateStr: string, lang: Language): string {
|
||||
if (!dateStr) return 'TBD'
|
||||
try {
|
||||
const d = new Date(dateStr)
|
||||
const quarter = Math.ceil((d.getMonth() + 1) / 3)
|
||||
return `Q${quarter} ${d.getFullYear()}`
|
||||
} catch {
|
||||
return dateStr
|
||||
}
|
||||
}
|
||||
|
||||
export default function TheAskSlide({ lang, funding }: TheAskSlideProps) {
|
||||
const i = t(lang)
|
||||
const useOfFunds = funding?.use_of_funds || []
|
||||
const amount = funding?.amount_eur || 0
|
||||
const { target, suffix } = formatFundingAmount(amount)
|
||||
|
||||
const pieData = useOfFunds.map((item) => ({
|
||||
name: lang === 'de' ? item.label_de : item.label_en,
|
||||
@@ -35,7 +58,7 @@ export default function TheAskSlide({ lang, funding }: TheAskSlideProps) {
|
||||
<p className="text-lg text-white/50 max-w-2xl mx-auto">{i.theAsk.subtitle}</p>
|
||||
</FadeInView>
|
||||
|
||||
{/* Main Number */}
|
||||
{/* Main Number — dynamisch aus funding.amount_eur */}
|
||||
<FadeInView delay={0.2} className="text-center mb-10">
|
||||
<motion.div
|
||||
initial={{ scale: 0.5, opacity: 0 }}
|
||||
@@ -43,13 +66,13 @@ export default function TheAskSlide({ lang, funding }: TheAskSlideProps) {
|
||||
transition={{ delay: 0.4, type: 'spring', stiffness: 200 }}
|
||||
>
|
||||
<p className="text-6xl md:text-8xl font-bold text-white mb-2">
|
||||
<AnimatedCounter target={200} suffix="k" duration={2000} />
|
||||
<AnimatedCounter target={target} suffix={suffix} duration={2000} decimals={suffix === ' Mio.' ? 1 : 0} />
|
||||
<span className="text-3xl md:text-4xl text-white/50 ml-2">EUR</span>
|
||||
</p>
|
||||
</motion.div>
|
||||
</FadeInView>
|
||||
|
||||
{/* Details */}
|
||||
{/* Details — dynamisch aus funding-Objekt */}
|
||||
<div className="grid md:grid-cols-3 gap-4 mb-8">
|
||||
<GlassCard delay={0.5} className="text-center p-5">
|
||||
<FileText className="w-6 h-6 text-indigo-400 mx-auto mb-2" />
|
||||
@@ -59,12 +82,14 @@ export default function TheAskSlide({ lang, funding }: TheAskSlideProps) {
|
||||
<GlassCard delay={0.6} className="text-center p-5">
|
||||
<Calendar className="w-6 h-6 text-purple-400 mx-auto mb-2" />
|
||||
<p className="text-xs text-white/40 mb-1">{i.theAsk.targetDate}</p>
|
||||
<p className="text-lg font-bold text-white">Q3 2026</p>
|
||||
<p className="text-lg font-bold text-white">
|
||||
{formatTargetDate(funding?.target_date, lang)}
|
||||
</p>
|
||||
</GlassCard>
|
||||
<GlassCard delay={0.7} className="text-center p-5">
|
||||
<Target className="w-6 h-6 text-blue-400 mx-auto mb-2" />
|
||||
<p className="text-xs text-white/40 mb-1">{lang === 'de' ? 'Runway' : 'Runway'}</p>
|
||||
<p className="text-lg font-bold text-white">18 {lang === 'de' ? 'Monate' : 'Months'}</p>
|
||||
<p className="text-xs text-white/40 mb-1">{lang === 'de' ? 'Runde' : 'Round'}</p>
|
||||
<p className="text-lg font-bold text-white">{funding?.round_name || 'Pre-Seed'}</p>
|
||||
</GlassCard>
|
||||
</div>
|
||||
|
||||
@@ -114,7 +139,7 @@ export default function TheAskSlide({ lang, funding }: TheAskSlideProps) {
|
||||
</span>
|
||||
<span className="text-sm font-bold text-white">{item.percentage}%</span>
|
||||
<span className="text-xs text-white/30">
|
||||
{((funding.amount_eur * item.percentage) / 100).toLocaleString('de-DE')} EUR
|
||||
{((amount * item.percentage) / 100).toLocaleString('de-DE')} EUR
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { motion } from 'framer-motion'
|
||||
import { FMResult } from '@/lib/types'
|
||||
import { Maximize2, Minimize2, ChevronDown, ChevronRight } from 'lucide-react'
|
||||
|
||||
interface AnnualPLTableProps {
|
||||
results: FMResult[]
|
||||
lang: 'de' | 'en'
|
||||
}
|
||||
|
||||
type AccountingStandard = 'hgb' | 'usgaap'
|
||||
|
||||
interface AnnualRow {
|
||||
year: number
|
||||
revenue: number
|
||||
@@ -24,13 +29,397 @@ interface AnnualRow {
|
||||
employees: number
|
||||
}
|
||||
|
||||
interface MonthlyRow {
|
||||
month: number
|
||||
monthInYear: number
|
||||
revenue: number
|
||||
cogs: number
|
||||
grossProfit: number
|
||||
grossMarginPct: number
|
||||
personnel: number
|
||||
marketing: number
|
||||
infra: number
|
||||
totalCosts: number
|
||||
ebitda: number
|
||||
ebitdaMarginPct: number
|
||||
customers: number
|
||||
employees: number
|
||||
mrr: number
|
||||
cashBalance: number
|
||||
}
|
||||
|
||||
function fmt(v: number): string {
|
||||
if (Math.abs(v) >= 1_000_000) return `${(v / 1_000_000).toFixed(1)}M`
|
||||
if (Math.abs(v) >= 1_000) return `${(v / 1_000).toFixed(0)}k`
|
||||
return Math.round(v).toLocaleString('de-DE')
|
||||
}
|
||||
|
||||
function fmtMonth(v: number): string {
|
||||
return Math.round(v).toLocaleString('de-DE')
|
||||
}
|
||||
|
||||
const MONTH_NAMES_DE = ['Jan', 'Feb', 'Mär', 'Apr', 'Mai', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dez']
|
||||
const MONTH_NAMES_EN = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
|
||||
|
||||
function getLineItems(lang: 'de' | 'en', standard: AccountingStandard) {
|
||||
const de = lang === 'de'
|
||||
const hgb = standard === 'hgb'
|
||||
|
||||
return [
|
||||
{
|
||||
label: hgb
|
||||
? (de ? 'Umsatzerloese' : 'Revenue (Umsatzerloese)')
|
||||
: (de ? 'Revenue' : 'Revenue'),
|
||||
key: 'revenue' as keyof AnnualRow,
|
||||
monthKey: 'revenue' as keyof MonthlyRow,
|
||||
isBold: true,
|
||||
},
|
||||
{
|
||||
label: hgb
|
||||
? (de ? '- Herstellungskosten' : '- Cost of Production')
|
||||
: (de ? '- COGS' : '- Cost of Goods Sold'),
|
||||
key: 'cogs' as keyof AnnualRow,
|
||||
monthKey: 'cogs' as keyof MonthlyRow,
|
||||
isNegative: true,
|
||||
},
|
||||
{
|
||||
label: hgb
|
||||
? (de ? '= Rohertrag' : '= Gross Profit')
|
||||
: (de ? '= Gross Profit' : '= Gross Profit'),
|
||||
key: 'grossProfit' as keyof AnnualRow,
|
||||
monthKey: 'grossProfit' as keyof MonthlyRow,
|
||||
isBold: true,
|
||||
isSeparator: true,
|
||||
},
|
||||
{
|
||||
label: hgb
|
||||
? (de ? ' Rohertragsmarge' : ' Gross Margin')
|
||||
: (de ? ' Gross Margin' : ' Gross Margin'),
|
||||
key: 'grossMarginPct' as keyof AnnualRow,
|
||||
monthKey: 'grossMarginPct' as keyof MonthlyRow,
|
||||
isPercent: true,
|
||||
},
|
||||
{
|
||||
label: hgb
|
||||
? (de ? '- Personalaufwand' : '- Personnel Expenses')
|
||||
: (de ? '- Personnel' : '- Personnel'),
|
||||
key: 'personnel' as keyof AnnualRow,
|
||||
monthKey: 'personnel' as keyof MonthlyRow,
|
||||
isNegative: true,
|
||||
},
|
||||
{
|
||||
label: hgb
|
||||
? (de ? '- Vertrieb & Marketing' : '- Sales & Marketing')
|
||||
: (de ? '- Sales & Marketing' : '- Sales & Marketing'),
|
||||
key: 'marketing' as keyof AnnualRow,
|
||||
monthKey: 'marketing' as keyof MonthlyRow,
|
||||
isNegative: true,
|
||||
},
|
||||
{
|
||||
label: hgb
|
||||
? (de ? '- sonstige betriebl. Aufwendungen' : '- Other Operating Expenses')
|
||||
: (de ? '- Infrastructure' : '- Infrastructure'),
|
||||
key: 'infra' as keyof AnnualRow,
|
||||
monthKey: 'infra' as keyof MonthlyRow,
|
||||
isNegative: true,
|
||||
},
|
||||
{
|
||||
label: hgb
|
||||
? (de ? '= Betriebsaufwand gesamt' : '= Total Operating Expenses')
|
||||
: (de ? '= Total OpEx' : '= Total OpEx'),
|
||||
key: 'totalOpex' as keyof AnnualRow,
|
||||
monthKey: 'totalCosts' as keyof MonthlyRow,
|
||||
isBold: true,
|
||||
isSeparator: true,
|
||||
isNegative: true,
|
||||
},
|
||||
{
|
||||
label: hgb
|
||||
? (de ? 'Betriebsergebnis (EBITDA)' : 'Operating Result (EBITDA)')
|
||||
: 'EBITDA',
|
||||
key: 'ebitda' as keyof AnnualRow,
|
||||
monthKey: 'ebitda' as keyof MonthlyRow,
|
||||
isBold: true,
|
||||
isSeparator: true,
|
||||
},
|
||||
{
|
||||
label: hgb
|
||||
? (de ? ' EBITDA-Marge' : ' EBITDA Margin')
|
||||
: (de ? ' EBITDA Margin' : ' EBITDA Margin'),
|
||||
key: 'ebitdaMarginPct' as keyof AnnualRow,
|
||||
monthKey: 'ebitdaMarginPct' as keyof MonthlyRow,
|
||||
isPercent: true,
|
||||
},
|
||||
{
|
||||
label: hgb
|
||||
? (de ? 'Kunden (Stichtag)' : 'Customers (Reporting Date)')
|
||||
: (de ? 'Kunden (Jahresende)' : 'Customers (Year End)'),
|
||||
key: 'customers' as keyof AnnualRow,
|
||||
monthKey: 'customers' as keyof MonthlyRow,
|
||||
},
|
||||
{
|
||||
label: hgb
|
||||
? (de ? 'Mitarbeiter (VZAe)' : 'Employees (FTE)')
|
||||
: (de ? 'Mitarbeiter' : 'Employees'),
|
||||
key: 'employees' as keyof AnnualRow,
|
||||
monthKey: 'employees' as keyof MonthlyRow,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
function AnnualTable({
|
||||
rows,
|
||||
lang,
|
||||
expandedYear,
|
||||
onToggleYear,
|
||||
monthlyData,
|
||||
isFullscreen,
|
||||
standard,
|
||||
}: {
|
||||
rows: AnnualRow[]
|
||||
lang: 'de' | 'en'
|
||||
expandedYear: number | null
|
||||
onToggleYear: (year: number) => void
|
||||
monthlyData: Map<number, MonthlyRow[]>
|
||||
isFullscreen: boolean
|
||||
standard: AccountingStandard
|
||||
}) {
|
||||
const de = lang === 'de'
|
||||
const monthNames = de ? MONTH_NAMES_DE : MONTH_NAMES_EN
|
||||
const lineItems = getLineItems(lang, standard)
|
||||
|
||||
const monthlyExtraItems: { label: string; key: keyof MonthlyRow; isBold?: boolean }[] = isFullscreen ? [
|
||||
{ label: 'MRR', key: 'mrr', isBold: true },
|
||||
{ label: de ? 'Cash-Bestand' : 'Cash Balance', key: 'cashBalance', isBold: true },
|
||||
] : []
|
||||
|
||||
const textSize = isFullscreen ? 'text-xs' : 'text-[11px]'
|
||||
const minColWidth = isFullscreen ? 'min-w-[70px]' : 'min-w-[80px]'
|
||||
|
||||
return (
|
||||
<table className={`w-full ${textSize}`}>
|
||||
<thead>
|
||||
<tr className="border-b border-white/10">
|
||||
<th className={`text-left py-2 pr-4 text-white/40 font-medium ${isFullscreen ? 'min-w-[220px]' : 'min-w-[180px]'}`}>
|
||||
{standard === 'hgb'
|
||||
? (de ? 'GuV-Position (HGB)' : 'P&L Line Item (HGB)')
|
||||
: (de ? 'GuV-Position (US GAAP)' : 'P&L Line Item (US GAAP)')
|
||||
}
|
||||
</th>
|
||||
{rows.map(r => (
|
||||
<th
|
||||
key={r.year}
|
||||
className={`text-right py-2 px-2 text-white/50 font-semibold ${minColWidth} cursor-pointer hover:text-indigo-400 transition-colors`}
|
||||
onClick={() => onToggleYear(r.year)}
|
||||
title={de ? 'Klicken fuer Monatsdetails' : 'Click for monthly details'}
|
||||
>
|
||||
<span className="inline-flex items-center gap-1">
|
||||
{expandedYear === r.year ? (
|
||||
<ChevronDown className="w-3 h-3" />
|
||||
) : (
|
||||
<ChevronRight className="w-3 h-3 opacity-30" />
|
||||
)}
|
||||
{r.year}
|
||||
</span>
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{lineItems.map((item) => (
|
||||
<tr
|
||||
key={item.key}
|
||||
className={item.isSeparator ? 'border-t border-white/10' : ''}
|
||||
>
|
||||
<td className={`py-1.5 pr-4 ${item.isBold ? 'text-white font-semibold' : 'text-white/50'} ${item.isPercent ? 'italic text-white/30' : ''}`}>
|
||||
{item.label}
|
||||
</td>
|
||||
{rows.map(r => {
|
||||
const val = r[item.key] as number
|
||||
return (
|
||||
<td
|
||||
key={r.year}
|
||||
className={`text-right py-1.5 px-2 font-mono
|
||||
${item.isBold ? 'font-semibold' : ''}
|
||||
${item.isPercent ? 'text-white/30 italic' : ''}
|
||||
${!item.isPercent && val < 0 ? 'text-red-400/80' : ''}
|
||||
${!item.isPercent && val > 0 && item.key === 'ebitda' ? 'text-emerald-400' : ''}
|
||||
${!item.isPercent && !item.isBold ? 'text-white/60' : 'text-white'}
|
||||
`}
|
||||
>
|
||||
{item.isPercent
|
||||
? `${val.toFixed(1)}%`
|
||||
: (item.isNegative && val > 0 ? '-' : '') + fmt(Math.abs(val))
|
||||
}
|
||||
</td>
|
||||
)
|
||||
})}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
|
||||
{/* Monthly Drill-Down */}
|
||||
{expandedYear && monthlyData.has(expandedYear) && (
|
||||
<tbody>
|
||||
<tr>
|
||||
<td colSpan={rows.length + 1} className="pt-4 pb-1">
|
||||
<div className="border-t border-indigo-500/30 pt-3">
|
||||
<p className="text-xs font-semibold text-indigo-400 mb-2">
|
||||
{de ? `Monatsdetails ${expandedYear}` : `Monthly Details ${expandedYear}`}
|
||||
</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr className="border-b border-white/10">
|
||||
<td className="text-left py-1 pr-4 text-white/40 font-medium text-[10px]">
|
||||
{de ? 'Monat' : 'Month'}
|
||||
</td>
|
||||
{monthlyData.get(expandedYear)!.map(m => (
|
||||
<td key={m.monthInYear} className="text-right py-1 px-1 text-white/40 font-medium text-[10px]">
|
||||
{monthNames[m.monthInYear - 1]}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
{lineItems.map((item) => {
|
||||
const mKey = item.monthKey
|
||||
if (!mKey) return null
|
||||
return (
|
||||
<tr key={`monthly-${item.key}`} className={item.isSeparator ? 'border-t border-white/5' : ''}>
|
||||
<td className={`py-1 pr-4 text-[10px] ${item.isBold ? 'text-white/70 font-medium' : 'text-white/30'} ${item.isPercent ? 'italic' : ''}`}>
|
||||
{item.label}
|
||||
</td>
|
||||
{monthlyData.get(expandedYear)!.map(m => {
|
||||
const val = m[mKey] as number
|
||||
return (
|
||||
<td
|
||||
key={m.monthInYear}
|
||||
className={`text-right py-1 px-1 font-mono text-[10px]
|
||||
${item.isPercent ? 'text-white/20 italic' : ''}
|
||||
${!item.isPercent && val < 0 ? 'text-red-400/60' : ''}
|
||||
${!item.isPercent && val > 0 && item.key === 'ebitda' ? 'text-emerald-400/70' : ''}
|
||||
${!item.isPercent ? 'text-white/40' : ''}
|
||||
`}
|
||||
>
|
||||
{item.isPercent
|
||||
? `${val.toFixed(0)}%`
|
||||
: (item.isNegative && val > 0 ? '-' : '') + fmtMonth(Math.abs(val))
|
||||
}
|
||||
</td>
|
||||
)
|
||||
})}
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
{monthlyExtraItems.map((item) => (
|
||||
<tr key={`monthly-extra-${item.key}`} className="border-t border-white/5">
|
||||
<td className="py-1 pr-4 text-[10px] text-indigo-300/70 font-medium">{item.label}</td>
|
||||
{monthlyData.get(expandedYear)!.map(m => {
|
||||
const val = m[item.key] as number
|
||||
return (
|
||||
<td key={m.monthInYear} className="text-right py-1 px-1 font-mono text-[10px] text-indigo-300/50">
|
||||
{fmtMonth(Math.round(val))}
|
||||
</td>
|
||||
)
|
||||
})}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
)}
|
||||
</table>
|
||||
)
|
||||
}
|
||||
|
||||
function FullscreenOverlay({
|
||||
children,
|
||||
onClose,
|
||||
lang,
|
||||
standard,
|
||||
onStandardChange,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
onClose: () => void
|
||||
lang: 'de' | 'en'
|
||||
standard: AccountingStandard
|
||||
onStandardChange: (s: AccountingStandard) => void
|
||||
}) {
|
||||
const de = lang === 'de'
|
||||
|
||||
// ESC to close
|
||||
useEffect(() => {
|
||||
const handler = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') onClose()
|
||||
}
|
||||
window.addEventListener('keydown', handler)
|
||||
return () => window.removeEventListener('keydown', handler)
|
||||
}, [onClose])
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-[9999] bg-slate-950/98 backdrop-blur-xl overflow-auto p-6">
|
||||
<div className="max-w-[1400px] mx-auto">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-white">
|
||||
{de ? 'Gewinn- und Verlustrechnung' : 'Profit & Loss Statement'}
|
||||
</h2>
|
||||
<p className="text-xs text-white/40">
|
||||
{de
|
||||
? 'Klicke auf ein Jahr um die Monatsdetails zu sehen · ESC zum Schliessen'
|
||||
: 'Click on a year to see monthly details · ESC to close'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
{/* HGB / US GAAP Toggle */}
|
||||
<div className="flex items-center bg-white/[0.06] border border-white/10 rounded-xl overflow-hidden">
|
||||
<button
|
||||
onClick={() => onStandardChange('hgb')}
|
||||
className={`px-3 py-1.5 text-xs font-medium transition-all ${
|
||||
standard === 'hgb'
|
||||
? 'bg-indigo-500/20 text-indigo-300'
|
||||
: 'text-white/40 hover:text-white/60'
|
||||
}`}
|
||||
>
|
||||
HGB
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onStandardChange('usgaap')}
|
||||
className={`px-3 py-1.5 text-xs font-medium transition-all ${
|
||||
standard === 'usgaap'
|
||||
? 'bg-indigo-500/20 text-indigo-300'
|
||||
: 'text-white/40 hover:text-white/60'
|
||||
}`}
|
||||
>
|
||||
US GAAP
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="flex items-center gap-2 px-3 py-2 bg-white/[0.08] hover:bg-white/[0.12] border border-white/10 rounded-xl text-sm text-white/70 hover:text-white transition-all"
|
||||
>
|
||||
<Minimize2 className="w-4 h-4" />
|
||||
{de ? 'Schliessen' : 'Close'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white/[0.03] border border-white/10 rounded-2xl p-6 overflow-x-auto">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function AnnualPLTable({ results, lang }: AnnualPLTableProps) {
|
||||
const [isFullscreen, setIsFullscreen] = useState(false)
|
||||
const [expandedYear, setExpandedYear] = useState<number | null>(null)
|
||||
const [standard, setStandard] = useState<AccountingStandard>('hgb')
|
||||
const [portalRoot, setPortalRoot] = useState<HTMLElement | null>(null)
|
||||
const de = lang === 'de'
|
||||
|
||||
// Portal mount point
|
||||
useEffect(() => {
|
||||
setPortalRoot(document.body)
|
||||
}, [])
|
||||
|
||||
// Aggregate monthly results into annual
|
||||
const annualMap = new Map<number, FMResult[]>()
|
||||
for (const r of results) {
|
||||
@@ -66,77 +455,120 @@ export default function AnnualPLTable({ results, lang }: AnnualPLTableProps) {
|
||||
}
|
||||
})
|
||||
|
||||
const de = lang === 'de'
|
||||
// Build monthly data for drill-down
|
||||
const monthlyData = new Map<number, MonthlyRow[]>()
|
||||
for (const [year, months] of annualMap.entries()) {
|
||||
monthlyData.set(year, months.map(m => {
|
||||
const grossProfit = m.revenue_eur - m.cogs_eur
|
||||
const totalCosts = m.personnel_eur + m.marketing_eur + m.infra_eur
|
||||
const ebitda = grossProfit - totalCosts
|
||||
return {
|
||||
month: m.month,
|
||||
monthInYear: m.month_in_year,
|
||||
revenue: m.revenue_eur,
|
||||
cogs: m.cogs_eur,
|
||||
grossProfit,
|
||||
grossMarginPct: m.revenue_eur > 0 ? (grossProfit / m.revenue_eur) * 100 : 0,
|
||||
personnel: m.personnel_eur,
|
||||
marketing: m.marketing_eur,
|
||||
infra: m.infra_eur,
|
||||
totalCosts,
|
||||
ebitda,
|
||||
ebitdaMarginPct: m.revenue_eur > 0 ? (ebitda / m.revenue_eur) * 100 : 0,
|
||||
customers: m.total_customers,
|
||||
employees: m.employees_count,
|
||||
mrr: m.mrr_eur,
|
||||
cashBalance: m.cash_balance_eur,
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
const lineItems: { label: string; key: keyof AnnualRow; isBold?: boolean; isPercent?: boolean; isSeparator?: boolean; isNegative?: boolean }[] = [
|
||||
{ label: de ? 'Umsatzerloese' : 'Revenue', key: 'revenue', isBold: true },
|
||||
{ label: de ? '- Herstellungskosten (COGS)' : '- Cost of Goods Sold', key: 'cogs', isNegative: true },
|
||||
{ label: de ? '= Rohertrag (Gross Profit)' : '= Gross Profit', key: 'grossProfit', isBold: true, isSeparator: true },
|
||||
{ label: de ? ' Rohertragsmarge' : ' Gross Margin', key: 'grossMarginPct', isPercent: true },
|
||||
{ label: de ? '- Personalkosten' : '- Personnel', key: 'personnel', isNegative: true },
|
||||
{ label: de ? '- Marketing & Vertrieb' : '- Marketing & Sales', key: 'marketing', isNegative: true },
|
||||
{ label: de ? '- Infrastruktur' : '- Infrastructure', key: 'infra', isNegative: true },
|
||||
{ label: de ? '= OpEx gesamt' : '= Total OpEx', key: 'totalOpex', isBold: true, isSeparator: true, isNegative: true },
|
||||
{ label: 'EBITDA', key: 'ebitda', isBold: true, isSeparator: true },
|
||||
{ label: de ? ' EBITDA-Marge' : ' EBITDA Margin', key: 'ebitdaMarginPct', isPercent: true },
|
||||
{ label: de ? 'Kunden (Jahresende)' : 'Customers (Year End)', key: 'customers' },
|
||||
{ label: de ? 'Mitarbeiter' : 'Employees', key: 'employees' },
|
||||
]
|
||||
const handleToggleYear = (year: number) => {
|
||||
setExpandedYear(prev => prev === year ? null : year)
|
||||
}
|
||||
|
||||
const tableContent = (
|
||||
<AnnualTable
|
||||
rows={rows}
|
||||
lang={lang}
|
||||
expandedYear={expandedYear}
|
||||
onToggleYear={handleToggleYear}
|
||||
monthlyData={monthlyData}
|
||||
isFullscreen={isFullscreen}
|
||||
standard={standard}
|
||||
/>
|
||||
)
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.4 }}
|
||||
className="overflow-x-auto"
|
||||
>
|
||||
<table className="w-full text-[11px]">
|
||||
<thead>
|
||||
<tr className="border-b border-white/10">
|
||||
<th className="text-left py-2 pr-4 text-white/40 font-medium min-w-[180px]">
|
||||
{de ? 'GuV-Position' : 'P&L Line Item'}
|
||||
</th>
|
||||
{rows.map(r => (
|
||||
<th key={r.year} className="text-right py-2 px-2 text-white/50 font-semibold min-w-[80px]">
|
||||
{r.year}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{lineItems.map((item) => (
|
||||
<tr
|
||||
key={item.key}
|
||||
className={`${item.isSeparator ? 'border-t border-white/10' : ''} ${item.isBold ? '' : ''}`}
|
||||
<>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.4 }}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
{/* HGB / US GAAP Toggle (inline) */}
|
||||
<div className="flex items-center bg-white/[0.04] border border-white/5 rounded-lg overflow-hidden">
|
||||
<button
|
||||
onClick={() => setStandard('hgb')}
|
||||
className={`px-2 py-0.5 text-[10px] font-medium transition-all ${
|
||||
standard === 'hgb'
|
||||
? 'bg-indigo-500/20 text-indigo-300'
|
||||
: 'text-white/30 hover:text-white/50'
|
||||
}`}
|
||||
>
|
||||
<td className={`py-1.5 pr-4 ${item.isBold ? 'text-white font-semibold' : 'text-white/50'} ${item.isPercent ? 'italic text-white/30' : ''}`}>
|
||||
{item.label}
|
||||
</td>
|
||||
{rows.map(r => {
|
||||
const val = r[item.key] as number
|
||||
const isNeg = val < 0 || item.isNegative
|
||||
return (
|
||||
<td
|
||||
key={r.year}
|
||||
className={`text-right py-1.5 px-2 font-mono
|
||||
${item.isBold ? 'font-semibold' : ''}
|
||||
${item.isPercent ? 'text-white/30 italic' : ''}
|
||||
${!item.isPercent && val < 0 ? 'text-red-400/80' : ''}
|
||||
${!item.isPercent && val > 0 && item.key === 'ebitda' ? 'text-emerald-400' : ''}
|
||||
${!item.isPercent && !item.isBold ? 'text-white/60' : 'text-white'}
|
||||
`}
|
||||
>
|
||||
{item.isPercent
|
||||
? `${val.toFixed(1)}%`
|
||||
: (item.isNegative && val > 0 ? '-' : '') + fmt(Math.abs(val))
|
||||
}
|
||||
</td>
|
||||
)
|
||||
})}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</motion.div>
|
||||
HGB
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setStandard('usgaap')}
|
||||
className={`px-2 py-0.5 text-[10px] font-medium transition-all ${
|
||||
standard === 'usgaap'
|
||||
? 'bg-indigo-500/20 text-indigo-300'
|
||||
: 'text-white/30 hover:text-white/50'
|
||||
}`}
|
||||
>
|
||||
US GAAP
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setIsFullscreen(true)}
|
||||
className="flex items-center gap-1.5 px-2 py-1 bg-white/[0.06] hover:bg-white/[0.1] border border-white/10 rounded-lg text-[10px] text-white/50 hover:text-white/80 transition-all"
|
||||
title={de ? 'Vollbild' : 'Fullscreen'}
|
||||
>
|
||||
<Maximize2 className="w-3 h-3" />
|
||||
{de ? 'Vollbild' : 'Fullscreen'}
|
||||
</button>
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
{tableContent}
|
||||
</div>
|
||||
<p className="text-[9px] text-white/20 mt-2 text-center">
|
||||
{de
|
||||
? 'Klicke auf ein Jahr fuer Monatsdetails'
|
||||
: 'Click on a year for monthly details'}
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
{/* Fullscreen via Portal — escapes parent stacking context */}
|
||||
{isFullscreen && portalRoot && createPortal(
|
||||
<FullscreenOverlay
|
||||
onClose={() => setIsFullscreen(false)}
|
||||
lang={lang}
|
||||
standard={standard}
|
||||
onStandardChange={setStandard}
|
||||
>
|
||||
<AnnualTable
|
||||
rows={rows}
|
||||
lang={lang}
|
||||
expandedYear={expandedYear}
|
||||
onToggleYear={handleToggleYear}
|
||||
monthlyData={monthlyData}
|
||||
isFullscreen={true}
|
||||
standard={standard}
|
||||
/>
|
||||
</FullscreenOverlay>,
|
||||
portalRoot
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -17,6 +17,12 @@ const SLIDE_ORDER: SlideId[] = [
|
||||
'financials',
|
||||
'the-ask',
|
||||
'ai-qa',
|
||||
'annex-assumptions',
|
||||
'annex-architecture',
|
||||
'annex-gtm',
|
||||
'annex-regulatory',
|
||||
'annex-engineering',
|
||||
'annex-aipipeline',
|
||||
]
|
||||
|
||||
export const TOTAL_SLIDES = SLIDE_ORDER.length
|
||||
|
||||
@@ -21,6 +21,10 @@ const translations = {
|
||||
'Finanzen',
|
||||
'The Ask',
|
||||
'KI Q&A',
|
||||
'Anhang: Annahmen',
|
||||
'Anhang: Architektur',
|
||||
'Anhang: Go-to-Market',
|
||||
'Anhang: Regulatorik',
|
||||
],
|
||||
cover: {
|
||||
tagline: 'Datensouveraenitaet meets KI-Compliance',
|
||||
@@ -190,6 +194,32 @@ const translations = {
|
||||
'Warum Self-Hosting statt Cloud?',
|
||||
],
|
||||
},
|
||||
annex: {
|
||||
assumptions: {
|
||||
title: 'Annahmen & Sensitivitaet',
|
||||
subtitle: 'Drei Szenarien fuer robuste Planung',
|
||||
},
|
||||
architecture: {
|
||||
title: 'Technische Architektur',
|
||||
subtitle: 'Self-Hosted KI-Stack fuer maximale Datensouveraenitaet',
|
||||
},
|
||||
gtm: {
|
||||
title: 'Go-to-Market Strategie',
|
||||
subtitle: 'Vom Pilot zum skalierbaren Vertrieb',
|
||||
},
|
||||
regulatory: {
|
||||
title: 'Regulatorische Details',
|
||||
subtitle: 'Die drei Saeulen der EU-Compliance',
|
||||
},
|
||||
engineering: {
|
||||
title: 'Engineering Deep Dive',
|
||||
subtitle: '691K Zeilen Code \u00b7 45 Container \u00b7 100% Self-Hosted',
|
||||
},
|
||||
aipipeline: {
|
||||
title: 'KI-Pipeline Deep Dive',
|
||||
subtitle: 'RAG \u00b7 Multi-Agent-System \u00b7 Document Intelligence \u00b7 Quality Assurance',
|
||||
},
|
||||
},
|
||||
},
|
||||
en: {
|
||||
nav: {
|
||||
@@ -211,6 +241,10 @@ const translations = {
|
||||
'Financials',
|
||||
'The Ask',
|
||||
'AI Q&A',
|
||||
'Appendix: Assumptions',
|
||||
'Appendix: Architecture',
|
||||
'Appendix: Go-to-Market',
|
||||
'Appendix: Regulatory',
|
||||
],
|
||||
cover: {
|
||||
tagline: 'Data Sovereignty meets AI Compliance',
|
||||
@@ -380,6 +414,32 @@ const translations = {
|
||||
'Why self-hosting instead of cloud?',
|
||||
],
|
||||
},
|
||||
annex: {
|
||||
assumptions: {
|
||||
title: 'Assumptions & Sensitivity',
|
||||
subtitle: 'Three scenarios for robust planning',
|
||||
},
|
||||
architecture: {
|
||||
title: 'Technical Architecture',
|
||||
subtitle: 'Self-hosted AI stack for maximum data sovereignty',
|
||||
},
|
||||
gtm: {
|
||||
title: 'Go-to-Market Strategy',
|
||||
subtitle: 'From pilot to scalable sales',
|
||||
},
|
||||
regulatory: {
|
||||
title: 'Regulatory Details',
|
||||
subtitle: 'The three pillars of EU compliance',
|
||||
},
|
||||
engineering: {
|
||||
title: 'Engineering Deep Dive',
|
||||
subtitle: '691K Lines of Code \u00b7 45 Containers \u00b7 100% Self-Hosted',
|
||||
},
|
||||
aipipeline: {
|
||||
title: 'AI Pipeline Deep Dive',
|
||||
subtitle: 'RAG \u00b7 Multi-Agent System \u00b7 Document Intelligence \u00b7 Quality Assurance',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -214,3 +214,9 @@ export type SlideId =
|
||||
| 'financials'
|
||||
| 'the-ask'
|
||||
| 'ai-qa'
|
||||
| 'annex-assumptions'
|
||||
| 'annex-architecture'
|
||||
| 'annex-gtm'
|
||||
| 'annex-regulatory'
|
||||
| 'annex-engineering'
|
||||
| 'annex-aipipeline'
|
||||
|
||||
12
vault/config.hcl
Normal file
12
vault/config.hcl
Normal file
@@ -0,0 +1,12 @@
|
||||
storage "file" {
|
||||
path = "/vault/data"
|
||||
}
|
||||
|
||||
listener "tcp" {
|
||||
address = "0.0.0.0:8200"
|
||||
tls_disable = 1
|
||||
}
|
||||
|
||||
ui = true
|
||||
api_addr = "http://0.0.0.0:8200"
|
||||
disable_mlock = true
|
||||
@@ -11,6 +11,11 @@
|
||||
|
||||
set -e
|
||||
|
||||
# Load root token from file (persistent storage mode)
|
||||
if [ -z "$VAULT_TOKEN" ] && [ -f /vault/data/root-token ]; then
|
||||
export VAULT_TOKEN=$(cat /vault/data/root-token)
|
||||
fi
|
||||
|
||||
echo "=== Vault PKI Initialization ==="
|
||||
echo "Waiting for Vault to be ready..."
|
||||
|
||||
|
||||
@@ -9,6 +9,11 @@
|
||||
|
||||
set -e
|
||||
|
||||
# Load root token from file (persistent storage mode)
|
||||
if [ -z "$VAULT_TOKEN" ] && [ -f /vault/data/root-token ]; then
|
||||
export VAULT_TOKEN=$(cat /vault/data/root-token)
|
||||
fi
|
||||
|
||||
echo "=== Vault Secret Initialization ==="
|
||||
echo "Waiting for Vault to be ready..."
|
||||
|
||||
|
||||
52
vault/init-vault.sh
Executable file
52
vault/init-vault.sh
Executable file
@@ -0,0 +1,52 @@
|
||||
#!/bin/sh
|
||||
# Vault Init + Unseal Script for persistent (file) storage
|
||||
set -e
|
||||
|
||||
export VAULT_ADDR="http://vault:8200"
|
||||
KEYS_FILE="/vault/data/init-keys.json"
|
||||
|
||||
echo "=== Vault Init/Unseal ==="
|
||||
echo "Waiting for Vault to be ready..."
|
||||
|
||||
until vault status >/dev/null 2>&1 || [ $? -eq 2 ]; do
|
||||
sleep 1
|
||||
done
|
||||
|
||||
INITIALIZED=$(vault status -format=json 2>/dev/null | grep '"initialized"' | tr -d ' ,"' | cut -d: -f2)
|
||||
|
||||
if [ "$INITIALIZED" = "false" ]; then
|
||||
echo "First start — initializing Vault..."
|
||||
vault operator init -key-shares=1 -key-threshold=1 -format=json > "$KEYS_FILE"
|
||||
chmod 600 "$KEYS_FILE"
|
||||
echo "Vault initialized. Keys saved."
|
||||
fi
|
||||
|
||||
SEALED=$(vault status -format=json 2>/dev/null | grep '"sealed"' | tr -d ' ,"' | cut -d: -f2)
|
||||
|
||||
if [ "$SEALED" = "true" ]; then
|
||||
echo "Unsealing Vault..."
|
||||
UNSEAL_KEY=$(grep -A1 unseal_keys_b64 "$KEYS_FILE" | tail -1 | tr -d ' ",')
|
||||
echo "Using key: ${UNSEAL_KEY}"
|
||||
vault operator unseal "$UNSEAL_KEY" > /dev/null
|
||||
echo "Vault unsealed."
|
||||
fi
|
||||
|
||||
# Extract root token
|
||||
ROOT_TOKEN=$(grep root_token "$KEYS_FILE" | tr -d ' ",' | cut -d: -f2)
|
||||
export VAULT_TOKEN="$ROOT_TOKEN"
|
||||
echo "$ROOT_TOKEN" > /vault/data/root-token
|
||||
chmod 600 /vault/data/root-token
|
||||
|
||||
echo "=== Vault ready (persistent file storage) ==="
|
||||
|
||||
# Run PKI init
|
||||
if [ -f /vault/scripts/init-pki.sh ]; then
|
||||
echo "Running PKI initialization..."
|
||||
sh /vault/scripts/init-pki.sh
|
||||
fi
|
||||
|
||||
# Run secrets init
|
||||
if [ -f /vault/scripts/init-secrets.sh ]; then
|
||||
echo "Running secrets initialization..."
|
||||
sh /vault/scripts/init-secrets.sh
|
||||
fi
|
||||
Reference in New Issue
Block a user