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 sqlite3
import json import json
import os import os
import logging import logging
from datetime import datetime 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') DB_PATH = os.environ.get('DB_PATH', 'content_engine.db')
GTM_DB_PATH = os.environ.get('GTM_DB_PATH', 'gtm_projects.db') GTM_DB_PATH = os.environ.get('GTM_DB_PATH', 'gtm_projects.db')
def get_db_connection(path=DB_PATH): def get_db_connection(path=DB_PATH):
try:
conn = sqlite3.connect(path) conn = sqlite3.connect(path)
conn.row_factory = sqlite3.Row conn.row_factory = sqlite3.Row
return conn return conn
except Exception as e:
logging.error(f"Failed to connect to DB at {path}: {e}")
raise
def init_db(): def init_db():
conn = get_db_connection() conn = get_db_connection()
@@ -51,16 +60,17 @@ def init_db():
conn.commit() conn.commit()
conn.close() conn.close()
logging.info(f"Database initialized at {DB_PATH}") logging.info(f"Database initialized/verified at {DB_PATH}")
# --- GTM READ ACCESS --- # --- GTM READ ACCESS ---
def get_all_gtm_projects(): def get_all_gtm_projects():
"""Lists all available GTM projects.""" """Lists all available GTM projects."""
if not os.path.exists(GTM_DB_PATH): 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 [] return []
logging.info(f"Connecting to GTM DB at {GTM_DB_PATH}")
conn = get_db_connection(GTM_DB_PATH) conn = get_db_connection(GTM_DB_PATH)
try: try:
query = """ query = """
@@ -73,16 +83,23 @@ def get_all_gtm_projects():
ORDER BY updated_at DESC ORDER BY updated_at DESC
""" """
projects = [dict(row) for row in conn.execute(query).fetchall()] projects = [dict(row) for row in conn.execute(query).fetchall()]
logging.info(f"Retrieved {len(projects)} GTM projects.")
return projects return projects
finally: finally:
conn.close() conn.close()
def get_gtm_project_data(gtm_id): def get_gtm_project_data(gtm_id):
"""Retrieves full data for a GTM project.""" """Retrieves full data for a GTM project."""
logging.info(f"Fetching GTM data for ID: {gtm_id}")
conn = get_db_connection(GTM_DB_PATH) conn = get_db_connection(GTM_DB_PATH)
try: try:
row = conn.execute("SELECT data FROM gtm_projects WHERE id = ?", (gtm_id,)).fetchone() 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: finally:
conn.close() conn.close()
@@ -90,14 +107,22 @@ def get_gtm_project_data(gtm_id):
def import_gtm_project(gtm_id): def import_gtm_project(gtm_id):
"""Imports a GTM project as a new Content Engine project.""" """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) gtm_data = get_gtm_project_data(gtm_id)
if not gtm_data: if not gtm_data:
logging.error("Import failed: No data returned from GTM DB.")
return None return None
name = gtm_data.get('name', 'Imported Project') name = gtm_data.get('name', 'Imported Project')
# Phase 1 has the category # Phase 1 has the category
phase1 = gtm_data.get('phases', {}).get('phase1_result', {}) 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') category = phase1.get('category', 'Unknown')
conn = get_db_connection() conn = get_db_connection()
@@ -109,6 +134,7 @@ def import_gtm_project(gtm_id):
project_id = cursor.lastrowid project_id = cursor.lastrowid
conn.commit() conn.commit()
conn.close() conn.close()
logging.info(f"Project imported successfully into Content DB. New ID: {project_id}")
return {"id": project_id, "name": name, "category": category} return {"id": project_id, "name": name, "category": category}
def get_all_content_projects(): def get_all_content_projects():

View File

@@ -1,4 +1,3 @@
import argparse import argparse
import base64 import base64
import json 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 helpers import call_gemini_flash, scrape_website_details
from config import Config from config import Config
LOG_DIR = "Log_from_docker" # --- LOGGING CONFIGURATION ---
LOG_DIR = "/app/Log_from_docker"
if not os.path.exists(LOG_DIR): if not os.path.exists(LOG_DIR):
try:
os.makedirs(LOG_DIR) os.makedirs(LOG_DIR)
except:
pass # Should be mounted
run_timestamp = datetime.now().strftime("%y-%m-%d_%H-%M-%S") run_timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)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() 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'): def get_copywriter_instruction(lang='de'):
return r""" return r"""
@@ -39,45 +58,78 @@ def get_copywriter_instruction(lang='de'):
# --- MODES --- # --- MODES ---
def list_gtm_projects(payload): def list_gtm_projects(payload):
logging.info("Executing list_gtm_projects")
try:
projects = db_manager.get_all_gtm_projects() projects = db_manager.get_all_gtm_projects()
logging.debug(f"Found {len(projects)} GTM projects")
return {"projects": 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): def import_project(payload):
logging.info(f"Executing import_project with payload keys: {list(payload.keys())}")
gtm_id = payload.get('gtmProjectId') gtm_id = payload.get('gtmProjectId')
if not gtm_id: if not gtm_id:
logging.error("Missing gtmProjectId in payload")
return {"error": "Missing gtmProjectId"} return {"error": "Missing gtmProjectId"}
try:
logging.info(f"Attempting to import GTM Project ID: {gtm_id}")
result = db_manager.import_gtm_project(gtm_id) result = db_manager.import_gtm_project(gtm_id)
if not result: if not result:
logging.error("Import returned None (Project not found or GTM DB issue)")
return {"error": "GTM Project not found or import failed"} return {"error": "GTM Project not found or import failed"}
logging.info(f"Successfully imported Project. ID: {result.get('id')}")
return result 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): def list_content_projects(payload):
logging.info("Executing list_content_projects")
try:
projects = db_manager.get_all_content_projects() projects = db_manager.get_all_content_projects()
logging.debug(f"Found {len(projects)} Content projects")
return {"projects": 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): def load_project_details(payload):
project_id = payload.get('projectId') project_id = payload.get('projectId')
logging.info(f"Loading project details for ID: {project_id}")
try:
project = db_manager.get_content_project(project_id) project = db_manager.get_content_project(project_id)
if not project: if not project:
logging.warning(f"Project ID {project_id} not found")
return {"error": "Project not found"} return {"error": "Project not found"}
assets = db_manager.get_project_assets(project_id) assets = db_manager.get_project_assets(project_id)
project['assets'] = assets project['assets'] = assets
logging.info(f"Loaded project '{project.get('name')}' with {len(assets)} assets.")
return project return project
except Exception as e:
logging.error(f"Error loading project: {e}", exc_info=True)
return {"error": str(e)}
def seo_brainstorming(payload): def seo_brainstorming(payload):
project_id = payload.get('projectId') project_id = payload.get('projectId')
lang = payload.get('lang', 'de') 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) project = db_manager.get_content_project(project_id)
if not project: if not project:
return {"error": "Project context not found"} return {"error": "Project context not found"}
gtm_data = project.get('gtm_data_snapshot', {}) 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""" prompt = r"""
Basierend auf folgendem GTM-Kontext (Strategie für ein technisches Produkt): Basierend auf folgendem GTM-Kontext (Strategie für ein technisches Produkt):
{gtm_context} {gtm_context}
@@ -92,31 +144,43 @@ def seo_brainstorming(payload):
Output NUR als JSON Liste von Strings. Output NUR als JSON Liste von Strings.
""".format(gtm_context=json.dumps(gtm_data)) """.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) response = call_gemini_flash(prompt, system_instruction=get_copywriter_instruction(lang), json_mode=True)
logging.info("Gemini response received.")
keywords = json.loads(response) keywords = json.loads(response)
db_manager.save_seo_strategy(project_id, {"seed_keywords": keywords}) db_manager.save_seo_strategy(project_id, {"seed_keywords": keywords})
logging.info("Keywords saved to DB.")
return {"keywords": keywords} 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): def generate_section(payload):
project_id = payload.get('projectId') 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') manual_content = payload.get('manualContent')
lang = payload.get('lang', 'de') lang = payload.get('lang', 'de')
keywords = payload.get('keywords', []) keywords = payload.get('keywords', [])
logging.info(f"Generating section '{section_key}' for Project {project_id}")
if manual_content: 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) db_manager.save_content_asset(project_id, 'website_section', section_key, f"Section: {section_key}", manual_content, keywords)
return {"status": "saved", "sectionKey": section_key} 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) project = db_manager.get_content_project(project_id)
if not project: if not project:
return {"error": "Project context not found"} return {"error": "Project context not found"}
gtm_data = project.get('gtm_data_snapshot', {}) gtm_data = project.get('gtm_data_snapshot', {})
# Context extraction
category = project.get('category') category = project.get('category')
prompt = r""" prompt = r"""
@@ -139,12 +203,16 @@ def generate_section(payload):
kws=json.dumps(keywords) 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) 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) db_manager.save_content_asset(project_id, 'website_section', section_key, f"Section: {section_key}", content, keywords)
return {"content": content, "sectionKey": section_key} 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(): def main():
parser = argparse.ArgumentParser(description="Content Engine Orchestrator") parser = argparse.ArgumentParser(description="Content Engine Orchestrator")
@@ -152,11 +220,18 @@ def main():
parser.add_argument("--payload_file", help="Path to JSON payload") parser.add_argument("--payload_file", help="Path to JSON payload")
args = parser.parse_args() args = parser.parse_args()
logging.info(f"Orchestrator called with mode: {args.mode}")
payload = {} payload = {}
if args.payload_file: if args.payload_file:
try:
with open(args.payload_file, 'r') as f: with open(args.payload_file, 'r') as f:
payload = json.load(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 = { modes = {
"list_gtm_projects": list_gtm_projects, "list_gtm_projects": list_gtm_projects,
@@ -170,11 +245,13 @@ def main():
if args.mode in modes: if args.mode in modes:
try: try:
result = modes[args.mode](payload) result = modes[args.mode](payload)
# DUMP RESULT TO STDOUT
print(json.dumps(result, ensure_ascii=False)) print(json.dumps(result, ensure_ascii=False))
except Exception as e: except Exception as e:
logging.error(f"Error in mode {args.mode}: {str(e)}") logging.critical(f"Unhandled exception in mode execution: {e}", exc_info=True)
print(json.dumps({"error": str(e)})) print(json.dumps({"error": f"Internal Error: {str(e)}"}))
else: else:
logging.error(f"Unknown mode: {args.mode}")
print(json.dumps({"error": f"Unknown mode: {args.mode}"})) print(json.dumps({"error": f"Unknown mode: {args.mode}"}))
if __name__ == "__main__": if __name__ == "__main__":

View File

@@ -39,21 +39,25 @@ interface ContentAsset {
// --- SUB-COMPONENTS --- // --- 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 [keywords, setKeywords] = useState<string[]>(project.seo_strategy?.seed_keywords || []);
const generateKeywords = async () => { const generateKeywords = async () => {
setLoading(true); setLoading(true);
try { try {
// FIX: Relative path
const res = await fetch('api/seo_brainstorming', { const res = await fetch('api/seo_brainstorming', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ projectId: project.id }) body: JSON.stringify({ projectId: project.id })
}); });
const data = await res.json(); const data = await res.json();
if (data.error) {
alert(`Error: ${data.error}`);
} else {
setKeywords(data.keywords || []); setKeywords(data.keywords || []);
} catch (err) { console.error(err); } onUpdate();
}
} catch (err) { console.error(err); alert("Network Error"); }
setLoading(false); 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 [sections, setSections] = useState<ContentAsset[]>(project.assets || []);
const [editingContent, setEditingContent] = useState<{ [key: string]: string }>({}); const [editingContent, setEditingContent] = useState<{ [key: string]: string }>({});
useEffect(() => { useEffect(() => {
// When project updates (e.g. via onUpdate->Parent Refresh), update local sections
if (project.assets) {
setSections(project.assets);
const newEditing: { [key: string]: string } = {}; const newEditing: { [key: string]: string } = {};
if (sections) { project.assets.forEach(s => {
sections.forEach(s => {
newEditing[s.section_key] = s.content; newEditing[s.section_key] = s.content;
}); });
}
setEditingContent(newEditing); setEditingContent(newEditing);
}, [sections]); }
}, [project.assets]);
const generateSection = async (key: string) => { const generateSection = async (key: string) => {
setLoading(true); setLoading(true);
try { try {
// FIX: Relative path
const res = await fetch('api/generate_section', { const res = await fetch('api/generate_section', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
@@ -118,11 +123,14 @@ function WebsiteBuilder({ project, setLoading }: { project: ContentProject, setL
}) })
}); });
const data = await res.json(); const data = await res.json();
setSections(prev => {
const other = prev.filter(s => s.section_key !== key); if (data.error) {
return [...other, { id: Date.now(), section_key: key, content: data.content, status: 'draft' }]; alert(`Error: ${data.error}`);
}); return;
} catch (err) { console.error(err); } }
onUpdate(); // Refresh parent to get fresh data from DB
} catch (err) { console.error(err); alert("Network Error"); }
setLoading(false); setLoading(false);
}; };
@@ -136,7 +144,6 @@ function WebsiteBuilder({ project, setLoading }: { project: ContentProject, setL
setLoading(true); setLoading(true);
try { try {
// FIX: Relative path
await fetch('api/generate_section', { await fetch('api/generate_section', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
@@ -146,8 +153,9 @@ function WebsiteBuilder({ project, setLoading }: { project: ContentProject, setL
manualContent: content manualContent: content
}) })
}); });
onUpdate(); // Refresh parent
alert("Saved successfully!"); alert("Saved successfully!");
} catch (err) { console.error(err); } } catch (err) { console.error(err); alert("Network Error"); }
setLoading(false); 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'); const [activeTab, setActiveTab] = useState<'SEO' | 'WEBSITE' | 'SOCIAL'>('SEO');
return ( return (
@@ -280,8 +288,8 @@ function ProjectDashboard({ project, onBack, setLoading }: { project: ContentPro
{/* Tab Content */} {/* Tab Content */}
<div className="mt-8 pt-8 border-t border-slate-700"> <div className="mt-8 pt-8 border-t border-slate-700">
{activeTab === 'SEO' && <SEOPlanner project={project} setLoading={setLoading} />} {activeTab === 'SEO' && <SEOPlanner project={project} setLoading={setLoading} onUpdate={onRefresh} />}
{activeTab === 'WEBSITE' && <WebsiteBuilder project={project} setLoading={setLoading} />} {activeTab === 'WEBSITE' && <WebsiteBuilder project={project} setLoading={setLoading} onUpdate={onRefresh} />}
{activeTab === 'SOCIAL' && ( {activeTab === 'SOCIAL' && (
<div className="text-center py-20 bg-slate-900/30 rounded-2xl border border-slate-700 border-dashed"> <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" /> <Edit3 size={48} className="mx-auto text-slate-700 mb-4" />
@@ -310,7 +318,6 @@ export default function App() {
const fetchContentProjects = async () => { const fetchContentProjects = async () => {
setLoading(true); setLoading(true);
try { try {
// FIX: Relative path
const res = await fetch('api/list_content_projects', { method: 'POST', body: '{}', headers: {'Content-Type': 'application/json'} }); const res = await fetch('api/list_content_projects', { method: 'POST', body: '{}', headers: {'Content-Type': 'application/json'} });
const data = await res.json(); const data = await res.json();
setContentProjects(data.projects || []); setContentProjects(data.projects || []);
@@ -321,7 +328,6 @@ export default function App() {
const fetchGtmProjects = async () => { const fetchGtmProjects = async () => {
setLoading(true); setLoading(true);
try { try {
// FIX: Relative path
const res = await fetch('api/list_gtm_projects', { method: 'POST', body: '{}', headers: {'Content-Type': 'application/json'} }); const res = await fetch('api/list_gtm_projects', { method: 'POST', body: '{}', headers: {'Content-Type': 'application/json'} });
const data = await res.json(); const data = await res.json();
setGtmProjects(data.projects || []); setGtmProjects(data.projects || []);
@@ -333,37 +339,50 @@ export default function App() {
const handleImport = async (gtmId: string) => { const handleImport = async (gtmId: string) => {
setLoading(true); setLoading(true);
try { try {
// FIX: Relative path
const res = await fetch('api/import_project', { const res = await fetch('api/import_project', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ gtmProjectId: gtmId }) body: JSON.stringify({ gtmProjectId: gtmId })
}); });
const data = await res.json(); const data = await res.json();
if (data.id) {
if (data.error) {
alert(`Import Error: ${data.error}`);
} else if (data.id) {
await fetchContentProjects(); await fetchContentProjects();
setView('LIST'); setView('LIST');
} }
} catch (err) { console.error(err); } } catch (err) { console.error(err); alert("Network Error"); }
setLoading(false); setLoading(false);
}; };
const loadProject = async (id: number) => { const loadProject = async (id: number) => {
setLoading(true); setLoading(true);
try { try {
// FIX: Relative path
const res = await fetch('api/load_project', { const res = await fetch('api/load_project', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ projectId: id }) body: JSON.stringify({ projectId: id })
}); });
const data = await res.json(); const data = await res.json();
if (data.error) {
alert(`Load Error: ${data.error}`);
} else {
setSelectedProject(data); setSelectedProject(data);
setView('DETAILS'); setView('DETAILS');
} catch (err) { console.error(err); } }
} catch (err) { console.error(err); alert("Network Error"); }
setLoading(false); setLoading(false);
}; };
// Wrapper for refreshing data inside Dashboard
const handleRefreshProject = async () => {
if (selectedProject) {
await loadProject(selectedProject.id);
}
};
return ( return (
<div className="min-h-screen bg-slate-900 text-slate-100 font-sans selection:bg-blue-500/30"> <div className="min-h-screen bg-slate-900 text-slate-100 font-sans selection:bg-blue-500/30">
{/* Header */} {/* Header */}
@@ -501,6 +520,7 @@ export default function App() {
project={selectedProject} project={selectedProject}
onBack={() => setView('LIST')} onBack={() => setView('LIST')}
setLoading={setLoading} setLoading={setLoading}
onRefresh={handleRefreshProject}
/> />
)} )}
</main> </main>

View File

@@ -1,3 +1,4 @@
const express = require('express'); const express = require('express');
const { spawn } = require('child_process'); const { spawn } = require('child_process');
const path = require('path'); const path = require('path');
@@ -6,21 +7,63 @@ const fs = require('fs');
const app = express(); const app = express();
const port = process.env.PORT || 3006; 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' })); app.use(express.json({ limit: '50mb' }));
// INITIALIZE DATABASE ON START // INITIALIZE DATABASE ON START
const dbScript = path.join(__dirname, 'content_db_manager.py'); const dbScript = path.join(__dirname, 'content_db_manager.py');
console.log("Initializing database..."); log(`Initializing database via ${dbScript}...`);
spawn('python3', [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 // Helper to run python commands
function runPython(mode, payload) { function runPython(mode, payload) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const payloadFile = path.join(__dirname, `payload_${Date.now()}.json`); const payloadFile = path.join(__dirname, `payload_${Date.now()}.json`);
try {
fs.writeFileSync(payloadFile, JSON.stringify(payload)); 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', [ const pythonProcess = spawn('python3', [
path.join(__dirname, 'content_orchestrator.py'), scriptPath,
'--mode', mode, '--mode', mode,
'--payload_file', payloadFile '--payload_file', payloadFile
]); ]);
@@ -28,18 +71,40 @@ function runPython(mode, payload) {
let stdout = ''; let stdout = '';
let stderr = ''; let stderr = '';
pythonProcess.stdout.on('data', (data) => stdout += data.toString()); pythonProcess.stdout.on('data', (data) => {
pythonProcess.stderr.on('data', (data) => stderr += data.toString()); stdout += data.toString();
});
pythonProcess.stderr.on('data', (data) => {
stderr += data.toString();
});
pythonProcess.on('close', (code) => { pythonProcess.on('close', (code) => {
if (fs.existsSync(payloadFile)) fs.unlinkSync(payloadFile); // Cleanup payload file
if (code !== 0) { if (fs.existsSync(payloadFile)) {
console.error(`Python error (code ${code}):`, stderr);
return reject(stderr);
}
try { try {
resolve(JSON.parse(stdout)); fs.unlinkSync(payloadFile);
} catch(e) { } 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); reject("Failed to parse Python output: " + stdout);
} }
}); });
@@ -47,15 +112,19 @@ function runPython(mode, payload) {
} }
app.post('/api/:mode', async (req, res) => { app.post('/api/:mode', async (req, res) => {
const mode = req.params.mode;
log(`Incoming POST request for mode: ${mode}`);
try { 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); res.json(result);
} catch (error) { } catch (error) {
log(`Request for ${mode} failed: ${error}`, 'ERROR');
res.status(500).json({ error: error.toString() }); res.status(500).json({ error: error.toString() });
} }
}); });
// Serve static assets from build (for production) // Serve static assets
if (fs.existsSync(path.join(__dirname, 'dist'))) { if (fs.existsSync(path.join(__dirname, 'dist'))) {
app.use(express.static(path.join(__dirname, 'dist'))); app.use(express.static(path.join(__dirname, 'dist')));
app.get('*', (req, res) => { app.get('*', (req, res) => {
@@ -64,5 +133,5 @@ if (fs.existsSync(path.join(__dirname, 'dist'))) {
} }
app.listen(port, () => { app.listen(port, () => {
console.log(`Content Engine Server running on port ${port}`); log(`Content Engine Server running on port ${port}`);
}); });