From a5f43fb977abc3a41a4b305cfed04d8123720760 Mon Sep 17 00:00:00 2001 From: Floke Date: Mon, 5 Jan 2026 11:42:15 +0000 Subject: [PATCH] feat(gtm): add aspect ratio & corporate design; fix(market): harden backend logging & json parsing --- GEMINI.md | 13 ++ config.py | 8 + general-market-intelligence/server.cjs | 120 +++++++++------ .../services/geminiService.ts | 8 +- gtm-architect/App.tsx | 20 ++- gtm-architect/geminiService.ts | 4 +- gtm_architect_documentation.md | 64 +++++--- gtm_architect_orchestrator.py | 10 +- helpers.py | 23 ++- market_intel_orchestrator.py | 140 +++++++----------- 10 files changed, 241 insertions(+), 169 deletions(-) diff --git a/GEMINI.md b/GEMINI.md index f0eeee91..f419fd92 100644 --- a/GEMINI.md +++ b/GEMINI.md @@ -52,6 +52,19 @@ The application will be available at `http://localhost:8080`. * **Logging:** The project uses the `logging` module to log information and errors. * **Error Handling:** The `readme.md` indicates a critical error related to the `openai` library. The next step is to downgrade the library to a compatible version. +## Current Status (Jan 05, 2026) - GTM & Market Intel Fixes + +* **GTM Architect (v2.4) - UI/UX Refinement:** + * **Corporate Design Integration:** A central, customizable `CORPORATE_DESIGN_PROMPT` was introduced in `config.py` to ensure all generated images strictly follow a "clean, professional, photorealistic" B2B style, avoiding comic aesthetics. + * **Aspect Ratio Control:** Implemented user-selectable aspect ratios (16:9, 9:16, 1:1, 4:3) in the frontend (Phase 6), passing through to the Google Imagen/Gemini 2.5 API. + * **Frontend Fix:** Resolved a double-declaration bug in `App.tsx` that prevented the build. + +* **Market Intelligence Tool (v1.2) - Backend Hardening:** + * **"Failed to fetch" Resolved:** Fixed a critical Nginx routing issue by forcing the frontend to use relative API paths (`./api`) instead of absolute ports, ensuring requests correctly pass through the reverse proxy in Docker. + * **JSON Stability:** The Python Orchestrator and Node.js bridge were hardened against invalid JSON output. The system now robustly handles stdout noise and logs full raw output to `/app/Log/server_dump.txt` in case of errors. + * **Language Support:** Implemented a `--language` flag. The tool now correctly respects the frontend language selection (defaulting to German) and forces the LLM to output German text for signals, ICPs, and outreach campaigns. + * **Logging:** Fixed log volume mounting paths to ensure debug logs are persisted and accessible. + ## Current Status (Jan 2026) - GTM Architect & Core Updates * **GTM Architect (v2.2) - FULLY OPERATIONAL:** diff --git a/config.py b/config.py index 3e9a7c0b..4a1848f3 100644 --- a/config.py +++ b/config.py @@ -97,6 +97,14 @@ class Config: PROCESSING_BRANCH_BATCH_SIZE = 20 SERPAPI_DELAY = 1.5 + # --- (NEU) GTM Architect: Stilvorgabe für Bildgenerierung --- + CORPORATE_DESIGN_PROMPT = ( + "cinematic industrial photography, sleek high-tech aesthetic, futuristic but grounded reality, " + "volumetric lighting, sharp focus on modern technology, 8k resolution, photorealistic, " + "highly detailed textures, cool steel-blue color grading with subtle safety-yellow accents, " + "wide angle lens, shallow depth of field." + ) + # --- Plausibilitäts-Schwellenwerte --- PLAUSI_UMSATZ_MIN_WARNUNG = 50000 PLAUSI_UMSATZ_MAX_WARNUNG = 200000000000 diff --git a/general-market-intelligence/server.cjs b/general-market-intelligence/server.cjs index 1a2e6ffd..9e49e81a 100644 --- a/general-market-intelligence/server.cjs +++ b/general-market-intelligence/server.cjs @@ -8,15 +8,48 @@ 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); @@ -33,31 +66,10 @@ const runPython = (args, res, tempFilesToDelete = []) => { }); 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); - } + logToFile(`Python exited with code ${code}`); - 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); - } - } - }); + // GLOBAL DUMP + dumpToFile(`STDOUT:\n${pythonOutput}\n\nSTDERR:\n${pythonError}`); if (code !== 0) { console.error(`Python script exited with error.`); @@ -65,19 +77,34 @@ const runPython = (args, res, tempFilesToDelete = []) => { } try { - const result = JSON.parse(pythonOutput); + // 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', rawOutput: pythonOutput, details: pythonError }); + 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) => { - console.error('FATAL: Failed to start python process itself.', err); - tempFilesToDelete.forEach(file => { - if (fs.existsSync(file)) fs.unlinkSync(file); - }); + logToFile(`FATAL: Failed to start python process: ${err.message}`); res.status(500).json({ error: 'Failed to start Python process', details: err.message }); }); }; @@ -85,8 +112,8 @@ const runPython = (args, res, tempFilesToDelete = []) => { // API-Endpunkt für generateSearchStrategy app.post('/api/generate-search-strategy', async (req, res) => { - console.log(`[${new Date().toISOString()}] HIT: /api/generate-search-strategy`); - const { referenceUrl, contextContent } = req.body; + 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' }); @@ -100,7 +127,13 @@ app.post('/api/generate-search-strategy', async (req, res) => { 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]; + const scriptArgs = [ + pythonScript, + '--mode', 'generate_strategy', + '--reference_url', referenceUrl, + '--context_file', tempContextFilePath, + '--language', language || 'de' + ]; runPython(scriptArgs, res, [tempContextFilePath]); @@ -112,8 +145,8 @@ app.post('/api/generate-search-strategy', async (req, res) => { // API-Endpunkt für identifyCompetitors app.post('/api/identify-competitors', async (req, res) => { - console.log(`[${new Date().toISOString()}] HIT: /api/identify-competitors`); - const { referenceUrl, targetMarket, contextContent, referenceCity, referenceCountry, summaryOfOffer } = req.body; + 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' }); @@ -135,7 +168,8 @@ app.post('/api/identify-competitors', async (req, res) => { pythonScript, '--mode', 'identify_competitors', '--reference_url', referenceUrl, - '--target_market', targetMarket + '--target_market', targetMarket, + '--language', language || 'de' ]; if (contextContent) scriptArgs.push('--context_file', tempContextFilePath); @@ -152,8 +186,8 @@ app.post('/api/identify-competitors', async (req, res) => { // API-Endpunkt für analyze-company (Deep Tech Audit) app.post('/api/analyze-company', async (req, res) => { - console.log(`[${new Date().toISOString()}] HIT: /api/analyze-company`); - const { companyName, strategy, targetMarket } = req.body; + 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' }); @@ -165,7 +199,8 @@ app.post('/api/analyze-company', async (req, res) => { '--mode', 'analyze_company', '--company_name', companyName, '--strategy_json', JSON.stringify(strategy), - '--target_market', targetMarket || 'Germany' + '--target_market', targetMarket || 'Germany', + '--language', language || 'de' ]; runPython(scriptArgs, res); @@ -173,12 +208,12 @@ app.post('/api/analyze-company', async (req, res) => { // API-Endpunkt für generate-outreach app.post('/api/generate-outreach', async (req, res) => { - console.log(`[${new Date().toISOString()}] HIT: /api/generate-outreach`); + 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 } = req.body; + const { companyData, knowledgeBase, referenceUrl, specific_role, language } = req.body; if (!companyData || !knowledgeBase) { return res.status(400).json({ error: 'Missing companyData or knowledgeBase' }); @@ -199,7 +234,8 @@ app.post('/api/generate-outreach', async (req, res) => { '--mode', 'generate_outreach', '--company_data_file', tempDataFilePath, '--context_file', tempContextFilePath, - '--reference_url', referenceUrl || '' + '--reference_url', referenceUrl || '', + '--language', language || 'de' ]; if (specific_role) { diff --git a/general-market-intelligence/services/geminiService.ts b/general-market-intelligence/services/geminiService.ts index 223afec0..846aac45 100644 --- a/general-market-intelligence/services/geminiService.ts +++ b/general-market-intelligence/services/geminiService.ts @@ -1,11 +1,9 @@ import { LeadStatus, AnalysisResult, Competitor, Language, Tier, EmailDraft, SearchStrategy, SearchSignal, OutreachResponse } from "../types"; // 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 as any).env.PROD - ? 'api' - : `http://${window.location.hostname}:3001/api`; +// Wir nutzen IMMER den relativen Pfad './api', damit Requests korrekt über den Nginx Proxy (/market/api/...) laufen. +// Direkter Zugriff auf Port 3001 ist im Docker-Deployment von außen nicht möglich. +const API_BASE_URL = './api'; // Helper to extract JSON (kann ggf. entfernt werden, wenn das Backend immer sauberes JSON liefert) const extractJson = (text: string): any => { diff --git a/gtm-architect/App.tsx b/gtm-architect/App.tsx index 1619418b..0264feb3 100644 --- a/gtm-architect/App.tsx +++ b/gtm-architect/App.tsx @@ -227,9 +227,8 @@ const App: React.FC = () => { // Phase 6 Image Generation State const [generatingImages, setGeneratingImages] = useState>({}); const [generatedImages, setGeneratedImages] = useState>({}); - - // Phase 6 Editing State - const [editingIndex, setEditingIndex] = useState(null); + const [aspectRatio, setAspectRatio] = useState('16:9'); + const [editingIndex, setEditingIndex] = useState(null); const canvasRef = useRef(null); const [isDrawing, setIsDrawing] = useState(false); const [brushColor, setBrushColor] = useState('#ef4444'); // Red for annotations @@ -487,7 +486,7 @@ const App: React.FC = () => { // If not sketching, use the array of uploaded product images const refImages = overrideReference ? [overrideReference] : state.productImages; - const imageUrl = await Gemini.generateConceptImage(prompt, refImages); + const imageUrl = await Gemini.generateConceptImage(prompt, refImages, aspectRatio); setGeneratedImages(prev => ({ ...prev, [index]: imageUrl })); // If we were editing, close edit mode @@ -1699,7 +1698,18 @@ const App: React.FC = () => {

{prompt.title}

{prompt.context}

-
+
+ +