Consolidate tools into a secure Docker architecture with landing page
- Implement a central reverse proxy (Nginx) with Basic Auth on port 8090. - Create a unified landing page (dashboard) to access B2B Assistant and Market Intelligence. - Update frontends with relative API paths and base paths for subdirectory routing (/b2b/, /market/). - Optimize Docker builds with .dockerignore and a Python-based image for market-backend. - Enable code sideloading for Python logic via Docker volumes. - Fix TypeScript errors in general-market-intelligence regarding ImportMeta.
This commit is contained in:
@@ -10,9 +10,9 @@ server {
|
||||
|
||||
# 2. Proxy API Requests to Backend Container
|
||||
location /api/ {
|
||||
# 'backend' is the service name in docker-compose.yml
|
||||
# 'market-backend' is the service name in docker-compose.yml
|
||||
# Port 3001 is where the Node.js bridge listens
|
||||
proxy_pass http://backend:3001/api/;
|
||||
proxy_pass http://market-backend:3001/api/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
|
||||
3692
general-market-intelligence/package-lock.json
generated
Normal file
3692
general-market-intelligence/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,3 @@
|
||||
|
||||
const express = require('express');
|
||||
const { spawn } = require('child_process');
|
||||
const bodyParser = require('body-parser');
|
||||
@@ -7,11 +6,82 @@ const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const app = express();
|
||||
const PORT = 3001; // Node.js Server läuft auf einem anderen Port als React (3000)
|
||||
const PORT = 3001;
|
||||
|
||||
// Middleware
|
||||
app.use(cors()); // Ermöglicht Cross-Origin-Requests von der React-App
|
||||
app.use(bodyParser.json()); // Parst JSON-Anfragen
|
||||
app.use(cors());
|
||||
app.use(bodyParser.json());
|
||||
|
||||
// Helper für Python-Aufrufe, um Code-Duplizierung zu vermeiden
|
||||
const runPython = (args, res, tempFilesToDelete = []) => {
|
||||
// Im Docker (python:3.11-slim Image) nutzen wir das globale python3
|
||||
const pythonExecutable = 'python3';
|
||||
|
||||
console.log(`Spawning command: ${pythonExecutable} ${args.join(' ')}`);
|
||||
|
||||
const pythonProcess = spawn(pythonExecutable, args);
|
||||
|
||||
let pythonOutput = '';
|
||||
let pythonError = '';
|
||||
|
||||
pythonProcess.stdout.on('data', (data) => {
|
||||
pythonOutput += data.toString();
|
||||
});
|
||||
|
||||
pythonProcess.stderr.on('data', (data) => {
|
||||
pythonError += data.toString();
|
||||
});
|
||||
|
||||
pythonProcess.on('close', (code) => {
|
||||
console.log(`Python script finished with exit code: ${code}`);
|
||||
if (pythonOutput.length > 500) {
|
||||
console.log(`--- STDOUT (Truncated) ---`);
|
||||
console.log(pythonOutput.substring(0, 500) + '...');
|
||||
} else {
|
||||
console.log(`--- STDOUT ---`);
|
||||
console.log(pythonOutput);
|
||||
}
|
||||
|
||||
if (pythonError) {
|
||||
console.log(`--- STDERR ---`);
|
||||
console.log(pythonError);
|
||||
}
|
||||
console.log(`----------------`);
|
||||
|
||||
// Aufräumen
|
||||
tempFilesToDelete.forEach(file => {
|
||||
if (fs.existsSync(file)) {
|
||||
try {
|
||||
fs.unlinkSync(file);
|
||||
} catch (e) {
|
||||
console.error(`Failed to delete temp file ${file}:`, e.message);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (code !== 0) {
|
||||
console.error(`Python script exited with error.`);
|
||||
return res.status(500).json({ error: 'Python script failed', details: pythonError });
|
||||
}
|
||||
|
||||
try {
|
||||
const result = JSON.parse(pythonOutput);
|
||||
res.json(result);
|
||||
} catch (parseError) {
|
||||
console.error('Failed to parse Python output as JSON:', parseError);
|
||||
res.status(500).json({ error: 'Invalid JSON from Python script', rawOutput: pythonOutput, details: pythonError });
|
||||
}
|
||||
});
|
||||
|
||||
pythonProcess.on('error', (err) => {
|
||||
console.error('FATAL: Failed to start python process itself.', err);
|
||||
tempFilesToDelete.forEach(file => {
|
||||
if (fs.existsSync(file)) fs.unlinkSync(file);
|
||||
});
|
||||
res.status(500).json({ error: 'Failed to start Python process', details: err.message });
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
// API-Endpunkt für generateSearchStrategy
|
||||
app.post('/api/generate-search-strategy', async (req, res) => {
|
||||
@@ -19,72 +89,20 @@ app.post('/api/generate-search-strategy', async (req, res) => {
|
||||
const { referenceUrl, contextContent } = req.body;
|
||||
|
||||
if (!referenceUrl || !contextContent) {
|
||||
console.error('Validation Error: Missing referenceUrl or contextContent.');
|
||||
return res.status(400).json({ error: 'Missing referenceUrl or contextContent' });
|
||||
}
|
||||
|
||||
const tempContextFilePath = path.join(__dirname, 'tmp', `context_${Date.now()}.md`);
|
||||
const tmpDir = path.join(__dirname, 'tmp');
|
||||
if (!fs.existsSync(tmpDir)) {
|
||||
fs.mkdirSync(tmpDir);
|
||||
}
|
||||
if (!fs.existsSync(tmpDir)) fs.mkdirSync(tmpDir);
|
||||
const tempContextFilePath = path.join(tmpDir, `context_${Date.now()}.md`);
|
||||
|
||||
try {
|
||||
fs.writeFileSync(tempContextFilePath, contextContent);
|
||||
console.log(`Successfully wrote context to ${tempContextFilePath}`);
|
||||
|
||||
const pythonExecutable = path.join(__dirname, '..', '.venv', 'bin', 'python3');
|
||||
|
||||
const pythonScript = path.join(__dirname, '..', 'market_intel_orchestrator.py');
|
||||
const scriptArgs = [pythonScript, '--mode', 'generate_strategy', '--reference_url', referenceUrl, '--context_file', tempContextFilePath];
|
||||
|
||||
console.log(`Spawning command: ${pythonExecutable}`);
|
||||
console.log(`With arguments: ${JSON.stringify(scriptArgs)}`);
|
||||
|
||||
const pythonProcess = spawn(pythonExecutable, scriptArgs, {
|
||||
env: { ...process.env, PYTHONPATH: path.join(__dirname, '..', '.venv', 'lib', 'python3.11', 'site-packages') }
|
||||
});
|
||||
|
||||
let pythonOutput = '';
|
||||
let pythonError = '';
|
||||
|
||||
pythonProcess.stdout.on('data', (data) => {
|
||||
pythonOutput += data.toString();
|
||||
});
|
||||
|
||||
pythonProcess.stderr.on('data', (data) => {
|
||||
pythonError += data.toString();
|
||||
});
|
||||
|
||||
pythonProcess.on('close', (code) => {
|
||||
console.log(`Python script finished with exit code: ${code}`);
|
||||
console.log(`--- STDOUT ---`);
|
||||
console.log(pythonOutput);
|
||||
console.log(`--- STDERR ---`);
|
||||
console.log(pythonError);
|
||||
console.log(`----------------`);
|
||||
|
||||
fs.unlinkSync(tempContextFilePath);
|
||||
|
||||
if (code !== 0) {
|
||||
console.error(`Python script exited with error.`);
|
||||
return res.status(500).json({ error: 'Python script failed', details: pythonError });
|
||||
}
|
||||
try {
|
||||
const result = JSON.parse(pythonOutput);
|
||||
res.json(result);
|
||||
} catch (parseError) {
|
||||
console.error('Failed to parse Python output as JSON:', parseError);
|
||||
res.status(500).json({ error: 'Invalid JSON from Python script', rawOutput: pythonOutput, details: pythonError });
|
||||
}
|
||||
});
|
||||
|
||||
pythonProcess.on('error', (err) => {
|
||||
console.error('FATAL: Failed to start python process itself.', err);
|
||||
if (fs.existsSync(tempContextFilePath)) {
|
||||
fs.unlinkSync(tempContextFilePath);
|
||||
}
|
||||
res.status(500).json({ error: 'Failed to start Python process', details: err.message });
|
||||
});
|
||||
runPython(scriptArgs, res, [tempContextFilePath]);
|
||||
|
||||
} catch (writeError) {
|
||||
console.error('Failed to write temporary context file:', writeError);
|
||||
@@ -98,25 +116,21 @@ app.post('/api/identify-competitors', async (req, res) => {
|
||||
const { referenceUrl, targetMarket, contextContent, referenceCity, referenceCountry, summaryOfOffer } = req.body;
|
||||
|
||||
if (!referenceUrl || !targetMarket) {
|
||||
console.error('Validation Error: Missing referenceUrl or targetMarket for identify_competitors.');
|
||||
return res.status(400).json({ error: 'Missing referenceUrl or targetMarket' });
|
||||
}
|
||||
|
||||
const tempContextFilePath = path.join(__dirname, 'tmp', `context_comp_${Date.now()}.md`);
|
||||
const tmpDir = path.join(__dirname, 'tmp');
|
||||
if (!fs.existsSync(tmpDir)) {
|
||||
fs.mkdirSync(tmpDir);
|
||||
}
|
||||
if (!fs.existsSync(tmpDir)) fs.mkdirSync(tmpDir);
|
||||
const tempContextFilePath = path.join(tmpDir, `context_comp_${Date.now()}.md`);
|
||||
|
||||
try {
|
||||
const cleanupFiles = [];
|
||||
if (contextContent) {
|
||||
fs.writeFileSync(tempContextFilePath, contextContent);
|
||||
console.log(`Successfully wrote context to ${tempContextFilePath} for competitors.`);
|
||||
cleanupFiles.push(tempContextFilePath);
|
||||
}
|
||||
|
||||
const pythonExecutable = path.join(__dirname, '..', '.venv', 'bin', 'python3');
|
||||
const pythonScript = path.join(__dirname, '..', 'market_intel_orchestrator.py');
|
||||
|
||||
const scriptArgs = [
|
||||
pythonScript,
|
||||
'--mode', 'identify_competitors',
|
||||
@@ -124,72 +138,14 @@ app.post('/api/identify-competitors', async (req, res) => {
|
||||
'--target_market', targetMarket
|
||||
];
|
||||
|
||||
if (contextContent) {
|
||||
scriptArgs.push('--context_file', tempContextFilePath);
|
||||
}
|
||||
if (referenceCity) {
|
||||
scriptArgs.push('--reference_city', referenceCity);
|
||||
}
|
||||
if (referenceCountry) {
|
||||
scriptArgs.push('--reference_country', referenceCountry);
|
||||
}
|
||||
if (summaryOfOffer) {
|
||||
scriptArgs.push('--summary_of_offer', summaryOfOffer);
|
||||
}
|
||||
if (contextContent) scriptArgs.push('--context_file', tempContextFilePath);
|
||||
if (referenceCity) scriptArgs.push('--reference_city', referenceCity);
|
||||
if (referenceCountry) scriptArgs.push('--reference_country', referenceCountry);
|
||||
if (summaryOfOffer) scriptArgs.push('--summary_of_offer', summaryOfOffer);
|
||||
|
||||
console.log(`Spawning command: ${pythonExecutable}`);
|
||||
console.log(`With arguments: ${JSON.stringify(scriptArgs)}`);
|
||||
|
||||
const pythonProcess = spawn(pythonExecutable, scriptArgs, {
|
||||
env: { ...process.env, PYTHONPATH: path.join(__dirname, '..', '.venv', 'lib', 'python3.11', 'site-packages') }
|
||||
});
|
||||
|
||||
let pythonOutput = '';
|
||||
let pythonError = '';
|
||||
|
||||
pythonProcess.stdout.on('data', (data) => {
|
||||
pythonOutput += data.toString();
|
||||
});
|
||||
|
||||
pythonProcess.stderr.on('data', (data) => {
|
||||
pythonError += data.toString();
|
||||
});
|
||||
|
||||
pythonProcess.on('close', (code) => {
|
||||
console.log(`Python script (identify_competitors) finished with exit code: ${code}`);
|
||||
console.log(`--- STDOUT (identify_competitors) ---`);
|
||||
console.log(pythonOutput);
|
||||
console.log(`--- STDERR (identify_competitors) ---`);
|
||||
console.log(pythonError);
|
||||
console.log(`-------------------------------------`);
|
||||
|
||||
if (contextContent) {
|
||||
fs.unlinkSync(tempContextFilePath);
|
||||
}
|
||||
|
||||
if (code !== 0) {
|
||||
console.error(`Python script (identify_competitors) exited with error.`);
|
||||
return res.status(500).json({ error: 'Python script failed', details: pythonError });
|
||||
}
|
||||
try {
|
||||
const result = JSON.parse(pythonOutput);
|
||||
res.json(result);
|
||||
} catch (parseError) {
|
||||
console.error('Failed to parse Python output (identify_competitors) as JSON:', parseError);
|
||||
res.status(500).json({ error: 'Invalid JSON from Python script', rawOutput: pythonOutput, details: pythonError });
|
||||
}
|
||||
});
|
||||
|
||||
pythonProcess.on('error', (err) => {
|
||||
console.error('FATAL: Failed to start python process itself (identify_competitors).', err);
|
||||
if (contextContent) {
|
||||
fs.unlinkSync(tempContextFilePath);
|
||||
}
|
||||
res.status(500).json({ error: 'Failed to start Python process', details: err.message });
|
||||
});
|
||||
runPython(scriptArgs, res, cleanupFiles);
|
||||
|
||||
} catch (writeError) {
|
||||
console.error('Failed to write temporary context file (identify_competitors):', writeError);
|
||||
res.status(500).json({ error: 'Failed to write temporary file', details: writeError.message });
|
||||
}
|
||||
});
|
||||
@@ -200,73 +156,19 @@ app.post('/api/analyze-company', async (req, res) => {
|
||||
const { companyName, strategy, targetMarket } = req.body;
|
||||
|
||||
if (!companyName || !strategy) {
|
||||
console.error('Validation Error: Missing companyName or strategy for analyze-company.');
|
||||
return res.status(400).json({ error: 'Missing companyName or strategy' });
|
||||
}
|
||||
|
||||
try {
|
||||
const pythonExecutable = path.join(__dirname, '..', '.venv', 'bin', 'python3');
|
||||
const pythonScript = path.join(__dirname, '..', 'market_intel_orchestrator.py');
|
||||
|
||||
const scriptArgs = [
|
||||
pythonScript,
|
||||
'--mode', 'analyze_company',
|
||||
'--company_name', companyName,
|
||||
'--strategy_json', JSON.stringify(strategy),
|
||||
'--target_market', targetMarket || 'Germany'
|
||||
];
|
||||
const pythonScript = path.join(__dirname, '..', 'market_intel_orchestrator.py');
|
||||
const scriptArgs = [
|
||||
pythonScript,
|
||||
'--mode', 'analyze_company',
|
||||
'--company_name', companyName,
|
||||
'--strategy_json', JSON.stringify(strategy),
|
||||
'--target_market', targetMarket || 'Germany'
|
||||
];
|
||||
|
||||
console.log(`Spawning Audit for ${companyName}: ${pythonExecutable} ...`);
|
||||
|
||||
const pythonProcess = spawn(pythonExecutable, scriptArgs, {
|
||||
env: { ...process.env, PYTHONPATH: path.join(__dirname, '..', '.venv', 'lib', 'python3.11', 'site-packages') }
|
||||
});
|
||||
|
||||
let pythonOutput = '';
|
||||
let pythonError = '';
|
||||
|
||||
pythonProcess.stdout.on('data', (data) => {
|
||||
pythonOutput += data.toString();
|
||||
});
|
||||
|
||||
pythonProcess.stderr.on('data', (data) => {
|
||||
pythonError += data.toString();
|
||||
});
|
||||
|
||||
pythonProcess.on('close', (code) => {
|
||||
console.log(`Audit for ${companyName} finished with exit code: ${code}`);
|
||||
|
||||
// Log stderr nur bei Fehler oder wenn nötig, um Logs nicht zu fluten
|
||||
if (pythonError) {
|
||||
console.log(`--- STDERR (Audit ${companyName}) ---`);
|
||||
console.log(pythonError);
|
||||
}
|
||||
|
||||
if (code !== 0) {
|
||||
console.error(`Python script (analyze_company) exited with error.`);
|
||||
return res.status(500).json({ error: 'Python script failed', details: pythonError });
|
||||
}
|
||||
try {
|
||||
// Versuche JSON zu parsen. Manchmal gibt Python zusätzlichen Text aus, den wir filtern müssen.
|
||||
// Da wir stderr für Logs nutzen, sollte stdout rein sein.
|
||||
const result = JSON.parse(pythonOutput);
|
||||
res.json(result);
|
||||
} catch (parseError) {
|
||||
console.error('Failed to parse Python output (analyze_company) as JSON:', parseError);
|
||||
console.log('Raw Output:', pythonOutput);
|
||||
res.status(500).json({ error: 'Invalid JSON from Python script', rawOutput: pythonOutput, details: pythonError });
|
||||
}
|
||||
});
|
||||
|
||||
pythonProcess.on('error', (err) => {
|
||||
console.error(`FATAL: Failed to start python process for ${companyName}.`, err);
|
||||
res.status(500).json({ error: 'Failed to start Python process', details: err.message });
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
console.error(`Internal Server Error in /api/analyze-company: ${err.message}`);
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
runPython(scriptArgs, res);
|
||||
});
|
||||
|
||||
// API-Endpunkt für generate-outreach
|
||||
@@ -275,25 +177,19 @@ app.post('/api/generate-outreach', async (req, res) => {
|
||||
const { companyData, knowledgeBase, referenceUrl } = req.body;
|
||||
|
||||
if (!companyData || !knowledgeBase) {
|
||||
console.error('Validation Error: Missing companyData or knowledgeBase for generate-outreach.');
|
||||
return res.status(400).json({ error: 'Missing companyData or knowledgeBase' });
|
||||
}
|
||||
|
||||
const tempDataFilePath = path.join(__dirname, 'tmp', `outreach_data_${Date.now()}.json`);
|
||||
const tempContextFilePath = path.join(__dirname, 'tmp', `outreach_context_${Date.now()}.md`);
|
||||
const tmpDir = path.join(__dirname, 'tmp');
|
||||
if (!fs.existsSync(tmpDir)) {
|
||||
fs.mkdirSync(tmpDir);
|
||||
}
|
||||
if (!fs.existsSync(tmpDir)) fs.mkdirSync(tmpDir);
|
||||
const tempDataFilePath = path.join(tmpDir, `outreach_data_${Date.now()}.json`);
|
||||
const tempContextFilePath = path.join(tmpDir, `outreach_context_${Date.now()}.md`);
|
||||
|
||||
try {
|
||||
fs.writeFileSync(tempDataFilePath, JSON.stringify(companyData));
|
||||
fs.writeFileSync(tempContextFilePath, knowledgeBase);
|
||||
console.log(`Successfully wrote temporary files for outreach.`);
|
||||
|
||||
const pythonExecutable = path.join(__dirname, '..', '.venv', 'bin', 'python3');
|
||||
const pythonScript = path.join(__dirname, '..', 'market_intel_orchestrator.py');
|
||||
|
||||
const scriptArgs = [
|
||||
pythonScript,
|
||||
'--mode', 'generate_outreach',
|
||||
@@ -302,57 +198,13 @@ app.post('/api/generate-outreach', async (req, res) => {
|
||||
'--reference_url', referenceUrl || ''
|
||||
];
|
||||
|
||||
console.log(`Spawning Outreach Generation for ${companyData.companyName}...`);
|
||||
|
||||
const pythonProcess = spawn(pythonExecutable, scriptArgs, {
|
||||
env: { ...process.env, PYTHONPATH: path.join(__dirname, '..', '.venv', 'lib', 'python3.11', 'site-packages') }
|
||||
});
|
||||
|
||||
let pythonOutput = '';
|
||||
let pythonError = '';
|
||||
|
||||
pythonProcess.stdout.on('data', (data) => {
|
||||
pythonOutput += data.toString();
|
||||
});
|
||||
|
||||
pythonProcess.stderr.on('data', (data) => {
|
||||
pythonError += data.toString();
|
||||
});
|
||||
|
||||
pythonProcess.on('close', (code) => {
|
||||
console.log(`Outreach Generation finished with exit code: ${code}`);
|
||||
|
||||
// Clean up
|
||||
if (fs.existsSync(tempDataFilePath)) fs.unlinkSync(tempDataFilePath);
|
||||
if (fs.existsSync(tempContextFilePath)) fs.unlinkSync(tempContextFilePath);
|
||||
|
||||
if (code !== 0) {
|
||||
console.error(`Python script (generate_outreach) exited with error.`);
|
||||
return res.status(500).json({ error: 'Python script failed', details: pythonError });
|
||||
}
|
||||
try {
|
||||
const result = JSON.parse(pythonOutput);
|
||||
res.json(result);
|
||||
} catch (parseError) {
|
||||
console.error('Failed to parse Python output (generate_outreach) as JSON:', parseError);
|
||||
res.status(500).json({ error: 'Invalid JSON from Python script', rawOutput: pythonOutput, details: pythonError });
|
||||
}
|
||||
});
|
||||
|
||||
pythonProcess.on('error', (err) => {
|
||||
console.error(`FATAL: Failed to start python process for outreach.`, err);
|
||||
if (fs.existsSync(tempDataFilePath)) fs.unlinkSync(tempDataFilePath);
|
||||
if (fs.existsSync(tempContextFilePath)) fs.unlinkSync(tempContextFilePath);
|
||||
res.status(500).json({ error: 'Failed to start Python process', details: err.message });
|
||||
});
|
||||
runPython(scriptArgs, res, [tempDataFilePath, tempContextFilePath]);
|
||||
|
||||
} catch (err) {
|
||||
console.error(`Internal Server Error in /api/generate-outreach: ${err.message}`);
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Start des Servers
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Node.js API Bridge running on http://localhost:${PORT}`);
|
||||
});
|
||||
});
|
||||
@@ -6,8 +6,8 @@ import { LeadStatus, AnalysisResult, Competitor, Language, Tier, EmailDraft, Sea
|
||||
// URL Konfiguration:
|
||||
// Im Production-Build (Docker/Nginx) nutzen wir den relativen Pfad '/api', da Nginx als Reverse Proxy fungiert.
|
||||
// Im Development-Modus (lokal) greifen wir direkt auf den Port 3001 zu.
|
||||
const API_BASE_URL = import.meta.env.PROD
|
||||
? '/api'
|
||||
const API_BASE_URL = (import.meta as any).env.PROD
|
||||
? 'api'
|
||||
: `http://${window.location.hostname}:3001/api`;
|
||||
|
||||
// Helper to extract JSON (kann ggf. entfernt werden, wenn das Backend immer sauberes JSON liefert)
|
||||
|
||||
10
general-market-intelligence/vite-env.d.ts
vendored
Normal file
10
general-market-intelligence/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_API_BASE_URL: string;
|
||||
// more env variables...
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv;
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import react from '@vitejs/plugin-react';
|
||||
export default defineConfig(({ mode }) => {
|
||||
const env = loadEnv(mode, '.', '');
|
||||
return {
|
||||
base: '/market/',
|
||||
server: {
|
||||
port: 3000,
|
||||
host: '0.0.0.0',
|
||||
|
||||
Reference in New Issue
Block a user