From b7d21daa241d88d09eb0c22c945928099da80801 Mon Sep 17 00:00:00 2001 From: Benjamin Boenisch Date: Tue, 17 Feb 2026 15:42:43 +0100 Subject: [PATCH] feat: Add DevSecOps tools, Woodpecker proxy, Vault persistent storage, pitch-deck annex slides - 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 --- .../admin/infrastructure/woodpecker/route.ts | 191 ++++-- backend-core/Dockerfile | 21 +- backend-core/main.py | 2 + backend-core/security_api.py | 3 + backend-core/woodpecker_proxy_api.py | 133 ++++ docker-compose.yml | 1 + docs-src/architecture/sdk-protection.md | 317 ++++++++++ mkdocs.yml | 47 +- nginx/html/index.html | 274 ++++++--- pitch-deck/components/PitchDeck.tsx | 20 +- pitch-deck/components/SlideContainer.tsx | 4 +- .../components/slides/AIPipelineSlide.tsx | 329 ++++++++++ .../components/slides/ArchitectureSlide.tsx | 130 ++++ .../components/slides/AssumptionsSlide.tsx | 198 ++++++ .../components/slides/BusinessModelSlide.tsx | 58 +- pitch-deck/components/slides/CoverSlide.tsx | 33 +- .../components/slides/EngineeringSlide.tsx | 274 +++++++++ pitch-deck/components/slides/GTMSlide.tsx | 141 +++++ pitch-deck/components/slides/MarketSlide.tsx | 210 ++++++- pitch-deck/components/slides/ProblemSlide.tsx | 172 +++++- .../components/slides/RegulatorySlide.tsx | 271 +++++++++ pitch-deck/components/slides/TeamSlide.tsx | 40 +- pitch-deck/components/slides/TheAskSlide.tsx | 39 +- pitch-deck/components/ui/AnnualPLTable.tsx | 568 +++++++++++++++--- pitch-deck/lib/hooks/useSlideNavigation.ts | 6 + pitch-deck/lib/i18n.ts | 60 ++ pitch-deck/lib/types.ts | 6 + vault/config.hcl | 12 + vault/init-pki.sh | 5 + vault/init-secrets.sh | 5 + vault/init-vault.sh | 52 ++ 31 files changed, 3323 insertions(+), 299 deletions(-) create mode 100644 backend-core/woodpecker_proxy_api.py create mode 100644 docs-src/architecture/sdk-protection.md create mode 100644 pitch-deck/components/slides/AIPipelineSlide.tsx create mode 100644 pitch-deck/components/slides/ArchitectureSlide.tsx create mode 100644 pitch-deck/components/slides/AssumptionsSlide.tsx create mode 100644 pitch-deck/components/slides/EngineeringSlide.tsx create mode 100644 pitch-deck/components/slides/GTMSlide.tsx create mode 100644 pitch-deck/components/slides/RegulatorySlide.tsx create mode 100644 vault/config.hcl create mode 100755 vault/init-vault.sh diff --git a/admin-core/app/api/admin/infrastructure/woodpecker/route.ts b/admin-core/app/api/admin/infrastructure/woodpecker/route.ts index 9f0a6c3..7f9fa3a 100644 --- a/admin-core/app/api/admin/infrastructure/woodpecker/route.ts +++ b/admin-core/app/api/admin/infrastructure/woodpecker/route.ts @@ -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 { + // 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 { + 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}`, { diff --git a/backend-core/Dockerfile b/backend-core/Dockerfile index daa2d20..b638317 100644 --- a/backend-core/Dockerfile +++ b/backend-core/Dockerfile @@ -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" diff --git a/backend-core/main.py b/backend-core/main.py index 908f8c1..37e5b98 100644 --- a/backend-core/main.py +++ b/backend-core/main.py @@ -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 diff --git a/backend-core/security_api.py b/backend-core/security_api.py index f86169a..b0dfac2 100644 --- a/backend-core/security_api.py +++ b/backend-core/security_api.py @@ -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) diff --git a/backend-core/woodpecker_proxy_api.py b/backend-core/woodpecker_proxy_api.py new file mode 100644 index 0000000..1a1ba52 --- /dev/null +++ b/backend-core/woodpecker_proxy_api.py @@ -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() diff --git a/docker-compose.yml b/docker-compose.yml index 624a681..8449106 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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 diff --git a/docs-src/architecture/sdk-protection.md b/docs-src/architecture/sdk-protection.md new file mode 100644 index 0000000..66b48de --- /dev/null +++ b/docs-src/architecture/sdk-protection.md @@ -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 | diff --git a/mkdocs.yml b/mkdocs.yml index d7c8ced..50e2a42 100644 --- a/mkdocs.yml +++ b/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: diff --git a/nginx/html/index.html b/nginx/html/index.html index 5be063c..b63cf0e 100644 --- a/nginx/html/index.html +++ b/nginx/html/index.html @@ -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 @@
Projekte
-
+
- -
-
-

Admin Core

-

Infrastruktur, Services, Monitoring

-
macmini:3008/dashboard
-
-
+ + - -
-
-

Website

-

Oeffentliche BreakPilot Website

-
macmini:3000
-
-
+ + + +
+
+

Katalogverwaltung

+

SDK-Kataloge & Auswahltabellen

+
macmini:3007/dashboard
+
+
-
+ +
+
+

Comply Website

+

Marketing-Website fuer den KI Compliance Hub

+
macmini:3010/compliance-hub
+
+
- -
-
Dokumentation
-
- - -
-
-

Lehrer Dokumentation

-

Klausur, Voice, Agent-Core, Studio

-
macmini:8010
-
-
+ +
diff --git a/pitch-deck/components/PitchDeck.tsx b/pitch-deck/components/PitchDeck.tsx index 4605be3..2c7817f 100644 --- a/pitch-deck/components/PitchDeck.tsx +++ b/pitch-deck/components/PitchDeck.tsx @@ -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 + return case 'problem': return case 'solution': @@ -116,6 +122,18 @@ export default function PitchDeck({ lang, onToggleLanguage }: PitchDeckProps) { return case 'ai-qa': return + case 'annex-assumptions': + return + case 'annex-architecture': + return + case 'annex-gtm': + return + case 'annex-regulatory': + return + case 'annex-engineering': + return + case 'annex-aipipeline': + return default: return null } diff --git a/pitch-deck/components/SlideContainer.tsx b/pitch-deck/components/SlideContainer.tsx index 0d84ae1..fc0706b 100644 --- a/pitch-deck/components/SlideContainer.tsx +++ b/pitch-deck/components/SlideContainer.tsx @@ -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" > -
+
{children}
diff --git a/pitch-deck/components/slides/AIPipelineSlide.tsx b/pitch-deck/components/slides/AIPipelineSlide.tsx new file mode 100644 index 0000000..5a02be9 --- /dev/null +++ b/pitch-deck/components/slides/AIPipelineSlide.tsx @@ -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('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 ( +
+ +

+ {de ? 'Anhang' : 'Appendix'} +

+

+ {i.annex.aipipeline.title} +

+

{i.annex.aipipeline.subtitle}

+
+ + {/* Hero Stats */} + +
+ {heroStats.map((stat, idx) => ( +
+

{stat.value}

+

{stat.label}

+

{stat.sub}

+
+ ))} +
+
+ + {/* Tab Navigation */} + +
+ {tabs.map((tab) => { + const Icon = tab.icon + return ( + + ) + })} +
+
+ + {/* Tab Content */} + + {activeTab === 'rag' && ( +
+ {/* Pipeline Flow Visualization */} +
+ {[ + { 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) => ( +
+
+ + {step.label} +
+ {idx < arr.length - 1 && } +
+ ))} +
+ {/* Pipeline Steps */} +
+ {ragPipelineSteps.map((step, idx) => { + const Icon = step.icon + return ( +
+
+ +

{step.title}

+
+
    + {step.items.map((item, iidx) => ( +
  • + + {item} +
  • + ))} +
+
+ ) + })} +
+
+ )} + + {activeTab === 'agents' && ( +
+ {/* Agent List */} +
+ +
+ +

+ {de ? 'Agenten-Fleet' : 'Agent Fleet'} +

+
+
+ {agents.map((agent, idx) => ( +
+
+
+

{agent.name}

+
+

{agent.desc}

+

{agent.soul}

+
+ ))} +
+ +
+ {/* Agent Infrastructure */} +
+ +
+ +

+ {de ? 'Infrastruktur' : 'Infrastructure'} +

+
+
+ {agentInfra.map((inf, idx) => { + const Icon = inf.icon + return ( +
+
+ +
+
+

{inf.label}

+

{inf.desc}

+
+
+ ) + })} +
+
+

+ {de + ? 'Alle Agenten laufen lokal · Kein API-Schluessel erforderlich · DSGVO-konform' + : 'All agents run locally · No API key required · GDPR-compliant'} +

+
+
+
+
+ )} + + {activeTab === 'quality' && ( +
+ {qaFeatures.map((feat, idx) => { + const Icon = feat.icon + return ( + +
+ +

{feat.title}

+
+
    + {feat.items.map((item, iidx) => ( +
  • + + {item} +
  • + ))} +
+
+ ) + })} +
+ )} + +
+ ) +} diff --git a/pitch-deck/components/slides/ArchitectureSlide.tsx b/pitch-deck/components/slides/ArchitectureSlide.tsx new file mode 100644 index 0000000..cd9cf56 --- /dev/null +++ b/pitch-deck/components/slides/ArchitectureSlide.tsx @@ -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 ( +
+ +

+ {de ? 'Anhang' : 'Appendix'} +

+

+ {i.annex.architecture.title} +

+

{i.annex.architecture.subtitle}

+
+ + {/* Architecture Layers */} +
+ {layers.map((layer, idx) => { + const Icon = layer.icon + return ( + +
+
+ +

{layer.title}

+
+
+ {layer.items.map((item, iidx) => ( +
+
+
+ {item.label} + {item.desc} +
+
+ ))} +
+
+ + ) + })} +
+ + {/* Security Bar */} + + +
+ {securityFeatures.map((feat, idx) => { + const Icon = feat.icon + return ( +
+ + {feat.label} +
+ ) + })} +
+
+
+
+ ) +} diff --git a/pitch-deck/components/slides/AssumptionsSlide.tsx b/pitch-deck/components/slides/AssumptionsSlide.tsx new file mode 100644 index 0000000..3e6a919 --- /dev/null +++ b/pitch-deck/components/slides/AssumptionsSlide.tsx @@ -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 ( +
+ +

+ {de ? 'Anhang' : 'Appendix'} +

+

+ {i.annex.assumptions.title} +

+

{i.annex.assumptions.subtitle}

+
+ + {/* Sensitivity Table */} + + + + + + + + + + + + + {rows.map((row, idx) => ( + + + + + + + ))} + +
+ {de ? 'Annahme' : 'Assumption'} + + + Bear + + + + Base + + + + Bull + +
{row.label}{row.bear}{row.base}{row.bull}
+
+
+ + {/* Outcome Summary */} + {baseSummary && ( +
+ {[ + { 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) => ( + +
+

{s.label} Case

+
+
+ ARR 2030 + + {s.summary ? `${(s.summary.final_arr / 1_000_000).toFixed(1)}M` : '-'} + +
+
+ {de ? 'Kunden 2030' : 'Customers 2030'} + + {s.summary?.final_customers?.toLocaleString('de-DE') || '-'} + +
+
+ Break-Even + + {s.summary?.break_even_month ? `${de ? 'Monat' : 'Month'} ${s.summary.break_even_month}` : (de ? 'Nicht erreicht' : 'Not reached')} + +
+
+ LTV/CAC + + {s.summary?.final_ltv_cac ? `${s.summary.final_ltv_cac.toFixed(1)}x` : '-'} + +
+
+
+
+ ))} +
+ )} +
+ ) +} diff --git a/pitch-deck/components/slides/BusinessModelSlide.tsx b/pitch-deck/components/slides/BusinessModelSlide.tsx index 7ba6d68..a31fe89 100644 --- a/pitch-deck/components/slides/BusinessModelSlide.tsx +++ b/pitch-deck/components/slides/BusinessModelSlide.tsx @@ -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 (
@@ -25,7 +65,7 @@ export default function BusinessModelSlide({ lang, products }: BusinessModelSlid

{i.businessModel.subtitle}

- {/* Key Metrics */} + {/* Key Metrics — dynamisch berechnet */}
@@ -36,14 +76,18 @@ export default function BusinessModelSlide({ lang, products }: BusinessModelSlid

{i.businessModel.margin}

-

>70%

-

{lang === 'de' ? 'nach Amortisation' : 'post amortization'}

+

>{weightedMarginAfter}%

+

+ {lang === 'de' ? 'nach Amortisation' : 'post amortization'} + {' · '} + {weightedMarginDuring}% {lang === 'de' ? 'waehrend' : 'during'} +

{i.businessModel.amortization}

-

24 {i.businessModel.months}

-

{lang === 'de' ? 'Hardware-Amortisation' : 'Hardware Amortization'}

+

{amortMonths} {i.businessModel.months}

+

{lang === 'de' ? 'max. Hardware-Amortisation' : 'max. Hardware Amortization'}

@@ -52,7 +96,7 @@ export default function BusinessModelSlide({ lang, products }: BusinessModelSlid

{i.businessModel.unitEconomics}

{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 && (
{i.businessModel.operatingCost} - -{p.operating_cost_eur.toLocaleString('de-DE')} EUR/Mo + -{p.operating_cost_eur} EUR/Mo
)}
diff --git a/pitch-deck/components/slides/CoverSlide.tsx b/pitch-deck/components/slides/CoverSlide.tsx index b4e5570..6cc8aec 100644 --- a/pitch-deck/components/slides/CoverSlide.tsx +++ b/pitch-deck/components/slides/CoverSlide.tsx @@ -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 (
{/* Logo / Brand */} @@ -63,14 +88,14 @@ export default function CoverSlide({ lang, onNext }: CoverSlideProps) { {i.cover.tagline} - {/* Subtitle */} + {/* Subtitle — dynamisch aus Funding */} - {i.cover.subtitle} + {subtitle} {/* CTA */} diff --git a/pitch-deck/components/slides/EngineeringSlide.tsx b/pitch-deck/components/slides/EngineeringSlide.tsx new file mode 100644 index 0000000..bf8e909 --- /dev/null +++ b/pitch-deck/components/slides/EngineeringSlide.tsx @@ -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 ( +
+ +

+ {de ? 'Anhang' : 'Appendix'} +

+

+ {i.annex.engineering.title} +

+

{i.annex.engineering.subtitle}

+
+ + {/* Hero Stats */} + +
+ {heroStats.map((stat, idx) => ( +
+

{stat.value}

+

{stat.label}

+

{stat.sub}

+
+ ))} +
+
+ +
+ {/* Left Column: Language Breakdown + Service Map */} +
+ {/* Language Breakdown */} + + +
+ +

+ {de ? 'Sprachen-Mix' : 'Language Mix'} +

+
+ {/* Stacked bar */} +
+ {languageBreakdown.map((l, idx) => ( +
+ ))} +
+
+ {languageBreakdown.map((l, idx) => { + const Icon = l.icon + return ( +
+
+
+ + {l.lang} +
+
+ {l.loc} + {l.pct}% +
+
+ ) + })} +
+ + + + {/* Service Map */} + + +
+ +

+ {de ? 'Service-Architektur' : 'Service Architecture'} +

+
+
+ {serviceArchitecture.map((proj, idx) => ( +
+

+ {proj.project} +

+
+ {proj.services.map((svc, sidx) => ( + + {svc} + + ))} +
+
+ ))} +
+
+
+
+ + {/* Right Column: DevOps Stack */} +
+ + +
+ +

+ {de ? 'DevOps & Toolchain' : 'DevOps & Toolchain'} +

+
+
+ {devopsStack.map((tool, idx) => { + const Icon = tool.icon + return ( +
+
+ +
+
+

{tool.label}

+

{tool.desc}

+
+
+ ) + })} +
+ {/* Footer note */} +
+

+ {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'} +

+
+
+
+
+
+
+ ) +} diff --git a/pitch-deck/components/slides/GTMSlide.tsx b/pitch-deck/components/slides/GTMSlide.tsx new file mode 100644 index 0000000..e0b6432 --- /dev/null +++ b/pitch-deck/components/slides/GTMSlide.tsx @@ -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 ( +
+ +

+ {de ? 'Anhang' : 'Appendix'} +

+

+ {i.annex.gtm.title} +

+

{i.annex.gtm.subtitle}

+
+ + {/* ICP */} + + +

+ {de ? 'Ideales Kundenprofil (ICP)' : 'Ideal Customer Profile (ICP)'} +

+
+ {idealCustomer.map((ic, idx) => { + const Icon = ic.icon + return ( +
+ + {ic.label} +
+ ) + })} +
+
+
+ + {/* Phases */} +
+ {phases.map((phase, idx) => ( + +
+

{phase.phase}

+
    + {phase.items.map((item, iidx) => ( +
  • + + {item} +
  • + ))} +
+
+
+ ))} +
+ + {/* Channel Mix */} + + +

+ {de ? 'Vertriebskanalmix (Ziel 2028)' : 'Channel Mix (Target 2028)'} +

+
+ {channels.map((ch, idx) => { + const Icon = ch.icon + return ( +
+ +

{ch.pct}

+

{ch.label}

+

{ch.desc}

+
+ ) + })} +
+
+
+
+ ) +} diff --git a/pitch-deck/components/slides/MarketSlide.tsx b/pitch-deck/components/slides/MarketSlide.tsx index d30922b..db6be98 100644 --- a/pitch-deck/components/slides/MarketSlide.tsx +++ b/pitch-deck/components/slides/MarketSlide.tsx @@ -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 = { + 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 ( + + {isOpen && ( + +
+ e.stopPropagation()} + > + + +

{segment}

+

+ {lang === 'de' ? 'Quellenangaben' : 'Sources'} +

+ +
+ {sources.map((src, idx) => ( +
+
+
+

{src.name}

+

{src.date}

+
+ + + +
+

+ {lang === 'de' ? src.excerpt_de : src.excerpt_en} +

+
+ ))} +
+
+ + )} + + ) +} + 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(null) return (
@@ -56,36 +177,67 @@ export default function MarketSlide({ lang, market }: MarketSlideProps) { {/* Labels */}
- {market.map((m, idx) => ( - -
-
-
- {segments[idx]} - {labels[idx]} + {market.map((m, idx) => { + const segKey = segmentKeys[idx] || m.market_segment + const sourceCount = marketSources[segKey]?.length || 0 + return ( + setActiveModal(segKey)} + > +
+
+
+
+ {segments[idx]} + {labels[idx]} +
+
+ +
+
+ {m.growth_rate_pct > 0 && ( + + + {m.growth_rate_pct}% p.a. + + )} + + {i.market.source}: {m.source} + +
+

+ {sourceCount} {lang === 'de' ? (sourceCount === 1 ? 'Quelle' : 'Quellen') : (sourceCount === 1 ? 'source' : 'sources')} + {' · '} + {lang === 'de' ? 'Klicken fuer Details' : 'Click for details'} +

+
-
- -
-
- {i.market.growth}: {m.growth_rate_pct}% · {i.market.source}: {m.source} -
-
-
- ))} + + ) + })}
+ + {/* Source Modals */} + {segmentKeys.map((seg) => ( + setActiveModal(null)} + segment={seg} + lang={lang} + /> + ))}
) } diff --git a/pitch-deck/components/slides/ProblemSlide.tsx b/pitch-deck/components/slides/ProblemSlide.tsx index 1bbcfda..0215717 100644 --- a/pitch-deck/components/slides/ProblemSlide.tsx +++ b/pitch-deck/components/slides/ProblemSlide.tsx @@ -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 ( + + {isOpen && ( + +
+ e.stopPropagation()} + > + + +

{cardTitle}

+

+ {lang === 'de' ? 'Quellenangaben' : 'Sources'} +

+ +
+ {sources.map((src, idx) => ( +
+
+
+

{src.name}

+

{src.date}

+
+ + + +
+

+ {lang === 'de' ? src.excerpt_de : src.excerpt_en} +

+
+ ))} +
+
+ + )} + + ) +} + export default function ProblemSlide({ lang }: ProblemSlideProps) { const i = t(lang) + const [activeModal, setActiveModal] = useState(null) return (
@@ -29,14 +170,25 @@ export default function ProblemSlide({ lang }: ProblemSlideProps) {
{i.problem.cards.map((card, idx) => { const Icon = icons[idx] + const sourceCount = cardSources[idx]?.sources.length || 0 return ( - + setActiveModal(idx)} + >

{card.title}

{card.stat}

-

{card.desc}

+

{card.desc}

+

+ {sourceCount} {lang === 'de' ? (sourceCount === 1 ? 'Quelle' : 'Quellen') : (sourceCount === 1 ? 'source' : 'sources')} + {' · '} + {lang === 'de' ? 'Klicken fuer Details' : 'Click for details'} +

) })} @@ -49,6 +201,18 @@ export default function ProblemSlide({ lang }: ProblemSlideProps) {

+ + {/* Source Modals */} + {i.problem.cards.map((card, idx) => ( + setActiveModal(null)} + cardIndex={idx} + lang={lang} + cardTitle={card.title} + /> + ))}
) } diff --git a/pitch-deck/components/slides/RegulatorySlide.tsx b/pitch-deck/components/slides/RegulatorySlide.tsx new file mode 100644 index 0000000..00388c4 --- /dev/null +++ b/pitch-deck/components/slides/RegulatorySlide.tsx @@ -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('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 = { + 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 ( +
+ +

+ {de ? 'Anhang' : 'Appendix'} +

+

+ {i.annex.regulatory.title} +

+

{i.annex.regulatory.subtitle}

+
+ + {/* Tab Navigation */} + +
+ {tabs.map((tab) => { + const Icon = tab.icon + return ( + + ) + })} +
+
+ + {/* Content */} + +
+ {/* Left: Overview */} +
+ +

{reg.fullName}

+
+
+ + {reg.status} +
+
+ + {reg.deadline} +
+
+ + {reg.fines} +
+
+
+ + +

+ {de ? 'Wie ComplAI hilft' : 'How ComplAI Helps'} +

+
    + {reg.howWeHelp.map((item, idx) => ( +
  • + + {item} +
  • + ))} +
+
+
+ + {/* Right: Requirements */} +
+ +

+ {de ? 'Kernanforderungen' : 'Key Requirements'} +

+
+ {reg.keyRequirements.map((req, idx) => ( +
+ {idx + 1} + {req} +
+ ))} +
+

+ {de ? 'Betroffene Unternehmen' : 'Affected companies'}: {reg.affectedCompanies} +

+
+
+
+
+
+ ) +} diff --git a/pitch-deck/components/slides/TeamSlide.tsx b/pitch-deck/components/slides/TeamSlide.tsx index c5bef4f..f50d033 100644 --- a/pitch-deck/components/slides/TeamSlide.tsx +++ b/pitch-deck/components/slides/TeamSlide.tsx @@ -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" >
- {/* Avatar */} -
- -
+ {/* Avatar — Foto oder Fallback */} + {member.photo_url ? ( +
+ {member.name} +
+ ) : ( +
+ +
+ )}
-

