redesign ArchitectureSlide with island map aesthetic + turbopack dev
All checks were successful
Build pitch-deck / build-push-deploy (push) Successful in 1m36s
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-consent (push) Successful in 41s
CI / test-python-voice (push) Successful in 37s
CI / test-bqas (push) Successful in 33s

- Replace grid/SVG-line layout with archipelago map: organic island blobs,
  quadratic bezier sea routes, circular map-marker nodes
- Fix SVG distortion: all strokes use vectorEffect=non-scaling-stroke
- No more preserveAspectRatio=none diagonal-line warping
- LiteLLM hub gets spinning ring + ripple pulse on active
- Ocean background with per-tier radial glows, dot grid, zone labels
- Switch dev server to --turbopack for faster HMR

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Sharang Parnerkar
2026-04-20 22:25:13 +02:00
parent 855e764911
commit 497be5fac9
5 changed files with 582 additions and 104 deletions

View File

@@ -78,6 +78,16 @@ export async function GET() {
} }
} catch (error) { } catch (error) {
console.error('Database query error:', error) console.error('Database query error:', error)
// Return minimal stub in dev so the pitch renders without a DB connection
if (process.env.NODE_ENV === 'development') {
return NextResponse.json({
company: { name: 'BreakPilot', tagline: '[dev mode — no DB]' },
team: [], financials: [], market: [], competitors: [],
features: [], milestones: [], metrics: [],
funding: { instrument: 'Wandeldarlehen', amount: 500000, valuation_cap: 3000000, currency: 'EUR' },
products: [],
})
}
return NextResponse.json({ error: 'Failed to load pitch data' }, { status: 500 }) return NextResponse.json({ error: 'Failed to load pitch data' }, { status: 500 })
} }
} }

View File

