Merge branch 'main' of ssh://gitea.meghsakha.com:22222/Benjamin_Boenisch/breakpilot-core
All checks were successful
Build pitch-deck / build-push-deploy (push) Successful in 1m22s
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 37s
CI / test-python-voice (push) Successful in 40s
CI / test-bqas (push) Successful in 39s

This commit is contained in:
Benjamin Admin
2026-04-18 13:03:31 +02:00
17 changed files with 128 additions and 77 deletions

View File

@@ -12,6 +12,10 @@ RUN npm install
# Copy source code
COPY . .
# Embed git commit hash into build
ARG GIT_SHA=dev
ENV GIT_SHA=$GIT_SHA
# Build the application
RUN npm run build

View File

@@ -6,7 +6,7 @@ import { finanzplanToFMResults } from '@/lib/finanzplan/adapter'
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const { scenarioId, source } = body
const { scenarioId, source, force } = body
// If source=finanzplan, use the Finanzplan engine instead
if (source === 'finanzplan') {
@@ -28,8 +28,8 @@ export async function POST(request: NextRequest) {
const client = await pool.connect()
try {
// Fast path: return cached results if they exist (avoid expensive recompute + 60 inserts)
const cached = await client.query(
// Fast path: return cached results if they exist (skip when force=true)
const cached = force ? { rows: [] } : await client.query(
'SELECT * FROM pitch_fm_results WHERE scenario_id = $1 ORDER BY month',
[scenarioId]
)

View File

@@ -39,7 +39,9 @@ export async function GET(
query += ' ORDER BY sort_order'
const { rows } = await pool.query(query, params)
return NextResponse.json({ sheet: sheetName, rows })
return NextResponse.json({ sheet: sheetName, rows }, {
headers: { 'Cache-Control': 'no-store' },
})
} catch (error) {
return NextResponse.json({ error: String(error) }, { status: 500 })
}

View File

@@ -25,6 +25,8 @@ export async function GET() {
sheets,
scenarios: scenarios.rows,
months: { start: '2026-01', end: '2030-12', count: 60, founding: '2026-08' },
}, {
headers: { 'Cache-Control': 'no-store' },
})
} catch (error) {
return NextResponse.json({ error: String(error) }, { status: 500 })

View File

@@ -3,7 +3,7 @@
import { useEffect, useState } from 'react'
import { useParams } from 'next/navigation'
import Link from 'next/link'
import { ArrowLeft, Save } from 'lucide-react'
import { ArrowLeft, RefreshCw, Save } from 'lucide-react'
interface Assumption {
id: string
@@ -36,6 +36,7 @@ export default function EditScenarioPage() {
const [loading, setLoading] = useState(true)
const [edits, setEdits] = useState<Record<string, string>>({})
const [savingId, setSavingId] = useState<string | null>(null)
const [recomputing, setRecomputing] = useState(false)
const [toast, setToast] = useState<string | null>(null)
function flashToast(msg: string) {
@@ -56,6 +57,17 @@ export default function EditScenarioPage() {
useEffect(() => { if (scenarioId) load() }, [scenarioId])
async function forceRecompute() {
setRecomputing(true)
const res = await fetch('/api/financial-model/compute', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ scenarioId, force: true }),
})
setRecomputing(false)
flashToast(res.ok ? 'Recomputed successfully' : 'Recompute failed')
}
function setEdit(id: string, val: string) {
setEdits(prev => ({ ...prev, [id]: val }))
}
@@ -108,17 +120,28 @@ export default function EditScenarioPage() {
<ArrowLeft className="w-4 h-4" /> Back to scenarios
</Link>
<div>
<div className="flex items-center gap-3 mb-1">
<div className="w-4 h-4 rounded-full" style={{ backgroundColor: scenario.color }} />
<h1 className="text-2xl font-semibold text-white">{scenario.name}</h1>
{scenario.is_default && (
<span className="text-[9px] px-1.5 py-0.5 rounded bg-indigo-500/20 text-indigo-300 uppercase font-semibold">
Default
</span>
)}
<div className="flex items-start justify-between gap-4">
<div>
<div className="flex items-center gap-3 mb-1">
<div className="w-4 h-4 rounded-full" style={{ backgroundColor: scenario.color }} />
<h1 className="text-2xl font-semibold text-white">{scenario.name}</h1>
{scenario.is_default && (
<span className="text-[9px] px-1.5 py-0.5 rounded bg-indigo-500/20 text-indigo-300 uppercase font-semibold">
Default
</span>
)}
</div>
{scenario.description && <p className="text-sm text-white/50">{scenario.description}</p>}
</div>
{scenario.description && <p className="text-sm text-white/50">{scenario.description}</p>}
<button
onClick={forceRecompute}
disabled={recomputing}
className="flex items-center gap-2 text-sm px-3 py-1.5 rounded-lg bg-white/[0.06] hover:bg-white/[0.1] text-white/70 hover:text-white disabled:opacity-40 disabled:cursor-wait transition-colors"
title="Clear cache and recompute financial model results"
>
<RefreshCw className={`w-3.5 h-3.5 ${recomputing ? 'animate-spin' : ''}`} />
{recomputing ? 'Computing…' : 'Force Recompute'}
</button>
</div>
<div className="space-y-6">

View File

@@ -92,6 +92,11 @@ export default function AdminShell({ admin, children }: AdminShellProps) {
<div className="px-3 py-2 mb-2">
<div className="text-sm font-medium text-white/90 truncate">{admin.name}</div>
<div className="text-xs text-white/40 truncate">{admin.email}</div>
<div className="mt-1.5 flex items-center gap-1.5">
<span className="text-[9px] font-mono bg-white/[0.06] text-white/30 px-1.5 py-0.5 rounded">
{process.env.NEXT_PUBLIC_GIT_SHA ?? 'dev'}
</span>
</div>
</div>
<button
onClick={logout}

View File

@@ -178,8 +178,8 @@ export default function AIPipelineSlide({ lang }: AIPipelineSlideProps) {
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 + Pipeline)', 'Coolify Auto-Deploy mit Health-Check-Monitoring', 'arm64 → amd64 Cross-Build für 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'],
? ['Gitea Actions: Lint → Tests → Validierung bei jedem Push', 'Go-Tests (AI SDK) + Python-Tests (Backend + Pipeline)', 'Orca Auto-Deploy mit Health-Check-Monitoring', 'arm64 → amd64 Cross-Build für Hetzner Production']
: ['Gitea Actions: Lint → Tests → Validation on every push', 'Go tests (AI SDK) + Python tests (Backend + Pipeline)', 'Orca auto-deploy with health check monitoring', 'arm64 → amd64 cross-build for Hetzner production'],
},
{
icon: Zap,

View File

@@ -61,8 +61,12 @@ function formatCell(v: number | undefined): string {
return Math.round(v).toLocaleString('de-DE', { maximumFractionDigits: 0 })
}
interface FpScenario { id: string; name: string; is_default: boolean }
export default function FinanzplanSlide({ lang, investorId, preferredScenarioId }: FinanzplanSlideProps) {
const [sheets, setSheets] = useState<SheetMeta[]>([])
const [scenarios, setScenarios] = useState<FpScenario[]>([])
const [selectedScenarioId, setSelectedScenarioId] = useState<string>('')
const [activeSheet, setActiveSheet] = useState<string>('personalkosten')
const [rows, setRows] = useState<SheetRow[]>([])
const [loading, setLoading] = useState(false)
@@ -77,19 +81,26 @@ export default function FinanzplanSlide({ lang, investorId, preferredScenarioId
[fm.activeResults],
)
// Determine fp_scenario_id from the active FM scenario name
const fpScenarioParam = fm.activeScenario?.name?.toLowerCase().includes('wandeldarlehen')
? '?scenarioId=c0000000-0000-0000-0000-000000000200'
: ''
// Load sheet list
// Load sheet list + scenarios
useEffect(() => {
fetch('/api/finanzplan')
fetch('/api/finanzplan', { cache: 'no-store' })
.then(r => r.json())
.then(data => setSheets(data.sheets || []))
.then(data => {
setSheets(data.sheets || [])
const scens: FpScenario[] = data.scenarios || []
setScenarios(scens)
// Pick default scenario on first load
if (!selectedScenarioId) {
const def = scens.find(s => s.is_default) ?? scens[0]
if (def) setSelectedScenarioId(def.id)
}
})
.catch(() => {})
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
const scenarioParam = selectedScenarioId ? `?scenarioId=${selectedScenarioId}` : ''
// Load sheet data
const loadSheet = useCallback(async (name: string) => {
if (name === 'kpis' || name === 'charts') {
@@ -99,12 +110,12 @@ export default function FinanzplanSlide({ lang, investorId, preferredScenarioId
}
setLoading(true)
try {
const r = await fetch(`/api/finanzplan/${name}${fpScenarioParam}`)
const r = await fetch(`/api/finanzplan/${name}${scenarioParam}`, { cache: 'no-store' })
const data = await r.json()
setRows(data.rows || [])
} catch { /* ignore */ }
setLoading(false)
}, [fpScenarioParam])
}, [scenarioParam])
useEffect(() => { loadSheet(activeSheet) }, [activeSheet, loadSheet])
@@ -112,7 +123,7 @@ export default function FinanzplanSlide({ lang, investorId, preferredScenarioId
const handleCompute = async () => {
setComputing(true)
try {
await fetch('/api/finanzplan/compute', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' })
await fetch('/api/finanzplan/compute', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ scenarioId: selectedScenarioId || undefined }) })
await loadSheet(activeSheet)
} catch { /* ignore */ }
setComputing(false)

View File

@@ -692,8 +692,8 @@ export const PRESENTER_FAQ: FAQEntry[] = [
keywords: ['syseleven', 'hetzner', 'cloud', 'hosting', 'infrastruktur', 'infrastructure', 'server', 'rechenzentrum', 'data center', 'wo laufen', 'where hosted'],
question_de: 'Auf welcher Infrastruktur laeuft die Plattform?',
question_en: 'What infrastructure does the platform run on?',
answer_de: 'Unsere Plattform laeuft zu 100 Prozent auf europaeischer Cloud-Infrastruktur — ohne einen einzigen US-Anbieter. Fuer LLM-Inferenz und KI-Workloads nutzen wir SysEleven, einen BSI-C5-zertifizierten deutschen Cloud-Provider mit GPU-Kapazitaet. Fuer Datenbanken, Vektorspeicher und Anwendungslogik setzen wir auf Hetzner — ebenfalls deutsch, ISO 27001-zertifiziert und deutlich kostenguenstiger als AWS oder Azure. Das CI/CD laeuft ueber Gitea Actions mit automatischem Deploy via Coolify auf Hetzner. Diese Kombination gibt uns einen strukturellen Kostenvorteil bei voller EU-Datensouveraenitaet.',
answer_en: 'Our platform runs 100 percent on European cloud infrastructure — without a single US provider. For LLM inference and AI workloads we use SysEleven, a BSI C5-certified German cloud provider with GPU capacity. For databases, vector storage and application logic we rely on Hetzner — also German, ISO 27001-certified and significantly more cost-effective than AWS or Azure. CI/CD runs via Gitea Actions with automatic deploy via Coolify on Hetzner. This combination gives us a structural cost advantage with full EU data sovereignty.',
answer_de: 'Unsere Plattform laeuft zu 100 Prozent auf europaeischer Cloud-Infrastruktur — ohne einen einzigen US-Anbieter. Fuer LLM-Inferenz und KI-Workloads nutzen wir SysEleven, einen BSI-C5-zertifizierten deutschen Cloud-Provider mit GPU-Kapazitaet. Fuer Datenbanken, Vektorspeicher und Anwendungslogik setzen wir auf Hetzner — ebenfalls deutsch, ISO 27001-zertifiziert und deutlich kostenguenstiger als AWS oder Azure. Das CI/CD laeuft ueber Gitea Actions mit automatischem Deploy via Orca auf Hetzner. Diese Kombination gibt uns einen strukturellen Kostenvorteil bei voller EU-Datensouveraenitaet.',
answer_en: 'Our platform runs 100 percent on European cloud infrastructure — without a single US provider. For LLM inference and AI workloads we use SysEleven, a BSI C5-certified German cloud provider with GPU capacity. For databases, vector storage and application logic we rely on Hetzner — also German, ISO 27001-certified and significantly more cost-effective than AWS or Azure. CI/CD runs via Gitea Actions with automatic deploy via Orca on Hetzner. This combination gives us a structural cost advantage with full EU data sovereignty.',
goto_slide: 'annex-architecture',
priority: 8,
},

View File

@@ -534,8 +534,8 @@ export const PRESENTER_SCRIPT: SlideScript[] = [
duration: 40,
paragraphs: [
{
text_de: 'Engineering Deep Dive: Über 500.000 Zeilen Code, 45 Container, 65 Compliance-Module. Tech-Stack: Go, Python, TypeScript mit Next.js. CI/CD über Gitea Actions mit automatischem Deploy via Coolify auf Hetzner.',
text_en: 'Engineering deep dive: Over 500,000 lines of code, 45 containers, 65 compliance modules. Tech stack: Go, Python, TypeScript with Next.js. CI/CD via Gitea Actions with automatic deploy via Coolify on Hetzner.',
text_de: 'Engineering Deep Dive: Über 500.000 Zeilen Code, 45 Container, 65 Compliance-Module. Tech-Stack: Go, Python, TypeScript mit Next.js. CI/CD über Gitea Actions mit automatischem Deploy via Orca auf Hetzner.',
text_en: 'Engineering deep dive: Over 500,000 lines of code, 45 containers, 65 compliance modules. Tech stack: Go, Python, TypeScript with Next.js. CI/CD via Gitea Actions with automatic deploy via Orca on Hetzner.',
pause_after: 2000,
},
{

View File

@@ -1,6 +1,9 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'standalone',
env: {
NEXT_PUBLIC_GIT_SHA: process.env.GIT_SHA || 'dev',
},
reactStrictMode: true,
typescript: {
ignoreBuildErrors: true,