diff --git a/profile_pictures/olivia.kibele@onyva.de.jpeg b/profile_pictures/olivia.kibele@onyva.de.jpeg new file mode 100644 index 0000000..b4130e7 Binary files /dev/null and b/profile_pictures/olivia.kibele@onyva.de.jpeg differ diff --git a/profile_pictures/timo.uttenweiler@onyva.de.jpeg b/profile_pictures/timo.uttenweiler@onyva.de.jpeg new file mode 100644 index 0000000..8323f5e Binary files /dev/null and b/profile_pictures/timo.uttenweiler@onyva.de.jpeg differ diff --git a/scripts/update_profile_pictures.py b/scripts/update_profile_pictures.py new file mode 100644 index 0000000..51a25e7 --- /dev/null +++ b/scripts/update_profile_pictures.py @@ -0,0 +1,116 @@ +#!/usr/bin/env python3 +""" +Upload local profile pictures to Supabase and update user/profile records. + +Usage: + python scripts/update_profile_pictures.py --dir profile_pictures --apply + python scripts/update_profile_pictures.py --dir profile_pictures # dry run +""" + +import argparse +import asyncio +import mimetypes +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +if str(ROOT) not in sys.path: + sys.path.insert(0, str(ROOT)) + +from loguru import logger + +from src.database import db +from src.services.storage_service import storage + + +def guess_content_type(path: Path) -> str: + content_type, _ = mimetypes.guess_type(str(path)) + return content_type or "image/jpeg" + + +async def process_directory(directory: Path, apply: bool) -> None: + if not directory.exists() or not directory.is_dir(): + raise ValueError(f"Directory not found: {directory}") + + files = sorted([p for p in directory.iterdir() if p.is_file()]) + if not files: + print("No files found.") + return + + updated = 0 + skipped = 0 + + for path in files: + email = path.stem.strip() + if "@" not in email: + print(f"Skip (no email in filename): {path.name}") + skipped += 1 + continue + + user = await db.get_user_by_email(email) + if not user: + print(f"User not found for email: {email}") + skipped += 1 + continue + + content_type = guess_content_type(path) + file_bytes = path.read_bytes() + + print(f"\n{path.name} -> {email} ({content_type}, {len(file_bytes)} bytes)") + + if not apply: + print("DRY RUN: would upload and update profile.") + continue + + try: + uploaded_url = await storage.upload_media( + file_content=file_bytes, + content_type=content_type, + user_id=str(user.id), + ) + + await db.update_profile(user.id, {"profile_picture": uploaded_url}) + linkedin_account = await db.get_linkedin_account(user.id) + if linkedin_account: + await db.update_linkedin_account( + linkedin_account.id, + {"linkedin_picture": uploaded_url} + ) + + if db.admin_client: + try: + await asyncio.to_thread( + lambda: db.admin_client.auth.admin.update_user_by_id( + str(user.id), + { + "user_metadata": { + "picture": uploaded_url, + "linkedin_picture": uploaded_url + } + } + ) + ) + except Exception as exc: + logger.warning(f"Failed to update auth user metadata: {exc}") + else: + logger.warning("No service role key available; cannot update auth.users metadata.") + + updated += 1 + print(f"Updated profile picture: {uploaded_url}") + except Exception as exc: + logger.error(f"Failed to update {email}: {exc}") + skipped += 1 + + print(f"\nDone. Updated: {updated}, Skipped: {skipped}") + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Upload profile pictures to Supabase and update users.") + parser.add_argument("--dir", default="profile_pictures", help="Directory with profile pictures") + parser.add_argument("--apply", action="store_true", help="Apply changes (otherwise dry run)") + return parser.parse_args() + + +if __name__ == "__main__": + args = parse_args() + asyncio.run(process_directory(Path(args.dir), apply=args.apply)) diff --git a/src/orchestrator.py b/src/orchestrator.py index 92ea072..f44d622 100644 --- a/src/orchestrator.py +++ b/src/orchestrator.py @@ -21,6 +21,7 @@ from src.agents.style_validator import StyleValidator from src.agents.readability_checker import ReadabilityChecker from src.agents.quality_refiner import QualityRefinerAgent from src.database.models import PostType +from src.utils.post_cleanup import sanitize_post_content class WorkflowOrchestrator: @@ -658,6 +659,7 @@ class WorkflowOrchestrator: company_strategy=company_strategy, # Pass company strategy strategy_weight=strategy_weight # NEW: Pass strategy weight ) + current_post = sanitize_post_content(current_post) else: # Revision based on feedback - pass full critic result for structured changes report_progress("Writer überarbeitet Post...", iteration, None, writer_versions, critic_feedback_list) @@ -677,8 +679,9 @@ class WorkflowOrchestrator: company_strategy=company_strategy, # Pass company strategy strategy_weight=strategy_weight # NEW: Pass strategy weight ) + current_post = sanitize_post_content(current_post) - writer_versions.append(current_post) + writer_versions.append(sanitize_post_content(current_post)) logger.info(f"Writer produced version {iteration}") # Report progress with new version @@ -750,7 +753,7 @@ class WorkflowOrchestrator: profile_analysis=profile_analysis.full_analysis, example_posts=example_post_texts ) - current_post = polished_post + current_post = sanitize_post_content(polished_post) logger.info("✅ Post polished (Formatierung erhalten)") else: logger.info("✅ No quality issues, skipping polish") @@ -772,7 +775,7 @@ class WorkflowOrchestrator: generated_post = GeneratedPost( user_id=user_id, topic_title=topic.get("title", "Unknown"), - post_content=current_post, + post_content=sanitize_post_content(current_post), iterations=iteration, writer_versions=writer_versions, critic_feedback=critic_feedback_list, diff --git a/src/utils/post_cleanup.py b/src/utils/post_cleanup.py new file mode 100644 index 0000000..6cac707 --- /dev/null +++ b/src/utils/post_cleanup.py @@ -0,0 +1,22 @@ +"""Utilities to sanitize generated post content.""" +import re + + +def sanitize_post_content(text: str) -> str: + """Remove markdown bold and leading 'Post' labels from generated content.""" + if not text: + return text + + cleaned = text.strip() + + # Remove leading "Post" or "**Post**" labels + cleaned = re.sub(r'^\s*(\*\*post\*\*|post)\s*[:\-–—]*\s*', '', cleaned, flags=re.IGNORECASE) + + # Remove markdown bold markers, keep inner text + cleaned = re.sub(r'\*\*(.+?)\*\*', r'\1', cleaned) + cleaned = re.sub(r'__(.+?)__', r'\1', cleaned) + + # Remove any leftover bold markers + cleaned = cleaned.replace('**', '').replace('__', '') + + return cleaned.strip() diff --git a/src/web/app.py b/src/web/app.py index 6f477ae..ea6c0b2 100644 --- a/src/web/app.py +++ b/src/web/app.py @@ -70,8 +70,8 @@ class SecurityHeadersMiddleware(BaseHTTPMiddleware): "default-src 'self'; " "script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.jsdelivr.net; " "style-src 'self' 'unsafe-inline'; " - "img-src 'self' data: blob: https://*.supabase.co https://*.linkedin.com https://media.licdn.com; " - "connect-src 'self' https://*.supabase.co; " + "img-src 'self' data: blob: https://*.supabase.co https://supabase.onyva.dev https://*.linkedin.com https://media.licdn.com; " + "connect-src 'self' https://*.supabase.co https://supabase.onyva.dev; " "font-src 'self' data:; " "frame-ancestors 'none'; " "base-uri 'self'; " diff --git a/src/web/templates/user/company_accounts.html b/src/web/templates/user/company_accounts.html index 63c3eb6..d8c4e28 100644 --- a/src/web/templates/user/company_accounts.html +++ b/src/web/templates/user/company_accounts.html @@ -78,8 +78,8 @@