const express = require('express'); const { spawn } = require('child_process'); const bodyParser = require('body-parser'); const cors = require('cors'); const fs = require('fs'); const path = require('path'); const app = express(); const PORT = 3001; // --- DEBUG LOGGING --- // WICHTIG: Im Docker-Container market-backend ist nur /app/Log gemountet! const LOG_DIR = '/app/Log'; const LOG_FILE = path.join(LOG_DIR, 'backend_debug.log'); const DUMP_FILE = path.join(LOG_DIR, 'server_dump.txt'); const logToFile = (message) => { try { if (!fs.existsSync(LOG_DIR)) return; const timestamp = new Date().toISOString(); const logLine = `[${timestamp}] ${message}\n`; fs.appendFileSync(LOG_FILE, logLine); } catch (e) { console.error("Failed to write to debug log:", e); } }; const dumpToFile = (content) => { try { if (!fs.existsSync(LOG_DIR)) return; const separator = `\n\n--- DUMP ${new Date().toISOString()} ---\n`; fs.appendFileSync(DUMP_FILE, separator + content + "\n--- END DUMP ---\n"); } catch (e) { console.error("Failed to dump:", e); } }; // Middleware app.use(cors()); app.use(bodyParser.json({ limit: '50mb' })); app.use((req, res, next) => { logToFile(`INCOMING REQUEST: ${req.method} ${req.url}`); next(); }); // 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'; logToFile(`Spawning python: ${args.join(' ')}`); 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) => { logToFile(`Python exited with code ${code}`); // GLOBAL DUMP dumpToFile(`STDOUT:\n${pythonOutput}\n\nSTDERR:\n${pythonError}`); if (code !== 0) { console.error(`Python script exited with error.`); return res.status(500).json({ error: 'Python script failed', details: pythonError }); } try { // Versuche JSON zu parsen // Suche nach dem ersten '{' und dem letzten '}', falls Müll davor/dahinter ist let jsonString = pythonOutput; const match = pythonOutput.match(/\{[\s\S]*\}/); if (match) { jsonString = match[0]; } const result = JSON.parse(jsonString); res.json(result); } catch (parseError) { logToFile(`JSON Parse Error. Full Output dumped to server_dump.txt`); console.error('Failed to parse Python output as JSON:', parseError); res.status(500).json({ error: 'Invalid JSON from Python script', rawOutputSnippet: pythonOutput.substring(0, 200), details: pythonError }); } // Aufräumen tempFilesToDelete.forEach(file => { if (fs.existsSync(file)) try { fs.unlinkSync(file); } catch(e){} }); }); pythonProcess.on('error', (err) => { logToFile(`FATAL: Failed to start python process: ${err.message}`); 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) => { logToFile(`[${new Date().toISOString()}] HIT: /api/generate-search-strategy`); const { referenceUrl, contextContent, language } = req.body; if (!referenceUrl || !contextContent) { return res.status(400).json({ error: 'Missing referenceUrl or contextContent' }); } const tmpDir = path.join(__dirname, 'tmp'); if (!fs.existsSync(tmpDir)) fs.mkdirSync(tmpDir); const tempContextFilePath = path.join(tmpDir, `context_${Date.now()}.md`); try { fs.writeFileSync(tempContextFilePath, contextContent); const pythonScript = path.join(__dirname, '..', 'market_intel_orchestrator.py'); const scriptArgs = [ pythonScript, '--mode', 'generate_strategy', '--reference_url', referenceUrl, '--context_file', tempContextFilePath, '--language', language || 'de' ]; runPython(scriptArgs, res, [tempContextFilePath]); } catch (writeError) { console.error('Failed to write temporary context file:', writeError); res.status(500).json({ error: 'Failed to write temporary file', details: writeError.message }); } }); // API-Endpunkt für identifyCompetitors app.post('/api/identify-competitors', async (req, res) => { logToFile(`[${new Date().toISOString()}] HIT: /api/identify-competitors`); const { referenceUrl, targetMarket, contextContent, referenceCity, referenceCountry, summaryOfOffer, language } = req.body; if (!referenceUrl || !targetMarket) { return res.status(400).json({ error: 'Missing referenceUrl or targetMarket' }); } const tmpDir = path.join(__dirname, 'tmp'); 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); cleanupFiles.push(tempContextFilePath); } const pythonScript = path.join(__dirname, '..', 'market_intel_orchestrator.py'); const scriptArgs = [ pythonScript, '--mode', 'identify_competitors', '--reference_url', referenceUrl, '--target_market', targetMarket, '--language', language || 'de' ]; 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); runPython(scriptArgs, res, cleanupFiles); } catch (writeError) { res.status(500).json({ error: 'Failed to write temporary file', details: writeError.message }); } }); // API-Endpunkt für analyze-company (Deep Tech Audit) app.post('/api/analyze-company', async (req, res) => { logToFile(`[${new Date().toISOString()}] HIT: /api/analyze-company`); const { companyName, strategy, targetMarket, language } = req.body; if (!companyName || !strategy) { return res.status(400).json({ error: 'Missing companyName or strategy' }); } 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', '--language', language || 'de' ]; runPython(scriptArgs, res); }); // API-Endpunkt für generate-outreach app.post('/api/generate-outreach', async (req, res) => { logToFile(`[${new Date().toISOString()}] HIT: /api/generate-outreach`); // Set a long timeout for this specific route (5 minutes) req.setTimeout(300000); const { companyData, knowledgeBase, referenceUrl, specific_role, language } = req.body; if (!companyData || !knowledgeBase) { return res.status(400).json({ error: 'Missing companyData or knowledgeBase' }); } const tmpDir = path.join(__dirname, 'tmp'); 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); const pythonScript = path.join(__dirname, '..', 'market_intel_orchestrator.py'); const scriptArgs = [ pythonScript, '--mode', 'generate_outreach', '--company_data_file', tempDataFilePath, '--context_file', tempContextFilePath, '--reference_url', referenceUrl || '', '--language', language || 'de' ]; if (specific_role) { scriptArgs.push('--specific_role', specific_role); } runPython(scriptArgs, res, [tempDataFilePath, tempContextFilePath]); } catch (err) { res.status(500).json({ error: err.message }); } }); // --- DATABASE ROUTES --- // Initialize DB on startup const dbScript = path.join(__dirname, '..', 'market_db_manager.py'); spawn('python3', [dbScript, 'init']); app.get('/api/projects', (req, res) => { runPython([dbScript, 'list'], res); }); app.get('/api/projects/:id', (req, res) => { runPython([dbScript, 'load', req.params.id], res); }); app.delete('/api/projects/:id', (req, res) => { runPython([dbScript, 'delete', req.params.id], res); }); app.post('/api/save-project', (req, res) => { const projectData = req.body; const tmpDir = path.join(__dirname, 'tmp'); if (!fs.existsSync(tmpDir)) fs.mkdirSync(tmpDir); const tempFilePath = path.join(tmpDir, `save_${Date.now()}.json`); try { fs.writeFileSync(tempFilePath, JSON.stringify(projectData)); runPython([dbScript, 'save', tempFilePath], res, [tempFilePath]); } catch (e) { res.status(500).json({ error: 'Failed to write project data to disk' }); } }); const server = app.listen(PORT, () => { console.log(`Node.js API Bridge running on http://localhost:${PORT} (Version: 1.1.0-Fix)`); }); // Set timeout to 10 minutes (600s) to handle long AI generation steps server.setTimeout(600000); server.keepAliveTimeout = 610000; server.headersTimeout = 620000;