{member.name}

+
+

{member.name}

+ {member.linkedin_url && ( + + + + )} +

{lang === 'de' ? member.role_de : member.role_en}

diff --git a/pitch-deck/components/slides/TheAskSlide.tsx b/pitch-deck/components/slides/TheAskSlide.tsx index 19ae88c..8382fa5 100644 --- a/pitch-deck/components/slides/TheAskSlide.tsx +++ b/pitch-deck/components/slides/TheAskSlide.tsx @@ -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) {

{i.theAsk.subtitle}

- {/* Main Number */} + {/* Main Number — dynamisch aus funding.amount_eur */}

- + EUR

- {/* Details */} + {/* Details — dynamisch aus funding-Objekt */}
@@ -59,12 +82,14 @@ export default function TheAskSlide({ lang, funding }: TheAskSlideProps) {

{i.theAsk.targetDate}

-

Q3 2026

+

+ {formatTargetDate(funding?.target_date, lang)} +

-

{lang === 'de' ? 'Runway' : 'Runway'}

-

18 {lang === 'de' ? 'Monate' : 'Months'}

+

{lang === 'de' ? 'Runde' : 'Round'}

+

{funding?.round_name || 'Pre-Seed'}

@@ -114,7 +139,7 @@ export default function TheAskSlide({ lang, funding }: TheAskSlideProps) { {item.percentage}% - {((funding.amount_eur * item.percentage) / 100).toLocaleString('de-DE')} EUR + {((amount * item.percentage) / 100).toLocaleString('de-DE')} EUR
))} diff --git a/pitch-deck/components/ui/AnnualPLTable.tsx b/pitch-deck/components/ui/AnnualPLTable.tsx index ae76be0..f7e782e 100644 --- a/pitch-deck/components/ui/AnnualPLTable.tsx +++ b/pitch-deck/components/ui/AnnualPLTable.tsx @@ -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 + 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 ( + + + + + {rows.map(r => ( + + ))} + + + + {lineItems.map((item) => ( + + + {rows.map(r => { + const val = r[item.key] as number + return ( + + ) + })} + + ))} + + + {/* Monthly Drill-Down */} + {expandedYear && monthlyData.has(expandedYear) && ( + + + + + + + {monthlyData.get(expandedYear)!.map(m => ( + + ))} + + {lineItems.map((item) => { + const mKey = item.monthKey + if (!mKey) return null + return ( + + + {monthlyData.get(expandedYear)!.map(m => { + const val = m[mKey] as number + return ( + + ) + })} + + ) + })} + {monthlyExtraItems.map((item) => ( + + + {monthlyData.get(expandedYear)!.map(m => { + const val = m[item.key] as number + return ( + + ) + })} + + ))} + + )} +
+ {standard === 'hgb' + ? (de ? 'GuV-Position (HGB)' : 'P&L Line Item (HGB)') + : (de ? 'GuV-Position (US GAAP)' : 'P&L Line Item (US GAAP)') + } + onToggleYear(r.year)} + title={de ? 'Klicken fuer Monatsdetails' : 'Click for monthly details'} + > + + {expandedYear === r.year ? ( + + ) : ( + + )} + {r.year} + +
+ {item.label} + 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)) + } +
+
+

