fix(content): implement state refresh on update to prevent data loss on tab switch
This commit is contained in:
@@ -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):
|
||||
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():
|
||||
|
||||
@@ -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):
|
||||
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")
|
||||
|
||||
# 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,45 +58,78 @@ def get_copywriter_instruction(lang='de'):
|
||||
# --- MODES ---
|
||||
|
||||
def list_gtm_projects(payload):
|
||||
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"}
|
||||
|
||||
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):
|
||||
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')
|
||||
logging.info(f"Loading project details for ID: {project_id}")
|
||||
|
||||
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}")
|
||||
|
||||
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")
|
||||
|
||||
# GOLDEN RULE: Use Raw Quotes and .format()
|
||||
prompt = r"""
|
||||
Basierend auf folgendem GTM-Kontext (Strategie für ein technisches Produkt):
|
||||
{gtm_context}
|
||||
@@ -92,31 +144,43 @@ def seo_brainstorming(payload):
|
||||
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', [])
|
||||
|
||||
logging.info(f"Generating section '{section_key}' for Project {project_id}")
|
||||
|
||||
if manual_content:
|
||||
# User is saving their manual edits
|
||||
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', {})
|
||||
|
||||
# Context extraction
|
||||
category = project.get('category')
|
||||
|
||||
prompt = r"""
|
||||
@@ -139,12 +203,16 @@ def generate_section(payload):
|
||||
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.")
|
||||
|
||||
# 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}
|
||||
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:
|
||||
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,11 +245,13 @@ 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__":
|
||||
|
||||
@@ -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();
|
||||
if (data.error) {
|
||||
alert(`Error: ${data.error}`);
|
||||
} else {
|
||||
setKeywords(data.keywords || []);
|
||||
} catch (err) { console.error(err); }
|
||||
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(() => {
|
||||
// When project updates (e.g. via onUpdate->Parent Refresh), update local sections
|
||||
if (project.assets) {
|
||||
setSections(project.assets);
|
||||
const newEditing: { [key: string]: string } = {};
|
||||
if (sections) {
|
||||
sections.forEach(s => {
|
||||
project.assets.forEach(s => {
|
||||
newEditing[s.section_key] = s.content;
|
||||
});
|
||||
}
|
||||
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();
|
||||
|
||||
if (data.error) {
|
||||
alert(`Load Error: ${data.error}`);
|
||||
} else {
|
||||
setSelectedProject(data);
|
||||
setView('DETAILS');
|
||||
} catch (err) { console.error(err); }
|
||||
}
|
||||
} 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>
|
||||
|
||||
@@ -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`);
|
||||
|
||||
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 {
|
||||
resolve(JSON.parse(stdout));
|
||||
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 {
|
||||
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}`);
|
||||
});
|
||||
Reference in New Issue
Block a user