fix(content): implement state refresh on update to prevent data loss on tab switch

This commit is contained in:
2026-01-20 15:35:33 +00:00
parent 23b3e709b9
commit 7624bc9531
4 changed files with 336 additions and 144 deletions

View File

@@ -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()
init_db()

View File

@@ -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()

View File

@@ -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<string[]>(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<ContentAsset[]>(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 */}
<div className="mt-8 pt-8 border-t border-slate-700">
{activeTab === 'SEO' && <SEOPlanner project={project} setLoading={setLoading} />}
{activeTab === 'WEBSITE' && <WebsiteBuilder project={project} setLoading={setLoading} />}
{activeTab === 'SEO' && <SEOPlanner project={project} setLoading={setLoading} onUpdate={onRefresh} />}
{activeTab === 'WEBSITE' && <WebsiteBuilder project={project} setLoading={setLoading} onUpdate={onRefresh} />}
{activeTab === 'SOCIAL' && (
<div className="text-center py-20 bg-slate-900/30 rounded-2xl border border-slate-700 border-dashed">
<Edit3 size={48} className="mx-auto text-slate-700 mb-4" />
@@ -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 (
<div className="min-h-screen bg-slate-900 text-slate-100 font-sans selection:bg-blue-500/30">
{/* Header */}
@@ -501,6 +520,7 @@ export default function App() {
project={selectedProject}
onBack={() => setView('LIST')}
setLoading={setLoading}
onRefresh={handleRefreshProject}
/>
)}
</main>

View File

@@ -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}`);
});
log(`Content Engine Server running on port ${port}`);
});