diff --git a/content-engine/content_db_manager.py b/content-engine/content_db_manager.py index bf2cbad4..3c94b086 100644 --- a/content-engine/content_db_manager.py +++ b/content-engine/content_db_manager.py @@ -1,16 +1,25 @@ + import sqlite3 import json import os import logging from datetime import datetime +# Logging setup (rely on orchestrator config if imported, or basic config) +if not logging.getLogger().handlers: + logging.basicConfig(level=logging.INFO) + DB_PATH = os.environ.get('DB_PATH', 'content_engine.db') GTM_DB_PATH = os.environ.get('GTM_DB_PATH', 'gtm_projects.db') def get_db_connection(path=DB_PATH): - conn = sqlite3.connect(path) - conn.row_factory = sqlite3.Row - return conn + try: + conn = sqlite3.connect(path) + conn.row_factory = sqlite3.Row + return conn + except Exception as e: + logging.error(f"Failed to connect to DB at {path}: {e}") + raise def init_db(): conn = get_db_connection() @@ -51,16 +60,17 @@ def init_db(): conn.commit() conn.close() - logging.info(f"Database initialized at {DB_PATH}") + logging.info(f"Database initialized/verified at {DB_PATH}") # --- GTM READ ACCESS --- def get_all_gtm_projects(): """Lists all available GTM projects.""" if not os.path.exists(GTM_DB_PATH): - logging.warning(f"GTM DB not found at {GTM_DB_PATH}") + logging.warning(f"GTM DB NOT FOUND at {GTM_DB_PATH}. Cannot list projects.") return [] + logging.info(f"Connecting to GTM DB at {GTM_DB_PATH}") conn = get_db_connection(GTM_DB_PATH) try: query = """ @@ -73,16 +83,23 @@ def get_all_gtm_projects(): ORDER BY updated_at DESC """ projects = [dict(row) for row in conn.execute(query).fetchall()] + logging.info(f"Retrieved {len(projects)} GTM projects.") return projects finally: conn.close() def get_gtm_project_data(gtm_id): """Retrieves full data for a GTM project.""" + logging.info(f"Fetching GTM data for ID: {gtm_id}") conn = get_db_connection(GTM_DB_PATH) try: row = conn.execute("SELECT data FROM gtm_projects WHERE id = ?", (gtm_id,)).fetchone() - return json.loads(row['data']) if row else None + if row: + logging.info("GTM Data found.") + return json.loads(row['data']) + else: + logging.warning("GTM Data NOT found.") + return None finally: conn.close() @@ -90,14 +107,22 @@ def get_gtm_project_data(gtm_id): def import_gtm_project(gtm_id): """Imports a GTM project as a new Content Engine project.""" + logging.info(f"Importing GTM ID: {gtm_id}") gtm_data = get_gtm_project_data(gtm_id) if not gtm_data: + logging.error("Import failed: No data returned from GTM DB.") return None name = gtm_data.get('name', 'Imported Project') # Phase 1 has the category phase1 = gtm_data.get('phases', {}).get('phase1_result', {}) - if isinstance(phase1, str): phase1 = json.loads(phase1) + if isinstance(phase1, str): + try: + phase1 = json.loads(phase1) + except: + logging.warning("Could not parse Phase 1 JSON string.") + phase1 = {} + category = phase1.get('category', 'Unknown') conn = get_db_connection() @@ -109,6 +134,7 @@ def import_gtm_project(gtm_id): project_id = cursor.lastrowid conn.commit() conn.close() + logging.info(f"Project imported successfully into Content DB. New ID: {project_id}") return {"id": project_id, "name": name, "category": category} def get_all_content_projects(): @@ -176,4 +202,4 @@ def get_project_assets(project_id): return assets if __name__ == "__main__": - init_db() \ No newline at end of file + init_db() diff --git a/content-engine/content_orchestrator.py b/content-engine/content_orchestrator.py index dbe5ec19..7f92f170 100644 --- a/content-engine/content_orchestrator.py +++ b/content-engine/content_orchestrator.py @@ -1,4 +1,3 @@ - import argparse import base64 import json @@ -15,14 +14,34 @@ sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")) from helpers import call_gemini_flash, scrape_website_details from config import Config -LOG_DIR = "Log_from_docker" +# --- LOGGING CONFIGURATION --- +LOG_DIR = "/app/Log_from_docker" if not os.path.exists(LOG_DIR): - os.makedirs(LOG_DIR) + try: + os.makedirs(LOG_DIR) + except: + pass # Should be mounted -run_timestamp = datetime.now().strftime("%y-%m-%d_%H-%M-%S") -logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') +run_timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") +log_file = os.path.join(LOG_DIR, f"content_python_debug.log") -Config.load_api_keys() +# Configure root logger +logging.basicConfig( + level=logging.DEBUG, + format='%(asctime)s [%(levelname)s] %(message)s', + handlers=[ + logging.FileHandler(log_file), + logging.StreamHandler(sys.stderr) # Write logs to stderr so stdout keeps clean JSON + ] +) + +logging.info(f"--- Content Orchestrator Started ({run_timestamp}) ---") + +try: + Config.load_api_keys() + logging.info("API Keys loaded successfully.") +except Exception as e: + logging.error(f"Failed to load API keys: {e}") def get_copywriter_instruction(lang='de'): return r""" @@ -39,112 +58,161 @@ def get_copywriter_instruction(lang='de'): # --- MODES --- def list_gtm_projects(payload): - projects = db_manager.get_all_gtm_projects() - return {"projects": projects} + logging.info("Executing list_gtm_projects") + try: + projects = db_manager.get_all_gtm_projects() + logging.debug(f"Found {len(projects)} GTM projects") + return {"projects": projects} + except Exception as e: + logging.error(f"Error listing GTM projects: {e}", exc_info=True) + return {"error": str(e), "projects": []} def import_project(payload): + logging.info(f"Executing import_project with payload keys: {list(payload.keys())}") gtm_id = payload.get('gtmProjectId') + if not gtm_id: + logging.error("Missing gtmProjectId in payload") return {"error": "Missing gtmProjectId"} - result = db_manager.import_gtm_project(gtm_id) - if not result: - return {"error": "GTM Project not found or import failed"} - - return result + try: + logging.info(f"Attempting to import GTM Project ID: {gtm_id}") + result = db_manager.import_gtm_project(gtm_id) + + if not result: + logging.error("Import returned None (Project not found or GTM DB issue)") + return {"error": "GTM Project not found or import failed"} + + logging.info(f"Successfully imported Project. ID: {result.get('id')}") + return result + except Exception as e: + logging.error(f"Exception during import: {e}", exc_info=True) + return {"error": str(e)} def list_content_projects(payload): - projects = db_manager.get_all_content_projects() - return {"projects": projects} + logging.info("Executing list_content_projects") + try: + projects = db_manager.get_all_content_projects() + logging.debug(f"Found {len(projects)} Content projects") + return {"projects": projects} + except Exception as e: + logging.error(f"Error listing Content projects: {e}", exc_info=True) + return {"error": str(e), "projects": []} def load_project_details(payload): project_id = payload.get('projectId') - project = db_manager.get_content_project(project_id) - if not project: - return {"error": "Project not found"} + logging.info(f"Loading project details for ID: {project_id}") - assets = db_manager.get_project_assets(project_id) - project['assets'] = assets - return project + try: + project = db_manager.get_content_project(project_id) + if not project: + logging.warning(f"Project ID {project_id} not found") + return {"error": "Project not found"} + + assets = db_manager.get_project_assets(project_id) + project['assets'] = assets + logging.info(f"Loaded project '{project.get('name')}' with {len(assets)} assets.") + return project + except Exception as e: + logging.error(f"Error loading project: {e}", exc_info=True) + return {"error": str(e)} def seo_brainstorming(payload): project_id = payload.get('projectId') lang = payload.get('lang', 'de') + logging.info(f"Starting SEO Brainstorming for Project ID: {project_id}") - project = db_manager.get_content_project(project_id) - if not project: - return {"error": "Project context not found"} - - gtm_data = project.get('gtm_data_snapshot', {}) - - # GOLDEN RULE: Use Raw Quotes and .format() - prompt = r""" - Basierend auf folgendem GTM-Kontext (Strategie für ein technisches Produkt): - {gtm_context} - - AUFGABE: - Generiere eine strategische Liste von 15 SEO-Keywords. - 1. 5 Short-Tail Fokus-Keywords (z.B. Produktkategorie + 'kaufen/mieten'). - 2. 10 Long-Tail Keywords, die spezifische Pain Points oder Usecases adressieren (z.B. 'Kostenreduktion bei Sicherheitsrundgängen'). - - Die Keywords müssen für Entscheider relevant sein (CFO, Head of Security, Operations Manager). - - Output NUR als JSON Liste von Strings. - """.format(gtm_context=json.dumps(gtm_data)) - - response = call_gemini_flash(prompt, system_instruction=get_copywriter_instruction(lang), json_mode=True) - keywords = json.loads(response) - - db_manager.save_seo_strategy(project_id, {"seed_keywords": keywords}) - return {"keywords": keywords} + try: + project = db_manager.get_content_project(project_id) + if not project: + return {"error": "Project context not found"} + + gtm_data = project.get('gtm_data_snapshot', {}) + logging.debug(f"Loaded GTM context snapshot size: {len(str(gtm_data))} chars") + + prompt = r""" + Basierend auf folgendem GTM-Kontext (Strategie für ein technisches Produkt): + {gtm_context} + + AUFGABE: + Generiere eine strategische Liste von 15 SEO-Keywords. + 1. 5 Short-Tail Fokus-Keywords (z.B. Produktkategorie + 'kaufen/mieten'). + 2. 10 Long-Tail Keywords, die spezifische Pain Points oder Usecases adressieren (z.B. 'Kostenreduktion bei Sicherheitsrundgängen'). + + Die Keywords müssen für Entscheider relevant sein (CFO, Head of Security, Operations Manager). + + Output NUR als JSON Liste von Strings. + """.format(gtm_context=json.dumps(gtm_data)) + + logging.info("Calling Gemini Flash for keywords...") + response = call_gemini_flash(prompt, system_instruction=get_copywriter_instruction(lang), json_mode=True) + logging.info("Gemini response received.") + + keywords = json.loads(response) + + db_manager.save_seo_strategy(project_id, {"seed_keywords": keywords}) + logging.info("Keywords saved to DB.") + return {"keywords": keywords} + except Exception as e: + logging.error(f"Error in SEO Brainstorming: {e}", exc_info=True) + return {"error": str(e)} def generate_section(payload): project_id = payload.get('projectId') - section_key = payload.get('sectionKey') # e.g., 'hero', 'problem', 'features' + section_key = payload.get('sectionKey') manual_content = payload.get('manualContent') lang = payload.get('lang', 'de') keywords = payload.get('keywords', []) - if manual_content: - # User is saving their manual edits - db_manager.save_content_asset(project_id, 'website_section', section_key, f"Section: {section_key}", manual_content, keywords) - return {"status": "saved", "sectionKey": section_key} + logging.info(f"Generating section '{section_key}' for Project {project_id}") - project = db_manager.get_content_project(project_id) - if not project: - return {"error": "Project context not found"} + if manual_content: + logging.info("Saving manual content update.") + try: + db_manager.save_content_asset(project_id, 'website_section', section_key, f"Section: {section_key}", manual_content, keywords) + return {"status": "saved", "sectionKey": section_key} + except Exception as e: + logging.error(f"Error saving manual content: {e}", exc_info=True) + return {"error": str(e)} + + try: + project = db_manager.get_content_project(project_id) + if not project: + return {"error": "Project context not found"} + + gtm_data = project.get('gtm_data_snapshot', {}) + category = project.get('category') - gtm_data = project.get('gtm_data_snapshot', {}) - - # Context extraction - category = project.get('category') - - prompt = r""" - Erstelle den Website-Inhalt für die Sektion '{section}' eines Produkts in der Kategorie '{cat}'. - - STRATEGIE-KONTEXT: - {gtm_context} - - SEO-KEYWORDS ZU NUTZEN: - {kws} - - ANFORDERUNG: - - Schreibe im Stil eines Senior Copywriters (fachlich fundiert, Challenger Sale). - - Format: Markdown. - - Die Sektion muss den Nutzer zur nächsten Aktion (CTA) führen. - """.format( - section=section_key, - cat=category, - gtm_context=json.dumps(gtm_data), - kws=json.dumps(keywords) - ) - - content = call_gemini_flash(prompt, system_instruction=get_copywriter_instruction(lang), json_mode=False) - - # Save as asset - db_manager.save_content_asset(project_id, 'website_section', section_key, f"Section: {section_key}", content, keywords) - - return {"content": content, "sectionKey": section_key} + prompt = r""" + Erstelle den Website-Inhalt für die Sektion '{section}' eines Produkts in der Kategorie '{cat}'. + + STRATEGIE-KONTEXT: + {gtm_context} + + SEO-KEYWORDS ZU NUTZEN: + {kws} + + ANFORDERUNG: + - Schreibe im Stil eines Senior Copywriters (fachlich fundiert, Challenger Sale). + - Format: Markdown. + - Die Sektion muss den Nutzer zur nächsten Aktion (CTA) führen. + """.format( + section=section_key, + cat=category, + gtm_context=json.dumps(gtm_data), + kws=json.dumps(keywords) + ) + + logging.info("Calling Gemini for copy generation...") + content = call_gemini_flash(prompt, system_instruction=get_copywriter_instruction(lang), json_mode=False) + logging.info("Copy generated.") + + db_manager.save_content_asset(project_id, 'website_section', section_key, f"Section: {section_key}", content, keywords) + + return {"content": content, "sectionKey": section_key} + except Exception as e: + logging.error(f"Error generating section: {e}", exc_info=True) + return {"error": str(e)} def main(): parser = argparse.ArgumentParser(description="Content Engine Orchestrator") @@ -152,11 +220,18 @@ def main(): parser.add_argument("--payload_file", help="Path to JSON payload") args = parser.parse_args() + logging.info(f"Orchestrator called with mode: {args.mode}") payload = {} if args.payload_file: - with open(args.payload_file, 'r') as f: - payload = json.load(f) + try: + with open(args.payload_file, 'r') as f: + payload = json.load(f) + logging.debug("Payload loaded successfully.") + except Exception as e: + logging.error(f"Failed to load payload file: {e}") + print(json.dumps({"error": "Failed to load payload"})) + sys.exit(1) modes = { "list_gtm_projects": list_gtm_projects, @@ -170,12 +245,14 @@ def main(): if args.mode in modes: try: result = modes[args.mode](payload) + # DUMP RESULT TO STDOUT print(json.dumps(result, ensure_ascii=False)) except Exception as e: - logging.error(f"Error in mode {args.mode}: {str(e)}") - print(json.dumps({"error": str(e)})) + logging.critical(f"Unhandled exception in mode execution: {e}", exc_info=True) + print(json.dumps({"error": f"Internal Error: {str(e)}"})) else: + logging.error(f"Unknown mode: {args.mode}") print(json.dumps({"error": f"Unknown mode: {args.mode}"})) if __name__ == "__main__": - main() + main() \ No newline at end of file diff --git a/content-engine/frontend/src/App.tsx b/content-engine/frontend/src/App.tsx index 4e064740..c8a7552e 100644 --- a/content-engine/frontend/src/App.tsx +++ b/content-engine/frontend/src/App.tsx @@ -39,21 +39,25 @@ interface ContentAsset { // --- SUB-COMPONENTS --- -function SEOPlanner({ project, setLoading }: { project: ContentProject, setLoading: (b: boolean) => void }) { +function SEOPlanner({ project, setLoading, onUpdate }: { project: ContentProject, setLoading: (b: boolean) => void, onUpdate: () => void }) { const [keywords, setKeywords] = useState(project.seo_strategy?.seed_keywords || []); const generateKeywords = async () => { setLoading(true); try { - // FIX: Relative path const res = await fetch('api/seo_brainstorming', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ projectId: project.id }) }); const data = await res.json(); - setKeywords(data.keywords || []); - } catch (err) { console.error(err); } + if (data.error) { + alert(`Error: ${data.error}`); + } else { + setKeywords(data.keywords || []); + onUpdate(); + } + } catch (err) { console.error(err); alert("Network Error"); } setLoading(false); }; @@ -90,24 +94,25 @@ function SEOPlanner({ project, setLoading }: { project: ContentProject, setLoadi ); } -function WebsiteBuilder({ project, setLoading }: { project: ContentProject, setLoading: (b: boolean) => void }) { +function WebsiteBuilder({ project, setLoading, onUpdate }: { project: ContentProject, setLoading: (b: boolean) => void, onUpdate: () => void }) { const [sections, setSections] = useState(project.assets || []); const [editingContent, setEditingContent] = useState<{ [key: string]: string }>({}); useEffect(() => { - const newEditing: { [key: string]: string } = {}; - if (sections) { - sections.forEach(s => { + // When project updates (e.g. via onUpdate->Parent Refresh), update local sections + if (project.assets) { + setSections(project.assets); + const newEditing: { [key: string]: string } = {}; + project.assets.forEach(s => { newEditing[s.section_key] = s.content; }); + setEditingContent(newEditing); } - setEditingContent(newEditing); - }, [sections]); + }, [project.assets]); const generateSection = async (key: string) => { setLoading(true); try { - // FIX: Relative path const res = await fetch('api/generate_section', { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -118,11 +123,14 @@ function WebsiteBuilder({ project, setLoading }: { project: ContentProject, setL }) }); const data = await res.json(); - setSections(prev => { - const other = prev.filter(s => s.section_key !== key); - return [...other, { id: Date.now(), section_key: key, content: data.content, status: 'draft' }]; - }); - } catch (err) { console.error(err); } + + if (data.error) { + alert(`Error: ${data.error}`); + return; + } + + onUpdate(); // Refresh parent to get fresh data from DB + } catch (err) { console.error(err); alert("Network Error"); } setLoading(false); }; @@ -136,7 +144,6 @@ function WebsiteBuilder({ project, setLoading }: { project: ContentProject, setL setLoading(true); try { - // FIX: Relative path await fetch('api/generate_section', { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -146,8 +153,9 @@ function WebsiteBuilder({ project, setLoading }: { project: ContentProject, setL manualContent: content }) }); + onUpdate(); // Refresh parent alert("Saved successfully!"); - } catch (err) { console.error(err); } + } catch (err) { console.error(err); alert("Network Error"); } setLoading(false); }; @@ -234,7 +242,7 @@ function WebsiteBuilder({ project, setLoading }: { project: ContentProject, setL ); } -function ProjectDashboard({ project, onBack, setLoading }: { project: ContentProject, onBack: () => void, setLoading: (b: boolean) => void }) { +function ProjectDashboard({ project, onBack, setLoading, onRefresh }: { project: ContentProject, onBack: () => void, setLoading: (b: boolean) => void, onRefresh: () => void }) { const [activeTab, setActiveTab] = useState<'SEO' | 'WEBSITE' | 'SOCIAL'>('SEO'); return ( @@ -280,8 +288,8 @@ function ProjectDashboard({ project, onBack, setLoading }: { project: ContentPro {/* Tab Content */}
- {activeTab === 'SEO' && } - {activeTab === 'WEBSITE' && } + {activeTab === 'SEO' && } + {activeTab === 'WEBSITE' && } {activeTab === 'SOCIAL' && (
@@ -310,7 +318,6 @@ export default function App() { const fetchContentProjects = async () => { setLoading(true); try { - // FIX: Relative path const res = await fetch('api/list_content_projects', { method: 'POST', body: '{}', headers: {'Content-Type': 'application/json'} }); const data = await res.json(); setContentProjects(data.projects || []); @@ -321,7 +328,6 @@ export default function App() { const fetchGtmProjects = async () => { setLoading(true); try { - // FIX: Relative path const res = await fetch('api/list_gtm_projects', { method: 'POST', body: '{}', headers: {'Content-Type': 'application/json'} }); const data = await res.json(); setGtmProjects(data.projects || []); @@ -333,37 +339,50 @@ export default function App() { const handleImport = async (gtmId: string) => { setLoading(true); try { - // FIX: Relative path const res = await fetch('api/import_project', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ gtmProjectId: gtmId }) }); const data = await res.json(); - if (data.id) { + + if (data.error) { + alert(`Import Error: ${data.error}`); + } else if (data.id) { await fetchContentProjects(); setView('LIST'); } - } catch (err) { console.error(err); } + } catch (err) { console.error(err); alert("Network Error"); } setLoading(false); }; const loadProject = async (id: number) => { setLoading(true); try { - // FIX: Relative path const res = await fetch('api/load_project', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ projectId: id }) }); const data = await res.json(); - setSelectedProject(data); - setView('DETAILS'); - } catch (err) { console.error(err); } + + if (data.error) { + alert(`Load Error: ${data.error}`); + } else { + setSelectedProject(data); + setView('DETAILS'); + } + } catch (err) { console.error(err); alert("Network Error"); } setLoading(false); }; + // Wrapper for refreshing data inside Dashboard + const handleRefreshProject = async () => { + if (selectedProject) { + await loadProject(selectedProject.id); + } + }; + return (
{/* Header */} @@ -501,6 +520,7 @@ export default function App() { project={selectedProject} onBack={() => setView('LIST')} setLoading={setLoading} + onRefresh={handleRefreshProject} /> )} diff --git a/content-engine/server.cjs b/content-engine/server.cjs index 2c4adbcf..bec0b059 100644 --- a/content-engine/server.cjs +++ b/content-engine/server.cjs @@ -1,3 +1,4 @@ + const express = require('express'); const { spawn } = require('child_process'); const path = require('path'); @@ -6,21 +7,63 @@ const fs = require('fs'); const app = express(); const port = process.env.PORT || 3006; +// LOGGING SETUP +const LOG_DIR = '/app/Log_from_docker'; +if (!fs.existsSync(LOG_DIR)) { + try { + fs.mkdirSync(LOG_DIR, { recursive: true }); + } catch (e) { + console.error("Could not create Log directory:", e); + } +} +const LOG_FILE = path.join(LOG_DIR, 'content_node_server.log'); + +function log(message, level = 'INFO') { + const timestamp = new Date().toISOString(); + const logLine = `[${timestamp}] [${level}] ${message}\n`; + + // Console output + if (level === 'ERROR') console.error(logLine); + else console.log(logLine); + + // File output + try { + fs.appendFileSync(LOG_FILE, logLine); + } catch (e) { + console.error("Failed to write to log file:", e); + } +} + app.use(express.json({ limit: '50mb' })); // INITIALIZE DATABASE ON START const dbScript = path.join(__dirname, 'content_db_manager.py'); -console.log("Initializing database..."); -spawn('python3', [dbScript]); +log(`Initializing database via ${dbScript}...`); +const dbInit = spawn('python3', [dbScript]); + +dbInit.stdout.on('data', (data) => log(`DB Init Output: ${data}`)); +dbInit.stderr.on('data', (data) => log(`DB Init Error: ${data}`, 'ERROR')); +dbInit.on('close', (code) => log(`DB Init finished with code ${code}`)); + // Helper to run python commands function runPython(mode, payload) { return new Promise((resolve, reject) => { const payloadFile = path.join(__dirname, `payload_${Date.now()}.json`); - fs.writeFileSync(payloadFile, JSON.stringify(payload)); + + try { + fs.writeFileSync(payloadFile, JSON.stringify(payload)); + log(`Created payload file: ${payloadFile} for mode: ${mode}`); + } catch (e) { + log(`Failed to write payload file: ${e.message}`, 'ERROR'); + return reject(e); + } + + const scriptPath = path.join(__dirname, 'content_orchestrator.py'); + log(`Spawning Python: python3 ${scriptPath} --mode ${mode} --payload_file ${payloadFile}`); const pythonProcess = spawn('python3', [ - path.join(__dirname, 'content_orchestrator.py'), + scriptPath, '--mode', mode, '--payload_file', payloadFile ]); @@ -28,18 +71,40 @@ function runPython(mode, payload) { let stdout = ''; let stderr = ''; - pythonProcess.stdout.on('data', (data) => stdout += data.toString()); - pythonProcess.stderr.on('data', (data) => stderr += data.toString()); + pythonProcess.stdout.on('data', (data) => { + stdout += data.toString(); + }); + + pythonProcess.stderr.on('data', (data) => { + stderr += data.toString(); + }); pythonProcess.on('close', (code) => { - if (fs.existsSync(payloadFile)) fs.unlinkSync(payloadFile); - if (code !== 0) { - console.error(`Python error (code ${code}):`, stderr); - return reject(stderr); + // Cleanup payload file + if (fs.existsSync(payloadFile)) { + try { + fs.unlinkSync(payloadFile); + } catch(e) { + log(`Warning: Could not delete payload file: ${e.message}`, 'WARN'); + } } + + if (code !== 0) { + log(`Python script exited with code ${code}. Stderr: ${stderr}`, 'ERROR'); + return reject(stderr || "Unknown Python Error"); + } + + // Log stderr anyway as it might contain debug info (Python logging often goes to stderr) + if (stderr) { + log(`Python Stderr (Debug): ${stderr}`); + } + try { - resolve(JSON.parse(stdout)); + log(`Python Stdout received (${stdout.length} bytes). Parsing JSON...`); + const parsed = JSON.parse(stdout); + resolve(parsed); } catch (e) { + log(`Failed to parse JSON output: ${stdout.substring(0, 200)}...`, 'ERROR'); reject("Failed to parse Python output: " + stdout); } }); @@ -47,15 +112,19 @@ function runPython(mode, payload) { } app.post('/api/:mode', async (req, res) => { + const mode = req.params.mode; + log(`Incoming POST request for mode: ${mode}`); try { - const result = await runPython(req.params.mode, req.body); + const result = await runPython(mode, req.body); + log(`Request for ${mode} completed successfully.`); res.json(result); } catch (error) { + log(`Request for ${mode} failed: ${error}`, 'ERROR'); res.status(500).json({ error: error.toString() }); } }); -// Serve static assets from build (for production) +// Serve static assets if (fs.existsSync(path.join(__dirname, 'dist'))) { app.use(express.static(path.join(__dirname, 'dist'))); app.get('*', (req, res) => { @@ -64,5 +133,5 @@ if (fs.existsSync(path.join(__dirname, 'dist'))) { } app.listen(port, () => { - console.log(`Content Engine Server running on port ${port}`); -}); \ No newline at end of file + log(`Content Engine Server running on port ${port}`); +});