+ {de ? `Monatsdetails ${expandedYear}` : `Monthly Details ${expandedYear}`} +

+
+
+ {de ? 'Monat' : 'Month'} + + {monthNames[m.monthInYear - 1]} +
+ {item.label} + 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)) + } +
{item.label} + {fmtMonth(Math.round(val))} +
+ ) +} + +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 ( +
+
+
+
+

+ {de ? 'Gewinn- und Verlustrechnung' : 'Profit & Loss Statement'} +

+

+ {de + ? 'Klicke auf ein Jahr um die Monatsdetails zu sehen · ESC zum Schliessen' + : 'Click on a year to see monthly details · ESC to close'} +

+
+
+ {/* HGB / US GAAP Toggle */} +
+ + +
+ +
+
+
+ {children} +
+
+
+ ) +} + export default function AnnualPLTable({ results, lang }: AnnualPLTableProps) { + const [isFullscreen, setIsFullscreen] = useState(false) + const [expandedYear, setExpandedYear] = useState(null) + const [standard, setStandard] = useState('hgb') + const [portalRoot, setPortalRoot] = useState(null) + const de = lang === 'de' + + // Portal mount point + useEffect(() => { + setPortalRoot(document.body) + }, []) + // Aggregate monthly results into annual const annualMap = new Map() 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() + 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 = ( + + ) return ( - - - - - - {rows.map(r => ( - - ))} - - - - {lineItems.map((item) => ( - + +
+ {/* HGB / US GAAP Toggle (inline) */} +
+
- {rows.map(r => { - const val = r[item.key] as number - const isNeg = val < 0 || item.isNegative - return ( - - ) - })} - - ))} - -
- {de ? 'GuV-Position' : 'P&L Line Item'} - - {r.year} -
- {item.label} - 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)) - } -
-
+ HGB + + +
+ +
+
+ {tableContent} +
+

