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
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:
@@ -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
|
||||
|
||||
|
||||
@@ -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]
|
||||
)
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user