@@ -1,129 +1,588 @@
'use client' 'use client'
import { useState } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
import { Language } from '@/lib/types' import { Language } from '@/lib/types'
import { t } from '@/lib/i18n' import { t } from '@/lib/i18n'
import GradientText from '../ui/GradientText' import GradientText from '../ui/GradientText'
import FadeInView from '../ui/FadeInView' import FadeInView from '../ui/FadeInView'
import GlassCard from '../ui/GlassCard' import {
import { Server, Cpu, Shield, Database, Globe, Lock, Layers, Workflow } from 'lucide-react' Shield, Brain, ScanLine, Zap, Cpu, Globe, Cloud,
X, Users, Lock, Server, Network, ChevronRight, Layers,
} from 'lucide-react'
interface ArchitectureSlideProps { interface ArchitectureSlideProps {
lang: Language lang: Language
} }
type NodeId = 'breakpilot' | 'certifai' | 'compliance-scanner' | 'litellm' | 'ollama' | 'claude' | 'openai'
interface NodeDef {
id: NodeId
icon: React.ElementType
title: string
subtitle: string
color: string
twColor: string
twBorder: string
twBg: string
twDot: string
cx: number
cy: number
tier: 'product' | 'proxy' | 'inference'
tech: string[]
services: { name: string; desc: string }[]
badge?: string
}
const CONNECTIONS: [NodeId, NodeId][] = [
['breakpilot', 'litellm'],
['certifai', 'litellm'],
['compliance-scanner', 'litellm'],
['litellm', 'ollama'],
['litellm', 'claude'],
['litellm', 'openai'],
]
// Quadratic bezier paths in 0-100 viewBox space
const ROUTES: Record<string, string> = {
'breakpilot-litellm': 'M 15 22 Q 26 43 50 50',
'certifai-litellm': 'M 50 18 Q 50 34 50 50',
'compliance-scanner-litellm': 'M 85 22 Q 74 43 50 50',
'litellm-ollama': 'M 50 50 Q 39 64 15 80',
'litellm-claude': 'M 50 50 Q 50 65 50 80',
'litellm-openai': 'M 50 50 Q 61 64 85 80',
}
function getNodes(de: boolean): NodeDef[] {
return [
{
id: 'breakpilot',
icon: Shield,
title: 'BreakPilot',
subtitle: de ? 'Compliance & Bildung' : 'Compliance & Education',
color: '#818cf8', twColor: 'text-indigo-400',
twBorder: 'border-indigo-500/50', twBg: 'bg-indigo-500/10', twDot: 'bg-indigo-400',
cx: 15, cy: 22, tier: 'product',
badge: de ? 'Kernprodukt' : 'Core Product',
tech: ['Next.js 15', 'FastAPI', 'Go/Gin', 'PostgreSQL', 'Qdrant', 'Valkey'],
services: [
{ name: de ? 'DSGVO / AI Act / NIS2' : 'GDPR / AI Act / NIS2', desc: de ? '70k+ auditierbare Controls' : '70k+ auditable controls' },
{ name: 'RAG Pipeline', desc: de ? '75+ Rechtsquellen, semantische Suche' : '75+ legal sources, semantic search' },
{ name: de ? 'Lehrer Plattform' : 'Lehrer Platform', desc: de ? 'KI-Unterrichtsassistent' : 'AI teaching assistant' },
{ name: 'Control Pipeline', desc: de ? 'Gesetzestextanalyse via Claude' : 'Legal text analysis via Claude' },
],
},
{
id: 'certifai',
icon: Brain,
title: 'CERTifAI',
subtitle: de ? 'GenAI Infrastruktur' : 'GenAI Infrastructure',
color: '#c084fc', twColor: 'text-purple-400',
twBorder: 'border-purple-500/50', twBg: 'bg-purple-500/10', twDot: 'bg-purple-400',
cx: 50, cy: 18, tier: 'product',
badge: de ? 'DSGVO-konform' : 'GDPR-Compliant',
tech: ['Rust', 'Dioxus', 'MongoDB', 'Keycloak', 'SearXNG'],
services: [
{ name: 'LiteLLM Dashboard', desc: de ? 'Modellverwaltung & Kosten' : 'Model mgmt & spend tracking' },
{ name: 'LibreChat + SSO', desc: de ? 'Mandanten-Chat' : 'Tenant chat with Keycloak' },
{ name: 'LangGraph Agents', desc: de ? 'Agent-Orchestrierung' : 'Agent orchestration' },
{ name: 'MCP Hub', desc: de ? 'Tool-Integration für KI' : 'Tool integration for AI clients' },
],
},
{
id: 'compliance-scanner',
icon: ScanLine,
title: 'Compliance Scanner',
subtitle: de ? 'Autonome Sicherheit' : 'Autonomous Security',
color: '#34d399', twColor: 'text-emerald-400',
twBorder: 'border-emerald-500/50', twBg: 'bg-emerald-500/10', twDot: 'bg-emerald-400',
cx: 85, cy: 22, tier: 'product',
badge: 'Rust + AI',
tech: ['Rust', 'Axum', 'MongoDB', 'Semgrep', 'Gitleaks', 'Syft'],
services: [
{ name: 'SAST / SBOM / CVE', desc: de ? 'Vollautomatische Pipeline' : 'Fully automated pipeline' },
{ name: de ? 'KI-Triage' : 'AI Triage', desc: de ? 'LLM filtert False Positives' : 'LLM filters false positives' },
{ name: de ? 'KI-Pentest' : 'AI Pentest', desc: de ? 'Autonome Angriffsketten' : 'Autonomous attack chains' },
{ name: 'MCP Server', desc: de ? 'Live-Findings für AI-Tools' : 'Live findings for AI tools' },
],
},
{
id: 'litellm',
icon: Zap,
title: 'LiteLLM Proxy',
subtitle: de ? 'Zentrale KI-Infrastruktur' : 'Central AI Proxy',
color: '#fbbf24', twColor: 'text-amber-400',
twBorder: 'border-amber-500/60', twBg: 'bg-amber-500/10', twDot: 'bg-amber-400',
cx: 50, cy: 50, tier: 'proxy',
badge: 'Hub',
tech: ['OpenAI-compatible API', 'Bearer Auth', 'Rate Limiting', 'Spend Tracking'],
services: [
{ name: de ? 'Multi-Provider' : 'Multi-Provider', desc: 'Ollama · Claude · OpenAI · HuggingFace' },
{ name: de ? 'Namespace-Isolierung' : 'Namespace Isolation', desc: de ? 'Mandantentrennung per API-Key' : 'Tenant isolation per API key' },
{ name: de ? 'Kosten-Tracking' : 'Cost Tracking', desc: de ? 'Token-Verbrauch & USD-Kosten' : 'Token usage & USD costs' },
{ name: de ? 'Failover-Routing' : 'Failover Routing', desc: de ? 'Automatisches Fallback' : 'Automatic fallback routing' },
],
},
{
id: 'ollama',
icon: Cpu,
title: 'Ollama',
subtitle: de ? 'Lokale Inferenz' : 'Local Inference',
color: '#60a5fa', twColor: 'text-blue-400',
twBorder: 'border-blue-500/50', twBg: 'bg-blue-500/10', twDot: 'bg-blue-400',
cx: 15, cy: 80, tier: 'inference',
badge: de ? 'On-Premise' : 'On-Premise',
tech: ['Qwen3-Coder-30B', 'Qwen3-32b', 'bge-m3', 'GPU-optimized'],
services: [
{ name: de ? 'Vollständig lokal' : 'Fully local', desc: de ? 'Daten verlassen nie den Server' : 'Data never leaves the server' },
{ name: de ? 'Air-Gap fähig' : 'Air-Gap Capable', desc: de ? 'Kein Internet erforderlich' : 'No internet required' },
],
},
{
id: 'claude',
icon: Globe,
title: 'Anthropic Claude',
subtitle: de ? 'Externe Inferenz' : 'External Inference',
color: '#fb7185', twColor: 'text-rose-400',
twBorder: 'border-rose-500/50', twBg: 'bg-rose-500/10', twDot: 'bg-rose-400',
cx: 50, cy: 80, tier: 'inference',
badge: de ? 'Optional' : 'Optional',
tech: ['claude-sonnet-4-6', 'claude-haiku-4-5', 'Direct API'],
services: [
{ name: 'Control Pipeline', desc: de ? 'Gesetzestextanalyse & Control-Gen.' : 'Legal text analysis & control gen.' },
{ name: 'Pitch Deck Chatbot', desc: de ? 'Investor-FAQ' : 'Investor FAQ' },
],
},
{
id: 'openai',
icon: Cloud,
title: de ? 'OpenAI / Weitere' : 'OpenAI / Others',
subtitle: de ? 'Cloud-Modelle' : 'Cloud Models',
color: '#2dd4bf', twColor: 'text-teal-400',
twBorder: 'border-teal-500/50', twBg: 'bg-teal-500/10', twDot: 'bg-teal-400',
cx: 85, cy: 80, tier: 'inference',
badge: de ? 'Optional' : 'Optional',
tech: ['gpt-oss-120b', 'HuggingFace', 'text-embedding-3-small'],
services: [
{ name: de ? 'Erweiterbar' : 'Extensible', desc: de ? 'Jeder OpenAI-kompatibler Anbieter' : 'Any OpenAI-compatible provider' },
],
},
]
}
function SeaRoute({ d, color, active }: { d: string; color: string; active: boolean }) {
return (
<g>
<path
d={d}
fill="none"
stroke={active ? color : '#ffffff'}
strokeWidth={active ? 1.5 : 0.6}
strokeOpacity={active ? 0.5 : 0.09}
vectorEffect="non-scaling-stroke"
/>
{active && (
<motion.path
d={d}
fill="none"
stroke={color}
strokeWidth={2.5}
strokeLinecap="round"
strokeDasharray="3 22"
strokeOpacity={0.9}
vectorEffect="non-scaling-stroke"
initial={{ strokeDashoffset: 0 }}
animate={{ strokeDashoffset: -25 }}
transition={{ duration: 1.4, repeat: Infinity, ease: 'linear' }}
/>
)}
</g>
)
}
function MapMarker({ node, active, onClick }: { node: NodeDef; active: boolean; onClick: () => void }) {
const Icon = node.icon
const isHub = node.id === 'litellm'
const iconSize = isHub ? 'w-[52px] h-[52px]' : 'w-[40px] h-[40px]'
return (
<motion.button
onClick={onClick}
whileHover={{ scale: 1.1, transition: { duration: 0.14 } }}
whileTap={{ scale: 0.93 }}
className="absolute flex flex-col items-center text-center focus:outline-none"
style={{ left: `${node.cx}%`, top: `${node.cy}%`, transform: 'translate(-50%, -50%)' }}
>
<div className="relative flex items-center justify-center">
{/* Ripple pulse when active */}
{active && (
<motion.div
className="absolute rounded-full pointer-events-none"
style={{
width: isHub ? 88 : 66,
height: isHub ? 88 : 66,
border: `1px solid ${node.color}`,
}}
initial={{ scale: 0.75, opacity: 0.7 }}
animate={{ scale: 1.55, opacity: 0 }}
transition={{ duration: 2, repeat: Infinity, ease: 'easeOut' }}
/>
)}
{/* Icon circle */}
<div
className={`relative flex items-center justify-center rounded-full transition-all duration-300 ${iconSize}`}
style={{
background: active
? `radial-gradient(circle at 38% 32%, ${node.color}38, ${node.color}14)`
: `${node.color}0a`,
border: `${active ? 2 : 1.5}px solid ${active ? node.color : node.color + '30'}`,
boxShadow: active
? `0 0 22px ${node.color}55, 0 0 6px ${node.color}28, inset 0 1px 0 ${node.color}30`
: 'none',
}}
>
<Icon
className={`transition-colors duration-300 ${isHub ? 'w-[20px] h-[20px]' : 'w-[14px] h-[14px]'} ${
active ? node.twColor : 'text-white/30'
}`}
/>
{/* Spinning dashed ring on active hub */}
{isHub && active && (
<motion.div
className="absolute rounded-full pointer-events-none"
style={{
inset: -6,
border: `1px dashed ${node.color}55`,
borderRadius: '50%',
}}
animate={{ rotate: 360 }}
transition={{ duration: 10, repeat: Infinity, ease: 'linear' }}
/>
)}
</div>
</div>
{/* Label */}
<div className="mt-1.5" style={{ maxWidth: isHub ? 96 : 80 }}>
<p className={`text-[9.5px] font-semibold leading-tight transition-colors duration-200 ${
active ? 'text-white' : 'text-white/48'
}`}>
{node.title}
</p>
{(active || isHub) && (
<p className={`text-[7.5px] mt-[2px] leading-snug transition-colors duration-200 ${
active ? node.twColor : 'text-white/28'
}`} style={{ opacity: active ? 0.85 : 1 }}>
{node.subtitle}
</p>
)}
{node.badge && (
<span className={`inline-block mt-[3px] text-[7px] px-[6px] py-[1px] rounded-full border transition-all duration-200 ${
active ? `${node.twBorder} ${node.twColor}` : 'border-white/10 text-white/22'
}`}>
{node.badge}
</span>
)}
</div>
</motion.button>
)
}
export default function ArchitectureSlide({ lang }: ArchitectureSlideProps) { export default function ArchitectureSlide({ lang }: ArchitectureSlideProps) {
const i = t(lang) const i = t(lang)
const de = lang === 'de' const de = lang === 'de'
const nodes = getNodes(de)
const layers = [ const [activeId, setActiveId] = useState<NodeId | null>(null)
{ const active = nodes.find(n => n.id === activeId) ?? null
icon: Server,
color: 'text-indigo-400',
bg: 'bg-indigo-500/10 border-indigo-500/20',
title: de ? 'Hardware-Schicht' : 'Hardware Layer',
items: [
{ label: 'ComplAI Mini', desc: de ? 'Mac Mini M4 (geplant, optional)' : 'Mac Mini M4 (planned, optional)' },
{ label: 'ComplAI Studio', desc: de ? 'Mac Studio M4 Max (geplant, optional)' : 'Mac Studio M4 Max (planned, optional)' },
{ label: 'ComplAI Cloud', desc: de ? 'BSI-zertifizierte Cloud in Deutschland' : 'BSI-certified cloud in Germany' },
],
},
{
icon: Cpu,
color: 'text-purple-400',
bg: 'bg-purple-500/10 border-purple-500/20',
title: de ? 'KI-Engine' : 'AI Engine',
items: [
{ label: 'Ollama Runtime', desc: de ? 'Lokale LLM-Inferenz, GPU-optimiert' : 'Local LLM inference, GPU-optimized' },
{ label: 'RAG Pipeline', desc: de ? 'Vektorsuche mit Compliance-Wissensbasis' : 'Vector search with compliance knowledge base' },
{ label: 'Agent Framework', desc: de ? 'Autonome Compliance-Agenten (Audit, Monitoring, Reporting)' : 'Autonomous compliance agents (Audit, Monitoring, Reporting)' },
],
},
{
icon: Shield,
color: 'text-emerald-400',
bg: 'bg-emerald-500/10 border-emerald-500/20',
title: de ? 'Compliance-Module' : 'Compliance Modules',
items: [
{ label: 'DSGVO Engine', desc: de ? 'VVT, DSFA, Betroffenenrechte, Löschkonzept' : 'RoPA, DPIA, Data Subject Rights, Deletion Concept' },
{ label: 'AI Act Module', desc: de ? 'Risikoklassifizierung, Konformitätsbewertung, Dokumentation' : 'Risk Classification, Conformity Assessment, Documentation' },
{ label: 'NIS2 Module', desc: de ? 'Cybersecurity-Policies, Incident Response, Meldewege' : 'Cybersecurity Policies, Incident Response, Reporting Chains' },
],
},
{
icon: Layers,
color: 'text-blue-400',
bg: 'bg-blue-500/10 border-blue-500/20',
title: de ? 'Plattform-Services' : 'Platform Services',
items: [
{ label: de ? 'Admin-Dashboard' : 'Admin Dashboard', desc: 'Next.js · ' + (de ? 'Mandantenfähig · Rollenbasiert' : 'Multi-Tenant · Role-Based') },
{ label: 'SDK API', desc: 'Go/Gin · REST · ' + (de ? 'Tenant-isoliert' : 'Tenant-Isolated') },
{ label: 'DevSecOps Suite', desc: 'Semgrep · Trivy · Gitleaks · CycloneDX SBOM' },
],
},
]
const securityFeatures = [ function toggle(id: NodeId) {
{ icon: Lock, label: de ? 'Zero-Trust Architektur' : 'Zero-Trust Architecture' }, setActiveId(prev => (prev === id ? null : id))
{ icon: Database, label: de ? 'Daten verlassen nie BSI-zertifizierte Server in DE' : 'Data Never Leaves BSI-Certified Servers in DE' }, }
{ icon: Globe, label: de ? '100% EU-Cloud · Keine US-Anbieter' : '100% EU Cloud · No US Providers' },
{ icon: Workflow, label: de ? 'Air-Gap fähig' : 'Air-Gap Capable' }, const tenants = de
] ? ['Mandant A', 'Mandant B', 'Mandant C', 'Mandant N…']
: ['Namespace A', 'Namespace B', 'Namespace C', 'Namespace N…']
return ( return (
<div> <div className="space-y-3">
<FadeInView className="text-center mb-8"> {/* Header */}
<p className="text-xs font-mono text-indigo-400/60 uppercase tracking-widest mb-2"> <FadeInView className="text-center mb-3">
<p className="text-[10px] font-mono text-indigo-400/50 uppercase tracking-widest mb-1.5">
{de ? 'Anhang' : 'Appendix'} {de ? 'Anhang' : 'Appendix'}
</p> </p>
<h2 className="text-4xl md:text-5xl font-bold mb-3"> <h2 className="text-3xl md:text-4xl font-bold mb-1.5">
<GradientText>{i.annex.architecture.title}</GradientText> <GradientText>{i.annex.architecture.title}</GradientText>
</h2> </h2>
<p className="text-lg text-white/50 max-w-2xl mx-auto">{i.annex.architecture.subtitle}</p> <p className="text-xs text-white/35">
{de ? 'Klicke auf eine Komponente für Details' : 'Click any component to explore'}
</p>
</FadeInView> </FadeInView>
{/* Architecture Layers */} <FadeInView delay={0.15} className="space-y-3">
<div className="grid md:grid-cols-2 gap-4 mb-6"> {/* Customer namespace strip */}
{layers.map((layer, idx) => { <div className="flex items-center gap-2 flex-wrap px-[4%]">
const Icon = layer.icon <Users className="w-3 h-3 text-white/25 flex-shrink-0" />
return ( <span className="text-[9px] font-mono text-white/25 uppercase tracking-widest mr-1">
<FadeInView key={idx} delay={0.2 + idx * 0.1}> {de ? 'Kundenmandanten' : 'Customer Namespaces'}
<div className={`border rounded-xl p-4 ${layer.bg}`}> </span>
<div className="flex items-center gap-2 mb-3"> {tenants.map(tn => (
<Icon className={`w-5 h-5 ${layer.color}`} /> <span
<h3 className="text-sm font-bold text-white">{layer.title}</h3> key={tn}
className="text-[9px] px-2 py-0.5 rounded-full border border-white/[0.08] bg-white/[0.03] text-white/35 font-mono"
>
{tn}
</span>
))}
<div className="flex-1 h-px bg-gradient-to-r from-white/10 to-transparent ml-1" />
</div> </div>
<div className="space-y-2">
{layer.items.map((item, iidx) => ( {/* ── THE MAP ──────────────────────────────────────────────────── */}
<div key={iidx} className="flex items-start gap-2"> <div className="relative w-full" style={{ height: '390px' }}>
<div className={`w-1.5 h-1.5 rounded-full mt-1.5 ${layer.color} bg-current opacity-50`} />
{/* Ocean background (clipped to rounded rect) */}
<div
className="absolute inset-0 rounded-xl overflow-hidden"
style={{
background: `
radial-gradient(ellipse 55% 35% at 50% 50%, rgba(251,191,36,0.055) 0%, transparent 68%),
radial-gradient(ellipse 65% 25% at 50% 20%, rgba(129,140,248,0.07) 0%, transparent 58%),
radial-gradient(ellipse 65% 22% at 50% 82%, rgba(96,165,250,0.04) 0%, transparent 58%),
linear-gradient(170deg, #030d1e 0%, #040f20 50%, #030c1c 100%)
`,
}}
>
{/* Fine dot grid */}
<div
className="absolute inset-0 opacity-[0.042]"
style={{
backgroundImage: 'radial-gradient(circle, #6b7280 1px, transparent 1px)',
backgroundSize: '22px 22px',
}}
/>
{/* Zone separator lines */}
{(['35%', '65%'] as const).map(y => (
<div
key={y}
className="absolute left-0 right-0 h-px"
style={{ top: y, background: 'linear-gradient(to right, transparent 0%, rgba(255,255,255,0.045) 20%, rgba(255,255,255,0.045) 80%, transparent 100%)' }}
/>
))}
</div>
{/* Zone labels */}
{[
{ y: '22%', label: de ? 'Produkte' : 'Products', clr: '#818cf8' },
{ y: '50%', label: de ? 'KI-Proxy' : 'AI Proxy', clr: '#fbbf24' },
{ y: '80%', label: de ? 'Inferenz' : 'Inference', clr: '#60a5fa' },
].map(({ y, label, clr }) => (
<div
key={label}
className="absolute left-3 flex items-center gap-1.5 select-none pointer-events-none"
style={{ top: y, transform: 'translateY(-50%)' }}
>
<div className="w-2.5 h-px" style={{ background: clr, opacity: 0.28 }} />
<span
className="text-[7px] font-mono tracking-[0.16em] uppercase"
style={{ color: clr, opacity: 0.38 }}
>
{label}
</span>
</div>
))}
{/* SVG: island territories + sea routes */}
<svg
className="absolute inset-0 w-full h-full pointer-events-none"
viewBox="0 0 100 100"
preserveAspectRatio="none"
>
{/* Wide zone territory fills */}
<ellipse cx="50" cy="18" rx="43" ry="11" fill="#818cf8" fillOpacity="0.025" />
<ellipse cx="50" cy="50" rx="13" ry="8" fill="#fbbf24" fillOpacity="0.04" />
<ellipse cx="50" cy="80" rx="43" ry="10" fill="#60a5fa" fillOpacity="0.02" />
{/* Per-node island blobs (outer contour + fill) */}
{nodes.map(node => {
const isHub = node.id === 'litellm'
const isActive = activeId === node.id
const rx = isHub ? 10 : 7
const ry = isHub ? 6 : 3.8
const rxOuter = isHub ? 14.5 : 10
const ryOuter = isHub ? 8.5 : 5.5
return (
<g key={`island-${node.id}`}>
{/* Outer contour ring */}
<ellipse
cx={node.cx} cy={node.cy}
rx={rxOuter} ry={ryOuter}
fill="none"
stroke={node.color}
strokeWidth={0.4}
vectorEffect="non-scaling-stroke"
strokeDasharray="1.5 4"
strokeOpacity={isActive ? 0.2 : 0.06}
/>
{/* Inner island fill */}
<ellipse
cx={node.cx} cy={node.cy}
rx={rx} ry={ry}
fill={node.color}
fillOpacity={isActive ? 0.14 : 0.042}
stroke={node.color}
strokeWidth={0.5}
vectorEffect="non-scaling-stroke"
strokeOpacity={isActive ? 0.38 : 0.1}
/>
</g>
)
})}
{/* Sea routes */}
{CONNECTIONS.map(([fromId, toId]) => {
const key = `${fromId}-${toId}`
const from = nodes.find(n => n.id === fromId)!
const to = nodes.find(n => n.id === toId)!
const isActive = activeId === fromId || activeId === toId
const color = isActive
? (activeId === fromId ? from.color : to.color)
: '#ffffff'
return (
<SeaRoute key={key} d={ROUTES[key]} color={color} active={isActive} />
)
})}
</svg>
{/* Node markers */}
{nodes.map(node => (
<MapMarker
key={node.id}
node={node}
active={activeId === node.id}
onClick={() => toggle(node.id)}
/>
))}
{!activeId && (
<div className="absolute bottom-3 right-3 flex items-center gap-1 text-[8.5px] text-white/18 pointer-events-none">
<ChevronRight className="w-3 h-3" />
{de ? 'Komponente anklicken' : 'Click any node'}
</div>
)}
</div>
{/* ── DETAIL PANEL ─────────────────────────────────────────────── */}
<AnimatePresence>
{active && (
<motion.div
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 8 }}
transition={{ duration: 0.22, ease: [0.22, 1, 0.36, 1] }}
className={`relative rounded-2xl border ${active.twBorder} ${active.twBg} p-4`}
style={{ boxShadow: `0 0 30px -10px ${active.color}40` }}
>
<button
onClick={() => setActiveId(null)}
className="absolute top-3 right-3 w-6 h-6 rounded-full bg-white/10 hover:bg-white/20 flex items-center justify-center transition-colors"
>
<X className="w-3 h-3 text-white/50" />
</button>
<div className="flex items-start gap-3 mb-4">
<div className={`w-9 h-9 rounded-xl flex items-center justify-center flex-shrink-0 border ${active.twBorder} ${active.twBg}`}>
<active.icon className={`w-5 h-5 ${active.twColor}`} />
</div>
<div className="min-w-0">
<div className="flex items-center gap-2 flex-wrap">
<h3 className="text-sm font-bold text-white">{active.title}</h3>
{active.badge && (
<span className={`text-[9px] px-1.5 py-0.5 rounded uppercase font-semibold tracking-wider border ${active.twBorder} ${active.twColor}`}>
{active.badge}
</span>
)}
</div>
<p className="text-xs text-white/45">{active.subtitle}</p>
</div>
</div>
<div className="grid grid-cols-2 gap-5">
<div> <div>
<span className="text-xs font-semibold text-white/80">{item.label}</span> <p className="text-[9px] uppercase tracking-widest text-white/30 mb-2 font-semibold">
<span className="text-xs text-white/40 ml-2">{item.desc}</span> {de ? 'Stack' : 'Tech Stack'}
</p>
<div className="flex flex-wrap gap-1.5">
{active.tech.map(tk => (
<span key={tk} className="text-[10px] px-2 py-0.5 rounded bg-white/[0.05] border border-white/[0.08] text-white/55 font-mono">
{tk}
</span>
))}
</div>
</div>
<div>
<p className="text-[9px] uppercase tracking-widest text-white/30 mb-2 font-semibold">
{de ? 'Funktionen' : 'Capabilities'}
</p>
<div className="space-y-1.5">
{active.services.map(s => (
<div key={s.name} className="flex items-start gap-2">
<div className={`w-1 h-1 rounded-full mt-1.5 flex-shrink-0 ${active.twDot} opacity-70`} />
<div className="min-w-0">
<span className="text-xs font-semibold text-white/80">{s.name}</span>
<span className="text-[10px] text-white/40 ml-1.5">{s.desc}</span>
</div> </div>
</div> </div>
))} ))}
</div> </div>
</div> </div>
</FadeInView>
)
})}
</div> </div>
{/* Security Bar */} <div className="mt-3 pt-3 border-t border-white/[0.06] flex items-center gap-2 flex-wrap">
<FadeInView delay={0.6}> <Network className="w-3 h-3 text-white/20 flex-shrink-0" />
<GlassCard hover={false} className="p-4"> <span className="text-[9px] text-white/30">
<div className="flex items-center justify-center gap-8 flex-wrap"> {de ? 'Verbunden mit:' : 'Connects to:'}
{securityFeatures.map((feat, idx) => { </span>
const Icon = feat.icon {CONNECTIONS
.filter(([a, b]) => a === active.id || b === active.id)
.map(([a, b]) => {
const peerId = a === active.id ? b : a
const peer = nodes.find(n => n.id === peerId)!
return ( return (
<div key={idx} className="flex items-center gap-2"> <button
<Icon className="w-4 h-4 text-emerald-400" /> key={peerId}
<span className="text-xs text-white/60">{feat.label}</span> onClick={() => setActiveId(peerId)}
</div> className={`text-[9px] px-2 py-0.5 rounded-full border ${peer.twBorder} ${peer.twColor} transition-opacity hover:opacity-80`}
>
{peer.title}
</button>
) )
})} })}
</div> </div>
</GlassCard> </motion.div>
)}
</AnimatePresence>
{/* Bottom legend */}
{!active && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="flex items-center justify-center gap-6 flex-wrap"
>
{[
{ icon: Lock, text: de ? 'DSGVO-konform · BSI-zertifizierbar' : 'GDPR-compliant · BSI-certifiable' },
{ icon: Server, text: de ? 'On-Premise oder EU-Cloud' : 'On-Premise or EU Cloud' },
{ icon: Layers, text: de ? 'Air-Gap fähig' : 'Air-Gap Capable' },
].map(({ icon: Icon, text }) => (
<span key={text} className="flex items-center gap-1.5 text-[10px] text-white/22">
<Icon className="w-3 h-3" />
{text}
</span>
))}
</motion.div>
)}
</FadeInView> </FadeInView>
</div> </div>
) )