+ {de + ? 'Klicke auf ein Jahr fuer Monatsdetails' + : 'Click on a year for monthly details'} +

+ + + {/* Fullscreen via Portal — escapes parent stacking context */} + {isFullscreen && portalRoot && createPortal( + setIsFullscreen(false)} + lang={lang} + standard={standard} + onStandardChange={setStandard} + > + + , + portalRoot + )} + ) } diff --git a/pitch-deck/lib/hooks/useSlideNavigation.ts b/pitch-deck/lib/hooks/useSlideNavigation.ts index fb26863..3823ae4 100644 --- a/pitch-deck/lib/hooks/useSlideNavigation.ts +++ b/pitch-deck/lib/hooks/useSlideNavigation.ts @@ -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 diff --git a/pitch-deck/lib/i18n.ts b/pitch-deck/lib/i18n.ts index 64a8dec..c565371 100644 --- a/pitch-deck/lib/i18n.ts +++ b/pitch-deck/lib/i18n.ts @@ -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', + }, + }, }, } diff --git a/pitch-deck/lib/types.ts b/pitch-deck/lib/types.ts index 253e4a0..b8aa659 100644 --- a/pitch-deck/lib/types.ts +++ b/pitch-deck/lib/types.ts @@ -214,3 +214,9 @@ export type SlideId = | 'financials' | 'the-ask' | 'ai-qa' + | 'annex-assumptions' + | 'annex-architecture' + | 'annex-gtm' + | 'annex-regulatory' + | 'annex-engineering' + | 'annex-aipipeline' diff --git a/vault/config.hcl b/vault/config.hcl new file mode 100644 index 0000000..f5d34f7 --- /dev/null +++ b/vault/config.hcl @@ -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 diff --git a/vault/init-pki.sh b/vault/init-pki.sh index 6c68577..bab270c 100755 --- a/vault/init-pki.sh +++ b/vault/init-pki.sh @@ -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..." diff --git a/vault/init-secrets.sh b/vault/init-secrets.sh index de0ce94..64f8429 100755 --- a/vault/init-secrets.sh +++ b/vault/init-secrets.sh @@ -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..." diff --git a/vault/init-vault.sh b/vault/init-vault.sh new file mode 100755 index 0000000..5ab582a --- /dev/null +++ b/vault/init-vault.sh @@ -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