Compare commits
56 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ea39418738 | |||
| 7f88ed0ed2 | |||
| 44659a9dd7 | |||
| 87d7da0198 | |||
| 9675c1f896 | |||
| 9736476a0c | |||
| 03d420c984 | |||
| 6b52719079 | |||
| a5b7d62969 | |||
| ef9e3699b2 | |||
| 440367b69d | |||
| 801a5a43f5 | |||
| 9c23068a4f | |||
| d359b7b734 | |||
| bd37ff807e | |||
| 40d2342086 | |||
| adf3bf8301 | |||
| 1b5ccd4dec | |||
| b5d8f9aed3 | |||
| c8171b0a1e | |||
| 7e15ef3725 | |||
| e3a3802f5b | |||
| 93e319e9fb | |||
| 6626d2a8f9 | |||
| 3dbc470158 | |||
| e5d0386cfb | |||
| ff071af2a0 | |||
| fcdcbc51e3 | |||
| 7b8f8d4b5a | |||
| f385c612f5 | |||
| 9166d9dade | |||
| 7ae5bc0fd5 | |||
| 242ed1101e | |||
| 8b2e9ac328 | |||
| 084d09e9bd | |||
| 646143ce5a | |||
| 00d802f965 | |||
| ebb7575f2c | |||
| d0539d0f2f | |||
| 8e92a93aa8 | |||
| f794347827 | |||
| 1af160eed0 | |||
| eb118ebf92 | |||
| dbb476cc3b | |||
| 9345efc3f0 | |||
| c4e993e3f8 | |||
| a58d1aa403 | |||
| d7ed5ce8c5 | |||
| 512088ab93 | |||
| 32b5e0223d | |||
| 9354cbf775 | |||
| 756d068b4f | |||
| c02a7bd8a6 | |||
| b6d3fad6ab | |||
| 27479ee553 | |||
| 82a5d62f44 |
@@ -1,5 +1,8 @@
|
||||
# Build + push pitch-deck Docker image to registry.meghsakha.com
|
||||
# on every push to main that touches pitch-deck/ files.
|
||||
# and trigger orca redeploy on every push to main that touches pitch-deck/.
|
||||
#
|
||||
# Requires Gitea Actions secret: ORCA_WEBHOOK_SECRET
|
||||
# (must match the `secret` field in ~/.orca/webhooks.json on the orca master)
|
||||
|
||||
name: Build pitch-deck
|
||||
|
||||
@@ -10,16 +13,23 @@ on:
|
||||
- 'pitch-deck/**'
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
build-push-deploy:
|
||||
runs-on: docker
|
||||
container:
|
||||
image: docker:27-cli
|
||||
steps:
|
||||
- name: Checkout
|
||||
run: |
|
||||
apk add --no-cache git
|
||||
apk add --no-cache git openssl curl
|
||||
git clone --depth 1 --branch ${GITHUB_REF_NAME} ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git .
|
||||
|
||||
- name: Login to registry
|
||||
env:
|
||||
REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }}
|
||||
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
run: |
|
||||
echo "$REGISTRY_PASSWORD" | docker login registry.meghsakha.com -u "$REGISTRY_USERNAME" --password-stdin
|
||||
|
||||
- name: Build image
|
||||
run: |
|
||||
cd pitch-deck
|
||||
@@ -34,4 +44,22 @@ jobs:
|
||||
SHORT_SHA=$(git rev-parse --short HEAD)
|
||||
docker push registry.meghsakha.com/breakpilot/pitch-deck:latest
|
||||
docker push registry.meghsakha.com/breakpilot/pitch-deck:${SHORT_SHA}
|
||||
echo "Pushed registry.meghsakha.com/breakpilot/pitch-deck:latest + :${SHORT_SHA}"
|
||||
echo "Pushed :latest + :${SHORT_SHA}"
|
||||
|
||||
- name: Trigger orca redeploy
|
||||
env:
|
||||
ORCA_WEBHOOK_SECRET: ${{ secrets.ORCA_WEBHOOK_SECRET }}
|
||||
ORCA_WEBHOOK_URL: http://46.225.100.82:6880/api/v1/webhooks/github
|
||||
run: |
|
||||
SHA=$(git rev-parse HEAD)
|
||||
PAYLOAD="{\"ref\":\"refs/heads/main\",\"repository\":{\"full_name\":\"${GITHUB_REPOSITORY}\"},\"head_commit\":{\"id\":\"$SHA\",\"message\":\"ci: pitch-deck image build\"}}"
|
||||
SIG=$(printf '%s' "$PAYLOAD" | openssl dgst -sha256 -hmac "$ORCA_WEBHOOK_SECRET" -r | awk '{print $1}')
|
||||
curl -sSf -k \
|
||||
-X POST \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-GitHub-Event: push" \
|
||||
-H "X-Hub-Signature-256: sha256=$SIG" \
|
||||
-d "$PAYLOAD" \
|
||||
"$ORCA_WEBHOOK_URL" \
|
||||
|| { echo "Orca redeploy failed"; exit 1; }
|
||||
echo "Orca redeploy triggered"
|
||||
|
||||
@@ -140,20 +140,6 @@ jobs:
|
||||
python -m pytest tests/bqas/ -v --tb=short || true
|
||||
|
||||
# ========================================
|
||||
# Deploy via Coolify (nur main, kein PR)
|
||||
# Deploys now handled by per-service workflows (e.g. build-pitch-deck.yml)
|
||||
# which trigger orca webhooks directly after building + pushing the image.
|
||||
# ========================================
|
||||
|
||||
deploy-coolify:
|
||||
name: Deploy
|
||||
runs-on: docker
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
needs:
|
||||
- test-go-consent
|
||||
container:
|
||||
image: alpine:latest
|
||||
steps:
|
||||
- name: Trigger Coolify deploy
|
||||
run: |
|
||||
apk add --no-cache curl
|
||||
curl -sf "${{ secrets.COOLIFY_WEBHOOK }}" \
|
||||
-H "Authorization: Bearer ${{ secrets.COOLIFY_TOKEN }}"
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
name: Deploy to Coolify
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- coolify
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Deploy via Coolify API
|
||||
run: |
|
||||
echo "Deploying breakpilot-core to Coolify..."
|
||||
HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
|
||||
-X POST \
|
||||
-H "Authorization: Bearer ${{ secrets.COOLIFY_API_TOKEN }}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"uuid": "${{ secrets.COOLIFY_RESOURCE_UUID }}", "force_rebuild": true}' \
|
||||
"${{ secrets.COOLIFY_BASE_URL }}/api/v1/deploy")
|
||||
|
||||
echo "HTTP Status: $HTTP_STATUS"
|
||||
if [ "$HTTP_STATUS" -ne 200 ] && [ "$HTTP_STATUS" -ne 201 ]; then
|
||||
echo "Deployment failed with status $HTTP_STATUS"
|
||||
exit 1
|
||||
fi
|
||||
echo "Deployment triggered successfully!"
|
||||
@@ -7,6 +7,7 @@
|
||||
secrets/
|
||||
*.pem
|
||||
*.key
|
||||
.mcp.json
|
||||
|
||||
# Node
|
||||
node_modules/
|
||||
|
||||
@@ -52,7 +52,7 @@ class GenerateRequest(BaseModel):
|
||||
max_controls: int = 50
|
||||
max_chunks: int = 1000 # Default: process max 1000 chunks per job (respects document boundaries)
|
||||
batch_size: int = 5
|
||||
skip_web_search: bool = False
|
||||
skip_web_search: bool = True # Default True — Anchors nachtraeglich batchen
|
||||
dry_run: bool = False
|
||||
regulation_filter: Optional[List[str]] = None # Only process these regulation_code prefixes
|
||||
regulation_exclude: Optional[List[str]] = None # Skip these regulation_code prefixes
|
||||
|
||||
@@ -489,7 +489,7 @@ class GeneratorConfig(BaseModel):
|
||||
max_controls: int = 0 # 0 = unlimited (process ALL chunks)
|
||||
max_chunks: int = 0 # 0 = unlimited; >0 = stop after N chunks (respects document boundaries)
|
||||
skip_processed: bool = True
|
||||
skip_web_search: bool = False
|
||||
skip_web_search: bool = True # Default True — Anchor-Search verlangsamt 5x, nachtraeglich batchen
|
||||
dry_run: bool = False
|
||||
existing_job_id: Optional[str] = None # If set, reuse this job instead of creating a new one
|
||||
regulation_filter: Optional[List[str]] = None # Only process chunks matching these regulation_code prefixes
|
||||
@@ -544,6 +544,8 @@ class GeneratorResult:
|
||||
controls_too_close: int = 0
|
||||
controls_duplicates_found: int = 0
|
||||
controls_qa_fixed: int = 0
|
||||
controls_stored: int = 0 # Actually persisted to DB
|
||||
controls_store_failed: int = 0 # Generated but failed to persist
|
||||
chunks_skipped_prefilter: int = 0
|
||||
errors: list = field(default_factory=list)
|
||||
controls: list = field(default_factory=list)
|
||||
@@ -645,6 +647,13 @@ async def _llm_anthropic(prompt: str, system_prompt: Optional[str] = None, max_r
|
||||
json=payload,
|
||||
)
|
||||
if resp.status_code != 200:
|
||||
# Retry on transient HTTP errors
|
||||
if resp.status_code in (429, 500, 502, 503, 504) and attempt < max_retries:
|
||||
wait = 2 ** attempt
|
||||
logger.warning("Anthropic API %d (transient) — retry in %ds...", resp.status_code, wait)
|
||||
import asyncio
|
||||
await asyncio.sleep(wait)
|
||||
continue
|
||||
logger.error("Anthropic API %d: %s", resp.status_code, resp.text[:300])
|
||||
return ""
|
||||
data = resp.json()
|
||||
@@ -1517,9 +1526,22 @@ Gib ein JSON-Array zurueck mit GENAU {len(chunks)} Elementen. Fuer Aspekte ohne
|
||||
final: list[Optional[GeneratedControl]] = []
|
||||
for i in range(len(batch_items)):
|
||||
control = all_controls.get(i)
|
||||
if not control or (not control.title and not control.objective):
|
||||
# Filter empty or invalid controls (LLM returned None/empty)
|
||||
if not control:
|
||||
final.append(None)
|
||||
continue
|
||||
title_invalid = not control.title or control.title.strip().lower() in ("none", "null", "")
|
||||
obj_invalid = not control.objective or control.objective.strip().lower() in ("none", "null", "")
|
||||
if title_invalid and obj_invalid:
|
||||
logger.warning("Leerer Control gefiltert (title=%s, objective=%s) — wird nicht gespeichert",
|
||||
control.title, control.objective)
|
||||
final.append(None)
|
||||
continue
|
||||
# Clean up "None" strings from LLM
|
||||
if title_invalid:
|
||||
control.title = control.objective[:120] if control.objective else "Unbenannt"
|
||||
if obj_invalid:
|
||||
control.objective = control.title
|
||||
|
||||
if control.release_state == "too_close":
|
||||
final.append(control)
|
||||
@@ -1732,20 +1754,52 @@ Gib ein JSON-Array zurueck mit GENAU {len(chunks)} Elementen. Fuer Aspekte ohne
|
||||
)
|
||||
|
||||
def _generate_control_id(self, domain: str, db: Session) -> str:
|
||||
"""Generate next sequential control ID like AUTH-011."""
|
||||
"""Generate unique control ID using numeric MAX + collision guard.
|
||||
|
||||
Uses CAST to INTEGER for correct numeric ordering (not string sort).
|
||||
Falls back to UUID suffix if collision is detected.
|
||||
"""
|
||||
prefix = domain.upper()[:4]
|
||||
try:
|
||||
# Numeric ordering — CAST to INTEGER, not string sort
|
||||
result = db.execute(
|
||||
text("SELECT control_id FROM canonical_controls WHERE control_id LIKE :prefix ORDER BY control_id DESC LIMIT 1"),
|
||||
{"prefix": f"{prefix}-%"},
|
||||
text("""
|
||||
SELECT COALESCE(
|
||||
MAX(CAST(SUBSTRING(control_id FROM :prefix_len) AS INTEGER)),
|
||||
0
|
||||
) + 1
|
||||
FROM canonical_controls
|
||||
WHERE control_id ~ (:pattern)
|
||||
"""),
|
||||
{"prefix_len": len(prefix) + 2, "pattern": f"^{prefix}-[0-9]+$"},
|
||||
)
|
||||
row = result.fetchone()
|
||||
if row:
|
||||
last_num = int(row[0].split("-")[-1])
|
||||
return f"{prefix}-{last_num + 1:03d}"
|
||||
except Exception:
|
||||
pass
|
||||
return f"{prefix}-001"
|
||||
next_num = result.scalar() or 1
|
||||
candidate = f"{prefix}-{next_num:03d}"
|
||||
|
||||
# Collision guard — check if ID already exists
|
||||
exists = db.execute(
|
||||
text("SELECT 1 FROM canonical_controls WHERE control_id = :cid LIMIT 1"),
|
||||
{"cid": candidate},
|
||||
).fetchone()
|
||||
|
||||
if exists:
|
||||
# UUID suffix as fallback for race conditions
|
||||
suffix = uuid.uuid4().hex[:6]
|
||||
candidate = f"{prefix}-{next_num:03d}-{suffix}"
|
||||
logger.warning(
|
||||
"ID collision for %s-%03d — using unique suffix: %s",
|
||||
prefix, next_num, candidate,
|
||||
)
|
||||
|
||||
return candidate
|
||||
except Exception as e:
|
||||
# NEVER swallow silently — UUID as safe fallback
|
||||
fallback = f"{prefix}-{uuid.uuid4().hex[:8]}"
|
||||
logger.error(
|
||||
"Failed to generate control_id for domain %s: %s — using fallback %s",
|
||||
domain, e, fallback,
|
||||
)
|
||||
return fallback
|
||||
|
||||
# ── Stage QA: Automated Quality Validation ───────────────────────
|
||||
|
||||
@@ -1890,6 +1944,14 @@ Kategorien: {CATEGORY_LIST_STR}"""
|
||||
|
||||
def _store_control(self, control: GeneratedControl, job_id: str) -> Optional[str]:
|
||||
"""Persist a generated control to DB. Returns the control UUID or None."""
|
||||
# Pre-store quality guard — reject empty/invalid controls
|
||||
if not control.title or control.title.strip().lower() in ("none", "null", ""):
|
||||
logger.warning("Rejected control with empty/None title: %s", control.control_id)
|
||||
return None
|
||||
if not control.objective or control.objective.strip().lower() in ("none", "null", ""):
|
||||
logger.warning("Rejected control with empty/None objective: %s — %s", control.control_id, control.title)
|
||||
return None
|
||||
|
||||
try:
|
||||
# Get framework UUID
|
||||
fw_result = self.db.execute(
|
||||
@@ -1929,7 +1991,11 @@ Kategorien: {CATEGORY_LIST_STR}"""
|
||||
:target_audience, :pipeline_version,
|
||||
:applicable_industries, :applicable_company_size, :scope_conditions
|
||||
)
|
||||
ON CONFLICT (framework_id, control_id) DO NOTHING
|
||||
ON CONFLICT (framework_id, control_id) DO UPDATE SET
|
||||
updated_at = NOW(),
|
||||
title = EXCLUDED.title,
|
||||
objective = EXCLUDED.objective,
|
||||
generation_metadata = EXCLUDED.generation_metadata
|
||||
RETURNING id
|
||||
"""),
|
||||
{
|
||||
@@ -2169,12 +2235,21 @@ Kategorien: {CATEGORY_LIST_STR}"""
|
||||
if ctrl_uuid:
|
||||
path = control.generation_metadata.get("processing_path", "structured_batch")
|
||||
self._mark_chunk_processed(chunk, lic_info, path, [ctrl_uuid], job_id)
|
||||
result.controls_generated += 1
|
||||
result.controls_stored += 1
|
||||
controls_count += 1
|
||||
else:
|
||||
self._mark_chunk_processed(chunk, lic_info, "store_failed", [], job_id)
|
||||
# CRITICAL FIX: Do NOT mark chunk as processed — allow retry
|
||||
logger.error(
|
||||
"STORE_FAILED: Control '%s' (%s) nicht gespeichert — Chunk bleibt unverarbeitet fuer Retry",
|
||||
control.control_id, control.title[:60],
|
||||
)
|
||||
result.controls_store_failed += 1
|
||||
else:
|
||||
result.controls_generated += 1
|
||||
controls_count += 1
|
||||
|
||||
result.controls_generated += 1
|
||||
result.controls.append(asdict(control))
|
||||
controls_count += 1
|
||||
|
||||
if self._existing_controls is not None:
|
||||
self._existing_controls.append({
|
||||
@@ -2187,10 +2262,18 @@ Kategorien: {CATEGORY_LIST_STR}"""
|
||||
try:
|
||||
# Progress logging every 50 chunks
|
||||
if i > 0 and i % 50 == 0:
|
||||
store_rate = (result.controls_stored / max(result.controls_generated, 1)) * 100 if result.controls_generated > 0 else 100
|
||||
logger.info(
|
||||
"Progress: %d/%d chunks processed, %d controls generated, %d skipped by prefilter",
|
||||
i, len(chunks), controls_count, chunks_skipped_prefilter,
|
||||
"Progress: %d/%d chunks | %d generated | %d stored (%.0f%%) | %d store_failed | %d skipped",
|
||||
i, len(chunks), result.controls_generated, result.controls_stored,
|
||||
store_rate, result.controls_store_failed, chunks_skipped_prefilter,
|
||||
)
|
||||
# ALARM bei niedriger Store-Rate
|
||||
if result.controls_generated > 10 and store_rate < 80:
|
||||
logger.error(
|
||||
"ALARM: Store-Erfolgsrate nur %.0f%% — moeglicherweise ID-Kollisionen!",
|
||||
store_rate,
|
||||
)
|
||||
self._update_job(job_id, result)
|
||||
|
||||
# Stage 1.5: Local LLM pre-filter — skip chunks without requirements
|
||||
@@ -2235,11 +2318,38 @@ Kategorien: {CATEGORY_LIST_STR}"""
|
||||
await _flush_batch()
|
||||
|
||||
result.chunks_skipped_prefilter = chunks_skipped_prefilter
|
||||
|
||||
# Post-Job Validierung — DB-Realitaet pruefen
|
||||
try:
|
||||
actual_stored = self.db.execute(
|
||||
text("SELECT count(*) FROM canonical_controls WHERE generation_metadata::text LIKE :jid"),
|
||||
{"jid": f"%{job_id}%"},
|
||||
).scalar() or 0
|
||||
except Exception:
|
||||
actual_stored = -1
|
||||
|
||||
final_store_rate = (result.controls_stored / max(result.controls_generated, 1)) * 100 if result.controls_generated > 0 else 0
|
||||
|
||||
logger.info(
|
||||
"Pipeline complete: %d controls generated, %d chunks skipped by prefilter, %d total chunks",
|
||||
controls_count, chunks_skipped_prefilter, len(chunks),
|
||||
"Pipeline complete: %d chunks | %d generated | %d stored (%.0f%%) | %d store_failed | %d skipped | DB actual: %d",
|
||||
len(chunks), result.controls_generated, result.controls_stored,
|
||||
final_store_rate, result.controls_store_failed,
|
||||
chunks_skipped_prefilter, actual_stored,
|
||||
)
|
||||
|
||||
if result.controls_store_failed > 0:
|
||||
logger.error(
|
||||
"WARNUNG: %d Controls konnten NICHT gespeichert werden! "
|
||||
"Diese Chunks bleiben unverarbeitet und muessen erneut verarbeitet werden.",
|
||||
result.controls_store_failed,
|
||||
)
|
||||
|
||||
if result.controls_generated > 0 and final_store_rate < 90:
|
||||
logger.error(
|
||||
"KRITISCH: Store-Rate nur %.0f%% — %d von %d Controls verloren!",
|
||||
final_store_rate, result.controls_store_failed, result.controls_generated,
|
||||
)
|
||||
|
||||
result.status = "completed"
|
||||
|
||||
except Exception as e:
|
||||
|
||||
+5
-2
@@ -61,6 +61,7 @@ services:
|
||||
- "3008:3008" # Admin Core
|
||||
- "3010:3010" # Portal Dashboard
|
||||
- "8011:8011" # Compliance Docs (MkDocs)
|
||||
- "3012:3012" # Pitch Deck
|
||||
volumes:
|
||||
- ./nginx/conf.d:/etc/nginx/conf.d:ro
|
||||
- vault_certs:/etc/nginx/certs:ro
|
||||
@@ -873,11 +874,13 @@ services:
|
||||
dockerfile: Dockerfile
|
||||
container_name: bp-core-pitch-deck
|
||||
platform: linux/arm64
|
||||
ports:
|
||||
- "3012:3000"
|
||||
expose:
|
||||
- "3000"
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
DATABASE_URL: postgres://${POSTGRES_USER:-breakpilot}:${POSTGRES_PASSWORD:-breakpilot123}@postgres:5432/${POSTGRES_DB:-breakpilot_db}
|
||||
PITCH_JWT_SECRET: ${PITCH_JWT_SECRET:-7025f5da6d2ea384353ea6debddae0ea9e2dbca151a1df4b65be8cb80a5cf002}
|
||||
PITCH_ADMIN_SECRET: ${PITCH_ADMIN_SECRET:-40df9e6f2ca2e90729030af37bf79199710b09c898cac9df}
|
||||
LITELLM_URL: ${LITELLM_URL:-https://llm-dev.meghsakha.com}
|
||||
LITELLM_MODEL: ${LITELLM_MODEL:-gpt-oss-120b}
|
||||
LITELLM_API_KEY: ${LITELLM_API_KEY:-sk-0nAyxaMVbIqmz_ntnndzag}
|
||||
|
||||
@@ -330,6 +330,14 @@ def _generate_risk_assessment(ctx: dict) -> str:
|
||||
if any(ctx.get(k) for k in ["third_country_transfer", "processes_in_third_country"]):
|
||||
risks.append(("Zugriff durch Behoerden in Drittlaendern", "mittel", "hoch", "hoch"))
|
||||
|
||||
# FISA 702 Risiko bei US-Cloud-Providern
|
||||
hosting = (ctx.get("hosting_provider") or "").lower()
|
||||
us_providers = ("aws", "azure", "google", "microsoft", "amazon", "openai", "anthropic", "oracle")
|
||||
if any(p in hosting for p in us_providers):
|
||||
risks.append(("FISA 702: Zugriff durch US-Behoerden auf EU-Daten nicht ausschliessbar", "mittel", "hoch", "hoch"))
|
||||
risks.append(("EU-Serverstandort schuetzt nicht gegen US-Rechtszugriff (Cloud Act + FISA)", "mittel", "hoch", "hoch"))
|
||||
risks.append(("Fehlende effektive Rechtsbehelfe fuer EU-Betroffene gegen US-Ueberwachung", "mittel", "hoch", "hoch"))
|
||||
|
||||
# Domain-spezifische Risiken (AI Act Annex III)
|
||||
domain = ctx.get("domain", "")
|
||||
if domain in ("hr", "recruiting") or ctx.get("has_hr_context"):
|
||||
|
||||
@@ -760,3 +760,33 @@ server {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
}
|
||||
|
||||
# =========================================================
|
||||
# PITCH DECK: Investor Presentation on port 3012
|
||||
# =========================================================
|
||||
server {
|
||||
listen 3012 ssl;
|
||||
http2 on;
|
||||
server_name macmini localhost;
|
||||
|
||||
ssl_certificate /etc/nginx/certs/macmini.crt;
|
||||
ssl_certificate_key /etc/nginx/certs/macmini.key;
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
|
||||
ssl_prefer_server_ciphers off;
|
||||
|
||||
location / {
|
||||
set $upstream_pitch bp-core-pitch-deck:3000;
|
||||
proxy_pass http://$upstream_pitch;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto https;
|
||||
proxy_read_timeout 300s;
|
||||
proxy_connect_timeout 60s;
|
||||
proxy_send_timeout 300s;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
|
||||
Tue Apr 14 09:22:10 AM CEST 2026
|
||||
|
||||
Tue Apr 14 09:27:05 AM CEST 2026
|
||||
Tue Apr 14 09:32:36 AM CEST 2026
|
||||
Tue Apr 15 rebuild trigger
|
||||
Tue Apr 15 rebuild 2
|
||||
@@ -0,0 +1,294 @@
|
||||
/**
|
||||
* Regression test for the "lost access" scenario:
|
||||
*
|
||||
* 1. Admin invites investor A → token T1 is created and emailed.
|
||||
* 2. Investor A opens the link successfully → T1 is marked used_at.
|
||||
* 3. Investor A clears their session (or a redeploy drops cookies).
|
||||
* 4. Investor A returns to / — redirected to /auth.
|
||||
* 5. Without this feature, A is stuck: T1 is already used, expired, or the
|
||||
* session is gone, and there is no self-service way to get back in.
|
||||
* 6. With this feature, A enters their email on /auth and the endpoint
|
||||
* issues a brand new, unused magic link T2 for the same investor row.
|
||||
*
|
||||
* This test wires together the request-link handler with the real verify
|
||||
* handler against an in-memory fake of the two tables the flow touches
|
||||
* (pitch_investors, pitch_magic_links) so we can assert end-to-end that a
|
||||
* second link works after the first one was used.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
||||
import { NextRequest } from 'next/server'
|
||||
|
||||
// ---- In-memory fake of the two tables touched by this flow ----
|
||||
|
||||
interface InvestorRow {
|
||||
id: string
|
||||
email: string
|
||||
name: string | null
|
||||
company: string | null
|
||||
status: 'invited' | 'active' | 'revoked'
|
||||
last_login_at: Date | null
|
||||
login_count: number
|
||||
}
|
||||
interface MagicLinkRow {
|
||||
id: string
|
||||
investor_id: string
|
||||
token: string
|
||||
expires_at: Date
|
||||
used_at: Date | null
|
||||
ip_address: string | null
|
||||
user_agent: string | null
|
||||
}
|
||||
|
||||
const db = {
|
||||
investors: [] as InvestorRow[],
|
||||
magicLinks: [] as MagicLinkRow[],
|
||||
sessions: [] as { id: string; investor_id: string; ip_address: string | null }[],
|
||||
}
|
||||
|
||||
let idCounter = 0
|
||||
const nextId = () => `row-${++idCounter}`
|
||||
|
||||
// A tiny query router: match the SQL fragment we care about, ignore the rest.
|
||||
const queryMock = vi.fn(async (sql: string, params: unknown[] = []) => {
|
||||
const s = sql.replace(/\s+/g, ' ').trim()
|
||||
|
||||
// Investor lookup by email (used by request-link)
|
||||
if (/SELECT id, email, name, status FROM pitch_investors WHERE email = \$1/i.test(s)) {
|
||||
const row = db.investors.find(i => i.email === params[0])
|
||||
return { rows: row ? [row] : [] }
|
||||
}
|
||||
|
||||
// Insert magic link
|
||||
if (/INSERT INTO pitch_magic_links \(investor_id, token, expires_at\)/i.test(s)) {
|
||||
db.magicLinks.push({
|
||||
id: nextId(),
|
||||
investor_id: params[0] as string,
|
||||
token: params[1] as string,
|
||||
expires_at: params[2] as Date,
|
||||
used_at: null,
|
||||
ip_address: null,
|
||||
user_agent: null,
|
||||
})
|
||||
return { rows: [] }
|
||||
}
|
||||
|
||||
// Verify: magic link + investor JOIN lookup
|
||||
if (/FROM pitch_magic_links ml JOIN pitch_investors i/i.test(s)) {
|
||||
const link = db.magicLinks.find(ml => ml.token === params[0])
|
||||
if (!link) return { rows: [] }
|
||||
const inv = db.investors.find(i => i.id === link.investor_id)!
|
||||
return {
|
||||
rows: [{
|
||||
id: link.id,
|
||||
investor_id: link.investor_id,
|
||||
expires_at: link.expires_at,
|
||||
used_at: link.used_at,
|
||||
email: inv.email,
|
||||
investor_status: inv.status,
|
||||
}],
|
||||
}
|
||||
}
|
||||
|
||||
// Mark magic link used
|
||||
if (/UPDATE pitch_magic_links SET used_at = NOW/i.test(s)) {
|
||||
const link = db.magicLinks.find(ml => ml.id === params[2])
|
||||
if (link) {
|
||||
link.used_at = new Date()
|
||||
link.ip_address = params[0] as string | null
|
||||
link.user_agent = params[1] as string | null
|
||||
}
|
||||
return { rows: [] }
|
||||
}
|
||||
|
||||
// Activate investor
|
||||
if (/UPDATE pitch_investors SET status = 'active'/i.test(s)) {
|
||||
const inv = db.investors.find(i => i.id === params[0])
|
||||
if (inv) {
|
||||
inv.status = 'active'
|
||||
inv.last_login_at = new Date()
|
||||
inv.login_count += 1
|
||||
}
|
||||
return { rows: [] }
|
||||
}
|
||||
|
||||
// createSession: revoke prior sessions (no-op in fake)
|
||||
if (/UPDATE pitch_sessions SET revoked = true WHERE investor_id/i.test(s)) {
|
||||
return { rows: [] }
|
||||
}
|
||||
|
||||
// createSession: insert
|
||||
if (/INSERT INTO pitch_sessions/i.test(s)) {
|
||||
const id = nextId()
|
||||
db.sessions.push({ id, investor_id: params[0] as string, ip_address: params[2] as string | null })
|
||||
return { rows: [{ id }] }
|
||||
}
|
||||
|
||||
// createSession: fetch investor email for JWT
|
||||
if (/SELECT email FROM pitch_investors WHERE id = \$1/i.test(s)) {
|
||||
const inv = db.investors.find(i => i.id === params[0])
|
||||
return { rows: inv ? [{ email: inv.email }] : [] }
|
||||
}
|
||||
|
||||
// new-ip detection query (verify route)
|
||||
if (/SELECT DISTINCT ip_address FROM pitch_sessions/i.test(s)) {
|
||||
return { rows: [] }
|
||||
}
|
||||
|
||||
// Audit log insert — accept everything
|
||||
if (/INSERT INTO pitch_audit_logs/i.test(s)) {
|
||||
return { rows: [] }
|
||||
}
|
||||
|
||||
throw new Error(`Unmocked query: ${s.slice(0, 120)}…`)
|
||||
})
|
||||
|
||||
vi.mock('@/lib/db', () => ({
|
||||
default: { query: (...args: unknown[]) => queryMock(args[0] as string, args[1] as unknown[]) },
|
||||
}))
|
||||
|
||||
// Capture emails instead of sending them
|
||||
const sentEmails: Array<{ to: string; url: string }> = []
|
||||
vi.mock('@/lib/email', () => ({
|
||||
sendMagicLinkEmail: vi.fn(async (to: string, _name: string | null, url: string) => {
|
||||
sentEmails.push({ to, url })
|
||||
}),
|
||||
}))
|
||||
|
||||
// next/headers cookies() needs to be stubbed — setSessionCookie calls it.
|
||||
vi.mock('next/headers', () => ({
|
||||
cookies: async () => ({
|
||||
set: vi.fn(),
|
||||
get: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
}),
|
||||
}))
|
||||
|
||||
// Import the handlers AFTER mocks are set up
|
||||
import { POST as requestLink } from '@/app/api/auth/request-link/route'
|
||||
import { POST as verifyLink } from '@/app/api/auth/verify/route'
|
||||
|
||||
function makeJsonRequest(url: string, body: unknown, ip = '203.0.113.1'): NextRequest {
|
||||
return new NextRequest(url, {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json', 'x-forwarded-for': ip },
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
}
|
||||
|
||||
function extractToken(url: string): string {
|
||||
const m = url.match(/token=([0-9a-f]+)/)
|
||||
if (!m) throw new Error(`No token in url: ${url}`)
|
||||
return m[1]
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
db.investors = []
|
||||
db.magicLinks = []
|
||||
db.sessions = []
|
||||
sentEmails.length = 0
|
||||
idCounter = 0
|
||||
queryMock.mockClear()
|
||||
})
|
||||
|
||||
describe('Regression: investor can re-request a working magic link after the first is consumed', () => {
|
||||
it('full flow — invite → use → request-link → new link works', async () => {
|
||||
// --- Setup: admin has already invited the investor (simulate the outcome) ---
|
||||
const investorId = 'investor-42'
|
||||
db.investors.push({
|
||||
id: investorId,
|
||||
email: 'vc@example.com',
|
||||
name: 'VC Partner',
|
||||
company: 'Acme Capital',
|
||||
status: 'invited',
|
||||
last_login_at: null,
|
||||
login_count: 0,
|
||||
})
|
||||
db.magicLinks.push({
|
||||
id: 'ml-original',
|
||||
investor_id: investorId,
|
||||
token: 'a'.repeat(96), // original invite token
|
||||
expires_at: new Date(Date.now() + 72 * 60 * 60 * 1000),
|
||||
used_at: null,
|
||||
ip_address: null,
|
||||
user_agent: null,
|
||||
})
|
||||
|
||||
// --- Step 1: investor uses the original invite link ---
|
||||
const firstVerify = await verifyLink(makeJsonRequest('http://localhost/api/auth/verify', { token: 'a'.repeat(96) }))
|
||||
expect(firstVerify.status).toBe(200)
|
||||
const first = db.magicLinks.find(ml => ml.id === 'ml-original')!
|
||||
expect(first.used_at).not.toBeNull()
|
||||
|
||||
// --- Step 2: investor comes back later; clicks the same link → rejected ---
|
||||
const replay = await verifyLink(makeJsonRequest('http://localhost/api/auth/verify', { token: 'a'.repeat(96) }))
|
||||
expect(replay.status).toBe(401)
|
||||
const replayBody = await replay.json()
|
||||
expect(replayBody.error).toMatch(/already been used/i)
|
||||
|
||||
// --- Step 3: investor visits /auth and submits their email ---
|
||||
const reissue = await requestLink(
|
||||
makeJsonRequest('http://localhost/api/auth/request-link', { email: 'vc@example.com' }, '203.0.113.99'),
|
||||
)
|
||||
expect(reissue.status).toBe(200)
|
||||
const reissueBody = await reissue.json()
|
||||
expect(reissueBody.success).toBe(true)
|
||||
|
||||
// --- Step 4: a fresh email was dispatched to the investor ---
|
||||
expect(sentEmails).toHaveLength(1)
|
||||
expect(sentEmails[0].to).toBe('vc@example.com')
|
||||
const newToken = extractToken(sentEmails[0].url)
|
||||
expect(newToken).not.toBe('a'.repeat(96))
|
||||
expect(newToken).toMatch(/^[0-9a-f]{96}$/)
|
||||
|
||||
// A second unused magic link row exists for the same investor
|
||||
const links = db.magicLinks.filter(ml => ml.investor_id === investorId)
|
||||
expect(links).toHaveLength(2)
|
||||
const newLink = links.find(ml => ml.token === newToken)!
|
||||
expect(newLink.used_at).toBeNull()
|
||||
|
||||
// --- Step 5: the new token validates successfully ---
|
||||
const secondVerify = await verifyLink(makeJsonRequest('http://localhost/api/auth/verify', { token: newToken }))
|
||||
expect(secondVerify.status).toBe(200)
|
||||
const secondBody = await secondVerify.json()
|
||||
expect(secondBody.success).toBe(true)
|
||||
expect(secondBody.redirect).toBe('/')
|
||||
|
||||
// And the new link is now used, mirroring the one-time-use contract
|
||||
expect(newLink.used_at).not.toBeNull()
|
||||
})
|
||||
|
||||
it('unknown emails do not create magic links or send email (prevents enumeration & abuse)', async () => {
|
||||
// No investors in the DB
|
||||
const res = await requestLink(
|
||||
makeJsonRequest('http://localhost/api/auth/request-link', { email: 'stranger@example.com' }),
|
||||
)
|
||||
expect(res.status).toBe(200)
|
||||
const body = await res.json()
|
||||
// Same generic message as the happy path
|
||||
expect(body.success).toBe(true)
|
||||
expect(body.message).toMatch(/if this email was invited/i)
|
||||
|
||||
expect(sentEmails).toHaveLength(0)
|
||||
expect(db.magicLinks).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('revoked investors cannot self-serve a new link', async () => {
|
||||
db.investors.push({
|
||||
id: 'revoked-1',
|
||||
email: 'gone@example.com',
|
||||
name: null,
|
||||
company: null,
|
||||
status: 'revoked',
|
||||
last_login_at: null,
|
||||
login_count: 0,
|
||||
})
|
||||
|
||||
const res = await requestLink(
|
||||
makeJsonRequest('http://localhost/api/auth/request-link', { email: 'gone@example.com' }),
|
||||
)
|
||||
expect(res.status).toBe(200) // generic success (no info leak)
|
||||
expect(sentEmails).toHaveLength(0)
|
||||
expect(db.magicLinks).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,213 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
||||
import { NextRequest } from 'next/server'
|
||||
|
||||
// Mock the DB pool before the route is imported
|
||||
const queryMock = vi.fn()
|
||||
vi.mock('@/lib/db', () => ({
|
||||
default: { query: (...args: unknown[]) => queryMock(...args) },
|
||||
}))
|
||||
|
||||
// Mock the email sender so no SMTP is attempted
|
||||
const sendMagicLinkEmailMock = vi.fn().mockResolvedValue(undefined)
|
||||
vi.mock('@/lib/email', () => ({
|
||||
sendMagicLinkEmail: (...args: unknown[]) => sendMagicLinkEmailMock(...args),
|
||||
}))
|
||||
|
||||
// Import after mocks are registered
|
||||
import { POST } from '@/app/api/auth/request-link/route'
|
||||
|
||||
// Unique suffix per test so the rate-limit store (keyed by IP / email) doesn't
|
||||
// bleed across cases — the rate-limiter holds state at module scope.
|
||||
let testId = 0
|
||||
function uniqueIp() {
|
||||
testId++
|
||||
return `10.0.${Math.floor(testId / 250)}.${testId % 250}`
|
||||
}
|
||||
|
||||
function makeRequest(body: unknown, ip = uniqueIp()): NextRequest {
|
||||
return new NextRequest('http://localhost/api/auth/request-link', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
'x-forwarded-for': ip,
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
}
|
||||
|
||||
function investorRow(overrides: Partial<{ id: string; email: string; name: string | null; status: string }> = {}) {
|
||||
return {
|
||||
id: overrides.id ?? 'investor-1',
|
||||
email: overrides.email ?? 'invited@example.com',
|
||||
name: overrides.name ?? 'Alice',
|
||||
status: overrides.status ?? 'invited',
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
queryMock.mockReset()
|
||||
sendMagicLinkEmailMock.mockReset()
|
||||
sendMagicLinkEmailMock.mockResolvedValue(undefined)
|
||||
})
|
||||
|
||||
describe('POST /api/auth/request-link — input validation', () => {
|
||||
it('returns 400 when email is missing', async () => {
|
||||
const res = await POST(makeRequest({}))
|
||||
expect(res.status).toBe(400)
|
||||
const body = await res.json()
|
||||
expect(body.error).toBe('Email required')
|
||||
expect(queryMock).not.toHaveBeenCalled()
|
||||
expect(sendMagicLinkEmailMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('returns 400 when email is not a string', async () => {
|
||||
const res = await POST(makeRequest({ email: 12345 }))
|
||||
expect(res.status).toBe(400)
|
||||
expect(sendMagicLinkEmailMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('handles malformed JSON body as missing email (400)', async () => {
|
||||
const req = new NextRequest('http://localhost/api/auth/request-link', {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json', 'x-forwarded-for': uniqueIp() },
|
||||
body: 'not-json',
|
||||
})
|
||||
const res = await POST(req)
|
||||
expect(res.status).toBe(400)
|
||||
})
|
||||
})
|
||||
|
||||
describe('POST /api/auth/request-link — unknown email (enumeration resistance)', () => {
|
||||
it('returns the generic success response without sending email', async () => {
|
||||
// First query: investor lookup → empty rows
|
||||
queryMock.mockResolvedValueOnce({ rows: [] })
|
||||
// Second query: the audit log insert
|
||||
queryMock.mockResolvedValueOnce({ rows: [] })
|
||||
|
||||
const res = await POST(makeRequest({ email: 'unknown@example.com' }))
|
||||
expect(res.status).toBe(200)
|
||||
const body = await res.json()
|
||||
expect(body.success).toBe(true)
|
||||
expect(body.message).toMatch(/if this email was invited/i)
|
||||
expect(sendMagicLinkEmailMock).not.toHaveBeenCalled()
|
||||
|
||||
// Verify the investor-lookup SQL was issued with the normalized email
|
||||
const [sql, params] = queryMock.mock.calls[0]
|
||||
expect(sql).toMatch(/FROM pitch_investors WHERE email/i)
|
||||
expect(params).toEqual(['unknown@example.com'])
|
||||
})
|
||||
|
||||
it('normalizes email (trim + lowercase) before lookup', async () => {
|
||||
queryMock.mockResolvedValueOnce({ rows: [] })
|
||||
queryMock.mockResolvedValueOnce({ rows: [] })
|
||||
|
||||
await POST(makeRequest({ email: ' Mixed@Example.COM ' }))
|
||||
|
||||
const [, params] = queryMock.mock.calls[0]
|
||||
expect(params).toEqual(['mixed@example.com'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('POST /api/auth/request-link — known investor', () => {
|
||||
it('creates a new magic link and sends the email with generic response', async () => {
|
||||
// 1st: investor lookup → found
|
||||
queryMock.mockResolvedValueOnce({ rows: [investorRow()] })
|
||||
// 2nd: magic link insert
|
||||
queryMock.mockResolvedValueOnce({ rows: [] })
|
||||
// 3rd: audit log insert
|
||||
queryMock.mockResolvedValueOnce({ rows: [] })
|
||||
|
||||
const res = await POST(makeRequest({ email: 'invited@example.com' }))
|
||||
expect(res.status).toBe(200)
|
||||
const body = await res.json()
|
||||
expect(body.success).toBe(true)
|
||||
// Response is identical to the unknown-email case (no information leak)
|
||||
expect(body.message).toMatch(/if this email was invited/i)
|
||||
|
||||
// Verify magic link insert
|
||||
const [insertSql, insertParams] = queryMock.mock.calls[1]
|
||||
expect(insertSql).toMatch(/INSERT INTO pitch_magic_links/i)
|
||||
expect(insertParams[0]).toBe('investor-1')
|
||||
expect(insertParams[1]).toMatch(/^[0-9a-f]{96}$/) // 96-char hex token
|
||||
expect(insertParams[2]).toBeInstanceOf(Date)
|
||||
|
||||
// Verify email was sent with the fresh token URL
|
||||
expect(sendMagicLinkEmailMock).toHaveBeenCalledTimes(1)
|
||||
const [emailTo, emailName, magicLinkUrl] = sendMagicLinkEmailMock.mock.calls[0]
|
||||
expect(emailTo).toBe('invited@example.com')
|
||||
expect(emailName).toBe('Alice')
|
||||
expect(magicLinkUrl).toMatch(/\/auth\/verify\?token=[0-9a-f]{96}$/)
|
||||
})
|
||||
|
||||
it('generates a different token on each call (re-invite is always fresh)', async () => {
|
||||
// Call 1
|
||||
queryMock.mockResolvedValueOnce({ rows: [investorRow({ email: 'a@x.com' })] })
|
||||
queryMock.mockResolvedValueOnce({ rows: [] })
|
||||
queryMock.mockResolvedValueOnce({ rows: [] })
|
||||
await POST(makeRequest({ email: 'a@x.com' }))
|
||||
|
||||
// Call 2 — different email to avoid the per-email rate limit
|
||||
queryMock.mockResolvedValueOnce({ rows: [investorRow({ email: 'b@x.com' })] })
|
||||
queryMock.mockResolvedValueOnce({ rows: [] })
|
||||
queryMock.mockResolvedValueOnce({ rows: [] })
|
||||
await POST(makeRequest({ email: 'b@x.com' }))
|
||||
|
||||
const token1 = queryMock.mock.calls[1][1][1]
|
||||
const token2 = queryMock.mock.calls[4][1][1]
|
||||
expect(token1).not.toBe(token2)
|
||||
})
|
||||
|
||||
it('skips email send for a revoked investor (returns generic response)', async () => {
|
||||
queryMock.mockResolvedValueOnce({ rows: [investorRow({ status: 'revoked' })] })
|
||||
queryMock.mockResolvedValueOnce({ rows: [] }) // audit log
|
||||
|
||||
const res = await POST(makeRequest({ email: 'invited@example.com' }))
|
||||
expect(res.status).toBe(200)
|
||||
const body = await res.json()
|
||||
expect(body.success).toBe(true)
|
||||
expect(sendMagicLinkEmailMock).not.toHaveBeenCalled()
|
||||
|
||||
// Ensure no magic link was inserted
|
||||
const inserts = queryMock.mock.calls.filter(c => /INSERT INTO pitch_magic_links/i.test(c[0]))
|
||||
expect(inserts.length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('POST /api/auth/request-link — rate limiting', () => {
|
||||
it('throttles after N requests per email and returns generic success (silent throttle)', async () => {
|
||||
const email = `throttle-${Date.now()}@example.com`
|
||||
|
||||
// First 3 requests succeed (RATE_LIMITS.magicLink.limit = 3)
|
||||
for (let i = 0; i < 3; i++) {
|
||||
queryMock.mockResolvedValueOnce({ rows: [investorRow({ email })] })
|
||||
queryMock.mockResolvedValueOnce({ rows: [] }) // magic link insert
|
||||
queryMock.mockResolvedValueOnce({ rows: [] }) // audit log
|
||||
const res = await POST(makeRequest({ email }))
|
||||
expect(res.status).toBe(200)
|
||||
}
|
||||
expect(sendMagicLinkEmailMock).toHaveBeenCalledTimes(3)
|
||||
|
||||
// 4th request is silently throttled — same generic response, no email sent
|
||||
queryMock.mockResolvedValueOnce({ rows: [] }) // audit log only
|
||||
const res4 = await POST(makeRequest({ email }))
|
||||
expect(res4.status).toBe(200)
|
||||
const body4 = await res4.json()
|
||||
expect(body4.success).toBe(true)
|
||||
// Still exactly 3 emails sent — nothing new
|
||||
expect(sendMagicLinkEmailMock).toHaveBeenCalledTimes(3)
|
||||
})
|
||||
|
||||
it('throttles with 429 after too many attempts from the same IP', async () => {
|
||||
const ip = '172.31.99.99'
|
||||
// RATE_LIMITS.authVerify.limit = 10 for IP-scoped checks
|
||||
for (let i = 0; i < 10; i++) {
|
||||
queryMock.mockResolvedValueOnce({ rows: [] }) // investor lookup returns empty
|
||||
queryMock.mockResolvedValueOnce({ rows: [] }) // audit
|
||||
const res = await POST(makeRequest({ email: `ip-test-${i}@example.com` }, ip))
|
||||
expect(res.status).toBe(200)
|
||||
}
|
||||
|
||||
const res = await POST(makeRequest({ email: 'final@example.com' }, ip))
|
||||
expect(res.status).toBe(429)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,73 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import pool from '@/lib/db'
|
||||
import { generateToken, getClientIp, logAudit } from '@/lib/auth'
|
||||
import { sendMagicLinkEmail } from '@/lib/email'
|
||||
import { checkRateLimit, RATE_LIMITS } from '@/lib/rate-limit'
|
||||
|
||||
// Generic response returned regardless of whether an investor exists, to
|
||||
// prevent email enumeration. The client always sees the same success message.
|
||||
const GENERIC_RESPONSE = {
|
||||
success: true,
|
||||
message: 'If this email was invited, a fresh access link has been sent.',
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const ip = getClientIp(request) || 'unknown'
|
||||
|
||||
// IP-based rate limit to prevent enumeration / abuse
|
||||
const ipRl = checkRateLimit(`request-link-ip:${ip}`, RATE_LIMITS.authVerify)
|
||||
if (!ipRl.allowed) {
|
||||
return NextResponse.json({ error: 'Too many attempts. Try again later.' }, { status: 429 })
|
||||
}
|
||||
|
||||
const body = await request.json().catch(() => ({}))
|
||||
const { email } = body
|
||||
|
||||
if (!email || typeof email !== 'string') {
|
||||
return NextResponse.json({ error: 'Email required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const normalizedEmail = email.toLowerCase().trim()
|
||||
|
||||
// Per-email rate limit (silent — same generic response on throttle so callers
|
||||
// can't distinguish a throttled-but-valid email from an unknown one)
|
||||
const emailRl = checkRateLimit(`magic-link:${normalizedEmail}`, RATE_LIMITS.magicLink)
|
||||
if (!emailRl.allowed) {
|
||||
await logAudit(null, 'request_link_throttled', { email: normalizedEmail }, request)
|
||||
return NextResponse.json(GENERIC_RESPONSE)
|
||||
}
|
||||
|
||||
const { rows } = await pool.query(
|
||||
`SELECT id, email, name, status FROM pitch_investors WHERE email = $1`,
|
||||
[normalizedEmail],
|
||||
)
|
||||
|
||||
if (rows.length === 0) {
|
||||
await logAudit(null, 'request_link_unknown_email', { email: normalizedEmail }, request)
|
||||
return NextResponse.json(GENERIC_RESPONSE)
|
||||
}
|
||||
|
||||
const investor = rows[0]
|
||||
|
||||
if (investor.status === 'revoked') {
|
||||
await logAudit(investor.id, 'request_link_revoked', { email: normalizedEmail }, request)
|
||||
return NextResponse.json(GENERIC_RESPONSE)
|
||||
}
|
||||
|
||||
const token = generateToken()
|
||||
const ttlHours = parseInt(process.env.MAGIC_LINK_TTL_HOURS || '72')
|
||||
const expiresAt = new Date(Date.now() + ttlHours * 60 * 60 * 1000)
|
||||
|
||||
await pool.query(
|
||||
`INSERT INTO pitch_magic_links (investor_id, token, expires_at) VALUES ($1, $2, $3)`,
|
||||
[investor.id, token, expiresAt],
|
||||
)
|
||||
|
||||
const baseUrl = process.env.PITCH_BASE_URL || 'https://pitch.breakpilot.ai'
|
||||
const magicLinkUrl = `${baseUrl}/auth/verify?token=${token}`
|
||||
await sendMagicLinkEmail(investor.email, investor.name, magicLinkUrl)
|
||||
|
||||
await logAudit(investor.id, 'request_link_sent', { email: normalizedEmail, expires_at: expiresAt.toISOString() }, request)
|
||||
|
||||
return NextResponse.json(GENERIC_RESPONSE)
|
||||
}
|
||||
@@ -12,11 +12,12 @@ const SLIDE_DISPLAY_NAMES: Record<string, { de: string; en: string }> = {
|
||||
'cover': { de: 'Cover', en: 'Cover' },
|
||||
'problem': { de: 'Das Problem', en: 'The Problem' },
|
||||
'solution': { de: 'Die Lösung', en: 'The Solution' },
|
||||
'usp': { de: 'USP', en: 'USP' },
|
||||
'product': { de: 'Produkte', en: 'Products' },
|
||||
'how-it-works': { de: 'So funktioniert\'s', en: 'How It Works' },
|
||||
'market': { de: 'Markt', en: 'Market' },
|
||||
'business-model': { de: 'Geschäftsmodell', en: 'Business Model' },
|
||||
'traction': { de: 'Traction', en: 'Traction' },
|
||||
'traction': { de: 'Meilensteine', en: 'Milestones' },
|
||||
'competition': { de: 'Wettbewerb', en: 'Competition' },
|
||||
'team': { de: 'Team', en: 'Team' },
|
||||
'financials': { de: 'Finanzen', en: 'Financials' },
|
||||
@@ -51,7 +52,7 @@ Du hast Zugriff auf alle Unternehmensdaten und zitierst immer konkrete Zahlen.
|
||||
2. Das Problem: "Unternehmen stehen vor einem strategischen Dilemma: Ohne KI verlieren sie Wettbewerbsfähigkeit. Mit US-KI riskieren sie Datenkontrollverlust. Über 30.000 Unternehmen in DE durch EU-Regulierungen belastet."
|
||||
3. 12 Module: "Code Security (SAST/DAST/SBOM/Pentesting), CE-Software-Risikobeurteilung, Compliance-Dokumente (VVT/DSFA/TOMs), Audit Manager, DSR/Betroffenenrechte, Consent Management, Notfallpläne, Cookie-Generator, Compliance LLM, Academy, Integration in Kundenprozesse, Sichere Kommunikation."
|
||||
4. Code & CE: "Kontinuierlich statt einmal im Jahr. CE-Software-Risikobeurteilung auf Code-Basis schon in der Entwicklung. Findings als Tickets mit Implementierungsvorschlägen."
|
||||
5. EU-Infrastruktur: "BSI-zertifizierte Cloud in Deutschland oder OVH in Frankreich. 100% Datensouveränität. KEINE US-Anbieter. Isolierte Namespaces."
|
||||
5. EU-Infrastruktur: "BSI-zertifizierte Cloud in Deutschland oder Frankreich. 100% Datensouveränität. KEINE US-Anbieter. Isolierte Namespaces."
|
||||
6. Zielgruppen: "Maschinen- und Anlagenbauer, Automobilindustrie, Zulieferer und alle produzierenden Unternehmen."
|
||||
7. Geschäftsmodell: "SaaS, mitarbeiterbasiertes Pricing. Kunden zahlen ~40-50k EUR/Jahr und sparen 50-110k EUR (Pentests 30k, CE-Beurteilungen 20k, Auditmanager 60k+). ROI ab Tag 1."
|
||||
8. Team: "Skalierung 5→10→17→25→35 MA in 4 Jahren. 37% Engineering, 20% Sales, 9% CS, 9% Compliance/Legal, 9% Marketing. Compliance Consultant als erster Hire — Domain-Expertise vor Engineering."
|
||||
@@ -68,9 +69,9 @@ Du hast Zugriff auf alle Unternehmensdaten und zitierst immer konkrete Zahlen.
|
||||
|
||||
## IP-Schutz-Layer (KRITISCH)
|
||||
NIEMALS offenbaren: Exakte Modellnamen, Frameworks, Code-Architektur, Datenbankschema, Sicherheitsdetails, Cloud-Provider.
|
||||
Stattdessen: "Proprietäre KI-Engine", "BSI-zertifizierte EU-Cloud (SysEleven, OVH, Hetzner)", "Isolierte Kunden-Namespaces", "Enterprise-Grade Verschlüsselung".
|
||||
Stattdessen: "Proprietäre KI-Engine", "BSI-zertifizierte EU-Cloud (SysEleven, Hetzner)", "Isolierte Kunden-Namespaces", "Enterprise-Grade Verschlüsselung".
|
||||
|
||||
## Erlaubt: Geschäftsmodell, Preise, Marktdaten, Features, Team, Finanzen, Use of Funds, Hardware-Specs (öffentlich), LLM-Größen (32b/40b/1000b), CE-Risikobeurteilung, Jira-Integration, Meeting-Recorder, Matrix/Jitsi.
|
||||
## Erlaubt: Geschäftsmodell, Preise, Marktdaten, Features, Team, Finanzen, Use of Funds, Hardware-Specs (öffentlich), LLM-Größen (32b/40b/1000b), CE-Risikobeurteilung, Issue-Tracker-Integration, Meeting-Recorder, Matrix/Jitsi.
|
||||
|
||||
## Team-Antworten (WICHTIG)
|
||||
Wenn nach dem Team gefragt wird: IMMER die Namen, Rollen und Expertise der Gründer aus den bereitgestellten Daten nennen. NIEMALS vage Antworten wie "unser Team vereint Expertise" ohne Namen. Zitiere die konkreten Personen aus den Unternehmensdaten.
|
||||
|
||||
@@ -13,6 +13,15 @@ export async function GET(request: NextRequest, ctx: Ctx) {
|
||||
|
||||
const { versionId } = await ctx.params
|
||||
|
||||
// Load version metadata
|
||||
const ver = await pool.query(
|
||||
`SELECT name, status FROM pitch_versions WHERE id = $1`,
|
||||
[versionId],
|
||||
)
|
||||
if (ver.rows.length === 0) {
|
||||
return NextResponse.json({ error: 'Version not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
// Load version data
|
||||
const { rows } = await pool.query(
|
||||
`SELECT table_name, data FROM pitch_version_data WHERE version_id = $1`,
|
||||
@@ -20,7 +29,7 @@ export async function GET(request: NextRequest, ctx: Ctx) {
|
||||
)
|
||||
|
||||
if (rows.length === 0) {
|
||||
return NextResponse.json({ error: 'Version not found or has no data' }, { status: 404 })
|
||||
return NextResponse.json({ error: 'Version has no data' }, { status: 404 })
|
||||
}
|
||||
|
||||
const map: Record<string, unknown[]> = {}
|
||||
@@ -28,7 +37,7 @@ export async function GET(request: NextRequest, ctx: Ctx) {
|
||||
map[row.table_name] = typeof row.data === 'string' ? JSON.parse(row.data) : row.data
|
||||
}
|
||||
|
||||
// Return PitchData format
|
||||
// Return PitchData format + version metadata
|
||||
return NextResponse.json({
|
||||
company: (map.company || [])[0] || null,
|
||||
team: map.team || [],
|
||||
@@ -40,5 +49,6 @@ export async function GET(request: NextRequest, ctx: Ctx) {
|
||||
metrics: map.metrics || [],
|
||||
funding: (map.funding || [])[0] || null,
|
||||
products: map.products || [],
|
||||
_version: { name: ver.rows[0].name, status: ver.rows[0].status },
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,8 +1,43 @@
|
||||
'use client'
|
||||
|
||||
import { motion } from 'framer-motion'
|
||||
import { useState, FormEvent } from 'react'
|
||||
|
||||
type Status = 'idle' | 'submitting' | 'sent' | 'error'
|
||||
|
||||
export default function AuthPage() {
|
||||
const [email, setEmail] = useState('')
|
||||
const [status, setStatus] = useState<Status>('idle')
|
||||
const [message, setMessage] = useState<string | null>(null)
|
||||
|
||||
async function handleSubmit(e: FormEvent) {
|
||||
e.preventDefault()
|
||||
if (!email.trim()) return
|
||||
|
||||
setStatus('submitting')
|
||||
setMessage(null)
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/auth/request-link', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email: email.trim() }),
|
||||
})
|
||||
const data = await res.json().catch(() => ({}))
|
||||
|
||||
if (res.ok) {
|
||||
setStatus('sent')
|
||||
setMessage(data.message || 'If this email was invited, a fresh access link has been sent.')
|
||||
} else {
|
||||
setStatus('error')
|
||||
setMessage(data.error || 'Something went wrong. Please try again.')
|
||||
}
|
||||
} catch {
|
||||
setStatus('error')
|
||||
setMessage('Network error. Please try again.')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-screen flex items-center justify-center bg-[#0a0a1a] relative overflow-hidden">
|
||||
{/* Background gradient */}
|
||||
@@ -35,9 +70,44 @@ export default function AuthPage() {
|
||||
|
||||
<p className="text-white/50 text-sm leading-relaxed mb-6">
|
||||
This interactive pitch deck is available by invitation only.
|
||||
Please check your email for an access link.
|
||||
If you were invited, enter your email below and we'll send you a fresh access link.
|
||||
</p>
|
||||
|
||||
{status === 'sent' ? (
|
||||
<div className="text-left bg-indigo-500/10 border border-indigo-500/20 rounded-lg p-4 mb-5">
|
||||
<p className="text-indigo-200/90 text-sm leading-relaxed">
|
||||
{message}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<form onSubmit={handleSubmit} className="text-left mb-5">
|
||||
<label htmlFor="email" className="block text-white/60 text-xs mb-2 uppercase tracking-wide">
|
||||
Email address
|
||||
</label>
|
||||
<input
|
||||
id="email"
|
||||
type="email"
|
||||
required
|
||||
autoComplete="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
disabled={status === 'submitting'}
|
||||
placeholder="you@example.com"
|
||||
className="w-full bg-white/[0.04] border border-white/[0.08] rounded-lg px-4 py-3 text-white/90 text-sm placeholder:text-white/20 focus:outline-none focus:border-indigo-400/50 focus:bg-white/[0.06] transition-colors disabled:opacity-50"
|
||||
/>
|
||||
{status === 'error' && message && (
|
||||
<p className="mt-2 text-rose-300/80 text-xs">{message}</p>
|
||||
)}
|
||||
<button
|
||||
type="submit"
|
||||
disabled={status === 'submitting' || !email.trim()}
|
||||
className="w-full mt-4 bg-gradient-to-br from-indigo-500 to-purple-500 hover:from-indigo-400 hover:to-purple-400 disabled:opacity-40 disabled:cursor-not-allowed text-white text-sm font-medium rounded-lg px-4 py-3 transition-all"
|
||||
>
|
||||
{status === 'submitting' ? 'Sending…' : 'Send access link'}
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
|
||||
<div className="border-t border-white/[0.06] pt-5">
|
||||
<p className="text-white/30 text-xs">
|
||||
Questions? Contact us at{' '}
|
||||
|
||||
@@ -8,6 +8,7 @@ import PitchDeck from '@/components/PitchDeck'
|
||||
export default function PreviewPage() {
|
||||
const { versionId } = useParams<{ versionId: string }>()
|
||||
const [data, setData] = useState<PitchData | null>(null)
|
||||
const [versionMeta, setVersionMeta] = useState<{ name: string; status: string } | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [lang, setLang] = useState<Language>('de')
|
||||
@@ -24,7 +25,13 @@ export default function PreviewPage() {
|
||||
if (!r.ok) throw new Error((await r.json().catch(() => ({}))).error || 'Failed to load')
|
||||
return r.json()
|
||||
})
|
||||
.then(setData)
|
||||
.then(d => {
|
||||
if (d._version) {
|
||||
setVersionMeta(d._version)
|
||||
delete d._version
|
||||
}
|
||||
setData(d)
|
||||
})
|
||||
.catch(e => setError(e.message))
|
||||
.finally(() => setLoading(false))
|
||||
}, [versionId])
|
||||
@@ -57,8 +64,8 @@ export default function PreviewPage() {
|
||||
return (
|
||||
<div className="relative">
|
||||
{/* Preview banner */}
|
||||
<div className="fixed top-0 left-0 right-0 z-[100] bg-amber-500/90 text-black text-center py-1.5 text-xs font-semibold">
|
||||
PREVIEW MODE — This is how investors will see this version
|
||||
<div className="fixed top-0 left-0 right-0 z-[100] bg-amber-500/90 text-black text-center py-1.5 text-xs font-semibold tracking-wide">
|
||||
PREVIEW: {versionMeta?.name ?? 'Loading...'} — {versionMeta?.status === 'draft' ? 'Draft' : 'Committed'}
|
||||
</div>
|
||||
<PitchDeck
|
||||
lang={lang}
|
||||
|
||||
@@ -286,10 +286,11 @@ export default function ChatFAB({
|
||||
}
|
||||
|
||||
// If FAQ matched and has a goto_slide, add a GOTO marker to the response
|
||||
if (faqMatch?.goto_slide) {
|
||||
const gotoIdx = SLIDE_ORDER.indexOf(faqMatch.goto_slide)
|
||||
const topMatch = faqMatches[0]
|
||||
if (topMatch?.goto_slide) {
|
||||
const gotoIdx = SLIDE_ORDER.indexOf(topMatch.goto_slide)
|
||||
if (gotoIdx >= 0) {
|
||||
const suffix = `\n\n[GOTO:${faqMatch.goto_slide}]`
|
||||
const suffix = `\n\n[GOTO:${topMatch.goto_slide}]`
|
||||
content += suffix
|
||||
setMessages(prev => {
|
||||
const updated = [...prev]
|
||||
|
||||
@@ -41,6 +41,16 @@ import GTMSlide from './slides/GTMSlide'
|
||||
import RegulatorySlide from './slides/RegulatorySlide'
|
||||
import EngineeringSlide from './slides/EngineeringSlide'
|
||||
import AIPipelineSlide from './slides/AIPipelineSlide'
|
||||
import USPSlide from './slides/USPSlide'
|
||||
import DisclaimerSlide from './slides/DisclaimerSlide'
|
||||
import ExecutiveSummarySlide from './slides/ExecutiveSummarySlide'
|
||||
import RegulatoryLandscapeSlide from './slides/RegulatoryLandscapeSlide'
|
||||
import CapTableSlide from './slides/CapTableSlide'
|
||||
import SavingsSlide from './slides/SavingsSlide'
|
||||
import SDKDemoSlide from './slides/SDKDemoSlide'
|
||||
import StrategySlide from './slides/StrategySlide'
|
||||
import FinanzplanSlide from './slides/FinanzplanSlide'
|
||||
import GlossarySlide from './slides/GlossarySlide'
|
||||
|
||||
interface PitchDeckProps {
|
||||
lang: Language
|
||||
@@ -132,12 +142,18 @@ export default function PitchDeck({ lang, onToggleLanguage, investor, onLogout,
|
||||
isPresenting={presenter.state !== 'idle'}
|
||||
/>
|
||||
)
|
||||
case 'executive-summary':
|
||||
return <ExecutiveSummarySlide lang={lang} data={data} />
|
||||
case 'cover':
|
||||
return <CoverSlide lang={lang} onNext={nav.nextSlide} funding={data.funding} />
|
||||
case 'problem':
|
||||
return <ProblemSlide lang={lang} />
|
||||
case 'solution':
|
||||
return <SolutionSlide lang={lang} />
|
||||
case 'usp':
|
||||
return <USPSlide lang={lang} />
|
||||
case 'regulatory-landscape':
|
||||
return <RegulatoryLandscapeSlide lang={lang} />
|
||||
case 'product':
|
||||
return <ProductSlide lang={lang} products={data.products} />
|
||||
case 'how-it-works':
|
||||
@@ -156,6 +172,10 @@ export default function PitchDeck({ lang, onToggleLanguage, investor, onLogout,
|
||||
return <FinancialsSlide lang={lang} investorId={investor?.id || null} />
|
||||
case 'the-ask':
|
||||
return <TheAskSlide lang={lang} funding={data.funding} />
|
||||
case 'cap-table':
|
||||
return <CapTableSlide lang={lang} />
|
||||
case 'customer-savings':
|
||||
return <SavingsSlide lang={lang} />
|
||||
case 'ai-qa':
|
||||
return <AIQASlide lang={lang} />
|
||||
case 'annex-assumptions':
|
||||
@@ -170,6 +190,16 @@ export default function PitchDeck({ lang, onToggleLanguage, investor, onLogout,
|
||||
return <EngineeringSlide lang={lang} />
|
||||
case 'annex-aipipeline':
|
||||
return <AIPipelineSlide lang={lang} />
|
||||
case 'annex-sdk-demo':
|
||||
return <SDKDemoSlide lang={lang} />
|
||||
case 'annex-strategy':
|
||||
return <StrategySlide lang={lang} />
|
||||
case 'annex-finanzplan':
|
||||
return <FinanzplanSlide lang={lang} />
|
||||
case 'annex-glossary':
|
||||
return <GlossarySlide lang={lang} />
|
||||
case 'legal-disclaimer':
|
||||
return <DisclaimerSlide lang={lang} />
|
||||
default:
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -18,11 +18,14 @@ import {
|
||||
Activity,
|
||||
Shield,
|
||||
Cpu,
|
||||
MessageSquare,
|
||||
Eye,
|
||||
Gauge,
|
||||
Network,
|
||||
Sparkles,
|
||||
Scale,
|
||||
BookOpen,
|
||||
Gavel,
|
||||
Globe,
|
||||
} from 'lucide-react'
|
||||
|
||||
interface AIPipelineSlideProps {
|
||||
@@ -37,10 +40,10 @@ export default function AIPipelineSlide({ lang }: AIPipelineSlideProps) {
|
||||
const [activeTab, setActiveTab] = useState<PipelineTab>('rag')
|
||||
|
||||
const heroStats = [
|
||||
{ value: '38+', label: de ? 'Indexierte Verordnungen' : 'Indexed Regulations', sub: 'DSGVO · AI Act · NIS2 · CRA · BDSG · DSA · ...', color: 'text-indigo-400' },
|
||||
{ value: '6.259', label: de ? 'Extrahierte Controls' : 'Extracted Controls', sub: de ? '79% Source-Match · 9 Verordnungen' : '79% source match · 9 regulations', color: 'text-purple-400' },
|
||||
{ value: '6', label: de ? 'Qdrant Collections' : 'Qdrant Collections', sub: de ? 'Legal Corpus · DSFA · Recht · Templates · ...' : 'Legal Corpus · DSFA · Law · Templates · ...', color: 'text-emerald-400' },
|
||||
{ value: '325+', label: de ? 'Abgeleitete Pflichten' : 'Derived Obligations', sub: de ? 'NIS2 · DSGVO · AI Act · CRA · ...' : 'NIS2 · GDPR · AI Act · CRA · ...', color: 'text-amber-400' },
|
||||
{ value: '75+', label: de ? 'Rechtsquellen' : 'Legal Sources', sub: de ? 'EU-Verordnungen · DACH-Gesetze · Frameworks' : 'EU regulations · DACH laws · Frameworks', color: 'text-indigo-400' },
|
||||
{ value: '70k+', label: de ? 'Unique Controls' : 'Unique Controls', sub: de ? 'Prüfbare Compliance-Anforderungen' : 'Auditable compliance requirements', color: 'text-purple-400' },
|
||||
{ value: '47k+', label: de ? 'Extrahierte Pflichten' : 'Extracted Obligations', sub: de ? 'Aus Gesetzestexten abgeleitet' : 'Derived from legal texts', color: 'text-emerald-400' },
|
||||
{ value: '6', label: de ? 'Pipeline-Versionen' : 'Pipeline Versions', sub: de ? 'Kontinuierliche Verbesserung' : 'Continuous improvement', color: 'text-amber-400' },
|
||||
]
|
||||
|
||||
const tabs: { id: PipelineTab; label: string; icon: typeof Brain }[] = [
|
||||
@@ -49,59 +52,105 @@ export default function AIPipelineSlide({ lang }: AIPipelineSlideProps) {
|
||||
{ id: 'quality', label: de ? 'QA & Infrastruktur' : 'QA & Infrastructure', icon: Gauge },
|
||||
]
|
||||
|
||||
// RAG Pipeline content
|
||||
// Source categories for investors
|
||||
const sourceCategories = [
|
||||
{
|
||||
icon: Globe,
|
||||
color: 'text-blue-400',
|
||||
bg: 'bg-blue-500/10 border-blue-500/20',
|
||||
title: de ? 'EU-Verordnungen (~15)' : 'EU Regulations (~15)',
|
||||
why: de
|
||||
? 'Bindende Vorgaben fuer alle EU-Unternehmen — Verstoesse fuehren zu Bussgeldern bis 4% des Jahresumsatzes.'
|
||||
: 'Binding requirements for all EU companies — violations lead to fines up to 4% of annual revenue.',
|
||||
examples: 'DSGVO · AI Act · NIS2 · CRA · MiCA · DSA · Maschinenverordnung · Batterieverordnung',
|
||||
},
|
||||
{
|
||||
icon: Scale,
|
||||
color: 'text-purple-400',
|
||||
bg: 'bg-purple-500/10 border-purple-500/20',
|
||||
title: de ? 'DACH-Gesetze (~20)' : 'DACH Laws (~20)',
|
||||
why: de
|
||||
? 'Nationale Umsetzungen und eigenstaendige Gesetze — oft strenger als EU-Mindeststandards.'
|
||||
: 'National implementations and standalone laws — often stricter than EU minimum standards.',
|
||||
examples: 'BDSG · TKG · GwG · HGB · BGB · UrhG · GewO · KRITIS-DachG · AT ABGB · AT KSchG',
|
||||
},
|
||||
{
|
||||
icon: BookOpen,
|
||||
color: 'text-emerald-400',
|
||||
bg: 'bg-emerald-500/10 border-emerald-500/20',
|
||||
title: de ? 'Frameworks & Standards (~15)' : 'Frameworks & Standards (~15)',
|
||||
why: de
|
||||
? 'Branchenstandards definieren den Stand der Technik — Aufsichtsbehoerden erwarten deren Einhaltung.'
|
||||
: 'Industry standards define state of the art — regulators expect compliance with them.',
|
||||
examples: 'NIST 800-53 · OWASP ASVS · OWASP SAMM · ENISA ICS · NIST Zero Trust · CISA Secure by Design',
|
||||
},
|
||||
{
|
||||
icon: Gavel,
|
||||
color: 'text-amber-400',
|
||||
bg: 'bg-amber-500/10 border-amber-500/20',
|
||||
title: de ? 'DSFA-Leitlinien & Urteile' : 'DPIA Guidelines & Rulings',
|
||||
why: de
|
||||
? 'Urteile zeigen wie Gerichte Gesetze auslegen — entscheidend fuer praezise Compliance-Beratung statt generischer Antworten.'
|
||||
: 'Court rulings show how laws are interpreted — critical for precise compliance advice instead of generic answers.',
|
||||
examples: de
|
||||
? '16 Bundeslaender DSFA-Leitlinien · BAG-Urteile · Datenschutzkonferenz-Beschluesse'
|
||||
: '16 federal state DPIA guidelines · Labor court rulings · Data protection conference decisions',
|
||||
},
|
||||
]
|
||||
|
||||
// RAG Pipeline steps
|
||||
const ragPipelineSteps = [
|
||||
{
|
||||
icon: FileText,
|
||||
color: 'text-blue-400',
|
||||
bg: 'bg-blue-500/10 border-blue-500/20',
|
||||
title: de ? '1. Ingestion & QA' : '1. Ingestion & QA',
|
||||
title: de ? '1. Dokument-Ingestion' : '1. Document Ingestion',
|
||||
items: de
|
||||
? ['110+ Verordnungen und Gesetze (EU + DACH)', 'Strukturelles Chunking an Artikel/Absatz-Grenzen', '25.000+ extrahierte Prüfaspekte', 'Deduplizierung + Cross-Regulation Harmonisierung']
|
||||
: ['110+ laws and regulations (EU + DACH)', 'Structural chunking at article/paragraph boundaries', '25,000+ extracted audit aspects', 'Deduplication + cross-regulation harmonization'],
|
||||
? ['75+ Rechtsquellen aus EU, Deutschland und Oesterreich', 'Strukturelles Chunking an Artikel- und Absatz-Grenzen', 'Automatische Lizenz-Klassifikation (frei / Zitat / geschuetzt)', 'Geschuetzte Normen (ISO, BSI) werden vollstaendig reformuliert']
|
||||
: ['75+ legal sources from EU, Germany and Austria', 'Structural chunking at article and paragraph boundaries', 'Automatic license classification (free / citation / restricted)', 'Protected standards (ISO, BSI) are fully reformulated'],
|
||||
},
|
||||
{
|
||||
icon: Cpu,
|
||||
color: 'text-purple-400',
|
||||
bg: 'bg-purple-500/10 border-purple-500/20',
|
||||
title: de ? '2. Embedding & LLM' : '2. Embedding & LLM',
|
||||
title: de ? '2. Control-Extraktion' : '2. Control Extraction',
|
||||
items: de
|
||||
? ['BGE-M3 Multilingual (1024-dim, lokal)', '120B LLM auf OVH (via LiteLLM, OpenAI-kompatibel)', '1000B LLM auf SysEleven (BSI-zertifiziert)', 'CrossEncoder Re-Ranking + HyDE']
|
||||
: ['BGE-M3 multilingual (1024-dim, local)', '120B LLM on OVH (via LiteLLM, OpenAI-compatible)', '1000B LLM on SysEleven (BSI-certified)', 'CrossEncoder re-ranking + HyDE'],
|
||||
? ['LLM extrahiert Pflichten und Anforderungen aus jedem Textabschnitt', '6 Pipeline-Versionen mit kontinuierlicher Qualitaetsverbesserung', 'Obligation Extraction: 47.000+ einzelne Pflichten identifiziert', 'Atomic Control Composition: Pflichten werden zu pruefbaren Controls']
|
||||
: ['LLM extracts obligations and requirements from each text section', '6 pipeline versions with continuous quality improvement', 'Obligation extraction: 47,000+ individual duties identified', 'Atomic control composition: duties become auditable controls'],
|
||||
},
|
||||
{
|
||||
icon: Database,
|
||||
color: 'text-emerald-400',
|
||||
bg: 'bg-emerald-500/10 border-emerald-500/20',
|
||||
title: de ? '3. Vektorspeicher' : '3. Vector Store',
|
||||
title: de ? '3. Deduplizierung & Speicherung' : '3. Deduplication & Storage',
|
||||
items: de
|
||||
? ['Qdrant Vector DB (Hetzner, API-Key gesichert)', '6 Collections: CE, Recht, Gesetze, Datenschutz, DSFA, Templates', 'MinIO Object Storage (Hetzner, S3-kompatibel, TLS)', '25.000+ Prüfaspekte · 110 Gesetze & Regularien · abgeleitete Pflichten']
|
||||
: ['Qdrant Vector DB (Hetzner, API-key secured)', '6 Collections: CE, Law, Statutes, Privacy, DSFA, Templates', 'MinIO object storage (Hetzner, S3-compatible, TLS)', '25,000+ audit aspects · 110 laws & regulations · derived obligations'],
|
||||
? ['97.000 generierte Controls → 70.000+ nach Deduplizierung', 'Embedding-basierte Aehnlichkeitserkennung (Cosine Similarity)', 'Cross-Regulation Harmonisierung: gleiche Pflicht aus verschiedenen Gesetzen wird zusammengefuehrt', 'Ziel: 25.000–50.000 atomare Master Controls']
|
||||
: ['97,000 generated controls → 70,000+ after deduplication', 'Embedding-based similarity detection (cosine similarity)', 'Cross-regulation harmonization: same obligation from different laws is merged', 'Target: 25,000–50,000 atomic master controls'],
|
||||
},
|
||||
{
|
||||
icon: Search,
|
||||
color: 'text-indigo-400',
|
||||
bg: 'bg-indigo-500/10 border-indigo-500/20',
|
||||
title: de ? '4. Hybrid Search' : '4. Hybrid Search',
|
||||
title: de ? '4. Hybrid Search & Beratung' : '4. Hybrid Search & Advisory',
|
||||
items: de
|
||||
? ['Multi-Collection-Suche mit Whitelist-Validierung', 'Deutsche Komposita-Zerlegung', 'Cross-Encoder Re-Ranking der Top-K Ergebnisse', 'Quellen-Attribution mit Artikel/Absatz-Referenz']
|
||||
: ['Multi-collection search with whitelist validation', 'German compound word decomposition', 'Cross-encoder re-ranking of top-K results', 'Source attribution with article/paragraph reference'],
|
||||
? ['Vektorsuche + Keyword-Suche ueber alle Rechtsquellen gleichzeitig', 'Cross-Encoder Re-Ranking fuer praezise Relevanz-Sortierung', 'Quellen-Attribution: Jede Antwort verweist auf Artikel und Absatz', 'Der Compliance-Agent antwortet mit Rechtsgrundlage — nicht mit Vermutungen']
|
||||
: ['Vector search + keyword search across all legal sources simultaneously', 'Cross-encoder re-ranking for precise relevance sorting', 'Source attribution: Every answer references article and paragraph', 'The compliance agent answers with legal basis — not guesswork'],
|
||||
},
|
||||
]
|
||||
|
||||
// Multi-Agent System content — UCCA + Policy Engine
|
||||
const agents = [
|
||||
{ name: 'UCCA', soul: de ? 'Use-Case Compliance' : 'Use-Case Compliance', desc: de ? 'Policy Engine (45 Regeln) + Eskalation E0–E3' : 'Policy engine (45 rules) + escalation E0–E3', color: 'text-indigo-400' },
|
||||
{ name: de ? 'Pflichten-Engine' : 'Obligations Engine', soul: de ? 'abgeleitete Pflichten' : 'derived obligations', desc: de ? 'Multi-Regulation: NIS2, DSGVO, AI Act, CRA, ...' : 'Multi-regulation: NIS2, GDPR, AI Act, CRA, ...', color: 'text-emerald-400' },
|
||||
{ name: de ? 'Compliance-Berater' : 'Compliance Advisor', soul: de ? 'Legal RAG + LLM' : 'Legal RAG + LLM', desc: de ? 'Wizard-basierter Chatbot mit Qdrant-Kontext' : 'Wizard-based chatbot with Qdrant context', color: 'text-purple-400' },
|
||||
{ name: de ? 'Dokument-Generator' : 'Document Generator', soul: de ? '20 Templates' : '20 templates', desc: de ? 'AGB, DSE, AV-Vertrag, Widerruf + 16 weitere' : 'T&C, Privacy Policy, DPA, Withdrawal + 16 more', color: 'text-amber-400' },
|
||||
{ name: de ? 'DSFA-Agent' : 'DSFA Agent', soul: de ? 'Art. 35 DSGVO' : 'Art. 35 GDPR', desc: de ? 'Risikobewertung mit Legal Context Injection' : 'Risk assessment with legal context injection', color: 'text-red-400' },
|
||||
{ name: de ? 'Schulungs-Engine' : 'Training Engine', soul: de ? 'Academy + TTS' : 'Academy + TTS', desc: de ? '28 Module · Piper TTS · Automatische Videos' : '28 modules · Piper TTS · Automatic videos', color: 'text-blue-400' },
|
||||
{ name: de ? 'Pflichten-Engine' : 'Obligations Engine', soul: de ? '47.000+ Pflichten' : '47,000+ obligations', desc: de ? 'Multi-Regulation: NIS2, DSGVO, AI Act, CRA, ...' : 'Multi-regulation: NIS2, GDPR, AI Act, CRA, ...', color: 'text-emerald-400' },
|
||||
{ name: de ? 'Compliance-Berater' : 'Compliance Advisor', soul: de ? 'Legal RAG + LLM' : 'Legal RAG + LLM', desc: de ? 'Chatbot mit 75+ Rechtsquellen als Wissenbasis' : 'Chatbot with 75+ legal sources as knowledge base', color: 'text-purple-400' },
|
||||
{ name: de ? 'Dokument-Generator' : 'Document Generator', soul: de ? '7+ Templates' : '7+ templates', desc: de ? 'AGB, DSE, AV-Vertrag, DSFA, FRIA, BV + weitere' : 'T&C, Privacy Policy, DPA, DPIA, FRIA, Works Agreement + more', color: 'text-amber-400' },
|
||||
{ name: de ? 'DSFA-Agent' : 'DPIA Agent', soul: de ? 'Art. 35 DSGVO' : 'Art. 35 GDPR', desc: de ? 'Risikobewertung mit 16 Bundeslaender-Leitlinien' : 'Risk assessment with 16 federal state guidelines', color: 'text-red-400' },
|
||||
{ name: de ? 'Control-Pipeline' : 'Control Pipeline', soul: de ? '70.000+ Controls' : '70,000+ controls', desc: de ? 'Automatische Extraktion aus neuen Rechtsquellen' : 'Automatic extraction from new legal sources', color: 'text-blue-400' },
|
||||
]
|
||||
|
||||
const agentInfra = [
|
||||
{ icon: Shield, label: de ? 'Policy Engine' : 'Policy Engine', desc: de ? 'Deterministisch · LLM ist NICHT Wahrheitsquelle' : 'Deterministic · LLM is NOT source of truth' },
|
||||
{ icon: Brain, label: de ? 'LLM-Schicht' : 'LLM Layer', desc: de ? '120B (OVH) + 1000B (SysEleven BSI) · EU-only' : '120B (OVH) + 1000B (SysEleven BSI) · EU-only' },
|
||||
{ icon: Brain, label: de ? 'LLM-Schicht' : 'LLM Layer', desc: de ? 'Claude + lokale Modelle · EU-only Hosting' : 'Claude + local models · EU-only hosting' },
|
||||
{ icon: Network, label: 'LiteLLM Gateway', desc: de ? 'OpenAI-kompatibel · Multi-Provider Routing' : 'OpenAI-compatible · Multi-provider routing' },
|
||||
{ icon: Activity, label: de ? 'Eskalation E0–E3' : 'Escalation E0–E3', desc: de ? 'Auto-Approve → Team-Lead → DSB → DSB+Legal' : 'Auto-approve → Team lead → DPO → DPO+Legal' },
|
||||
]
|
||||
@@ -113,32 +162,32 @@ export default function AIPipelineSlide({ lang }: AIPipelineSlideProps) {
|
||||
color: 'text-emerald-400',
|
||||
title: de ? 'Control Quality Pipeline' : 'Control Quality Pipeline',
|
||||
items: de
|
||||
? ['6.259 Controls extrahiert (79% Source-Match)', '3.301 Duplikate entfernt (Phase 5 Normalisierung)', '90+ QA-Skripte: Deduplizierung, Match-Validierung', 'Canonical Controls JSON-Schema-Validierung in CI']
|
||||
: ['6,259 controls extracted (79% source match)', '3,301 duplicates removed (Phase 5 normalization)', '90+ QA scripts: deduplication, match validation', 'Canonical Controls JSON schema validation in CI'],
|
||||
? ['97.000 Controls generiert, 70.000+ nach Deduplizierung', '6 Pipeline-Versionen mit steigender Extraktionsqualitaet', 'Automatische Lizenz-Pruefung: geschuetzte Normen werden reformuliert', 'Jeder Control hat Quellen-Referenz auf Artikel und Absatz']
|
||||
: ['97,000 controls generated, 70,000+ after deduplication', '6 pipeline versions with increasing extraction quality', 'Automatic license check: protected standards are reformulated', 'Every control has source reference to article and paragraph'],
|
||||
},
|
||||
{
|
||||
icon: Eye,
|
||||
color: 'text-indigo-400',
|
||||
title: de ? 'RAG Quality & Monitoring' : 'RAG Quality & Monitoring',
|
||||
title: de ? 'Kontinuierliche Erweiterung' : 'Continuous Expansion',
|
||||
items: de
|
||||
? ['PDF-QA-Pipeline: 86% Artikel-Extraktion', 'Multi-Collection-Whitelist-Validierung', 'Qdrant-Deduplizierung: 8-Stufen-Bereinigung', 'Fallback-Handling: RAG-Fehler brechen nie Hauptfunktion']
|
||||
: ['PDF QA pipeline: 86% article extraction', 'Multi-collection whitelist validation', 'Qdrant deduplication: 8-step cleanup', 'Fallback handling: RAG failures never break main function'],
|
||||
? ['Neue Gesetze werden automatisch ingestiert und verarbeitet', 'Pipeline erkennt Ueberschneidungen mit bestehenden Controls', 'Cross-Regulation Mapping: gleiche Pflicht aus DSGVO und BDSG wird verknuepft', 'Wachsender Wissensvorsprung gegenueber manueller Compliance-Beratung']
|
||||
: ['New laws are automatically ingested and processed', 'Pipeline detects overlaps with existing controls', 'Cross-regulation mapping: same obligation from GDPR and BDSG is linked', 'Growing knowledge advantage over manual compliance consulting'],
|
||||
},
|
||||
{
|
||||
icon: Sparkles,
|
||||
color: 'text-purple-400',
|
||||
title: de ? 'CI/CD & Testing' : 'CI/CD & Testing',
|
||||
items: de
|
||||
? ['Gitea Actions: Lint → Tests → Validierung bei jedem Push', 'Go-Tests (AI SDK) + Python-Tests (Backend + Crawler + Gateway)', 'Coolify Auto-Deploy mit Health-Check-Monitoring', 'arm64 → amd64 Cross-Build fuer Hetzner Production']
|
||||
: ['Gitea Actions: Lint → Tests → Validation on every push', 'Go tests (AI SDK) + Python tests (Backend + Crawler + Gateway)', 'Coolify auto-deploy with health check monitoring', 'arm64 → amd64 cross-build for Hetzner production'],
|
||||
? ['Gitea Actions: Lint → Tests → Validierung bei jedem Push', 'Go-Tests (AI SDK) + Python-Tests (Backend + Pipeline)', 'Coolify Auto-Deploy mit Health-Check-Monitoring', 'arm64 → amd64 Cross-Build fuer Hetzner Production']
|
||||
: ['Gitea Actions: Lint → Tests → Validation on every push', 'Go tests (AI SDK) + Python tests (Backend + Pipeline)', 'Coolify auto-deploy with health check monitoring', 'arm64 → amd64 cross-build for Hetzner production'],
|
||||
},
|
||||
{
|
||||
icon: Zap,
|
||||
color: 'text-amber-400',
|
||||
title: de ? 'LLM-Infrastruktur' : 'LLM Infrastructure',
|
||||
title: de ? 'Infrastruktur' : 'Infrastructure',
|
||||
items: de
|
||||
? ['120B Modell auf OVH via LiteLLM (OpenAI-kompatibel)', '1000B Modell auf SysEleven (BSI-zertifiziert)', 'Isolierte Namespaces pro Kunde · Keine US-Provider', 'BGE-M3 Embedding lokal · Lazy Model Loading']
|
||||
: ['120B model on OVH via LiteLLM (OpenAI-compatible)', '1000B model on SysEleven (BSI-certified)', 'Isolated namespaces per customer · No US providers', 'BGE-M3 embedding local · Lazy model loading'],
|
||||
? ['Qdrant Vektordatenbank fuer semantische Suche', 'BGE-M3 Multilingual Embedding (lokal gehostet)', 'MinIO Object Storage (S3-kompatibel, TLS-verschluesselt)', '100% EU-Cloud · Keine US-Provider · BSI-konforme Hosting-Partner']
|
||||
: ['Qdrant vector database for semantic search', 'BGE-M3 multilingual embedding (locally hosted)', 'MinIO object storage (S3-compatible, TLS-encrypted)', '100% EU cloud · No US providers · BSI-compliant hosting partners'],
|
||||
},
|
||||
]
|
||||
|
||||
@@ -194,15 +243,32 @@ export default function AIPipelineSlide({ lang }: AIPipelineSlideProps) {
|
||||
<FadeInView delay={0.2} key={activeTab}>
|
||||
{activeTab === 'rag' && (
|
||||
<div>
|
||||
{/* Source Categories — Why each matters */}
|
||||
<div className="grid md:grid-cols-2 gap-2 mb-4">
|
||||
{sourceCategories.map((cat, idx) => {
|
||||
const Icon = cat.icon
|
||||
return (
|
||||
<div key={idx} className={`border rounded-xl p-2.5 ${cat.bg}`}>
|
||||
<div className="flex items-center gap-2 mb-1.5">
|
||||
<Icon className={`w-4 h-4 ${cat.color}`} />
|
||||
<h3 className="text-xs font-bold text-white">{cat.title}</h3>
|
||||
</div>
|
||||
<p className="text-[10px] text-white/50 mb-1.5 leading-relaxed">{cat.why}</p>
|
||||
<p className="text-[9px] text-white/25 font-mono leading-tight">{cat.examples}</p>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Pipeline Flow Visualization */}
|
||||
<div className="flex items-center justify-center gap-1 mb-4 flex-wrap">
|
||||
<div className="flex items-center justify-center gap-1 flex-wrap">
|
||||
{[
|
||||
{ icon: FileText, label: '38+ PDFs' },
|
||||
{ icon: Layers, label: 'QA + Chunking' },
|
||||
{ icon: Cpu, label: 'BGE-M3' },
|
||||
{ icon: Database, label: '6 Collections' },
|
||||
{ icon: Search, label: 'Hybrid Search' },
|
||||
{ icon: Brain, label: '120B / 1000B' },
|
||||
{ icon: FileText, label: de ? '75+ Quellen' : '75+ Sources' },
|
||||
{ icon: Layers, label: de ? 'Chunking & Lizenz' : 'Chunking & License' },
|
||||
{ icon: Cpu, label: de ? 'LLM-Extraktion' : 'LLM Extraction' },
|
||||
{ icon: Database, label: de ? '70k+ Controls' : '70k+ Controls' },
|
||||
{ icon: Search, label: de ? 'Hybrid Search' : 'Hybrid Search' },
|
||||
{ icon: Brain, label: de ? 'Beratung' : 'Advisory' },
|
||||
].map((step, idx, arr) => (
|
||||
<div key={idx} className="flex items-center gap-1">
|
||||
<div className="flex items-center gap-1 px-2 py-1 rounded-lg bg-white/[0.05] border border-white/[0.08]">
|
||||
@@ -213,28 +279,6 @@ export default function AIPipelineSlide({ lang }: AIPipelineSlideProps) {
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{/* Pipeline Steps */}
|
||||
<div className="grid md:grid-cols-2 gap-3">
|
||||
{ragPipelineSteps.map((step, idx) => {
|
||||
const Icon = step.icon
|
||||
return (
|
||||
<div key={idx} className={`border rounded-xl p-3 ${step.bg}`}>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Icon className={`w-4 h-4 ${step.color}`} />
|
||||
<h3 className="text-xs font-bold text-white">{step.title}</h3>
|
||||
</div>
|
||||
<ul className="space-y-1">
|
||||
{step.items.map((item, iidx) => (
|
||||
<li key={iidx} className="flex items-start gap-1.5 text-[11px] text-white/50">
|
||||
<span className={`w-1 h-1 rounded-full mt-1.5 ${step.color} bg-current shrink-0`} />
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import { Language } from '@/lib/types'
|
||||
import ProjectionFooter from '../ui/ProjectionFooter'
|
||||
import GradientText from '../ui/GradientText'
|
||||
import FadeInView from '../ui/FadeInView'
|
||||
import GlassCard from '../ui/GlassCard'
|
||||
@@ -212,6 +213,7 @@ export default function CapTableSlide({ lang }: CapTableSlideProps) {
|
||||
</GlassCard>
|
||||
</FadeInView>
|
||||
</div>
|
||||
<ProjectionFooter lang={lang} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -277,7 +277,7 @@ const PRICING_COMPARISON: CompetitorPricing[] = [
|
||||
{
|
||||
name: 'ComplAI',
|
||||
flag: '🇩🇪',
|
||||
model: 'Cloud (BSI DE / OVH FR)',
|
||||
model: 'Cloud (BSI DE / FR)',
|
||||
publicPricing: true,
|
||||
tiers: [
|
||||
{ name: { de: 'Startup/<10', en: 'Startup/<10' }, price: 'ab €300/mo', annual: 'ab €3.600/yr', notes: { de: '14-Tage-Test, Kreditkarte', en: '14-day trial, credit card' } },
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
'use client'
|
||||
|
||||
import { Language } from '@/lib/types'
|
||||
import GradientText from '../ui/GradientText'
|
||||
import FadeInView from '../ui/FadeInView'
|
||||
import { Shield, Lock } from 'lucide-react'
|
||||
|
||||
interface DisclaimerSlideProps {
|
||||
lang: Language
|
||||
}
|
||||
|
||||
export default function DisclaimerSlide({ lang }: DisclaimerSlideProps) {
|
||||
const de = lang === 'de'
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<FadeInView className="text-center mb-6">
|
||||
<h2 className="text-3xl md:text-4xl font-bold mb-2">
|
||||
<GradientText>{de ? 'Rechtlicher Hinweis' : 'Legal Notice'}</GradientText>
|
||||
</h2>
|
||||
</FadeInView>
|
||||
|
||||
<div className="space-y-4 max-h-[70vh] overflow-y-auto pr-2">
|
||||
|
||||
{/* Disclaimer */}
|
||||
<FadeInView delay={0.1}>
|
||||
<div className="bg-white/[0.03] border border-white/[0.06] rounded-xl p-5">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Shield className="w-4 h-4 text-indigo-400" />
|
||||
<h3 className="text-sm font-bold text-indigo-400 uppercase tracking-wider">
|
||||
{de ? 'Haftungsausschluss' : 'Disclaimer'}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 text-xs text-white/40 leading-relaxed">
|
||||
<p>
|
||||
{de
|
||||
? 'Dieses Dokument wird vorgelegt von Benjamin Boenisch, wohnhaft in Konstanz, Deutschland (nachfolgend „Gründer"). Der Gründer beabsichtigt die Gründung der BreakPilot GmbH im dritten Quartal 2026. Zum Zeitpunkt der Erstellung dieses Dokuments ist die Gesellschaft weder gegründet noch im Handelsregister eingetragen. Der Gründer handelt ausschließlich als Privatperson im Rahmen der Gründungsvorbereitung.'
|
||||
: 'This document is presented by Benjamin Boenisch, residing in Konstanz, Germany (hereinafter "Founder"). The Founder intends to establish BreakPilot GmbH in Q3 2026. At the time of this document, the company is neither founded nor registered in the commercial register. The Founder acts exclusively as a private individual in preparation of the founding.'}
|
||||
</p>
|
||||
<p>
|
||||
{de
|
||||
? 'Dieses Dokument stellt weder ein Angebot zum Verkauf noch eine Aufforderung zur Abgabe eines Angebots zum Erwerb von Wertpapieren, Gesellschaftsanteilen oder sonstigen Vermögensanlagen dar. Es handelt sich nicht um einen Wertpapierprospekt im Sinne des VermAnlG oder der EU-Prospektverordnung. Jede etwaige künftige Beteiligung begründet sich ausschließlich auf gesonderten, rechtlich geprüften Beteiligungsverträgen.'
|
||||
: 'This document constitutes neither an offer to sell nor a solicitation of an offer to acquire securities, company shares or other financial instruments. It is not a securities prospectus within the meaning of the VermAnlG or the EU Prospectus Regulation. Any future participation shall be based exclusively on separate, legally reviewed participation agreements.'}
|
||||
</p>
|
||||
<p>
|
||||
{de
|
||||
? 'Dieses Dokument enthält zukunftsgerichtete Aussagen, die auf gegenwärtigen Erwartungen und Annahmen beruhen. Solche Aussagen beinhalten Risiken und Ungewissheiten, die dazu führen können, dass tatsächliche Ergebnisse wesentlich von den dargestellten Erwartungen abweichen. Sämtliche Finanzangaben in dieser Präsentation sind Planzahlen und stellen keine Garantie für künftige Ergebnisse dar.'
|
||||
: 'This document contains forward-looking statements based on current expectations and assumptions. Such statements involve risks and uncertainties that may cause actual results to differ materially from stated expectations. All financial figures in this presentation are projections and do not constitute a guarantee of future results.'}
|
||||
</p>
|
||||
<p>
|
||||
{de
|
||||
? 'Sämtliche Angaben wurden mit Sorgfalt zusammengestellt, erheben jedoch keinen Anspruch auf Vollständigkeit oder Richtigkeit. Der Gründer übernimmt keine Haftung für die Aktualität, Korrektheit oder Vollständigkeit der Informationen, sofern kein vorsätzliches oder grob fahrlässiges Verschulden vorliegt. Eine Beteiligung an einem jungen Unternehmen ist mit erheblichen Risiken verbunden, einschließlich des Risikos eines Totalverlusts des eingesetzten Kapitals.'
|
||||
: 'All information has been compiled with care but makes no claim to completeness or accuracy. The Founder assumes no liability for the timeliness, correctness or completeness of the information, unless there is intentional or grossly negligent fault. An investment in a young company involves significant risks, including the risk of total loss of invested capital.'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</FadeInView>
|
||||
|
||||
{/* Confidentiality */}
|
||||
<FadeInView delay={0.2}>
|
||||
<div className="bg-white/[0.03] border border-white/[0.06] rounded-xl p-5">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Lock className="w-4 h-4 text-purple-400" />
|
||||
<h3 className="text-sm font-bold text-purple-400 uppercase tracking-wider">
|
||||
{de ? 'Vertraulichkeit' : 'Confidentiality'}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 text-xs text-white/40 leading-relaxed">
|
||||
<p>
|
||||
{de
|
||||
? 'Dieses Dokument ist vertraulich und wurde ausschließlich für den namentlich eingeladenen Empfänger erstellt. Durch die Kenntnisnahme erklärt sich der Empfänger mit folgenden Bedingungen einverstanden:'
|
||||
: 'This document is confidential and has been prepared exclusively for the personally invited recipient. By accessing this document, the recipient agrees to the following terms:'}
|
||||
</p>
|
||||
<p>
|
||||
{de
|
||||
? '(a) Geheimhaltung — Der Empfänger verpflichtet sich, den Inhalt vertraulich zu behandeln und nicht an Dritte weiterzugeben, zu kopieren oder zugänglich zu machen. Ausgenommen sind Berater (Rechtsanwälte, Steuerberater), die berufsrechtlich zur Verschwiegenheit verpflichtet sind.'
|
||||
: '(a) Confidentiality — The recipient undertakes to treat the content confidentially and not to disclose, copy or make it accessible to third parties. Excluded are advisors (lawyers, tax advisors) who are professionally bound to secrecy.'}
|
||||
</p>
|
||||
<p>
|
||||
{de
|
||||
? '(b) Zweckbindung — Die Informationen dürfen ausschließlich zur Bewertung einer möglichen Beteiligung verwendet werden. Jede anderweitige Nutzung ist untersagt.'
|
||||
: '(b) Purpose limitation — The information may only be used for the purpose of evaluating a possible participation. Any other use is prohibited.'}
|
||||
</p>
|
||||
<p>
|
||||
{de
|
||||
? '(c) Geltungsdauer — Diese Vertraulichkeitsverpflichtung gilt für drei (3) Jahre ab Übermittlung, unabhängig davon, ob eine Beteiligung zustande kommt. Es gilt deutsches Recht. Gerichtsstand ist Konstanz, Deutschland.'
|
||||
: '(c) Duration — This confidentiality obligation applies for three (3) years from transmission, regardless of whether a participation materializes. German law applies. Place of jurisdiction is Konstanz, Germany.'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</FadeInView>
|
||||
|
||||
<FadeInView delay={0.3}>
|
||||
<p className="text-center text-[10px] text-white/20">
|
||||
{de ? 'Stand: April 2026 · Dieser Hinweis ersetzt keine Rechtsberatung.' : 'As of: April 2026 · This notice does not replace legal advice.'}
|
||||
</p>
|
||||
</FadeInView>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -38,8 +38,8 @@ export default function EngineeringSlide({ lang }: EngineeringSlideProps) {
|
||||
},
|
||||
{
|
||||
value: '10',
|
||||
label: de ? 'Docker Container' : 'Docker Containers',
|
||||
sub: de ? 'Coolify → Hetzner (amd64)' : 'Coolify → Hetzner (amd64)',
|
||||
label: de ? 'Services' : 'Services',
|
||||
sub: de ? 'orca → Hetzner (amd64)' : 'orca → Hetzner (amd64)',
|
||||
color: 'text-emerald-400',
|
||||
borderColor: 'border-emerald-500/30',
|
||||
},
|
||||
@@ -51,9 +51,9 @@ export default function EngineeringSlide({ lang }: EngineeringSlideProps) {
|
||||
borderColor: 'border-purple-500/30',
|
||||
},
|
||||
{
|
||||
value: '14',
|
||||
label: 'Dockerfiles',
|
||||
sub: de ? 'Vollstaendig containerisiert' : 'Fully containerized',
|
||||
value: '5',
|
||||
label: de ? 'Infra-Komponenten' : 'Infra Components',
|
||||
sub: 'orca (Rust) · infisical · pg · qdrant',
|
||||
color: 'text-amber-400',
|
||||
borderColor: 'border-amber-500/30',
|
||||
},
|
||||
@@ -69,17 +69,17 @@ export default function EngineeringSlide({ lang }: EngineeringSlideProps) {
|
||||
{
|
||||
icon: GitBranch,
|
||||
label: 'Gitea + Actions',
|
||||
desc: de ? 'Self-hosted Git + CI/CD · Lint → Tests → Validierung' : 'Self-hosted Git + CI/CD · Lint → Tests → Validation',
|
||||
desc: de ? 'Self-hosted Git + CI/CD · Lint → Tests → Image-Build' : 'Self-hosted Git + CI/CD · Lint → Tests → Image build',
|
||||
},
|
||||
{
|
||||
icon: Workflow,
|
||||
label: 'Coolify',
|
||||
desc: de ? 'Auto-Deploy bei Push · Docker Compose auf Hetzner · Health Checks' : 'Auto-deploy on push · Docker Compose on Hetzner · Health checks',
|
||||
label: 'orca',
|
||||
desc: de ? 'Single-Binary Orchestrator (Rust) · Webhook-Deploy · Auto-TLS · Raft' : 'Single-binary orchestrator (Rust) · Webhook deploys · Auto-TLS · Raft',
|
||||
},
|
||||
{
|
||||
icon: Container,
|
||||
label: 'Docker Compose',
|
||||
desc: de ? 'arm64 → amd64 Build-Pipeline · Multi-Stage Builds' : 'arm64 → amd64 build pipeline · Multi-stage builds',
|
||||
label: 'Private Registry',
|
||||
desc: de ? 'registry.meghsakha.com · Signed Images · Tag pro Commit (:SHA + :latest)' : 'registry.meghsakha.com · Signed images · Per-commit tags (:SHA + :latest)',
|
||||
},
|
||||
{
|
||||
icon: ShieldCheck,
|
||||
@@ -88,13 +88,13 @@ export default function EngineeringSlide({ lang }: EngineeringSlideProps) {
|
||||
},
|
||||
{
|
||||
icon: Database,
|
||||
label: 'HashiCorp Vault',
|
||||
desc: de ? 'Secrets Management · Auto-Rotation · PKI' : 'Secrets Management · Auto-Rotation · PKI',
|
||||
label: 'Infisical',
|
||||
desc: de ? 'Secrets Management · Rotation · RBAC · End-to-End verschlüsselt' : 'Secrets Management · Rotation · RBAC · End-to-end encrypted',
|
||||
},
|
||||
{
|
||||
icon: Server,
|
||||
label: de ? 'EU-Cloud Infrastruktur' : 'EU Cloud Infrastructure',
|
||||
desc: de ? 'Hetzner · SysEleven (BSI) · OVH · PostgreSQL · Qdrant' : 'Hetzner · SysEleven (BSI) · OVH · PostgreSQL · Qdrant',
|
||||
desc: de ? 'Hetzner · SysEleven (BSI) · PostgreSQL · Qdrant' : 'Hetzner · SysEleven (BSI) · PostgreSQL · Qdrant',
|
||||
},
|
||||
]
|
||||
|
||||
@@ -120,8 +120,8 @@ export default function EngineeringSlide({ lang }: EngineeringSlideProps) {
|
||||
color: 'text-emerald-400',
|
||||
dotColor: 'bg-emerald-400',
|
||||
services: de
|
||||
? ['PostgreSQL 17 (Hetzner)', 'Qdrant Vector DB', 'DSMS/IPFS Node + Gateway', 'MinIO Object Storage']
|
||||
: ['PostgreSQL 17 (Hetzner)', 'Qdrant Vector DB', 'DSMS/IPFS Node + Gateway', 'MinIO Object Storage'],
|
||||
? ['orca (Rust) Orchestrator', 'Infisical Secrets', 'PostgreSQL 17 (Hetzner)', 'Qdrant Vector DB', 'DSMS/IPFS Node + Gateway', 'Private Registry']
|
||||
: ['orca (Rust) Orchestrator', 'Infisical Secrets', 'PostgreSQL 17 (Hetzner)', 'Qdrant Vector DB', 'DSMS/IPFS Node + Gateway', 'Private Registry'],
|
||||
},
|
||||
]
|
||||
|
||||
@@ -261,8 +261,8 @@ export default function EngineeringSlide({ lang }: EngineeringSlideProps) {
|
||||
<div className="mt-3 pt-3 border-t border-white/5">
|
||||
<p className="text-[10px] text-white/20 text-center">
|
||||
{de
|
||||
? '100% EU-Cloud · Hetzner + SysEleven (BSI) + OVH · Keine US-Anbieter · Volle Datenkontrolle'
|
||||
: '100% EU Cloud · Hetzner + SysEleven (BSI) + OVH · No US Providers · Full Data Control'}
|
||||
? '100% EU-Cloud · Hetzner + SysEleven (BSI) · Keine US-Anbieter · Volle Datenkontrolle'
|
||||
: '100% EU Cloud · Hetzner + SysEleven (BSI) · No US Providers · Full Data Control'}
|
||||
</p>
|
||||
</div>
|
||||
</GlassCard>
|
||||
|
||||
@@ -179,7 +179,7 @@ export default function ExecutiveSummarySlide({ lang, data }: ExecutiveSummarySl
|
||||
<li><strong>SAST + DAST + SBOM</strong> ${de ? '\\u2014 Vollumf\\u00e4ngliche Sicherheitstests bei jeder Code-\\u00c4nderung' : '\\u2014 Full security testing on every code change'}</li>
|
||||
<li><strong>${de ? 'KI-gest\\u00fctztes Pentesting' : 'AI-powered Pentesting'}</strong> ${de ? '\\u2014 Kontinuierlich statt einmal im Jahr' : '\\u2014 Continuous instead of once a year'}</li>
|
||||
<li><strong>CE-Software-Risikobeurteilung</strong> ${de ? '\\u2014 F\\u00fcr Maschinenverordnung und Produktsicherheit' : '\\u2014 For Machinery Regulation and product safety'}</li>
|
||||
<li><strong>Jira-Integration</strong> ${de ? '\\u2014 Findings als Tickets mit Implementierungsvorschl\\u00e4gen' : '\\u2014 Findings as tickets with implementation suggestions'}</li>
|
||||
<li><strong>Issue-Tracker-Integration</strong> ${de ? '\\u2014 Findings als Tickets mit Implementierungsvorschl\\u00e4gen' : '\\u2014 Findings as tickets with implementation suggestions'}</li>
|
||||
<li><strong>Audit-Trail</strong> ${de ? '\\u2014 L\\u00fcckenloser Nachweis von Erkennung bis Behebung' : '\\u2014 Complete evidence from detection to remediation'}</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -191,7 +191,7 @@ export default function ExecutiveSummarySlide({ lang, data }: ExecutiveSummarySl
|
||||
<li><strong>Audit Manager</strong> ${de ? '\\u2014 Abweichungen End-to-End: Rollen, Stichtage, Eskalation' : '\\u2014 Deviations end-to-end: roles, deadlines, escalation'}</li>
|
||||
<li><strong>Compliance LLM</strong> ${de ? '\\u2014 GPT f\\u00fcr Text und Audio, sicher in der EU gehostet' : '\\u2014 GPT for text and audio, securely hosted in EU'}</li>
|
||||
<li><strong>Academy</strong> ${de ? '\\u2014 Online-Schulungen f\\u00fcr GF und Mitarbeiter' : '\\u2014 Online training for management and employees'}</li>
|
||||
<li><strong>${de ? 'BSI-Cloud DE / OVH FR' : 'BSI Cloud DE / OVH FR'}</strong> ${de ? '\\u2014 Keine US-SaaS, Jitsi, Matrix, volle Integration' : '\\u2014 No US SaaS, Jitsi, Matrix, full integration'}</li>
|
||||
<li><strong>${de ? 'BSI-Cloud DE / FR' : 'BSI Cloud DE / FR'}</strong> ${de ? '\\u2014 Keine US-SaaS, Jitsi, Matrix, volle Integration' : '\\u2014 No US SaaS, Jitsi, Matrix, full integration'}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -208,7 +208,7 @@ export default function ExecutiveSummarySlide({ lang, data }: ExecutiveSummarySl
|
||||
<div class="card bottom-card">
|
||||
<div class="section-title">${de ? 'Gesch\\u00e4ftsmodell' : 'Business Model'}</div>
|
||||
<ul>
|
||||
<li><strong>SaaS Cloud</strong> ${de ? '\\u2014 BSI DE / OVH FR, mitarbeiterbasiert' : '\\u2014 BSI DE / OVH FR, employee-based'}</li>
|
||||
<li><strong>SaaS Cloud</strong> ${de ? '\\u2014 BSI DE / FR, mitarbeiterbasiert' : '\\u2014 BSI DE / FR, employee-based'}</li>
|
||||
<li><strong>${de ? 'Modular w\\u00e4hlbar' : 'Modular choice'}</strong> ${de ? '\\u2014 Einzelne Module oder Full Compliance' : '\\u2014 Single modules or full compliance'}</li>
|
||||
<li><strong>${de ? 'ROI ab Tag 1' : 'ROI from day 1'}</strong> ${de ? '\\u2014 Kunde spart 50.000+ EUR/Jahr' : '\\u2014 Customer saves EUR 50,000+/year'}</li>
|
||||
</ul>
|
||||
@@ -416,7 +416,7 @@ export default function ExecutiveSummarySlide({ lang, data }: ExecutiveSummarySl
|
||||
de ? 'Audit Manager — Abweichungen End-to-End mit Eskalation' : 'Audit Manager — deviations end-to-end with escalation',
|
||||
de ? 'Compliance LLM — GPT für Text und Audio, EU-gehostet' : 'Compliance LLM — GPT for text and audio, EU-hosted',
|
||||
de ? 'Academy — Online-Schulungen für GF und Mitarbeiter' : 'Academy — online training for management and employees',
|
||||
de ? 'BSI-Cloud DE / OVH FR' : 'BSI Cloud DE / OVH FR',
|
||||
de ? 'BSI-Cloud DE / FR' : 'BSI Cloud DE / FR',
|
||||
].map((item, idx) => (
|
||||
<p key={idx} className="text-xs text-white/60 pl-3 relative">
|
||||
<span className="absolute left-0 top-1 w-1.5 h-1.5 rounded-full bg-cyan-400/60" />
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { useState } from 'react'
|
||||
import { Language } from '@/lib/types'
|
||||
import { t } from '@/lib/i18n'
|
||||
import ProjectionFooter from '../ui/ProjectionFooter'
|
||||
import { useFinancialModel } from '@/lib/hooks/useFinancialModel'
|
||||
import GradientText from '../ui/GradientText'
|
||||
import FadeInView from '../ui/FadeInView'
|
||||
@@ -293,6 +294,7 @@ export default function FinancialsSlide({ lang, investorId }: FinancialsSlidePro
|
||||
</FadeInView>
|
||||
</div>
|
||||
</div>
|
||||
<ProjectionFooter lang={lang} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { Language } from '@/lib/types'
|
||||
import { t } from '@/lib/i18n'
|
||||
import ProjectionFooter from '../ui/ProjectionFooter'
|
||||
import GradientText from '../ui/GradientText'
|
||||
import FadeInView from '../ui/FadeInView'
|
||||
import GlassCard from '../ui/GlassCard'
|
||||
@@ -513,11 +514,7 @@ export default function FinanzplanSlide({ lang }: FinanzplanSlideProps) {
|
||||
</GlassCard>
|
||||
)}
|
||||
|
||||
<p className="text-center text-[9px] text-white/40 mt-2">
|
||||
{de
|
||||
? 'Doppelklick auf blaue Zellen zum Bearbeiten · Gründung: 01.08.2026'
|
||||
: 'Double-click blue cells to edit · Founding: 01.08.2026'}
|
||||
</p>
|
||||
<ProjectionFooter lang={lang} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -27,8 +27,8 @@ export default function HowItWorksSlide({ lang }: HowItWorksSlideProps) {
|
||||
</FadeInView>
|
||||
|
||||
<div className="relative max-w-4xl mx-auto">
|
||||
{/* Connection Line */}
|
||||
<div className="absolute left-8 top-12 bottom-12 w-px bg-gradient-to-b from-blue-500 via-purple-500 to-green-500 hidden md:block" />
|
||||
{/* Connection Line — behind icons (z-0), icons have z-10 with opaque bg */}
|
||||
<div className="absolute left-8 top-20 bottom-20 w-px bg-gradient-to-b from-blue-500/40 via-purple-500/40 to-green-500/40 hidden md:block z-0" />
|
||||
|
||||
<div className="space-y-8">
|
||||
{i.howItWorks.steps.map((step, idx) => {
|
||||
@@ -42,7 +42,7 @@ export default function HowItWorksSlide({ lang }: HowItWorksSlideProps) {
|
||||
className="flex items-start gap-6 relative"
|
||||
>
|
||||
<div className={`
|
||||
w-16 h-16 rounded-2xl bg-white/[0.06] border border-white/10
|
||||
w-16 h-16 rounded-2xl bg-[#0c0c1d] border border-white/10
|
||||
flex items-center justify-center shrink-0 relative z-10
|
||||
${stepColors[idx]}
|
||||
`}>
|
||||
|
||||
@@ -24,9 +24,9 @@ const MODULES = [
|
||||
{ icon: UserCheck, color: '#14b8a6', de: 'Consent Management', en: 'Consent Management', descDe: 'Einwilligungen, Cookie-Banner, Widerruf', descEn: 'Consent, cookie banner, withdrawal' },
|
||||
{ icon: AlertTriangle, color: '#f59e0b', de: 'Notfallpläne', en: 'Incident Response', descDe: 'Datenschutzvorfälle, Meldepflichten, Eskalation', descEn: 'Data breaches, reporting obligations, escalation' },
|
||||
{ icon: Brain, color: '#a855f7', de: 'Compliance LLM', en: 'Compliance LLM', descDe: 'GPT für Text und Audio — sicher in der EU', descEn: 'GPT for text and audio — securely in EU' },
|
||||
{ icon: Shield, color: '#8b5cf6', de: 'Cookie-Generator', en: 'Cookie Generator', descDe: 'Cookie-Banner, Consent-Konfiguration', descEn: 'Cookie banner, consent configuration' },
|
||||
{ icon: Shield, color: '#8b5cf6', de: 'Tender Matching', en: 'Tender Matching', descDe: 'Kundenanfragen (RFQ) gegen Codebase prüfen', descEn: 'Verify customer RFQs against codebase' },
|
||||
{ icon: GraduationCap, color: '#ec4899', de: 'Academy', en: 'Academy', descDe: 'Online-Schulungen für GF und Mitarbeiter', descEn: 'Online training for management and employees' },
|
||||
{ icon: Puzzle, color: '#0ea5e9', de: 'Integration in Kundenprozesse', en: 'Process Integration', descDe: 'Ticketsysteme, Workflows', descEn: 'Ticket systems, workflows' },
|
||||
{ icon: Puzzle, color: '#0ea5e9', de: 'AI Act Compliance', en: 'AI Act Compliance', descDe: 'UCCA, Use-Case-Bewertung, Betriebsratsmodul', descEn: 'UCCA, use case assessment, works council module' },
|
||||
{ icon: CheckCircle2, color: '#22c55e', de: 'Sichere Kommunikation', en: 'Secure Communication', descDe: 'Chat + Video mit AI Notetaker', descEn: 'Chat + video with AI notetaker' },
|
||||
]
|
||||
|
||||
@@ -109,7 +109,6 @@ export default function ProductSlide({ lang }: ProductSlideProps) {
|
||||
<p className="text-[10px] text-white/50 leading-relaxed">{i.product.cloudDesc}</p>
|
||||
<div className="flex gap-2 mt-2">
|
||||
<span className="text-[9px] bg-blue-500/15 text-blue-300 px-2 py-0.5 rounded-full">BSI DE</span>
|
||||
<span className="text-[9px] bg-blue-500/15 text-blue-300 px-2 py-0.5 rounded-full">OVH FR</span>
|
||||
<span className="text-[9px] bg-blue-500/15 text-blue-300 px-2 py-0.5 rounded-full">{de ? 'Fix oder flexibel' : 'Fixed or flexible'}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -90,32 +90,38 @@ export default function RegulatoryLandscapeSlide({ lang }: RegulatoryLandscapeSl
|
||||
{/* Matrix */}
|
||||
<FadeInView delay={0.5}>
|
||||
<GlassCard hover={false} className="p-4 overflow-x-auto">
|
||||
{/* Category Legend */}
|
||||
<div className="flex flex-wrap gap-3 mb-4 justify-center">
|
||||
{CATEGORIES.map((cat) => (
|
||||
<div key={cat.id} className="flex items-center gap-1.5">
|
||||
<div className="w-2.5 h-2.5 rounded-full" style={{ backgroundColor: cat.color }} />
|
||||
<span className="text-[10px] text-white/50">{categoryLabels[cat.id]}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Matrix Grid */}
|
||||
<div className="space-y-1.5">
|
||||
{/* Header row */}
|
||||
<div className="grid items-center gap-1" style={{ gridTemplateColumns: '140px repeat(8, 1fr) 50px' }}>
|
||||
<div className="text-[9px] text-white/30 uppercase tracking-wider pl-1">
|
||||
{/* Staggered header rows — odd columns on top row, even on bottom */}
|
||||
<div className="grid items-end gap-1" style={{ gridTemplateColumns: '140px repeat(8, 1fr) 70px' }}>
|
||||
<div className="text-[9px] text-white/70 uppercase tracking-wider pl-1 font-semibold">
|
||||
{lang === 'de' ? 'Branche' : 'Industry'}
|
||||
</div>
|
||||
{CATEGORIES.map((cat) => {
|
||||
const CatIcon = cat.icon
|
||||
return (
|
||||
<div key={cat.id} className="flex justify-center">
|
||||
<CatIcon className="w-3.5 h-3.5 opacity-50" style={{ color: cat.color }} />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<div className="text-[9px] text-white/30 text-center">#</div>
|
||||
{CATEGORIES.map((cat, idx) => (
|
||||
<div key={cat.id} className="text-center">
|
||||
{idx % 2 === 0 ? (
|
||||
<span className="text-[8px] font-semibold uppercase tracking-wider" style={{ color: cat.color }}>
|
||||
{categoryLabels[cat.id]}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
))}
|
||||
<div className="text-[8px] text-indigo-400 text-center font-semibold uppercase tracking-wider">
|
||||
{lang === 'de' ? 'Regulatorien' : 'Regulations'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid items-start gap-1" style={{ gridTemplateColumns: '140px repeat(8, 1fr) 70px' }}>
|
||||
<div />
|
||||
{CATEGORIES.map((cat, idx) => (
|
||||
<div key={cat.id} className="text-center">
|
||||
{idx % 2 === 1 ? (
|
||||
<span className="text-[8px] font-semibold uppercase tracking-wider" style={{ color: cat.color }}>
|
||||
{categoryLabels[cat.id]}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
))}
|
||||
<div />
|
||||
</div>
|
||||
|
||||
{/* Industry rows */}
|
||||
@@ -125,7 +131,7 @@ export default function RegulatoryLandscapeSlide({ lang }: RegulatoryLandscapeSl
|
||||
<div
|
||||
key={industry.id}
|
||||
className="grid items-center gap-1 py-1.5 rounded-lg hover:bg-white/[0.04] transition-colors"
|
||||
style={{ gridTemplateColumns: '140px repeat(8, 1fr) 50px' }}
|
||||
style={{ gridTemplateColumns: '140px repeat(8, 1fr) 70px' }}
|
||||
>
|
||||
<div className="flex items-center gap-2 pl-1">
|
||||
<Icon className="w-3.5 h-3.5 text-white/40" />
|
||||
|
||||
@@ -90,7 +90,7 @@ export default function SDKDemoSlide({ lang }: SDKDemoSlideProps) {
|
||||
</div>
|
||||
<div className="flex-1 ml-3">
|
||||
<div className="bg-white/[0.06] rounded-md px-3 py-1 text-xs text-white/30 font-mono max-w-md">
|
||||
admin.breakpilot.ai/sdk/{shot.file.replace(/^\d+-/, '').replace('.png', '')}
|
||||
admin-dev.breakpilot.ai/sdk/{shot.file.replace(/^\d+-/, '').replace('.png', '')}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { motion } from 'framer-motion'
|
||||
import { Language, PitchTeamMember } from '@/lib/types'
|
||||
import { t } from '@/lib/i18n'
|
||||
import { User, Linkedin } from 'lucide-react'
|
||||
import { User, Linkedin, Github } from 'lucide-react'
|
||||
import GradientText from '../ui/GradientText'
|
||||
import FadeInView from '../ui/FadeInView'
|
||||
import Image from 'next/image'
|
||||
@@ -13,89 +13,109 @@ interface TeamSlideProps {
|
||||
team: PitchTeamMember[]
|
||||
}
|
||||
|
||||
function equityDisplay(pct: number | string | null | undefined): string {
|
||||
const n = Number(pct)
|
||||
if (!Number.isFinite(n)) return '—'
|
||||
return Number.isInteger(n) ? `${n}%` : `${n.toFixed(1)}%`
|
||||
}
|
||||
|
||||
function detectProfileLink(url: string | null | undefined): { icon: typeof Linkedin | typeof Github; label: string } | null {
|
||||
if (!url) return null
|
||||
if (url.includes('github.com')) return { icon: Github, label: 'GitHub' }
|
||||
if (url.includes('linkedin.com')) return { icon: Linkedin, label: 'LinkedIn' }
|
||||
return { icon: Linkedin, label: 'Profile' }
|
||||
}
|
||||
|
||||
export default function TeamSlide({ lang, team }: TeamSlideProps) {
|
||||
const i = t(lang)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<FadeInView className="text-center mb-12">
|
||||
<FadeInView className="text-center mb-8">
|
||||
<h2 className="text-4xl md:text-5xl font-bold mb-3">
|
||||
<GradientText>{i.team.title}</GradientText>
|
||||
</h2>
|
||||
<p className="text-lg text-white/50 max-w-2xl mx-auto">{i.team.subtitle}</p>
|
||||
</FadeInView>
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-8 max-w-4xl mx-auto">
|
||||
{team.map((member, idx) => (
|
||||
<motion.div
|
||||
key={member.id}
|
||||
initial={{ opacity: 0, x: idx === 0 ? -40 : 40 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: 0.3 + idx * 0.2, duration: 0.6 }}
|
||||
className="bg-white/[0.08] backdrop-blur-xl border border-white/10 rounded-3xl p-8"
|
||||
>
|
||||
<div className="flex items-start gap-5">
|
||||
{/* Avatar — Foto oder Fallback */}
|
||||
{member.photo_url ? (
|
||||
<div className="w-20 h-20 rounded-2xl overflow-hidden shrink-0 shadow-lg">
|
||||
<Image
|
||||
src={member.photo_url}
|
||||
alt={member.name}
|
||||
width={80}
|
||||
height={80}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-20 h-20 rounded-2xl bg-gradient-to-br from-indigo-500 to-purple-600
|
||||
flex items-center justify-center shrink-0 shadow-lg">
|
||||
<User className="w-10 h-10 text-white" />
|
||||
</div>
|
||||
)}
|
||||
<div className="grid md:grid-cols-2 gap-6 max-w-5xl mx-auto items-stretch">
|
||||
{team.map((member, idx) => {
|
||||
const link = detectProfileLink(member.linkedin_url)
|
||||
const LinkIcon = link?.icon
|
||||
return (
|
||||
<motion.div
|
||||
key={member.id}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.2 + idx * 0.15, duration: 0.5 }}
|
||||
className="bg-white/[0.04] backdrop-blur-xl border border-white/[0.08] rounded-2xl p-6 flex flex-col hover:border-indigo-500/20 transition-colors"
|
||||
>
|
||||
{/* Header: avatar + name + role */}
|
||||
<div className="flex items-center gap-4 mb-5">
|
||||
{member.photo_url ? (
|
||||
<div className="w-16 h-16 rounded-2xl overflow-hidden shrink-0 shadow-lg">
|
||||
<Image
|
||||
src={member.photo_url}
|
||||
alt={member.name}
|
||||
width={64}
|
||||
height={64}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-16 h-16 rounded-2xl bg-gradient-to-br from-indigo-500 to-purple-600 flex items-center justify-center shrink-0 shadow-lg shadow-indigo-500/20">
|
||||
<User className="w-8 h-8 text-white" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<h3 className="text-xl font-bold text-white">{member.name}</h3>
|
||||
{member.linkedin_url && (
|
||||
<a
|
||||
href={member.linkedin_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="p-1 text-white/30 hover:text-[#0A66C2] transition-colors"
|
||||
title="LinkedIn"
|
||||
>
|
||||
<Linkedin className="w-4 h-4" />
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-indigo-400 text-sm font-medium mb-3">
|
||||
{lang === 'de' ? member.role_de : member.role_en}
|
||||
</p>
|
||||
<p className="text-sm text-white/50 leading-relaxed mb-4">
|
||||
{lang === 'de' ? member.bio_de : member.bio_en}
|
||||
</p>
|
||||
|
||||
{/* Equity */}
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<span className="text-xs text-white/40">{i.team.equity}:</span>
|
||||
<span className="text-sm font-bold text-white">{member.equity_pct}%</span>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2 mb-0.5 flex-wrap">
|
||||
<h3 className="text-xl font-bold text-white truncate">{member.name}</h3>
|
||||
{link && LinkIcon && (
|
||||
<a
|
||||
href={member.linkedin_url!}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-white/30 hover:text-indigo-300 transition-colors"
|
||||
title={link.label}
|
||||
>
|
||||
<LinkIcon className="w-4 h-4" />
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-indigo-400 text-sm font-medium">
|
||||
{lang === 'de' ? member.role_de : member.role_en}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Expertise Tags */}
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{/* Equity pill in top-right */}
|
||||
<div className="text-right shrink-0">
|
||||
<div className="text-[10px] uppercase tracking-wider text-white/30">{i.team.equity}</div>
|
||||
<div className="text-base font-bold text-white tabular-nums">{equityDisplay(member.equity_pct)}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bio */}
|
||||
<p className="text-sm text-white/60 leading-relaxed mb-5 flex-1">
|
||||
{lang === 'de' ? member.bio_de : member.bio_en}
|
||||
</p>
|
||||
|
||||
{/* Expertise tags */}
|
||||
{(member.expertise || []).length > 0 && (
|
||||
<div className="flex flex-wrap gap-1.5 pt-4 border-t border-white/[0.06]">
|
||||
{(member.expertise || []).map((skill, sidx) => (
|
||||
<span
|
||||
key={sidx}
|
||||
className="text-xs px-2.5 py-1 rounded-full bg-indigo-500/10 text-indigo-300 border border-indigo-500/20"
|
||||
className="text-[11px] px-2.5 py-1 rounded-full bg-indigo-500/10 text-indigo-300 border border-indigo-500/20"
|
||||
>
|
||||
{skill}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
)}
|
||||
</motion.div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { motion } from 'framer-motion'
|
||||
import { Language, PitchFunding } from '@/lib/types'
|
||||
import { t } from '@/lib/i18n'
|
||||
import ProjectionFooter from '../ui/ProjectionFooter'
|
||||
import GradientText from '../ui/GradientText'
|
||||
import FadeInView from '../ui/FadeInView'
|
||||
import AnimatedCounter from '../ui/AnimatedCounter'
|
||||
@@ -147,6 +148,7 @@ export default function TheAskSlide({ lang, funding }: TheAskSlideProps) {
|
||||
</div>
|
||||
</GlassCard>
|
||||
</FadeInView>
|
||||
<ProjectionFooter lang={lang} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
'use client'
|
||||
|
||||
import { Language } from '@/lib/types'
|
||||
import GradientText from '../ui/GradientText'
|
||||
import FadeInView from '../ui/FadeInView'
|
||||
import GlassCard from '../ui/GlassCard'
|
||||
import {
|
||||
FileCheck,
|
||||
Code,
|
||||
Zap,
|
||||
Shield,
|
||||
GitPullRequest,
|
||||
ArrowLeftRight,
|
||||
} from 'lucide-react'
|
||||
|
||||
interface USPSlideProps {
|
||||
lang: Language
|
||||
}
|
||||
|
||||
export default function USPSlide({ lang }: USPSlideProps) {
|
||||
const de = lang === 'de'
|
||||
|
||||
const subtitle = de
|
||||
? 'Die erste Plattform, die Compliance-Dokumente und tatsächliche Code-Umsetzung verbindet'
|
||||
: 'The first platform that connects compliance documents with actual code implementation'
|
||||
|
||||
const complianceItems = de
|
||||
? ['DSGVO-Dokumente', 'Audit-Management', 'RFQ-Anforderungen', 'CE-Bewertungen']
|
||||
: ['GDPR documents', 'Audit management', 'RFQ requirements', 'CE assessments']
|
||||
|
||||
const codeItems = de
|
||||
? ['SAST / DAST / SBOM', 'Pentesting', 'Issue-Tracker', 'Auto-Fixes']
|
||||
: ['SAST / DAST / SBOM', 'Pentesting', 'Issue tracker', 'Auto-fixes']
|
||||
|
||||
const capabilities = [
|
||||
{
|
||||
icon: GitPullRequest,
|
||||
color: 'text-indigo-400',
|
||||
label: de ? 'RFQ-Prüfung' : 'RFQ Verification',
|
||||
desc: de
|
||||
? 'Kunden-Anforderungsdokumente werden automatisiert gegen die aktuelle Source-Code-Umsetzung geprüft. Abweichungen werden erkannt, Änderungen vorgeschlagen und auf Wunsch direkt im Code umgesetzt — ohne manuelles Nacharbeiten.'
|
||||
: 'Customer requirement documents are automatically verified against current source code. Deviations are detected, changes proposed and implemented directly in code on request — no manual rework needed.',
|
||||
},
|
||||
{
|
||||
icon: ArrowLeftRight,
|
||||
color: 'text-purple-400',
|
||||
label: de ? 'Bidirektional' : 'Bidirectional',
|
||||
desc: de
|
||||
? 'Compliance-Anforderungen fliessen direkt in den Code. Umgekehrt aktualisieren Code-Änderungen automatisch die Compliance-Dokumentation. Beide Seiten sind immer synchron — kein Informationsverlust zwischen Audit und Entwicklung.'
|
||||
: 'Compliance requirements flow directly into code. Conversely, code changes automatically update compliance documentation. Both sides always stay in sync — no information loss between audit and development.',
|
||||
},
|
||||
{
|
||||
icon: Zap,
|
||||
color: 'text-amber-400',
|
||||
label: de ? 'Prozess-Compliance' : 'Process Compliance',
|
||||
desc: de
|
||||
? 'Vom Audit-Finding über das Ticket bis zur Code-Änderung läuft der gesamte Prozess automatisiert durch. Rollen, Fristen und Eskalation werden End-to-End verwaltet. Nachweise werden automatisch generiert und archiviert.'
|
||||
: 'From audit finding to ticket to code change, the entire process runs automatically. Roles, deadlines and escalation are managed end-to-end. Evidence is automatically generated and archived.',
|
||||
},
|
||||
{
|
||||
icon: Shield,
|
||||
color: 'text-emerald-400',
|
||||
label: de ? 'Kontinuierlich' : 'Continuous',
|
||||
desc: de
|
||||
? 'Klassische Compliance prüft einmal im Jahr und hofft auf das Beste. Unsere Plattform prüft bei jeder Code-Änderung. Findings werden sofort zu Tickets mit konkreten Implementierungsvorschlägen im Issue-Tracker der Wahl.'
|
||||
: 'Traditional compliance checks once a year and hopes for the best. Our platform checks on every code change. Findings immediately become tickets with concrete implementation proposals in the issue tracker of choice.',
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div>
|
||||
<FadeInView className="text-center mb-8">
|
||||
<h2 className="text-4xl md:text-5xl font-bold mb-3">
|
||||
<GradientText>USP</GradientText>
|
||||
</h2>
|
||||
<p className="text-lg text-white/50 max-w-3xl mx-auto">{subtitle}</p>
|
||||
</FadeInView>
|
||||
|
||||
<FadeInView delay={0.2}>
|
||||
<div className="relative max-w-6xl mx-auto" style={{ height: '580px' }}>
|
||||
|
||||
{/* CENTER: Large circle */}
|
||||
<div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" style={{ width: '440px', height: '440px' }}>
|
||||
<div className="absolute inset-0 rounded-full border-2 border-dashed border-indigo-500/20 animate-[spin_20s_linear_infinite]" />
|
||||
<div className="absolute inset-4 rounded-full border border-white/[0.06] bg-white/[0.015]" />
|
||||
|
||||
<div className="absolute inset-0 flex items-center justify-center z-10">
|
||||
<div className="w-20 h-20 rounded-full bg-gradient-to-br from-indigo-500 to-purple-600 flex items-center justify-center shadow-xl shadow-indigo-500/30">
|
||||
<span className="text-3xl font-black text-white">∞</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="absolute left-7 top-1/2 -translate-y-1/2 w-[120px] z-10">
|
||||
<div className="flex items-center gap-1.5 mb-2">
|
||||
<FileCheck className="w-4 h-4 text-indigo-400" />
|
||||
<span className="text-sm font-bold text-indigo-400">Compliance</span>
|
||||
</div>
|
||||
<ul className="space-y-1.5">
|
||||
{complianceItems.map((item, idx) => (
|
||||
<li key={idx} className="flex items-center gap-1.5 text-sm text-white/50">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-indigo-400 shrink-0" />
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="absolute right-7 top-1/2 -translate-y-1/2 w-[120px] z-10">
|
||||
<div className="flex items-center gap-1.5 mb-2">
|
||||
<Code className="w-4 h-4 text-purple-400" />
|
||||
<span className="text-sm font-bold text-purple-400">Code</span>
|
||||
</div>
|
||||
<ul className="space-y-1.5">
|
||||
{codeItems.map((item, idx) => (
|
||||
<li key={idx} className="flex items-center gap-1.5 text-sm text-white/50">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-purple-400 shrink-0" />
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="absolute top-[12%] left-[12%] text-indigo-400/60 text-lg z-20">◀</div>
|
||||
<div className="absolute top-[12%] right-[12%] text-purple-400/60 text-lg z-20">▶</div>
|
||||
<div className="absolute bottom-[12%] left-[12%] text-amber-400/60 text-lg z-20">◀</div>
|
||||
<div className="absolute bottom-[12%] right-[12%] text-emerald-400/60 text-lg z-20">▶</div>
|
||||
</div>
|
||||
|
||||
{/* 4 CORNER CARDS */}
|
||||
{capabilities.map((cap, idx) => {
|
||||
const Icon = cap.icon
|
||||
const posClass = idx === 0 ? 'top-0 left-0'
|
||||
: idx === 1 ? 'top-0 right-0'
|
||||
: idx === 2 ? 'bottom-0 left-0'
|
||||
: 'bottom-0 right-0'
|
||||
return (
|
||||
<div key={idx} className={`absolute ${posClass} w-[290px]`}>
|
||||
<GlassCard hover={false} className="p-4" delay={0}>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Icon className={`w-5 h-5 ${cap.color}`} />
|
||||
<h3 className={`text-base font-bold ${cap.color}`}>{cap.label}</h3>
|
||||
</div>
|
||||
<p className="text-sm text-white/50 leading-relaxed">{cap.desc}</p>
|
||||
</GlassCard>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
|
||||
{/* SVG connection lines */}
|
||||
<svg className="absolute inset-0 w-full h-full pointer-events-none z-0" preserveAspectRatio="none" viewBox="0 0 100 100">
|
||||
<line x1="30" y1="25" x2="22" y2="15" stroke="rgba(99,102,241,0.15)" strokeWidth="0.3" strokeDasharray="1 1" />
|
||||
<line x1="70" y1="25" x2="78" y2="15" stroke="rgba(168,85,247,0.15)" strokeWidth="0.3" strokeDasharray="1 1" />
|
||||
<line x1="30" y1="75" x2="22" y2="85" stroke="rgba(245,158,11,0.15)" strokeWidth="0.3" strokeDasharray="1 1" />
|
||||
<line x1="70" y1="75" x2="78" y2="85" stroke="rgba(16,185,129,0.15)" strokeWidth="0.3" strokeDasharray="1 1" />
|
||||
</svg>
|
||||
</div>
|
||||
</FadeInView>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
'use client'
|
||||
|
||||
import { Language } from '@/lib/types'
|
||||
|
||||
interface ProjectionFooterProps {
|
||||
lang: Language
|
||||
}
|
||||
|
||||
export default function ProjectionFooter({ lang }: ProjectionFooterProps) {
|
||||
const de = lang === 'de'
|
||||
return (
|
||||
<div className="mt-3 pt-2 border-t border-white/5">
|
||||
<p className="text-[9px] text-white/20 text-center italic">
|
||||
{de
|
||||
? 'Alle Finanzdaten sind Planzahlen und stellen keine Garantie für künftige Ergebnisse dar (Stand: Q2 2026)'
|
||||
: 'All financial data are projections and do not constitute a guarantee of future results (as of: Q2 2026)'}
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+42
-38
@@ -13,12 +13,13 @@ const translations = {
|
||||
'Cover',
|
||||
'Das Problem',
|
||||
'Die Lösung',
|
||||
'USP',
|
||||
'Regulatorische Landschaft',
|
||||
'Modularer Baukasten',
|
||||
'So funktioniert\'s',
|
||||
'Markt',
|
||||
'Geschäftsmodell',
|
||||
'Traction',
|
||||
'Meilensteine',
|
||||
'Wettbewerb',
|
||||
'Team',
|
||||
'Finanzen',
|
||||
@@ -36,6 +37,7 @@ const translations = {
|
||||
'Anhang: Strategie',
|
||||
'Anhang: Finanzplan',
|
||||
'Glossar',
|
||||
'Rechtlicher Hinweis',
|
||||
],
|
||||
executiveSummary: {
|
||||
title: 'Executive Summary',
|
||||
@@ -43,12 +45,12 @@ const translations = {
|
||||
problem: 'Das Problem',
|
||||
problemText: 'Unternehmen – insbesondere im Maschinenbau – stehen vor einem strategischen Dilemma: Um wettbewerbsfähig zu bleiben, müssen sie KI einsetzen. Gleichzeitig können oder wollen sie keine US-basierten KI-Anbieter in ihre sensibelsten Systeme integrieren. Wer auf US-SaaS verzichtet, verliert den Anschluss an die KI-Transformation. Wer sie nutzt, riskiert den Abfluss kritischer Daten und regulatorische Unsicherheit. Parallel dazu werden über 30.000 Unternehmen in Deutschland durch neue EU-Regulierungen wie AI Act, Data Act, CRA und NIS2 massiv belastet – unabhängig von ihrer Größe oder digitalen Reife. Das Ergebnis: Entscheidungsblockade statt Innovation.',
|
||||
solution: 'Unsere Lösung',
|
||||
solutionText: 'Breakpilot ersetzt punktuelle Audits durch kontinuierliche, automatisierte Compliance und Security. Bei jeder Code-Änderung werden SAST, DAST, SBOM und Pentests automatisch ausgeführt. VVT, TOMs, DSFA, Löschfristen und CE-Risikobeurteilungen werden fortlaufend generiert. Audit-Abweichungen End-to-End: Rollen, Fristen, Tickets, Nachweise, Eskalation bis zur GF. Nahtlose Integration in bestehende Workflows (z.\u00a0B. Jira). BSI-Cloud DE oder OVH FR. Ergebnis: kontinuierliche Compliance statt punktueller Prüfungen.',
|
||||
solutionText: 'Breakpilot ersetzt punktuelle Audits durch kontinuierliche, automatisierte Compliance und Security. Bei jeder Code-Änderung werden SAST, DAST, SBOM und Pentests automatisch ausgeführt. VVT, TOMs, DSFA, Löschfristen und CE-Risikobeurteilungen werden fortlaufend generiert. Audit-Abweichungen End-to-End: Rollen, Fristen, Tickets, Nachweise, Eskalation bis zur GF. Nahtlose Integration in bestehende Workflows über den Issue-Tracker deiner Wahl. BSI-Cloud DE. Ergebnis: kontinuierliche Compliance statt punktueller Prüfungen.',
|
||||
roi: 'Kundenersparnis',
|
||||
roiText: 'Kunden zahlen ca. 50.000 EUR/Jahr und sparen: 30.000 EUR Pentests, 20.000 EUR CE-Beurteilungen, Auditmanager-Kosten und Strafrisiko. ROI ab Tag 1.',
|
||||
market: 'Markt',
|
||||
businessModel: 'Geschäftsmodell',
|
||||
businessModelText: 'Kunden zahlen ~50.000 EUR/Jahr und sparen 50.000+ EUR (Pentests, CE-Beurteilungen, Auditmanager). ROI ab Tag 1. BSI-Cloud DE oder OVH FR.',
|
||||
businessModelText: 'Kunden zahlen ~50.000 EUR/Jahr und sparen 50.000+ EUR (Pentests, CE-Beurteilungen, Auditmanager). ROI ab Tag 1. BSI-Cloud DE.',
|
||||
keyMetrics: 'Kennzahlen',
|
||||
documents: 'Originaldokumente',
|
||||
controls: 'Prüfaspekte',
|
||||
@@ -63,31 +65,31 @@ const translations = {
|
||||
uspText: 'Einzige Plattform mit kontinuierlicher Code-Security, automatischer Compliance-Dokumentation und CE-Software-Risikobeurteilung — auf deutscher oder französischer Cloud.',
|
||||
},
|
||||
cover: {
|
||||
tagline: 'Compliance & Code-Security für den Maschinenbau',
|
||||
tagline: 'Compliance & Code-Security',
|
||||
subtitle: 'Pre-Seed · Q4 2026',
|
||||
cta: 'Pitch starten',
|
||||
},
|
||||
problem: {
|
||||
title: 'Das Problem',
|
||||
subtitle: 'Maschinenbauer wollen KI nutzen — aber nicht um den Preis ihrer Datensouveränität',
|
||||
subtitle: 'Deutsche und europäische Unternehmen wollen KI nutzen — aber nicht um den Preis ihrer Datensouveränität',
|
||||
cards: [
|
||||
{
|
||||
title: 'KI-Dilemma',
|
||||
stat: 'Abgehängt',
|
||||
desc: 'Maschinenbauer wollen KI nutzen, aber keinen Microsoft Copilot oder Claude auf ihr Herzstück lassen. Angst vor Datenmissbrauch durch US-Konzerne ist real. Wer US-SaaS meidet, bleibt von der KI-Revolution abgeschnitten.',
|
||||
desc: 'Produzierende Unternehmen brauchen KI, um wettbewerbsfähig zu bleiben. Aber Microsoft Copilot, ChatGPT oder Claude an den eigenen Quellcode und die Konstruktionsdaten zu lassen, kommt für die meisten nicht in Frage. Wer auf US-KI verzichtet, verliert den Anschluss. Wer sie nutzt, riskiert seine Datensouveränität.',
|
||||
},
|
||||
{
|
||||
title: 'Patriots Act',
|
||||
title: 'Patriot Act + FISA 702',
|
||||
stat: 'Kein Schutz',
|
||||
desc: 'Die Alternative: Alles zu AWS, Google oder Microsoft schieben. Aber selbst europäische Server der US-Player können über den Patriots Act abgesaugt werden. Deutsche KMU sitzen in der Falle.',
|
||||
desc: 'Selbst wer EU-Server bei AWS, Google oder Microsoft bucht, ist nicht geschützt. US-Gesetze wie FISA 702 und der Cloud Act gelten extraterritorial — US-Behörden können auf Daten zugreifen, egal wo der Server steht. Das Schrems-II-Urteil des EuGH hat das bestätigt.',
|
||||
},
|
||||
{
|
||||
title: 'Regulierungs-Tsunami',
|
||||
stat: '50.000+ EUR/Jahr',
|
||||
desc: 'AI Act, NIS2, CRA, DSGVO, Maschinenverordnung — 5+ Gesetze gleichzeitig. Pentests und CE-Zertifizierungen kosten 50.000+ EUR/Jahr, prüfen aber nur einmal. KMU mit 10-500 MA haben weder Personal noch Budget.',
|
||||
stat: 'Nicht tragbar',
|
||||
desc: 'Seit 2024 greifen AI Act, NIS2 und Cyber Resilience Act — zusätzlich zu DSGVO, Data Act, Maschinenverordnung und Lieferkettengesetz. Europäische Unternehmen tragen damit Compliance-Kosten, die US- und Asien-Konkurrenten nicht haben. Gleichzeitig steigen Kosten durch Rohstoffengpässe und geopolitische Krisen. KMU können das nicht mehr stemmen.',
|
||||
},
|
||||
],
|
||||
quote: 'Maschinenbauer brauchen eine KI-Lösung, die in Deutschland läuft, ihren Code schützt und Compliance automatisiert — ohne ihre Daten an US-Konzerne zu geben.',
|
||||
quote: 'Produzierende Unternehmen brauchen eine KI-Lösung, die in Europa läuft, ihren Code schützt und Compliance automatisiert — ohne ihre Daten an US-Konzerne zu geben.',
|
||||
},
|
||||
solution: {
|
||||
title: 'Die Lösung',
|
||||
@@ -95,7 +97,7 @@ const translations = {
|
||||
pillars: [
|
||||
{
|
||||
title: 'Kontinuierliche Code-Security',
|
||||
desc: 'SAST, DAST, SBOM und Pentesting bei jeder Code-Änderung — nicht einmal im Jahr. Findings direkt als Jira-Tickets mit Implementierungsvorschlägen. 30.000+ EUR/Jahr Pentest-Kosten gespart.',
|
||||
desc: 'SAST, DAST, SBOM und Pentesting bei jeder Code-Änderung — nicht einmal im Jahr. Findings direkt als Tickets im Issue-Tracker deiner Wahl, mit Implementierungsvorschlägen. 15.000+ EUR pro Jahr und Anwendung an Pentest-Kosten gespart.',
|
||||
icon: 'scan',
|
||||
},
|
||||
{
|
||||
@@ -105,7 +107,7 @@ const translations = {
|
||||
},
|
||||
{
|
||||
title: 'Deutsche Cloud, volle Integration',
|
||||
desc: 'BSI-zertifizierte Cloud in DE oder OVH in FR. Jitsi (Video), Matrix (Chat), KI-Aufgabenerstellung aus Audio. Keine US-SaaS im Source Code. Optional Mac Mini für maximale Privacy.',
|
||||
desc: 'BSI-zertifizierte Cloud in Deutschland. Live-Support über Jitsi (Video) und Matrix (Chat). Keine US-SaaS im Source Code. Optional Mac Mini/Studio für maximale Privacy.',
|
||||
icon: 'server',
|
||||
},
|
||||
],
|
||||
@@ -148,7 +150,7 @@ const translations = {
|
||||
pricingTitle: 'Pricing nach Unternehmensgröße',
|
||||
pricingSubtitle: 'Mitarbeiterbasiert — validiert am Markt',
|
||||
cloud: 'Cloud-Lösung (Standard)',
|
||||
cloudDesc: 'BSI-Cloud DE oder OVH FR. Für alle Unternehmensgrößen.',
|
||||
cloudDesc: 'BSI-Cloud DE. Für alle Unternehmensgrößen.',
|
||||
privacy: 'Privacy-Hardware (optional)',
|
||||
privacyDesc: 'Mac Mini / Studio für Kleinstunternehmen (<10 MA) mit absolutem Privacy-Bedarf.',
|
||||
},
|
||||
@@ -158,7 +160,7 @@ const translations = {
|
||||
steps: [
|
||||
{
|
||||
title: 'Cloud-Vertrag abschließen',
|
||||
desc: 'BSI-zertifizierte Cloud in Deutschland oder OVH in Frankreich. Fixe oder flexible Kosten. Für Kleinstunternehmen optional: Mac Mini vorkonfiguriert.',
|
||||
desc: 'BSI-zertifizierte Cloud in Deutschland. Fixe oder flexible Kosten.',
|
||||
},
|
||||
{
|
||||
title: 'Code-Repos verbinden',
|
||||
@@ -176,13 +178,13 @@ const translations = {
|
||||
},
|
||||
market: {
|
||||
title: 'Marktchance',
|
||||
subtitle: 'Der Maschinenbau braucht Compliance & Code-Security',
|
||||
subtitle: 'Compliance & Code-Security für produzierende Unternehmen',
|
||||
tam: 'TAM',
|
||||
sam: 'SAM',
|
||||
som: 'SOM',
|
||||
tamLabel: 'Total Addressable Market',
|
||||
samLabel: 'Serviceable Addressable Market',
|
||||
somLabel: 'Serviceable Obtainable Market',
|
||||
somLabel: 'Serviceable Obtainable Market (nur Maschinen- und Anlagenbauer als Kernmarkt betrachtet)',
|
||||
source: 'Quelle',
|
||||
growth: 'Wachstum p.a.',
|
||||
},
|
||||
@@ -204,8 +206,8 @@ const translations = {
|
||||
savingsTotal: 'Ersparnis',
|
||||
},
|
||||
traction: {
|
||||
title: 'Traction & Meilensteine',
|
||||
subtitle: 'Unser bisheriger Fortschritt',
|
||||
title: 'Meilensteine',
|
||||
subtitle: 'Was wir bereits erreicht haben — und was als Nächstes kommt',
|
||||
completed: 'Abgeschlossen',
|
||||
inProgress: 'In Arbeit',
|
||||
planned: 'Geplant',
|
||||
@@ -308,12 +310,13 @@ const translations = {
|
||||
'Cover',
|
||||
'The Problem',
|
||||
'The Solution',
|
||||
'USP',
|
||||
'Regulatory Landscape',
|
||||
'Modular Toolkit',
|
||||
'How It Works',
|
||||
'Market',
|
||||
'Business Model',
|
||||
'Traction',
|
||||
'Milestones',
|
||||
'Competition',
|
||||
'Team',
|
||||
'Financials',
|
||||
@@ -331,6 +334,7 @@ const translations = {
|
||||
'Appendix: Strategy',
|
||||
'Appendix: Financial Plan',
|
||||
'Glossary',
|
||||
'Legal Notice',
|
||||
],
|
||||
executiveSummary: {
|
||||
title: 'Executive Summary',
|
||||
@@ -338,12 +342,12 @@ const translations = {
|
||||
problem: 'The Problem',
|
||||
problemText: 'Many companies, especially in manufacturing, want to use AI — but refuse to let American AI providers access their core IP. Those avoiding US SaaS are cut off from the AI revolution. Those using these providers accept that data may be processed in the US. Meanwhile, new EU regulations (AI Act, Data Act, CRA, NIS2 etc.) affect over 30,000 companies in Germany alone — regardless of size.',
|
||||
solution: 'Our Solution',
|
||||
solutionText: 'Continuous code security instead of annual spot checks: SAST, DAST, SBOM, pentesting on every change. RoPA, TOMs, DPIA, retention policies, CE risk assessment automatically. Audit deviations end-to-end: roles, deadlines, tickets, evidence, escalation to management. Jira integration. Academy. BSI cloud DE or OVH FR.',
|
||||
solutionText: 'Continuous code security instead of annual spot checks: SAST, DAST, SBOM, pentesting on every change. RoPA, TOMs, DPIA, retention policies, CE risk assessment automatically. Audit deviations end-to-end: roles, deadlines, tickets, evidence, escalation to management. Issue tracker of your choice. Academy. BSI cloud DE.',
|
||||
roi: 'Customer Savings',
|
||||
roiText: 'Customers pay ~EUR 50,000/year and save: EUR 30,000 pentests, EUR 20,000 CE assessments, audit manager costs and penalty risk. ROI from day 1.',
|
||||
market: 'Market',
|
||||
businessModel: 'Business Model',
|
||||
businessModelText: 'Customers pay ~EUR 50,000/year and save EUR 50,000+ (pentests, CE assessments, audit managers). ROI from day 1. BSI cloud DE or OVH FR.',
|
||||
businessModelText: 'Customers pay ~EUR 50,000/year and save EUR 50,000+ (pentests, CE assessments, audit managers). ROI from day 1. BSI cloud DE.',
|
||||
keyMetrics: 'Key Metrics',
|
||||
documents: 'Original Documents',
|
||||
controls: 'Audit Aspects',
|
||||
@@ -358,31 +362,31 @@ const translations = {
|
||||
uspText: 'Only platform with continuous code security, automatic compliance documentation and CE software risk assessment — on German or French cloud.',
|
||||
},
|
||||
cover: {
|
||||
tagline: 'Compliance & Code Security for Machine Manufacturers',
|
||||
tagline: 'Compliance & Code Security',
|
||||
subtitle: 'Pre-Seed · Q4 2026',
|
||||
cta: 'Start Pitch',
|
||||
},
|
||||
problem: {
|
||||
title: 'The Problem',
|
||||
subtitle: 'Machine manufacturers want AI — but not at the cost of their data sovereignty',
|
||||
subtitle: 'German and European companies want AI — but not at the cost of their data sovereignty',
|
||||
cards: [
|
||||
{
|
||||
title: 'AI Dilemma',
|
||||
stat: 'Left Behind',
|
||||
desc: 'Machine manufacturers want to use AI but refuse Microsoft Copilot or Claude on their core IP. Fear of data misuse by US corporations is real. Those avoiding US SaaS are cut off from the AI revolution.',
|
||||
desc: 'Manufacturing companies need AI to stay competitive. But letting Microsoft Copilot, ChatGPT or Claude access their source code and engineering data is out of the question for most. Those avoiding US AI fall behind. Those using it risk their data sovereignty.',
|
||||
},
|
||||
{
|
||||
title: 'Patriot Act',
|
||||
title: 'Patriot Act + FISA 702',
|
||||
stat: 'No Protection',
|
||||
desc: 'The alternative: push everything to AWS, Google or Microsoft. But even European servers of US players can be accessed via the Patriot Act. German SMEs are trapped.',
|
||||
desc: 'Even booking EU servers at AWS, Google or Microsoft offers no protection. US laws like FISA 702 and the Cloud Act apply extraterritorially — US authorities can access data regardless of server location. The Schrems II ruling by the CJEU confirmed this.',
|
||||
},
|
||||
{
|
||||
title: 'Regulation Tsunami',
|
||||
stat: 'EUR 50,000+/yr',
|
||||
desc: 'AI Act, NIS2, CRA, GDPR, Machinery Regulation — 5+ laws simultaneously. Pentests and CE certifications cost EUR 50,000+/year but only check once. SMEs with 10-500 employees lack staff and budget.',
|
||||
stat: 'Unsustainable',
|
||||
desc: 'Since 2024, the AI Act, NIS2 and Cyber Resilience Act apply — on top of GDPR, Data Act, Machinery Regulation and Supply Chain Act. European companies bear compliance costs that US and Asian competitors do not face. At the same time, costs rise from raw material shortages and geopolitical crises. SMEs can no longer handle this alone.',
|
||||
},
|
||||
],
|
||||
quote: 'Machine manufacturers need an AI solution that runs in Germany, protects their code and automates compliance — without giving their data to US corporations.',
|
||||
quote: 'Manufacturing companies need an AI solution that runs in Europe, protects their code and automates compliance — without giving their data to US corporations.',
|
||||
},
|
||||
solution: {
|
||||
title: 'The Solution',
|
||||
@@ -390,7 +394,7 @@ const translations = {
|
||||
pillars: [
|
||||
{
|
||||
title: 'Continuous Code Security',
|
||||
desc: 'SAST, DAST, SBOM and pentesting on every code change — not once a year. Findings as Jira tickets with implementation suggestions. EUR 30,000+/year pentest costs saved.',
|
||||
desc: 'SAST, DAST, SBOM and pentesting on every code change — not once a year. Findings as tickets in the issue tracker of your choice, with implementation suggestions. EUR 15,000+ per year per application in pentest costs saved.',
|
||||
icon: 'scan',
|
||||
},
|
||||
{
|
||||
@@ -400,7 +404,7 @@ const translations = {
|
||||
},
|
||||
{
|
||||
title: 'German Cloud, Full Integration',
|
||||
desc: 'BSI-certified cloud in DE or OVH in FR. Jitsi (video), Matrix (chat), AI task creation from audio. No US SaaS in source code. Optional Mac Mini for maximum privacy.',
|
||||
desc: 'BSI-certified cloud in Germany. Live support via Jitsi (video) and Matrix (chat). No US SaaS in source code. Optional Mac Mini/Studio for maximum privacy.',
|
||||
icon: 'server',
|
||||
},
|
||||
],
|
||||
@@ -443,7 +447,7 @@ const translations = {
|
||||
pricingTitle: 'Pricing by Company Size',
|
||||
pricingSubtitle: 'Employee-based — market validated',
|
||||
cloud: 'Cloud Solution (Standard)',
|
||||
cloudDesc: 'BSI cloud DE or OVH FR. For all company sizes.',
|
||||
cloudDesc: 'BSI cloud DE. For all company sizes.',
|
||||
privacy: 'Privacy Hardware (optional)',
|
||||
privacyDesc: 'Mac Mini / Studio for micro businesses (<10 employees) with absolute privacy needs.',
|
||||
},
|
||||
@@ -453,7 +457,7 @@ const translations = {
|
||||
steps: [
|
||||
{
|
||||
title: 'Sign Cloud Contract',
|
||||
desc: 'BSI-certified cloud in Germany or OVH in France. Fixed or flexible costs. For micro businesses optionally: pre-configured Mac Mini.',
|
||||
desc: 'BSI-certified cloud in Germany. Fixed or flexible costs.',
|
||||
},
|
||||
{
|
||||
title: 'Connect Code Repos',
|
||||
@@ -471,13 +475,13 @@ const translations = {
|
||||
},
|
||||
market: {
|
||||
title: 'Market Opportunity',
|
||||
subtitle: 'Machine manufacturing needs compliance & code security',
|
||||
subtitle: 'Compliance & code security for manufacturing companies',
|
||||
tam: 'TAM',
|
||||
sam: 'SAM',
|
||||
som: 'SOM',
|
||||
tamLabel: 'Total Addressable Market',
|
||||
samLabel: 'Serviceable Addressable Market',
|
||||
somLabel: 'Serviceable Obtainable Market',
|
||||
somLabel: 'Serviceable Obtainable Market (machine & plant manufacturers as core market only)',
|
||||
source: 'Source',
|
||||
growth: 'Growth p.a.',
|
||||
},
|
||||
@@ -499,8 +503,8 @@ const translations = {
|
||||
savingsTotal: 'Total Savings',
|
||||
},
|
||||
traction: {
|
||||
title: 'Traction & Milestones',
|
||||
subtitle: 'Our progress so far',
|
||||
title: 'Milestones',
|
||||
subtitle: 'What we have achieved — and what comes next',
|
||||
completed: 'Completed',
|
||||
inProgress: 'In Progress',
|
||||
planned: 'Planned',
|
||||
|
||||
@@ -17,8 +17,8 @@ export const PRESENTER_FAQ: FAQEntry[] = [
|
||||
keywords: ['module', 'modules', 'funktionen', 'features', 'umfang', 'scope', 'wieviele', 'how many'],
|
||||
question_de: 'Welche Module hat die Plattform?',
|
||||
question_en: 'What modules does the platform have?',
|
||||
answer_de: '65+ Compliance-Module in zwei Saeulen: Unternehmens-Compliance — alle Datenkategorien, Verarbeitungen, Dienstleister und Auftragsverarbeiter erfassen, automatische Dokumentenerstellung (AGB, DSE, Cookie Banner, Nutzungsbedingungen), DSR-Prozess, Dokumentenversionierung, Rollenverwaltung, Academy und Schulungen, Audit und Nachweismanagement. Code/CE-Seite — Repo-Scanning (SAST + DAST), Pentesting, CE Software Risk Assessment (IACE), automatische SBOM-Generierung, Jira/Atlassian-Integration mit konkreten Code-Änderungsvorschlaegen.',
|
||||
answer_en: 'modular compliance platform in two pillars: Company-side compliance — capture all data categories, processes, providers and processors, auto-generate legal documents (Terms of Service, Privacy Policy, Cookie Banner, Terms of Use), DSR process, document versioning, role management, academy and training, audit and evidence management. Code/CE side — repo scanning (SAST + DAST), pentesting, CE Software Risk Assessment (IACE), automatic SBOM generation, Jira/Atlassian integration with specific code change suggestions.',
|
||||
answer_de: '65+ Compliance-Module in zwei Saeulen: Unternehmens-Compliance — alle Datenkategorien, Verarbeitungen, Dienstleister und Auftragsverarbeiter erfassen, automatische Dokumentenerstellung (AGB, DSE, Cookie Banner, Nutzungsbedingungen), DSR-Prozess, Dokumentenversionierung, Rollenverwaltung, Academy und Schulungen, Audit und Nachweismanagement. Code/CE-Seite — Repo-Scanning (SAST + DAST), Pentesting, CE Software Risk Assessment (IACE), automatische SBOM-Generierung, Integration in den Issue-Tracker deiner Wahl mit konkreten Code-Änderungsvorschlaegen.',
|
||||
answer_en: 'modular compliance platform in two pillars: Company-side compliance — capture all data categories, processes, providers and processors, auto-generate legal documents (Terms of Service, Privacy Policy, Cookie Banner, Terms of Use), DSR process, document versioning, role management, academy and training, audit and evidence management. Code/CE side — repo scanning (SAST + DAST), pentesting, CE Software Risk Assessment (IACE), automatic SBOM generation, integration with the issue tracker of your choice with specific code change suggestions.',
|
||||
goto_slide: 'solution',
|
||||
priority: 8,
|
||||
},
|
||||
@@ -37,8 +37,8 @@ export const PRESENTER_FAQ: FAQEntry[] = [
|
||||
keywords: ['code', 'ce', 'scanning', 'sast', 'dast', 'sbom', 'iace', 'firmware', 'repo', 'repository', 'devsecops'],
|
||||
question_de: 'Wie funktioniert die Code- und CE-Compliance?',
|
||||
question_en: 'How does code and CE compliance work?',
|
||||
answer_de: 'Die Code/CE-Seite umfasst vollständiges Repo-Scanning mit SAST und DAST, automatisiertes Pentesting sowie CE Software Risk Assessment (IACE) — auch für Elektronik-Produkte. Gefundene Schwachstellen werden direkt als Jira/Atlassian-Tickets mit konkreten Code-Änderungsvorschlaegen erstellt. Die KI implementiert Fixes automatisch. Zusaetzlich wird eine vollständige SBOM (Software Bill of Materials) generiert.',
|
||||
answer_en: 'The code/CE side includes full repo scanning with SAST and DAST, automated pentesting, and CE Software Risk Assessment (IACE) — also for electronics products. Findings are directly created as Jira/Atlassian tickets with specific code change suggestions. The AI implements fixes automatically. Additionally, a complete SBOM (Software Bill of Materials) is generated.',
|
||||
answer_de: 'Die Code/CE-Seite umfasst vollständiges Repo-Scanning mit SAST und DAST, automatisiertes Pentesting sowie CE Software Risk Assessment (IACE) — auch für Elektronik-Produkte. Gefundene Schwachstellen werden direkt als Tickets im Issue-Tracker deiner Wahl mit konkreten Code-Änderungsvorschlaegen erstellt. Die KI implementiert Fixes automatisch. Zusaetzlich wird eine vollständige SBOM (Software Bill of Materials) generiert.',
|
||||
answer_en: 'The code/CE side includes full repo scanning with SAST and DAST, automated pentesting, and CE Software Risk Assessment (IACE) — also for electronics products. Findings are directly created as tickets in the issue tracker of your choice with specific code change suggestions. The AI implements fixes automatically. Additionally, a complete SBOM (Software Bill of Materials) is generated.',
|
||||
goto_slide: 'how-it-works',
|
||||
priority: 8,
|
||||
},
|
||||
@@ -58,8 +58,8 @@ export const PRESENTER_FAQ: FAQEntry[] = [
|
||||
keywords: ['llm', 'modell', 'model', 'ki', 'ai', 'kuenstliche intelligenz', 'artificial intelligence', 'welches', 'which', '1000b'],
|
||||
question_de: 'Welches KI-Modell nutzt ihr?',
|
||||
question_en: 'Which AI model do you use?',
|
||||
answer_de: 'Unser Haupt-LLM hat 1000 Milliarden Parameter und läuft ausschliesslich bei BSI-zertifizierten Hostern in Deutschland und Frankreich — SysEleven, OVH und Hetzner. Keine amerikanischen Anbieter, keine Daten verlassen die Server. Jeder Kunde bekommt einen isolierten Namespace. Für die Mac Mini/Studio Variante setzen wir kleinere Modelle ein, die lokale Dokumentenverarbeitung und RAG-Abfragen uebernehmen.',
|
||||
answer_en: 'Our main LLM has 1,000 billion parameters and runs exclusively at BSI-certified hosters in Germany and France — SysEleven, OVH and Hetzner. No American providers, no data leaves the servers. Each customer gets an isolated namespace. For the Mac Mini/Studio variant, we use smaller models for local document processing and RAG queries.',
|
||||
answer_de: 'Unser Haupt-LLM hat 1000 Milliarden Parameter und läuft ausschliesslich bei BSI-zertifizierten Hostern in Deutschland und Frankreich — SysEleven und Hetzner. Keine amerikanischen Anbieter, keine Daten verlassen die Server. Jeder Kunde bekommt einen isolierten Namespace. Für die Mac Mini/Studio Variante setzen wir kleinere Modelle ein, die lokale Dokumentenverarbeitung und RAG-Abfragen uebernehmen.',
|
||||
answer_en: 'Our main LLM has 1,000 billion parameters and runs exclusively at BSI-certified hosters in Germany and France — SysEleven and Hetzner. No American providers, no data leaves the servers. Each customer gets an isolated namespace. For the Mac Mini/Studio variant, we use smaller models for local document processing and RAG queries.',
|
||||
goto_slide: 'product',
|
||||
priority: 8,
|
||||
},
|
||||
@@ -68,8 +68,8 @@ export const PRESENTER_FAQ: FAQEntry[] = [
|
||||
keywords: ['hosting', 'europa', 'europe', 'eu', 'deutschland', 'germany', 'frankreich', 'france', 'bsi', 'syseleven', 'ovh', 'hetzner', 'souveraenitaet', 'sovereignty', 'american', 'us', 'usa'],
|
||||
question_de: 'Wo werden die Daten gehostet?',
|
||||
question_en: 'Where is the data hosted?',
|
||||
answer_de: 'Ausschliesslich in Deutschland und Frankreich bei BSI-zertifizierten Anbietern: SysEleven, OVH und Hetzner. Keine amerikanischen Cloud-Provider, kein AWS, kein Azure, kein GCP. Die Daten verlassen niemals die europäischen Server. Jeder Kunde erhaelt einen vollständig isolierten Namespace — es gibt keine geteilte Datenverarbeitung zwischen Mandanten.',
|
||||
answer_en: 'Exclusively in Germany and France at BSI-certified providers: SysEleven, OVH and Hetzner. No American cloud providers, no AWS, no Azure, no GCP. Data never leaves the European servers. Each customer receives a fully isolated namespace — there is no shared data processing between tenants.',
|
||||
answer_de: 'Ausschliesslich in Deutschland und Frankreich bei BSI-zertifizierten Anbietern: SysEleven und Hetzner. Keine amerikanischen Cloud-Provider, kein AWS, kein Azure, kein GCP. Die Daten verlassen niemals die europäischen Server. Jeder Kunde erhaelt einen vollständig isolierten Namespace — es gibt keine geteilte Datenverarbeitung zwischen Mandanten.',
|
||||
answer_en: 'Exclusively in Germany and France at BSI-certified providers: SysEleven and Hetzner. No American cloud providers, no AWS, no Azure, no GCP. Data never leaves the European servers. Each customer receives a fully isolated namespace — there is no shared data processing between tenants.',
|
||||
goto_slide: 'annex-architecture',
|
||||
priority: 9,
|
||||
},
|
||||
@@ -103,12 +103,12 @@ export const PRESENTER_FAQ: FAQEntry[] = [
|
||||
priority: 8,
|
||||
},
|
||||
{
|
||||
id: 'tech-jira-integration',
|
||||
keywords: ['jira', 'atlassian', 'integration', 'ticket', 'tickets', 'issue', 'issues', 'fix', 'fixes', 'code change'],
|
||||
question_de: 'Wie funktioniert die Jira/Atlassian-Integration?',
|
||||
question_en: 'How does the Jira/Atlassian integration work?',
|
||||
answer_de: 'Wenn das Code-Scanning (SAST/DAST) oder Pentesting Schwachstellen findet, erstellt die Plattform automatisch Jira-Tickets mit exakten Code-Änderungsvorschlaegen — welche Datei, welche Zeile, welcher Fix. Die KI kann den Fix auch direkt implementieren. So schliesst sich der Kreis von Finding zu Fix vollständig automatisiert.',
|
||||
answer_en: 'When code scanning (SAST/DAST) or pentesting finds vulnerabilities, the platform automatically creates Jira tickets with exact code change suggestions — which file, which line, which fix. The AI can also implement the fix directly. This closes the loop from finding to fix fully automated.',
|
||||
id: 'tech-issue-tracker-integration',
|
||||
keywords: ['jira', 'atlassian', 'linear', 'issue tracker', 'integration', 'ticket', 'tickets', 'issue', 'issues', 'fix', 'fixes', 'code change'],
|
||||
question_de: 'Wie funktioniert die Issue-Tracker-Integration?',
|
||||
question_en: 'How does the issue tracker integration work?',
|
||||
answer_de: 'Wenn das Code-Scanning (SAST/DAST) oder Pentesting Schwachstellen findet, erstellt die Plattform automatisch Tickets im Issue-Tracker deiner Wahl — Jira, GitLab, Linear, Gitea — mit exakten Code-Änderungsvorschlaegen, welche Datei, welche Zeile, welcher Fix. Die KI kann den Fix auch direkt implementieren. So schliesst sich der Kreis von Finding zu Fix vollständig automatisiert.',
|
||||
answer_en: 'When code scanning (SAST/DAST) or pentesting finds vulnerabilities, the platform automatically creates tickets in the issue tracker of your choice — Jira, GitLab, Linear, Gitea — with exact code change suggestions: which file, which line, which fix. The AI can also implement the fix directly. This closes the loop from finding to fix fully automated.',
|
||||
goto_slide: 'how-it-works',
|
||||
priority: 7,
|
||||
},
|
||||
@@ -137,8 +137,8 @@ export const PRESENTER_FAQ: FAQEntry[] = [
|
||||
keywords: ['meeting', 'recorder', 'aufzeichnung', 'recording', 'nvidia', 'transkription', 'transcription', 'protokoll', 'minutes', 'tasks', 'aufgaben'],
|
||||
question_de: 'Wie funktioniert der Meeting-Recorder?',
|
||||
question_en: 'How does the meeting recorder work?',
|
||||
answer_de: 'Unser NVIDIA-basierter Meeting-Recorder zeichnet Besprechungen auf, transkribiert sie automatisch und extrahiert Aufgaben und Beschluesse. Diese werden direkt als Jira-Tasks angelegt — inklusive Zuweisung, Deadline und Kontext aus dem Meeting. So geht nichts mehr verloren.',
|
||||
answer_en: 'Our NVIDIA-based meeting recorder records meetings, transcribes them automatically and extracts tasks and decisions. These are directly created as Jira tasks — including assignment, deadline and context from the meeting. Nothing gets lost.',
|
||||
answer_de: 'Unser NVIDIA-basierter Meeting-Recorder zeichnet Besprechungen auf, transkribiert sie automatisch und extrahiert Aufgaben und Beschluesse. Diese werden direkt als Tasks im Issue-Tracker deiner Wahl angelegt — inklusive Zuweisung, Deadline und Kontext aus dem Meeting. So geht nichts mehr verloren.',
|
||||
answer_en: 'Our NVIDIA-based meeting recorder records meetings, transcribes them automatically and extracts tasks and decisions. These are directly created as tasks in the issue tracker of your choice — including assignment, deadline and context from the meeting. Nothing gets lost.',
|
||||
priority: 6,
|
||||
},
|
||||
{
|
||||
@@ -210,8 +210,8 @@ export const PRESENTER_FAQ: FAQEntry[] = [
|
||||
keywords: ['proliance', 'dataguard', 'heydata', 'vergleich', 'comparison', 'versus'],
|
||||
question_de: 'Warum können Proliance und DataGuard das nicht?',
|
||||
question_en: 'Why can\'t Proliance and DataGuard do this?',
|
||||
answer_de: 'Proliance, DataGuard und heyData fokussieren auf organisatorische DSGVO-Compliance — Verarbeitungsverzeichnisse, Datenschutzerklaerungen, Schulungen. Keiner bietet Code-Scanning, CE-Risikobewertung, Pentesting oder automatische Jira-Integration mit Code-Fixes. Sie machen das Unternehmen teilweise compliant, aber nicht die Produkte. Und keiner hostet die KI ausschliesslich in Europa.',
|
||||
answer_en: 'Proliance, DataGuard and heyData focus on organizational GDPR compliance — records of processing, privacy policies, training. None offer code scanning, CE risk assessment, pentesting or automatic Jira integration with code fixes. They make the company partially compliant, but not the products. And none host the AI exclusively in Europe.',
|
||||
answer_de: 'Proliance, DataGuard und heyData fokussieren auf organisatorische DSGVO-Compliance — Verarbeitungsverzeichnisse, Datenschutzerklaerungen, Schulungen. Keiner bietet Code-Scanning, CE-Risikobewertung, Pentesting oder automatische Issue-Tracker-Integration mit Code-Fixes. Sie machen das Unternehmen teilweise compliant, aber nicht die Produkte. Und keiner hostet die KI ausschliesslich in Europa.',
|
||||
answer_en: 'Proliance, DataGuard and heyData focus on organizational GDPR compliance — records of processing, privacy policies, training. None offer code scanning, CE risk assessment, pentesting or automatic issue tracker integration with code fixes. They make the company partially compliant, but not the products. And none host the AI exclusively in Europe.',
|
||||
goto_slide: 'competition',
|
||||
priority: 8,
|
||||
},
|
||||
@@ -232,8 +232,8 @@ export const PRESENTER_FAQ: FAQEntry[] = [
|
||||
keywords: ['cloud', 'mini', 'studio', 'unterschied', 'difference', 'vergleich', 'comparison', 'tier', 'tiers', 'varianten', 'variants'],
|
||||
question_de: 'Cloud oder Hardware?',
|
||||
question_en: 'Cloud or hardware?',
|
||||
answer_de: 'Cloud ist der Standard: BSI-zertifiziert in Deutschland oder OVH in Frankreich. Fixe oder flexible Kosten, modulare Module, Jira-Integration, Matrix/Jitsi, Compliance-LLM. Für Kleinstunternehmen unter 10 Mitarbeitern mit absolutem Privacy-Bedarf bieten wir optional einen vorkonfigurierten Mac Mini mit kleineren lokalen LLMs.',
|
||||
answer_en: 'Cloud is the standard: BSI-certified in Germany or OVH in France. Fixed or flexible costs, modular modules, Jira integration, Matrix/Jitsi, compliance LLM. For micro businesses under 10 employees with absolute privacy needs, we optionally offer a pre-configured Mac Mini with smaller local LLMs.',
|
||||
answer_de: 'Cloud ist der Standard: BSI-zertifiziert in Deutschland oder Frankreich. Fixe oder flexible Kosten, modulare Module, Integration in den Issue-Tracker deiner Wahl, Matrix/Jitsi, Compliance-LLM. Für Kleinstunternehmen unter 10 Mitarbeitern mit absolutem Privacy-Bedarf bieten wir optional einen vorkonfigurierten Mac Mini oder Mac Studio mit kleineren lokalen LLMs.',
|
||||
answer_en: 'Cloud is the standard: BSI-certified in Germany or France. Fixed or flexible costs, modular modules, integration with the issue tracker of your choice, Matrix/Jitsi, compliance LLM. For micro businesses under 10 employees with absolute privacy needs, we optionally offer a pre-configured Mac Mini or Mac Studio with smaller local LLMs.',
|
||||
goto_slide: 'product',
|
||||
priority: 8,
|
||||
},
|
||||
@@ -252,8 +252,8 @@ export const PRESENTER_FAQ: FAQEntry[] = [
|
||||
keywords: ['unit economics', 'marge', 'margin', 'ltv', 'cac', 'amortisation', 'amortization'],
|
||||
question_de: 'Wie sind die Unit Economics?',
|
||||
question_en: 'What are the unit economics?',
|
||||
answer_de: 'Bruttomarge über 80% beim Cloud-Produkt — keine Hardware-Kosten. Die AI-First Architektur haelt die operativen Kosten pro Kunde extrem niedrig. Europaeisches Hosting bei SysEleven/OVH/Hetzner ist deutlich günstiger als AWS/Azure. LTV/CAC verbessert sich durch die Plattform-Stickiness: modulare Plattform schaffen natürlichen Lock-in.',
|
||||
answer_en: 'Gross margin above 80% on the cloud product — no hardware costs. The AI-first architecture keeps operational costs per customer extremely low. European hosting at SysEleven/OVH/Hetzner is significantly cheaper than AWS/Azure. LTV/CAC improves through platform stickiness: all modules create natural lock-in.',
|
||||
answer_de: 'Bruttomarge über 80% beim Cloud-Produkt — keine Hardware-Kosten. Die AI-First Architektur haelt die operativen Kosten pro Kunde extrem niedrig. Europaeisches Hosting bei SysEleven und Hetzner ist deutlich günstiger als AWS/Azure. LTV/CAC verbessert sich durch die Plattform-Stickiness: modulare Plattform schaffen natürlichen Lock-in.',
|
||||
answer_en: 'Gross margin above 80% on the cloud product — no hardware costs. The AI-first architecture keeps operational costs per customer extremely low. European hosting at SysEleven and Hetzner is significantly cheaper than AWS/Azure. LTV/CAC improves through platform stickiness: all modules create natural lock-in.',
|
||||
goto_slide: 'business-model',
|
||||
priority: 7,
|
||||
},
|
||||
@@ -296,8 +296,8 @@ export const PRESENTER_FAQ: FAQEntry[] = [
|
||||
keywords: ['use of funds', 'wofuer', 'what for', 'verwendung', 'allocation', 'mittelverwendung'],
|
||||
question_de: 'Wofür wird das Kapital verwendet?',
|
||||
question_en: 'What will the capital be used for?',
|
||||
answer_de: 'Vier Bereiche: 1) Cloud-Infrastruktur — Skalierung der europäischen Server-Kapazitaet bei SysEleven/OVH/Hetzner. 2) Engineering — weitere Module und Integrationen. 3) Vertrieb — Pilotkunden bei Maschinenbauern, CE-Zertifizierern und produzierenden Unternehmen. 4) Reserve — regulatorische Anforderungen und Working Capital.',
|
||||
answer_en: 'Four areas: 1) Cloud infrastructure — scaling European server capacity at SysEleven/OVH/Hetzner. 2) Engineering — additional modules and integrations. 3) Sales — pilot customers among machine builders, CE certifiers and producing companies. 4) Reserve — regulatory requirements and working capital.',
|
||||
answer_de: 'Vier Bereiche: 1) Cloud-Infrastruktur — Skalierung der europäischen Server-Kapazitaet bei SysEleven/Hetzner. 2) Engineering — weitere Module und Integrationen. 3) Vertrieb — Pilotkunden bei Maschinenbauern, CE-Zertifizierern und produzierenden Unternehmen. 4) Reserve — regulatorische Anforderungen und Working Capital.',
|
||||
answer_en: 'Four areas: 1) Cloud infrastructure — scaling European server capacity at SysEleven/Hetzner. 2) Engineering — additional modules and integrations. 3) Sales — pilot customers among machine builders, CE certifiers and producing companies. 4) Reserve — regulatory requirements and working capital.',
|
||||
goto_slide: 'the-ask',
|
||||
priority: 8,
|
||||
},
|
||||
@@ -328,8 +328,8 @@ export const PRESENTER_FAQ: FAQEntry[] = [
|
||||
keywords: ['cra', 'cyber resilience', 'cyber resilience act', 'firmware', 'produktsicherheit', 'product security'],
|
||||
question_de: 'Was ist der Cyber Resilience Act?',
|
||||
question_en: 'What is the Cyber Resilience Act?',
|
||||
answer_de: 'Der CRA verpflichtet Hersteller, Software in ihren Produkten abzusichern — über den gesamten Lebenszyklus. Für produzierende Unternehmen mit Firmware, embedded Software und Elektronik bedeutet das: Vulnerability Management, SBOM, Incident Reporting. Unsere Plattform automatisiert all das — vom Repo-Scan bis zum Jira-Ticket mit Code-Fix.',
|
||||
answer_en: 'The CRA obligates manufacturers to secure software in their products — throughout the entire lifecycle. For producing companies with firmware, embedded software and electronics this means: vulnerability management, SBOM, incident reporting. Our platform automates all of this — from repo scan to Jira ticket with code fix.',
|
||||
answer_de: 'Der CRA verpflichtet Hersteller, Software in ihren Produkten abzusichern — über den gesamten Lebenszyklus. Für produzierende Unternehmen mit Firmware, embedded Software und Elektronik bedeutet das: Vulnerability Management, SBOM, Incident Reporting. Unsere Plattform automatisiert all das — vom Repo-Scan bis zum Ticket im Issue-Tracker deiner Wahl mit Code-Fix.',
|
||||
answer_en: 'The CRA obligates manufacturers to secure software in their products — throughout the entire lifecycle. For producing companies with firmware, embedded software and electronics this means: vulnerability management, SBOM, incident reporting. Our platform automates all of this — from repo scan to ticket in your issue tracker with code fix.',
|
||||
goto_slide: 'annex-regulatory',
|
||||
priority: 7,
|
||||
},
|
||||
@@ -384,8 +384,8 @@ export const PRESENTER_FAQ: FAQEntry[] = [
|
||||
keywords: ['pentesting', 'penetrationstest', 'penetration test', 'security testing', 'pentests'],
|
||||
question_de: 'Wie funktioniert das Pentesting?',
|
||||
question_en: 'How does pentesting work?',
|
||||
answer_de: 'Pentesting ist fester Bestandteil unserer Code/CE-Saule. Automatisierte Penetrationstests laufen gegen die Kunden-Anwendungen und -Infrastruktur. Gefundene Schwachstellen werden automatisch als Jira-Tickets mit konkreten Code-Änderungsvorschlaegen erstellt — die KI kann die Fixes direkt implementieren. So wird der gesamte Zyklus von Finding bis Fix automatisiert.',
|
||||
answer_en: 'Pentesting is a core part of our code/CE pillar. Automated penetration tests run against customer applications and infrastructure. Found vulnerabilities are automatically created as Jira tickets with specific code change suggestions — the AI can implement fixes directly. This automates the entire cycle from finding to fix.',
|
||||
answer_de: 'Pentesting ist fester Bestandteil unserer Code/CE-Saule. Automatisierte Penetrationstests laufen gegen die Kunden-Anwendungen und -Infrastruktur. Gefundene Schwachstellen werden automatisch als Tickets im Issue-Tracker deiner Wahl mit konkreten Code-Änderungsvorschlaegen erstellt — die KI kann die Fixes direkt implementieren. So wird der gesamte Zyklus von Finding bis Fix automatisiert.',
|
||||
answer_en: 'Pentesting is a core part of our code/CE pillar. Automated penetration tests run against customer applications and infrastructure. Found vulnerabilities are automatically created as tickets in the issue tracker of your choice with specific code change suggestions — the AI can implement fixes directly. This automates the entire cycle from finding to fix.',
|
||||
goto_slide: 'how-it-works',
|
||||
priority: 7,
|
||||
},
|
||||
@@ -396,8 +396,8 @@ export const PRESENTER_FAQ: FAQEntry[] = [
|
||||
keywords: ['demo', 'test', 'testen', 'try', 'ausprobieren', 'live', 'showcase'],
|
||||
question_de: 'Kann ich eine Demo sehen?',
|
||||
question_en: 'Can I see a demo?',
|
||||
answer_de: 'Sehr gerne! Wir zeigen Ihnen die Cloud-Plattform live — inklusive Code-Scanning, Compliance-Module, KI-Analyse, Jira-Integration und den Meeting-Recorder. Ein Cloud-Demo-Zugang kann sofort bereitgestellt werden. Kontaktieren Sie uns für einen Termin.',
|
||||
answer_en: 'Absolutely! We will show you the cloud platform live — including code scanning, compliance modules, AI analysis, Jira integration and the meeting recorder. A cloud demo access can be provisioned immediately. Contact us for an appointment.',
|
||||
answer_de: 'Sehr gerne! Wir zeigen Ihnen die Cloud-Plattform live — inklusive Code-Scanning, Compliance-Module, KI-Analyse, Issue-Tracker-Integration und den Meeting-Recorder. Ein Cloud-Demo-Zugang kann sofort bereitgestellt werden. Kontaktieren Sie uns für einen Termin.',
|
||||
answer_en: 'Absolutely! We will show you the cloud platform live — including code scanning, compliance modules, AI analysis, issue tracker integration and the meeting recorder. A cloud demo access can be provisioned immediately. Contact us for an appointment.',
|
||||
priority: 6,
|
||||
},
|
||||
{
|
||||
@@ -530,14 +530,66 @@ export const PRESENTER_FAQ: FAQEntry[] = [
|
||||
priority: 7,
|
||||
},
|
||||
|
||||
// === FISA 702 / DRITTLANDTRANSFER / EU-SOUVERAENITAET ===
|
||||
{
|
||||
id: 'fisa-702-what',
|
||||
keywords: ['fisa', 'fisa 702', 'surveillance', 'ueberwachung', 'us-zugriff', 'us zugriff', 'nsa', 'prism', 'upstream', 'foreign intelligence'],
|
||||
question_de: 'Was ist FISA 702 und warum ist das relevant?',
|
||||
question_en: 'What is FISA 702 and why does it matter?',
|
||||
answer_de: 'FISA 702 ist ein US-Ueberwachungsgesetz, das US-Behoerden erlaubt, gezielt Daten von Nicht-US-Personen im Ausland zu ueberwachen — ohne individuellen richterlichen Beschluss. Es gibt zwei Hauptprogramme: PRISM, bei dem US-Unternehmen wie Microsoft, Google und Amazon Daten an Behoerden herausgeben muessen, und Upstream, bei dem direkt auf Internet-Infrastruktur zugegriffen wird. Das betrifft jeden EU-Nutzer eines US-Dienstes. Genau deshalb wurde das Privacy Shield durch das Schrems-II-Urteil des EuGH gekippt. Fuer BreakPilot ist das hochrelevant: Jedes Unternehmen, das US-Cloud oder US-KI nutzt, hat ein strukturelles FISA-702-Risiko. Unsere EU-only Architektur eliminiert dieses Risiko vollstaendig.',
|
||||
answer_en: 'FISA 702 is a US surveillance law that allows US authorities to target data of non-US persons abroad — without individual court orders. There are two main programs: PRISM, where US companies like Microsoft, Google and Amazon must hand over data to authorities, and Upstream, which directly taps internet infrastructure. This affects every EU user of a US service. This is exactly why the EU-US Privacy Shield was invalidated by the Schrems II ruling. For BreakPilot this is highly relevant: every company using US cloud or US AI has a structural FISA 702 risk. Our EU-only architecture eliminates this risk entirely.',
|
||||
goto_slide: 'annex-regulatory',
|
||||
priority: 9,
|
||||
},
|
||||
{
|
||||
id: 'fisa-eu-cloud-myth',
|
||||
keywords: ['eu cloud', 'eu-cloud', 'eu region', 'serverstandort', 'aws eu', 'azure eu', 'google eu', 'rechenzentrum', 'data center', 'frankfurt', 'ireland'],
|
||||
question_de: 'Schuetzt eine EU-Cloud-Region bei US-Anbietern vor FISA 702?',
|
||||
question_en: 'Does an EU cloud region at US providers protect against FISA 702?',
|
||||
answer_de: 'Nein. Das ist einer der groessten Irrtuemer im Markt. Wenn der Cloud-Anbieter ein US-Unternehmen ist — also AWS, Microsoft Azure oder Google Cloud — dann unterliegt er US-Recht, egal wo der Server steht. FISA 702 und der Cloud Act gelten extraterritorial. Das bedeutet: Ein Server in Frankfurt bei AWS ist rechtlich genauso exponiert wie einer in Virginia. Unternehmen zahlen mehr fuer die EU-Region und wiegen sich in falscher Sicherheit. BreakPilot setzt deshalb konsequent auf EU-Anbieter ohne US-Muttergesellschaft — SysEleven und Hetzner, beide BSI-konform und ohne FISA-Exposition.',
|
||||
answer_en: 'No. This is one of the biggest misconceptions in the market. If the cloud provider is a US company — AWS, Microsoft Azure or Google Cloud — it is subject to US law regardless of where the server is located. FISA 702 and the Cloud Act apply extraterritorially. A server in Frankfurt at AWS is legally just as exposed as one in Virginia. Companies pay more for the EU region and create a false sense of security. BreakPilot therefore exclusively uses EU providers without US parent companies — SysEleven and Hetzner, both BSI-compliant and without FISA exposure.',
|
||||
goto_slide: 'annex-architecture',
|
||||
priority: 9,
|
||||
},
|
||||
{
|
||||
id: 'fisa-dsfa-contradiction',
|
||||
keywords: ['dsfa', 'dpia', 'risikoakzeptanz', 'risk acceptance', 'restrisiko', 'residual risk', 'widerspruch', 'contradiction', 'schoenreden', 'fake compliance'],
|
||||
question_de: 'Warum reicht eine DSFA alleine nicht aus, um US-Cloud-Risiken zu bewaeltigen?',
|
||||
question_en: 'Why is a DPIA alone not enough to manage US cloud risks?',
|
||||
answer_de: 'Das ist ein zentraler Widerspruch im aktuellen Compliance-Markt. Unternehmen erstellen eine Datenschutz-Folgenabschaetzung, dokumentieren das FISA-702-Risiko, treffen Massnahmen wie Verschluesselung und EU-Region — und akzeptieren dann das Restrisiko. Faktisch sagen sie damit: Wir wissen, dass US-Behoerden zugreifen koennten, und nehmen das in Kauf. Das ist kein Sicherheitsnachweis, sondern ein Rechtfertigungsinstrument. Die DSGVO erlaubt das ueber den risikobasierten Ansatz, aber es bleibt eine bewusste Risikoakzeptanz fuer ein technisch nicht loesbares Problem. BreakPilot geht einen fundamental anderen Weg: Wir eliminieren das Restrisiko strukturell, statt es zu dokumentieren. Kein US-Anbieter, keine FISA-Exposition, kein Restrisiko. Das ist der Unterschied zwischen Compliance simulieren und Compliance loesen.',
|
||||
answer_en: 'This is a central contradiction in the current compliance market. Companies create a Data Protection Impact Assessment, document the FISA 702 risk, implement measures like encryption and EU region — and then accept the residual risk. In effect they are saying: we know US authorities could access the data, and we accept that. This is not a security proof but a justification instrument. GDPR allows this through the risk-based approach, but it remains a deliberate risk acceptance for a technically unsolvable problem. BreakPilot takes a fundamentally different approach: we structurally eliminate the residual risk instead of documenting it. No US provider, no FISA exposure, no residual risk. That is the difference between simulating compliance and solving compliance.',
|
||||
goto_slide: 'annex-regulatory',
|
||||
priority: 8,
|
||||
},
|
||||
{
|
||||
id: 'fisa-market-opportunity',
|
||||
keywords: ['marktchance', 'market opportunity', 'warum eu', 'why eu', 'eu-first', 'souveraenitaet', 'sovereignty', 'datensouveraenitaet', 'digital sovereignty', 'schrems', 'privacy shield'],
|
||||
question_de: 'Warum ist FISA 702 eine Marktchance fuer BreakPilot?',
|
||||
question_en: 'Why is FISA 702 a market opportunity for BreakPilot?',
|
||||
answer_de: 'FISA 702 ist der zentrale Grund, warum EU-US-Datentransfers rechtlich schwierig sind, warum AI-Compliance ein riesiger Markt ist und warum unser EU-first-Ansatz strategischen Vorteil hat. Das Schrems-II-Urteil hat gezeigt, dass politische Loesungen wie Privacy Shield scheitern. Das neue EU-US Data Privacy Framework wird von Experten als naechstes Schrems-III angesehen. Jedes Mal wenn ein solches Abkommen kippt, stehen tausende Unternehmen vor dem Problem, ihre US-Dienste nicht mehr rechtskonform nutzen zu koennen. BreakPilot ist davon nicht betroffen — unsere Architektur ist strukturell unabhaengig von US-Recht. Das ist kein Nice-to-have, sondern konkrete Risikovermeidung fuer unsere Kunden.',
|
||||
answer_en: 'FISA 702 is the central reason why EU-US data transfers are legally difficult, why AI compliance is a huge market, and why our EU-first approach has strategic advantage. The Schrems II ruling showed that political solutions like Privacy Shield fail. The new EU-US Data Privacy Framework is seen by experts as the next Schrems III. Every time such an agreement falls, thousands of companies face the problem of no longer being able to use their US services in compliance. BreakPilot is not affected — our architecture is structurally independent of US law. This is not a nice-to-have but concrete risk avoidance for our customers.',
|
||||
goto_slide: 'market',
|
||||
priority: 8,
|
||||
},
|
||||
{
|
||||
id: 'fisa-breakpilot-architecture',
|
||||
keywords: ['architektur', 'architecture', 'eu-only', 'kein us', 'no us', 'syseleven', 'hetzner', 'bsi', 'hosting', 'wo gehostet', 'where hosted'],
|
||||
question_de: 'Wie schuetzt sich BreakPilot konkret gegen FISA 702?',
|
||||
question_en: 'How does BreakPilot specifically protect against FISA 702?',
|
||||
answer_de: 'Unsere gesamte Infrastruktur laeuft auf EU-Anbietern ohne US-Muttergesellschaft. Wir nutzen SysEleven und Hetzner — beide BSI-konform und in Deutschland. Unsere LLMs laufen lokal oder auf BSI-zertifizierten EU-Servern. Keine personenbezogenen Daten verlassen jemals die EU. Wir setzen keine US-KI-Dienste wie OpenAI, Anthropic Cloud oder Google AI ein. Isolierte Namespaces pro Kunde stellen sicher, dass Daten strikt getrennt sind. Die Schluesselhoheit liegt vollstaendig beim Kunden. Damit ist FISA 702 fuer unsere Kunden schlicht nicht anwendbar — es gibt keinen US-Anbieter in der Kette, der zur Herausgabe verpflichtet werden koennte.',
|
||||
answer_en: 'Our entire infrastructure runs on EU providers without US parent companies. We use SysEleven and Hetzner — both BSI-compliant and located in Germany. Our LLMs run locally or on BSI-certified EU servers. No personal data ever leaves the EU. We do not use US AI services like OpenAI, Anthropic Cloud or Google AI. Isolated namespaces per customer ensure strict data separation. Key management is entirely under customer control. This makes FISA 702 simply inapplicable for our customers — there is no US provider in the chain that could be compelled to hand over data.',
|
||||
goto_slide: 'annex-architecture',
|
||||
priority: 9,
|
||||
},
|
||||
|
||||
// === MODULE ===
|
||||
{
|
||||
id: 'modules-overview',
|
||||
keywords: ['module', 'modules', 'baukasten', 'toolkit', '12 module', 'welche module', 'which modules', 'funktionen', 'features', 'leistungen'],
|
||||
question_de: 'Welche 12 Module bietet ihr an?',
|
||||
question_en: 'Which 12 modules do you offer?',
|
||||
answer_de: 'Unsere Plattform besteht aus zwölf Modulen, die Kunden einzeln oder als Gesamtpaket nutzen können. Den Kern bildet das Code-Security-Modul mit SAST, DAST, SBOM-Analysen und kontinuierlichem Pentesting bei jeder Code-Änderung. Dazu kommt die CE-Software-Risikobeurteilung, die Hersteller für die CE-Kennzeichnung ihrer Produkte brauchen. Für die laufende Compliance-Dokumentation erstellen wir automatisch Verarbeitungsverzeichnisse, technisch-organisatorische Maßnahmen, Datenschutz-Folgenabschätzungen und Löschfristen. Der Audit Manager verwaltet Haupt- und Nebenabweichungen nach Audits vollständig End-to-End mit Stichtagen, Tickets und Eskalation. Darüber hinaus bieten wir Module für Betroffenenrechte, Einwilligungsmanagement, Notfallpläne bei Datenschutzvorfällen und einen Cookie-Generator. Das Compliance LLM ist ein eigenes Sprachmodell für Text und Audio, das sicher in der EU gehostet wird. Die Academy bietet Online-Schulungen für Geschäftsführung und Mitarbeiter. Abgerundet wird das Ganze durch die Integration in bestehende Kundenprozesse wie Jira und eine sichere Kommunikationslösung mit Chat, Video und einem KI-Assistenten für automatische Besprechungsnotizen.',
|
||||
answer_en: 'Our platform consists of twelve modules that customers can use individually or as a complete package. The core is the code security module with SAST, DAST, SBOM analysis and continuous pentesting on every code change. Then there is the CE software risk assessment that manufacturers need for CE marking their products. For ongoing compliance documentation, we automatically generate records of processing activities, technical and organizational measures, data protection impact assessments and retention schedules. The audit manager handles major and minor deviations after audits completely end-to-end with deadlines, tickets and escalation. Beyond that, we offer modules for data subject rights, consent management, incident response for data breaches and a cookie generator. The compliance LLM is a dedicated language model for text and audio, securely hosted in the EU. The academy provides online training for management and employees. Everything is rounded off by integration into existing customer processes like Jira and a secure communication solution with chat, video and an AI assistant for automatic meeting notes.',
|
||||
answer_de: 'Unsere Plattform besteht aus zwölf Modulen, die Kunden einzeln oder als Gesamtpaket nutzen können. Den Kern bildet das Code-Security-Modul mit SAST, DAST, SBOM-Analysen und kontinuierlichem Pentesting bei jeder Code-Änderung. Dazu kommt die CE-Software-Risikobeurteilung, die Hersteller für die CE-Kennzeichnung ihrer Produkte brauchen. Für die laufende Compliance-Dokumentation erstellen wir automatisch Verarbeitungsverzeichnisse, technisch-organisatorische Maßnahmen, Datenschutz-Folgenabschätzungen und Löschfristen. Der Audit Manager verwaltet Haupt- und Nebenabweichungen nach Audits vollständig End-to-End mit Stichtagen, Tickets und Eskalation. Darüber hinaus bieten wir Module für Betroffenenrechte, Einwilligungsmanagement, Notfallpläne bei Datenschutzvorfällen und einen Cookie-Generator. Das Compliance LLM ist ein eigenes Sprachmodell für Text und Audio, das sicher in der EU gehostet wird. Die Academy bietet Online-Schulungen für Geschäftsführung und Mitarbeiter. Abgerundet wird das Ganze durch die Integration in bestehende Kundenprozesse wie Jira, GitLab, Linear oder Gitea und eine sichere Kommunikationslösung mit Chat, Video und einem KI-Assistenten für automatische Besprechungsnotizen.',
|
||||
answer_en: 'Our platform consists of twelve modules that customers can use individually or as a complete package. The core is the code security module with SAST, DAST, SBOM analysis and continuous pentesting on every code change. Then there is the CE software risk assessment that manufacturers need for CE marking their products. For ongoing compliance documentation, we automatically generate records of processing activities, technical and organizational measures, data protection impact assessments and retention schedules. The audit manager handles major and minor deviations after audits completely end-to-end with deadlines, tickets and escalation. Beyond that, we offer modules for data subject rights, consent management, incident response for data breaches and a cookie generator. The compliance LLM is a dedicated language model for text and audio, securely hosted in the EU. The academy provides online training for management and employees. Everything is rounded off by integration into existing customer processes like Jira, GitLab, Linear or Gitea and a secure communication solution with chat, video and an AI assistant for automatic meeting notes.',
|
||||
goto_slide: 'product',
|
||||
priority: 9,
|
||||
},
|
||||
|
||||
@@ -92,8 +92,8 @@ export const PRESENTER_SCRIPT: SlideScript[] = [
|
||||
duration: 70,
|
||||
paragraphs: [
|
||||
{
|
||||
text_de: 'Unsere Lösung: Kontinuierliche Software-Compliance statt jährlicher Stichproben. SAST, DAST, SBOM und Pentesting bei jeder Code-Änderung. Findings landen direkt als Jira-Tickets mit konkreten Implementierungsvorschlägen.',
|
||||
text_en: 'Our solution: Continuous software compliance instead of annual spot checks. SAST, DAST, SBOM and pentesting on every code change. Findings land directly as Jira tickets with concrete implementation suggestions.',
|
||||
text_de: 'Unsere Lösung: Kontinuierliche Software-Compliance statt jährlicher Stichproben. SAST, DAST, SBOM und Pentesting bei jeder Code-Änderung. Findings landen direkt als Tickets im Issue-Tracker deiner Wahl mit konkreten Implementierungsvorschlägen.',
|
||||
text_en: 'Our solution: Continuous software compliance instead of annual spot checks. SAST, DAST, SBOM and pentesting on every code change. Findings land directly as tickets in the issue tracker of your choice with concrete implementation suggestions.',
|
||||
pause_after: 2000,
|
||||
},
|
||||
{
|
||||
@@ -102,8 +102,8 @@ export const PRESENTER_SCRIPT: SlideScript[] = [
|
||||
pause_after: 2500,
|
||||
},
|
||||
{
|
||||
text_de: 'Die Plattform läuft auf einer BSI-zertifizierten Cloud in Deutschland oder OVH in Frankreich. Jitsi für Video, Matrix für Chat, KI-Aufgabenerstellung aus Audiomitschnitten direkt in Kundensysteme. Keine US-SaaS im Source Code.',
|
||||
text_en: 'The platform runs on a BSI-certified cloud in Germany or OVH in France. Jitsi for video, Matrix for chat, AI task creation from audio recordings directly into customer systems. No US SaaS in source code.',
|
||||
text_de: 'Die Plattform läuft auf einer BSI-zertifizierten Cloud in Deutschland oder Frankreich. Live-Support über Jitsi (Video) und Matrix (Chat). Keine US-SaaS im Source Code.',
|
||||
text_en: 'The platform runs on a BSI-certified cloud in Germany or France. Live support via Jitsi (video) and Matrix (chat). No US SaaS in source code.',
|
||||
pause_after: 2500,
|
||||
},
|
||||
{
|
||||
@@ -152,8 +152,8 @@ export const PRESENTER_SCRIPT: SlideScript[] = [
|
||||
pause_after: 2500,
|
||||
},
|
||||
{
|
||||
text_de: 'Die Plattform läuft standardmäßig in der Cloud — BSI-zertifiziert in Deutschland oder OVH in Frankreich. Für Kleinstunternehmen unter 10 Mitarbeitern bieten wir optional einen vorkonfigurierten Mac Mini für absolute Privacy.',
|
||||
text_en: 'The platform runs by default in the cloud — BSI-certified in Germany or OVH in France. For micro businesses under 10 employees, we optionally offer a pre-configured Mac Mini for absolute privacy.',
|
||||
text_de: 'Die Plattform läuft standardmäßig in der Cloud — BSI-zertifiziert in Deutschland oder Frankreich. Für Kleinstunternehmen unter 10 Mitarbeitern bieten wir optional einen vorkonfigurierten Mac Mini oder Mac Studio fuer absolute Privacy.',
|
||||
text_en: 'The platform runs by default in the cloud — BSI-certified in Germany or France. For micro businesses under 10 employees, we optionally offer a pre-configured Mac Mini or Mac Studio for absolute privacy.',
|
||||
pause_after: 2000,
|
||||
},
|
||||
],
|
||||
@@ -167,8 +167,8 @@ export const PRESENTER_SCRIPT: SlideScript[] = [
|
||||
duration: 50,
|
||||
paragraphs: [
|
||||
{
|
||||
text_de: 'Schritt eins: Cloud-Vertrag abschließen. BSI-Cloud in Deutschland oder OVH in Frankreich — fixe oder flexible Kosten, keine US-Anbieter.',
|
||||
text_en: 'Step one: sign a cloud contract. BSI cloud in Germany or OVH in France — fixed or flexible costs, no US providers.',
|
||||
text_de: 'Schritt eins: Cloud-Vertrag abschließen. BSI-Cloud in Deutschland oder Frankreich — fixe oder flexible Kosten, keine US-Anbieter.',
|
||||
text_en: 'Step one: sign a cloud contract. BSI cloud in Germany or France — fixed or flexible costs, no US providers.',
|
||||
pause_after: 1500,
|
||||
},
|
||||
{
|
||||
@@ -237,8 +237,8 @@ export const PRESENTER_SCRIPT: SlideScript[] = [
|
||||
pause_after: 2500,
|
||||
},
|
||||
{
|
||||
text_de: 'Die Unit Economics: Bruttomarge über 80 Prozent. Cloud-native auf SysEleven, OVH und Hetzner — deutlich günstiger als AWS oder Azure. Mitarbeiterbasiertes Pricing, modular wählbar.',
|
||||
text_en: 'The unit economics: gross margin above 80 percent. Cloud-native on SysEleven, OVH and Hetzner — significantly cheaper than AWS or Azure. Employee-based pricing, modular choice.',
|
||||
text_de: 'Die Unit Economics: Bruttomarge über 80 Prozent. Cloud-native auf SysEleven und Hetzner — deutlich günstiger als AWS oder Azure. Mitarbeiterbasiertes Pricing, modular wählbar.',
|
||||
text_en: 'The unit economics: gross margin above 80 percent. Cloud-native on SysEleven and Hetzner — significantly cheaper than AWS or Azure. Employee-based pricing, modular choice.',
|
||||
pause_after: 1500,
|
||||
},
|
||||
],
|
||||
@@ -342,8 +342,8 @@ export const PRESENTER_SCRIPT: SlideScript[] = [
|
||||
pause_after: 2000,
|
||||
},
|
||||
{
|
||||
text_de: 'Infrastrukturkosten bleiben niedrig dank europäischer Provider. SysEleven, OVH und Hetzner kosten einen Bruchteil von AWS. Break-Even erreichen wir voraussichtlich Ende 2028.',
|
||||
text_en: 'Infrastructure costs remain low thanks to European providers. SysEleven, OVH and Hetzner cost a fraction of AWS. We expect to reach break-even by end of 2028.',
|
||||
text_de: 'Infrastrukturkosten bleiben niedrig dank europäischer Provider. SysEleven und Hetzner kosten einen Bruchteil von AWS. Break-Even erreichen wir voraussichtlich Ende 2028.',
|
||||
text_en: 'Infrastructure costs remain low thanks to European providers. SysEleven and Hetzner cost a fraction of AWS. We expect to reach break-even by end of 2028.',
|
||||
pause_after: 2000,
|
||||
},
|
||||
],
|
||||
@@ -479,8 +479,8 @@ export const PRESENTER_SCRIPT: SlideScript[] = [
|
||||
pause_after: 2000,
|
||||
},
|
||||
{
|
||||
text_de: 'Infrastruktur: 100 Prozent EU-Cloud. PostgreSQL und Qdrant auf Hetzner, 120-Milliarden-Parameter-LLM auf OVH, 1000-Milliarden-Parameter-LLM auf SysEleven — BSI-zertifiziert. Keine US-Anbieter.',
|
||||
text_en: 'Infrastructure: 100 percent EU cloud. PostgreSQL and Qdrant on Hetzner, 120 billion parameter LLM on OVH, 1 trillion parameter LLM on SysEleven — BSI certified. No US providers.',
|
||||
text_de: 'Infrastruktur: 100 Prozent EU-Cloud. PostgreSQL und Qdrant auf Hetzner, LLMs auf SysEleven — BSI-zertifiziert. Keine US-Anbieter.',
|
||||
text_en: 'Infrastructure: 100 percent EU cloud. PostgreSQL and Qdrant on Hetzner, LLMs on SysEleven — BSI certified. No US providers.',
|
||||
pause_after: 1500,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -6,6 +6,7 @@ export const SLIDE_ORDER: SlideId[] = [
|
||||
'cover',
|
||||
'problem',
|
||||
'solution',
|
||||
'usp',
|
||||
'regulatory-landscape',
|
||||
'product',
|
||||
'how-it-works',
|
||||
@@ -29,6 +30,7 @@ export const SLIDE_ORDER: SlideId[] = [
|
||||
'annex-strategy',
|
||||
'annex-finanzplan',
|
||||
'annex-glossary',
|
||||
'legal-disclaimer',
|
||||
]
|
||||
|
||||
export const TOTAL_SLIDES = SLIDE_ORDER.length
|
||||
|
||||
@@ -227,6 +227,7 @@ export type SlideId =
|
||||
| 'cover'
|
||||
| 'problem'
|
||||
| 'solution'
|
||||
| 'usp'
|
||||
| 'regulatory-landscape'
|
||||
| 'product'
|
||||
| 'how-it-works'
|
||||
@@ -250,3 +251,4 @@ export type SlideId =
|
||||
| 'annex-strategy'
|
||||
| 'annex-finanzplan'
|
||||
| 'annex-glossary'
|
||||
| 'legal-disclaimer'
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
# BreakPilot Pitch MCP Server
|
||||
|
||||
MCP server that lets Claude Code directly manage pitch versions, invite investors, and assign versions — without touching the browser admin UI.
|
||||
|
||||
## What it does
|
||||
|
||||
11 tools exposed to Claude Code:
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `list_versions` | List all pitch versions with status + investor counts |
|
||||
| `create_version` | Create a draft (snapshot base tables or fork from parent) |
|
||||
| `get_version` | Get full version detail with all 12 data table snapshots |
|
||||
| `get_table_data` | Get one table's data (company, team, financials, market, etc.) |
|
||||
| `update_table_data` | Replace a table's data in a draft version |
|
||||
| `commit_version` | Lock a draft as immutable |
|
||||
| `fork_version` | Create new draft by copying an existing version |
|
||||
| `diff_versions` | Per-table diff between any two versions |
|
||||
| `list_investors` | List all investors with stats + version assignments |
|
||||
| `assign_version` | Assign a committed version to an investor |
|
||||
| `invite_investor` | Send magic-link email to a new investor |
|
||||
|
||||
All actions go through the existing admin API at `pitch.breakpilot.com`, so they show up in the audit log.
|
||||
|
||||
## Setup
|
||||
|
||||
### 1. Build
|
||||
|
||||
```bash
|
||||
cd pitch-deck/mcp-server
|
||||
npm install
|
||||
npm run build
|
||||
```
|
||||
|
||||
### 2. Get the admin secret
|
||||
|
||||
The `PITCH_ADMIN_SECRET` is stored in orca secrets on the server. SSH in and retrieve it:
|
||||
|
||||
```bash
|
||||
ssh breakpilot-infra-vm1
|
||||
cat ~/orca/services/breakpilot-dsms/secrets.json | grep PITCH_ADMIN_SECRET
|
||||
```
|
||||
|
||||
### 3. Configure
|
||||
|
||||
Edit `.mcp.json` in the **breakpilot-core** repo root (already created):
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"pitch-versions": {
|
||||
"command": "node",
|
||||
"args": ["pitch-deck/mcp-server/dist/index.js"],
|
||||
"env": {
|
||||
"PITCH_API_URL": "https://pitch.breakpilot.com",
|
||||
"PITCH_ADMIN_SECRET": "paste-your-secret-here"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> **Important:** `.mcp.json` contains a secret. It's already in `.gitignore` — never commit it.
|
||||
|
||||
### 4. Restart Claude Code
|
||||
|
||||
Exit and reopen Claude Code, or run `/mcp` to check it loaded:
|
||||
|
||||
```
|
||||
/mcp
|
||||
```
|
||||
|
||||
You should see `pitch-versions` listed with 11 tools.
|
||||
|
||||
## Usage examples
|
||||
|
||||
Just talk to Claude naturally:
|
||||
|
||||
- **"List all pitch versions"** → calls `list_versions`
|
||||
- **"Create a new version called 'Series A Aggressive'"** → calls `create_version`
|
||||
- **"Show me the company data from version X"** → calls `get_table_data`
|
||||
- **"Update the company tagline to 'AI-Powered Compliance' in version X"** → calls `update_table_data`
|
||||
- **"Commit version X"** → calls `commit_version`
|
||||
- **"Fork version X into a new draft called 'Conservative'"** → calls `fork_version`
|
||||
- **"Compare version X with version Y"** → calls `diff_versions`
|
||||
- **"Assign version X to investor jane@vc.com"** → calls `assign_version`
|
||||
- **"Invite john@fund.com from Big Fund"** → calls `invite_investor`
|
||||
|
||||
## Data tables
|
||||
|
||||
Each version stores 12 data tables as JSONB snapshots:
|
||||
|
||||
| Table | Content |
|
||||
|-------|---------|
|
||||
| `company` | Name, tagline (DE/EN), mission (DE/EN), website, HQ city |
|
||||
| `team` | Members with roles, bios, equity, expertise (all bilingual) |
|
||||
| `financials` | Year-by-year revenue, costs, MRR, ARR, customers, employees |
|
||||
| `market` | TAM/SAM/SOM with values, growth rates, sources |
|
||||
| `competitors` | Names, customer counts, pricing, strengths, weaknesses |
|
||||
| `features` | Feature comparison matrix (BreakPilot vs competitors) |
|
||||
| `milestones` | Timeline with dates, titles, descriptions, status (bilingual) |
|
||||
| `metrics` | Key metrics with labels (bilingual) and values |
|
||||
| `funding` | Round details, amount, instrument, use of funds breakdown |
|
||||
| `products` | Product tiers with pricing, LLM specs, features (bilingual) |
|
||||
| `fm_scenarios` | Financial model scenario names, colors, default flag |
|
||||
| `fm_assumptions` | Per-scenario assumptions (growth rate, ARPU, churn, etc.) |
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
Claude Code ←stdio→ MCP Server ←HTTP→ pitch.breakpilot.com/api/admin/*
|
||||
(local) (deployed on orca)
|
||||
```
|
||||
|
||||
The MCP server is a thin HTTP client. All auth, validation, and audit logging happens on the server side. The bearer token authenticates as a CLI admin actor.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**"PITCH_ADMIN_SECRET is required"** → The env var is missing in `.mcp.json`
|
||||
|
||||
**401 errors** → The secret is wrong or the pitch-deck container isn't running. Check: `curl -s -H "Authorization: Bearer YOUR_SECRET" https://pitch.breakpilot.com/api/admin/investors`
|
||||
|
||||
**MCP server not showing in `/mcp`** → Make sure you're in the `breakpilot-core` directory when you launch Claude Code (`.mcp.json` is project-scoped)
|
||||
Generated
+1173
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "breakpilot-pitch-mcp",
|
||||
"version": "1.0.0",
|
||||
"description": "MCP server for managing BreakPilot pitch versions via Claude Code",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"start": "node dist/index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.12.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.7.2",
|
||||
"@types/node": "^22.10.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
#!/usr/bin/env node
|
||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import { z } from "zod";
|
||||
|
||||
const API_URL = process.env.PITCH_API_URL || "https://pitch.breakpilot.com";
|
||||
const API_SECRET = process.env.PITCH_ADMIN_SECRET || "";
|
||||
|
||||
if (!API_SECRET) {
|
||||
console.error("PITCH_ADMIN_SECRET is required");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// --- HTTP client ---
|
||||
|
||||
async function api(
|
||||
method: string,
|
||||
path: string,
|
||||
body?: unknown
|
||||
): Promise<unknown> {
|
||||
const url = `${API_URL}${path}`;
|
||||
const res = await fetch(url, {
|
||||
method,
|
||||
headers: {
|
||||
Authorization: `Bearer ${API_SECRET}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
});
|
||||
|
||||
const text = await res.text();
|
||||
let data: unknown;
|
||||
try {
|
||||
data = JSON.parse(text);
|
||||
} catch {
|
||||
data = text;
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
const msg =
|
||||
typeof data === "object" && data && "error" in data
|
||||
? (data as { error: string }).error
|
||||
: `HTTP ${res.status}`;
|
||||
throw new Error(msg);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
const TABLE_NAMES = [
|
||||
"company",
|
||||
"team",
|
||||
"financials",
|
||||
"market",
|
||||
"competitors",
|
||||
"features",
|
||||
"milestones",
|
||||
"metrics",
|
||||
"funding",
|
||||
"products",
|
||||
"fm_scenarios",
|
||||
"fm_assumptions",
|
||||
] as const;
|
||||
|
||||
// --- MCP Server ---
|
||||
|
||||
const server = new McpServer({
|
||||
name: "breakpilot-pitch",
|
||||
version: "1.0.0",
|
||||
});
|
||||
|
||||
// 1. list_versions
|
||||
server.tool(
|
||||
"list_versions",
|
||||
"List all pitch versions with status, parent chain, and investor assignment counts",
|
||||
{},
|
||||
async () => {
|
||||
const data = await api("GET", "/api/admin/versions");
|
||||
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
||||
}
|
||||
);
|
||||
|
||||
// 2. create_version
|
||||
server.tool(
|
||||
"create_version",
|
||||
"Create a new draft version. Optionally fork from a parent version ID, otherwise snapshots current base tables.",
|
||||
{
|
||||
name: z.string().describe("Version name, e.g. 'Conservative Q4'"),
|
||||
description: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Optional description"),
|
||||
parent_id: z
|
||||
.string()
|
||||
.uuid()
|
||||
.optional()
|
||||
.describe("UUID of parent version to fork from. Omit to snapshot base tables."),
|
||||
},
|
||||
async ({ name, description, parent_id }) => {
|
||||
const data = await api("POST", "/api/admin/versions", {
|
||||
name,
|
||||
description,
|
||||
parent_id,
|
||||
});
|
||||
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
||||
}
|
||||
);
|
||||
|
||||
// 3. get_version
|
||||
server.tool(
|
||||
"get_version",
|
||||
"Get full version detail including all 12 data table snapshots",
|
||||
{
|
||||
version_id: z.string().uuid().describe("Version UUID"),
|
||||
},
|
||||
async ({ version_id }) => {
|
||||
const data = await api("GET", `/api/admin/versions/${version_id}`);
|
||||
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
||||
}
|
||||
);
|
||||
|
||||
// 4. get_table_data
|
||||
server.tool(
|
||||
"get_table_data",
|
||||
"Get a specific table's data for a version. Tables: company, team, financials, market, competitors, features, milestones, metrics, funding, products, fm_scenarios, fm_assumptions",
|
||||
{
|
||||
version_id: z.string().uuid().describe("Version UUID"),
|
||||
table_name: z
|
||||
.enum(TABLE_NAMES)
|
||||
.describe("Which data table to retrieve"),
|
||||
},
|
||||
async ({ version_id, table_name }) => {
|
||||
const data = await api(
|
||||
"GET",
|
||||
`/api/admin/versions/${version_id}/data/${table_name}`
|
||||
);
|
||||
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
||||
}
|
||||
);
|
||||
|
||||
// 5. update_table_data
|
||||
server.tool(
|
||||
"update_table_data",
|
||||
"Replace a table's data in a DRAFT version. Pass the full array of row objects. Single-record tables (company, funding) should still be wrapped in an array.",
|
||||
{
|
||||
version_id: z.string().uuid().describe("Version UUID (must be a draft)"),
|
||||
table_name: z.enum(TABLE_NAMES).describe("Which data table to update"),
|
||||
data: z
|
||||
.string()
|
||||
.describe(
|
||||
"JSON string of the new data — an array of row objects. Example for company: [{\"name\":\"BreakPilot\",\"tagline_en\":\"...\"}]"
|
||||
),
|
||||
},
|
||||
async ({ version_id, table_name, data: dataStr }) => {
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(dataStr);
|
||||
} catch {
|
||||
return {
|
||||
content: [{ type: "text", text: "Error: invalid JSON in data parameter" }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
const result = await api(
|
||||
"PUT",
|
||||
`/api/admin/versions/${version_id}/data/${table_name}`,
|
||||
{ data: parsed }
|
||||
);
|
||||
return {
|
||||
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
// 6. commit_version
|
||||
server.tool(
|
||||
"commit_version",
|
||||
"Commit a draft version, making it immutable and available for investor assignment",
|
||||
{
|
||||
version_id: z.string().uuid().describe("Draft version UUID to commit"),
|
||||
},
|
||||
async ({ version_id }) => {
|
||||
const data = await api(
|
||||
"POST",
|
||||
`/api/admin/versions/${version_id}/commit`
|
||||
);
|
||||
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
||||
}
|
||||
);
|
||||
|
||||
// 7. fork_version
|
||||
server.tool(
|
||||
"fork_version",
|
||||
"Create a new draft by forking an existing version (copies all data)",
|
||||
{
|
||||
version_id: z.string().uuid().describe("Version UUID to fork from"),
|
||||
name: z.string().describe("Name for the new forked draft"),
|
||||
},
|
||||
async ({ version_id, name }) => {
|
||||
const data = await api(
|
||||
"POST",
|
||||
`/api/admin/versions/${version_id}/fork`,
|
||||
{ name }
|
||||
);
|
||||
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
||||
}
|
||||
);
|
||||
|
||||
// 8. diff_versions
|
||||
server.tool(
|
||||
"diff_versions",
|
||||
"Compare two versions and see per-table diffs (added/removed/changed rows and fields)",
|
||||
{
|
||||
version_a: z.string().uuid().describe("First version UUID"),
|
||||
version_b: z.string().uuid().describe("Second version UUID"),
|
||||
},
|
||||
async ({ version_a, version_b }) => {
|
||||
const data = await api(
|
||||
"GET",
|
||||
`/api/admin/versions/${version_a}/diff/${version_b}`
|
||||
);
|
||||
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
||||
}
|
||||
);
|
||||
|
||||
// 9. list_investors
|
||||
server.tool(
|
||||
"list_investors",
|
||||
"List all investors with their login stats, assigned version, and activity",
|
||||
{},
|
||||
async () => {
|
||||
const data = await api("GET", "/api/admin/investors");
|
||||
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
||||
}
|
||||
);
|
||||
|
||||
// 10. assign_version
|
||||
server.tool(
|
||||
"assign_version",
|
||||
"Assign a committed version to an investor (determines what pitch data they see). Pass null to reset to default base tables.",
|
||||
{
|
||||
investor_id: z.string().uuid().describe("Investor UUID"),
|
||||
version_id: z
|
||||
.string()
|
||||
.uuid()
|
||||
.nullable()
|
||||
.describe("Committed version UUID to assign, or null for default"),
|
||||
},
|
||||
async ({ investor_id, version_id }) => {
|
||||
const data = await api("PATCH", `/api/admin/investors/${investor_id}`, {
|
||||
assigned_version_id: version_id,
|
||||
});
|
||||
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
||||
}
|
||||
);
|
||||
|
||||
// 11. invite_investor
|
||||
server.tool(
|
||||
"invite_investor",
|
||||
"Invite a new investor by email — sends a magic link for passwordless access to the pitch deck",
|
||||
{
|
||||
email: z.string().email().describe("Investor email address"),
|
||||
name: z.string().optional().describe("Investor name"),
|
||||
company: z.string().optional().describe("Investor company"),
|
||||
},
|
||||
async ({ email, name, company }) => {
|
||||
const data = await api("POST", "/api/admin/invite", {
|
||||
email,
|
||||
name,
|
||||
company,
|
||||
});
|
||||
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
||||
}
|
||||
);
|
||||
|
||||
// --- Start ---
|
||||
|
||||
async function main() {
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error("MCP server error:", err);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "Node16",
|
||||
"moduleResolution": "Node16",
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"declaration": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
@@ -12,6 +12,7 @@ const PUBLIC_PATHS = [
|
||||
'/manifest.json',
|
||||
'/sw.js',
|
||||
'/icons',
|
||||
'/screenshots', // SDK demo screenshots: public marketing assets. Must bypass auth because the next/image optimizer fetches them server-side without investor cookies.
|
||||
'/favicon.ico',
|
||||
]
|
||||
|
||||
@@ -67,6 +68,17 @@ export async function middleware(request: NextRequest) {
|
||||
}
|
||||
}
|
||||
|
||||
// ----- Allow admins to access investor routes (e.g. /api/chat in preview) -----
|
||||
const adminFallback = request.cookies.get('pitch_admin_session')?.value
|
||||
if (adminFallback && secret) {
|
||||
try {
|
||||
await jwtVerify(adminFallback, new TextEncoder().encode(secret), { audience: ADMIN_AUDIENCE })
|
||||
return NextResponse.next()
|
||||
} catch {
|
||||
// Invalid admin token, fall through to investor auth
|
||||
}
|
||||
}
|
||||
|
||||
// ----- Investor-gated routes (everything else) -----
|
||||
const token = request.cookies.get('pitch_session')?.value
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 952 KiB |
Reference in New Issue
Block a user