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):
conn = sqlite3.connect(path) try:
conn.row_factory = sqlite3.Row conn = sqlite3.connect(path)
return conn 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(): 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():
@@ -176,4 +202,4 @@ def get_project_assets(project_id):
return assets return assets
if __name__ == "__main__": if __name__ == "__main__":
init_db() init_db()

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):
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") 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")
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'): def get_copywriter_instruction(lang='de'):
return r""" return r"""
@@ -39,112 +58,161 @@ def get_copywriter_instruction(lang='de'):
# --- MODES --- # --- MODES ---
def list_gtm_projects(payload): def list_gtm_projects(payload):
projects = db_manager.get_all_gtm_projects() logging.info("Executing list_gtm_projects")
return {"projects": 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): 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"}
result = db_manager.import_gtm_project(gtm_id) try:
if not result: logging.info(f"Attempting to import GTM Project ID: {gtm_id}")
return {"error": "GTM Project not found or import failed"} result = db_manager.import_gtm_project(gtm_id)
return 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"}
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): def list_content_projects(payload):
projects = db_manager.get_all_content_projects() logging.info("Executing list_content_projects")
return {"projects": 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): def load_project_details(payload):
project_id = payload.get('projectId') project_id = payload.get('projectId')
project = db_manager.get_content_project(project_id) logging.info(f"Loading project details for ID: {project_id}")
if not project:
return {"error": "Project not found"}
assets = db_manager.get_project_assets(project_id) try:
project['assets'] = assets project = db_manager.get_content_project(project_id)
return project 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): 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}")
project = db_manager.get_content_project(project_id) try:
if not project: project = db_manager.get_content_project(project_id)
return {"error": "Project context not found"} if not project:
return {"error": "Project context not found"}
gtm_data = project.get('gtm_data_snapshot', {})
gtm_data = project.get('gtm_data_snapshot', {})
# GOLDEN RULE: Use Raw Quotes and .format() 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): prompt = r"""
{gtm_context} Basierend auf folgendem GTM-Kontext (Strategie für ein technisches Produkt):
{gtm_context}
AUFGABE:
Generiere eine strategische Liste von 15 SEO-Keywords. AUFGABE:
1. 5 Short-Tail Fokus-Keywords (z.B. Produktkategorie + 'kaufen/mieten'). Generiere eine strategische Liste von 15 SEO-Keywords.
2. 10 Long-Tail Keywords, die spezifische Pain Points oder Usecases adressieren (z.B. 'Kostenreduktion bei Sicherheitsrundgängen'). 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).
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)) 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) logging.info("Calling Gemini Flash for keywords...")
response = call_gemini_flash(prompt, system_instruction=get_copywriter_instruction(lang), json_mode=True)
db_manager.save_seo_strategy(project_id, {"seed_keywords": keywords}) logging.info("Gemini response received.")
return {"keywords": keywords}
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): 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', [])
if manual_content: logging.info(f"Generating section '{section_key}' for Project {project_id}")
# 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}
project = db_manager.get_content_project(project_id) if manual_content:
if not project: logging.info("Saving manual content update.")
return {"error": "Project context not found"} 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', {}) prompt = r"""
Erstelle den Website-Inhalt für die Sektion '{section}' eines Produkts in der Kategorie '{cat}'.
# Context extraction
category = project.get('category') STRATEGIE-KONTEXT:
{gtm_context}
prompt = r"""
Erstelle den Website-Inhalt für die Sektion '{section}' eines Produkts in der Kategorie '{cat}'. SEO-KEYWORDS ZU NUTZEN:
{kws}
STRATEGIE-KONTEXT:
{gtm_context} ANFORDERUNG:
- Schreibe im Stil eines Senior Copywriters (fachlich fundiert, Challenger Sale).
SEO-KEYWORDS ZU NUTZEN: - Format: Markdown.
{kws} - Die Sektion muss den Nutzer zur nächsten Aktion (CTA) führen.
""".format(
ANFORDERUNG: section=section_key,
- Schreibe im Stil eines Senior Copywriters (fachlich fundiert, Challenger Sale). cat=category,
- Format: Markdown. gtm_context=json.dumps(gtm_data),
- Die Sektion muss den Nutzer zur nächsten Aktion (CTA) führen. kws=json.dumps(keywords)
""".format( )
section=section_key,
cat=category, logging.info("Calling Gemini for copy generation...")
gtm_context=json.dumps(gtm_data), content = call_gemini_flash(prompt, system_instruction=get_copywriter_instruction(lang), json_mode=False)
kws=json.dumps(keywords) logging.info("Copy generated.")
)
db_manager.save_content_asset(project_id, 'website_section', section_key, f"Section: {section_key}", content, keywords)
content = call_gemini_flash(prompt, system_instruction=get_copywriter_instruction(lang), json_mode=False)
return {"content": content, "sectionKey": section_key}
# Save as asset except Exception as e:
db_manager.save_content_asset(project_id, 'website_section', section_key, f"Section: {section_key}", content, keywords) logging.error(f"Error generating section: {e}", exc_info=True)
return {"error": str(e)}
return {"content": content, "sectionKey": section_key}
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:
with open(args.payload_file, 'r') as f: try:
payload = json.load(f) 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 = { modes = {
"list_gtm_projects": list_gtm_projects, "list_gtm_projects": list_gtm_projects,
@@ -170,12 +245,14 @@ 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__":
main() 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();
setKeywords(data.keywords || []); if (data.error) {
} catch (err) { console.error(err); } alert(`Error: ${data.error}`);
} else {
setKeywords(data.keywords || []);
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(() => {
const newEditing: { [key: string]: string } = {}; // When project updates (e.g. via onUpdate->Parent Refresh), update local sections
if (sections) { if (project.assets) {
sections.forEach(s => { setSections(project.assets);
const newEditing: { [key: string]: string } = {};
project.assets.forEach(s => {
newEditing[s.section_key] = s.content; newEditing[s.section_key] = s.content;
}); });
setEditingContent(newEditing);
} }
setEditingContent(newEditing); }, [project.assets]);
}, [sections]);
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();
setSelectedProject(data);
setView('DETAILS'); if (data.error) {
} catch (err) { console.error(err); } alert(`Load Error: ${data.error}`);
} else {
setSelectedProject(data);
setView('DETAILS');
}
} 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`);
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', [ 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); try {
return reject(stderr); 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 { try {
resolve(JSON.parse(stdout)); log(`Python Stdout received (${stdout.length} bytes). Parsing JSON...`);
const parsed = JSON.parse(stdout);
resolve(parsed);
} catch (e) { } 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}`);
}); });