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