View File

@@ -26,7 +26,9 @@ const translations = {
'Investition & Cap Table', 'Investition & Cap Table',
'Kundenersparnis', 'Kundenersparnis',
'KI Q&A', 'KI Q&A',
'Anhang: Architektur', 'Anhang: Annahmen',
'Anhang: Systemarchitektur',
'Anhang: Go-to-Market',
'Anhang: Regulatorik', 'Anhang: Regulatorik',
'Anhang: Engineering', 'Anhang: Engineering',
'Anhang: KI-Pipeline', 'Anhang: KI-Pipeline',
@@ -276,8 +278,8 @@ const translations = {
subtitle: 'Drei Szenarien für robuste Planung', subtitle: 'Drei Szenarien für robuste Planung',
}, },
architecture: { architecture: {
title: 'Technische Architektur', title: 'Systemarchitektur',
subtitle: 'Self-Hosted KI-Stack für maximale Datensouveränität', subtitle: 'BreakPilot · CERTifAI · Compliance Scanner — verbunden über LiteLLM',
}, },
gtm: { gtm: {
title: 'Go-to-Market Strategie', title: 'Go-to-Market Strategie',
@@ -322,7 +324,9 @@ const translations = {
'Investment & Cap Table', 'Investment & Cap Table',
'Customer Savings', 'Customer Savings',
'AI Q&A', 'AI Q&A',
'Appendix: Architecture', 'Appendix: Assumptions',
'Appendix: System Architecture',
'Appendix: Go-to-Market',
'Appendix: Regulatory', 'Appendix: Regulatory',
'Appendix: Engineering', 'Appendix: Engineering',
'Appendix: AI Pipeline', 'Appendix: AI Pipeline',
@@ -572,8 +576,8 @@ const translations = {
subtitle: 'Three scenarios for robust planning', subtitle: 'Three scenarios for robust planning',
}, },
architecture: { architecture: {
title: 'Technical Architecture', title: 'System Architecture',
subtitle: 'Self-hosted AI stack for maximum data sovereignty', subtitle: 'BreakPilot · CERTifAI · Compliance Scanner — connected via LiteLLM',
}, },
gtm: { gtm: {
title: 'Go-to-Market Strategy', title: 'Go-to-Market Strategy',

View File

@@ -34,6 +34,11 @@ export async function middleware(request: NextRequest) {
const { pathname } = request.nextUrl const { pathname } = request.nextUrl
const secret = process.env.PITCH_JWT_SECRET const secret = process.env.PITCH_JWT_SECRET
// Skip all auth in local dev when no secret is configured
if (!secret && process.env.NODE_ENV === 'development') {
return NextResponse.next()
}
// Allow public paths // Allow public paths
if (isPublicPath(pathname)) { if (isPublicPath(pathname)) {
return NextResponse.next() return NextResponse.next()

View File

@@ -3,7 +3,7 @@
"version": "1.0.0", "version": "1.0.0",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev -p 3012", "dev": "next dev --turbopack -p 3012",
"build": "next build", "build": "next build",
"start": "next start -p 3012", "start": "next start -p 3012",
"admin:create": "tsx scripts/create-admin.ts", "admin:create": "tsx scripts/create-admin.ts",