attempt to fix profile picture error

This commit is contained in:
2026-03-16 13:23:58 +01:00
parent 46793f4acf
commit 17799a05ce
9 changed files with 219 additions and 25 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@@ -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))

View File

@@ -21,6 +21,7 @@ from src.agents.style_validator import StyleValidator
from src.agents.readability_checker import ReadabilityChecker from src.agents.readability_checker import ReadabilityChecker
from src.agents.quality_refiner import QualityRefinerAgent from src.agents.quality_refiner import QualityRefinerAgent
from src.database.models import PostType from src.database.models import PostType
from src.utils.post_cleanup import sanitize_post_content
class WorkflowOrchestrator: class WorkflowOrchestrator:
@@ -658,6 +659,7 @@ class WorkflowOrchestrator:
company_strategy=company_strategy, # Pass company strategy company_strategy=company_strategy, # Pass company strategy
strategy_weight=strategy_weight # NEW: Pass strategy weight strategy_weight=strategy_weight # NEW: Pass strategy weight
) )
current_post = sanitize_post_content(current_post)
else: else:
# Revision based on feedback - pass full critic result for structured changes # Revision based on feedback - pass full critic result for structured changes
report_progress("Writer überarbeitet Post...", iteration, None, writer_versions, critic_feedback_list) 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 company_strategy=company_strategy, # Pass company strategy
strategy_weight=strategy_weight # NEW: Pass strategy weight 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}") logger.info(f"Writer produced version {iteration}")
# Report progress with new version # Report progress with new version
@@ -750,7 +753,7 @@ class WorkflowOrchestrator:
profile_analysis=profile_analysis.full_analysis, profile_analysis=profile_analysis.full_analysis,
example_posts=example_post_texts example_posts=example_post_texts
) )
current_post = polished_post current_post = sanitize_post_content(polished_post)
logger.info("✅ Post polished (Formatierung erhalten)") logger.info("✅ Post polished (Formatierung erhalten)")
else: else:
logger.info("✅ No quality issues, skipping polish") logger.info("✅ No quality issues, skipping polish")
@@ -772,7 +775,7 @@ class WorkflowOrchestrator:
generated_post = GeneratedPost( generated_post = GeneratedPost(
user_id=user_id, user_id=user_id,
topic_title=topic.get("title", "Unknown"), topic_title=topic.get("title", "Unknown"),
post_content=current_post, post_content=sanitize_post_content(current_post),
iterations=iteration, iterations=iteration,
writer_versions=writer_versions, writer_versions=writer_versions,
critic_feedback=critic_feedback_list, critic_feedback=critic_feedback_list,

22
src/utils/post_cleanup.py Normal file
View File

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

View File

@@ -70,8 +70,8 @@ class SecurityHeadersMiddleware(BaseHTTPMiddleware):
"default-src 'self'; " "default-src 'self'; "
"script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.jsdelivr.net; " "script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.jsdelivr.net; "
"style-src 'self' 'unsafe-inline'; " "style-src 'self' 'unsafe-inline'; "
"img-src 'self' data: blob: https://*.supabase.co https://*.linkedin.com https://media.licdn.com; " "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; " "connect-src 'self' https://*.supabase.co https://supabase.onyva.dev; "
"font-src 'self' data:; " "font-src 'self' data:; "
"frame-ancestors 'none'; " "frame-ancestors 'none'; "
"base-uri 'self'; " "base-uri 'self'; "

View File

@@ -78,8 +78,8 @@
<div class="flex items-center justify-between p-4 bg-brand-bg border border-gray-600 rounded-lg"> <div class="flex items-center justify-between p-4 bg-brand-bg border border-gray-600 rounded-lg">
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-full bg-brand-highlight/20 flex items-center justify-center overflow-hidden"> <div class="w-10 h-10 rounded-full bg-brand-highlight/20 flex items-center justify-center overflow-hidden">
{% if employee.linkedin_picture %} {% if employee.profile_picture or employee.linkedin_picture %}
<img src="{{ employee.linkedin_picture }}" alt="" class="w-10 h-10 rounded-full"> <img src="{{ employee.profile_picture or employee.linkedin_picture }}" alt="" class="w-10 h-10 rounded-full">
{% else %} {% else %}
<span class="text-brand-highlight font-bold">{{ (employee.display_name or employee.linkedin_name or employee.email)[0] | upper }}</span> <span class="text-brand-highlight font-bold">{{ (employee.display_name or employee.linkedin_name or employee.email)[0] | upper }}</span>
{% endif %} {% endif %}

View File

@@ -15,8 +15,8 @@
<a href="/company/manage?employee_id={{ emp.id }}" <a href="/company/manage?employee_id={{ emp.id }}"
class="flex items-center gap-3 px-4 py-3 rounded-lg transition-colors {% if selected_employee and selected_employee.id == emp.id %}bg-brand-highlight text-brand-bg-dark{% else %}bg-brand-bg hover:bg-brand-bg-light text-white{% endif %}"> class="flex items-center gap-3 px-4 py-3 rounded-lg transition-colors {% if selected_employee and selected_employee.id == emp.id %}bg-brand-highlight text-brand-bg-dark{% else %}bg-brand-bg hover:bg-brand-bg-light text-white{% endif %}">
<div class="w-8 h-8 rounded-full {% if selected_employee and selected_employee.id == emp.id %}bg-brand-bg-dark/20{% else %}bg-brand-highlight/20{% endif %} flex items-center justify-center"> <div class="w-8 h-8 rounded-full {% if selected_employee and selected_employee.id == emp.id %}bg-brand-bg-dark/20{% else %}bg-brand-highlight/20{% endif %} flex items-center justify-center">
{% if emp.linkedin_picture %} {% if emp.profile_picture or emp.linkedin_picture %}
<img src="{{ emp.linkedin_picture }}" alt="" class="w-8 h-8 rounded-full"> <img src="{{ emp.profile_picture or emp.linkedin_picture }}" alt="" class="w-8 h-8 rounded-full">
{% else %} {% else %}
<span class="{% if selected_employee and selected_employee.id == emp.id %}text-brand-bg-dark{% else %}text-brand-highlight{% endif %} text-sm font-medium">{{ (emp.display_name or emp.email)[0] | upper }}</span> <span class="{% if selected_employee and selected_employee.id == emp.id %}text-brand-bg-dark{% else %}text-brand-highlight{% endif %} text-sm font-medium">{{ (emp.display_name or emp.email)[0] | upper }}</span>
{% endif %} {% endif %}
@@ -44,8 +44,8 @@
<div class="card-bg border rounded-xl p-6 mb-8"> <div class="card-bg border rounded-xl p-6 mb-8">
<div class="flex items-center gap-4"> <div class="flex items-center gap-4">
<div class="w-16 h-16 rounded-full bg-brand-highlight/20 flex items-center justify-center overflow-hidden"> <div class="w-16 h-16 rounded-full bg-brand-highlight/20 flex items-center justify-center overflow-hidden">
{% if selected_employee.linkedin_picture %} {% if selected_employee.profile_picture or selected_employee.linkedin_picture %}
<img src="{{ selected_employee.linkedin_picture }}" alt="" class="w-16 h-16 rounded-full"> <img src="{{ selected_employee.profile_picture or selected_employee.linkedin_picture }}" alt="" class="w-16 h-16 rounded-full">
{% else %} {% else %}
<span class="text-brand-highlight text-2xl font-medium">{{ (selected_employee.display_name or selected_employee.email)[0] | upper }}</span> <span class="text-brand-highlight text-2xl font-medium">{{ (selected_employee.display_name or selected_employee.email)[0] | upper }}</span>
{% endif %} {% endif %}

View File

@@ -47,6 +47,7 @@ from src.agents.link_topic_builder import LinkTopicBuilderAgent
from src.agents.strategy_importer import StrategyImporterAgent from src.agents.strategy_importer import StrategyImporterAgent
from src.services.post_insights_service import compute_post_insights, refresh_post_insights_for_account from src.services.post_insights_service import compute_post_insights, refresh_post_insights_for_account
from src.services.insights_summary_service import generate_insights_summary from src.services.insights_summary_service import generate_insights_summary
from src.utils.post_cleanup import sanitize_post_content
# Router for user frontend # Router for user frontend
user_router = APIRouter(tags=["user"]) user_router = APIRouter(tags=["user"])
@@ -68,16 +69,16 @@ async def get_user_profile_picture(user_id: UUID) -> Optional[str]:
Note: session.linkedin_picture (OAuth login) should be checked by caller first. Note: session.linkedin_picture (OAuth login) should be checked by caller first.
""" """
# Check for connected LinkedIn account first # Prefer cached profile picture (Supabase) to avoid LinkedIn hotlink blocking
linkedin_account = await db.get_linkedin_account(user_id)
if linkedin_account and linkedin_account.is_active and linkedin_account.linkedin_picture:
return linkedin_account.linkedin_picture
# Fall back to profile picture from setup process
profile = await db.get_profile(user_id) profile = await db.get_profile(user_id)
if profile and profile.profile_picture: if profile and profile.profile_picture:
return profile.profile_picture return profile.profile_picture
# Fall back to connected LinkedIn account
linkedin_account = await db.get_linkedin_account(user_id)
if linkedin_account and linkedin_account.is_active and linkedin_account.linkedin_picture:
return linkedin_account.linkedin_picture
return None return None
@@ -103,6 +104,48 @@ async def get_user_avatar(session: UserSession, user_id: UUID) -> Optional[str]:
return None return None
async def cache_linkedin_picture(
picture_url: Optional[str],
user_id: UUID,
http_client: Optional["httpx.AsyncClient"] = None
) -> Optional[str]:
"""Download LinkedIn profile picture once and store in Supabase."""
if not picture_url:
return None
try:
import httpx
close_client = False
client = http_client
if client is None:
client = httpx.AsyncClient(timeout=30.0)
close_client = True
headers = {
"User-Agent": "Mozilla/5.0",
"Referer": "https://www.linkedin.com/",
"Accept": "image/avif,image/webp,image/apng,image/*,*/*;q=0.8",
}
response = await client.get(picture_url, headers=headers)
if response.status_code != 200 or not response.content:
return picture_url
content_type = response.headers.get("content-type", "image/jpeg")
uploaded_url = await storage.upload_media(
file_content=response.content,
content_type=content_type,
user_id=user_id
)
if close_client:
await client.aclose()
return uploaded_url or picture_url
except Exception as e:
logger.warning(f"Failed to cache LinkedIn picture: {e}")
return picture_url
async def get_employee_permissions_or_default(user_id: UUID, company_id: UUID) -> dict: async def get_employee_permissions_or_default(user_id: UUID, company_id: UUID) -> dict:
"""Get employee permissions for a company, returning all-true defaults if no row exists.""" """Get employee permissions for a company, returning all-true defaults if no row exists."""
@@ -2926,6 +2969,7 @@ async def linkedin_callback(
linkedin_user_id = userinfo.get("sub") linkedin_user_id = userinfo.get("sub")
linkedin_name = userinfo.get("name", "") linkedin_name = userinfo.get("name", "")
linkedin_picture = userinfo.get("picture") linkedin_picture = userinfo.get("picture")
cached_picture = await cache_linkedin_picture(linkedin_picture, UUID(session.user_id), http_client=client)
# Get vanity name if available (from profile API - optional) # Get vanity name if available (from profile API - optional)
linkedin_vanity_name = None linkedin_vanity_name = None
@@ -2955,7 +2999,7 @@ async def linkedin_callback(
"linkedin_user_id": linkedin_user_id, "linkedin_user_id": linkedin_user_id,
"linkedin_vanity_name": linkedin_vanity_name, "linkedin_vanity_name": linkedin_vanity_name,
"linkedin_name": linkedin_name, "linkedin_name": linkedin_name,
"linkedin_picture": linkedin_picture, "linkedin_picture": cached_picture or linkedin_picture,
"access_token": encrypted_access, "access_token": encrypted_access,
"refresh_token": encrypted_refresh, "refresh_token": encrypted_refresh,
"token_expires_at": datetime.now(timezone.utc) + timedelta(seconds=expires_in), "token_expires_at": datetime.now(timezone.utc) + timedelta(seconds=expires_in),
@@ -2973,7 +3017,7 @@ async def linkedin_callback(
linkedin_user_id=linkedin_user_id, linkedin_user_id=linkedin_user_id,
linkedin_vanity_name=linkedin_vanity_name, linkedin_vanity_name=linkedin_vanity_name,
linkedin_name=linkedin_name, linkedin_name=linkedin_name,
linkedin_picture=linkedin_picture, linkedin_picture=cached_picture or linkedin_picture,
access_token=encrypted_access, access_token=encrypted_access,
refresh_token=encrypted_refresh, refresh_token=encrypted_refresh,
token_expires_at=datetime.now(timezone.utc) + timedelta(seconds=expires_in), token_expires_at=datetime.now(timezone.utc) + timedelta(seconds=expires_in),
@@ -2982,6 +3026,12 @@ async def linkedin_callback(
await db.create_linkedin_account(new_account) await db.create_linkedin_account(new_account)
logger.info(f"Created LinkedIn account for user {session.user_id}") logger.info(f"Created LinkedIn account for user {session.user_id}")
if cached_picture:
try:
await db.update_profile(UUID(session.user_id), {"profile_picture": cached_picture})
except Exception as e:
logger.warning(f"Failed to update profile picture: {e}")
# Clear state cookie and redirect to settings # Clear state cookie and redirect to settings
response = RedirectResponse(url="/settings?success=linkedin_connected", status_code=302) response = RedirectResponse(url="/settings?success=linkedin_connected", status_code=302)
response.delete_cookie("linkedin_oauth_state") response.delete_cookie("linkedin_oauth_state")
@@ -4333,6 +4383,7 @@ async def chat_generate_post(request: Request):
company_strategy=company_strategy, company_strategy=company_strategy,
strategy_weight=post_type.strategy_weight strategy_weight=post_type.strategy_weight
) )
post_content = sanitize_post_content(post_content)
# Generate conversation ID # Generate conversation ID
import uuid import uuid
@@ -4446,6 +4497,7 @@ async def chat_refine_post(request: Request):
company_strategy=company_strategy, company_strategy=company_strategy,
strategy_weight=getattr(post_type, 'strategy_weight', 0.5) strategy_weight=getattr(post_type, 'strategy_weight', 0.5)
) )
refined_post = sanitize_post_content(refined_post)
return JSONResponse({ return JSONResponse({
"success": True, "success": True,
@@ -4470,7 +4522,7 @@ async def chat_save_post(request: Request):
try: try:
data = await request.json() data = await request.json()
post_content = data.get("post_content", "").strip() post_content = sanitize_post_content(data.get("post_content", "").strip())
post_type_id = data.get("post_type_id") post_type_id = data.get("post_type_id")
chat_history = data.get("chat_history", []) chat_history = data.get("chat_history", [])
@@ -4501,7 +4553,7 @@ async def chat_save_post(request: Request):
for item in chat_history: for item in chat_history:
if 'ai' in item and item['ai']: if 'ai' in item and item['ai']:
writer_versions.append(item['ai']) writer_versions.append(sanitize_post_content(item['ai']))
# Store user feedback as "critic feedback" # Store user feedback as "critic feedback"
if 'user' in item and item['user']: if 'user' in item and item['user']:
critic_feedback_list.append({ critic_feedback_list.append({
@@ -4620,7 +4672,7 @@ async def update_chat_post(request: Request, post_id: str):
post_uuid = UUID(post_id) post_uuid = UUID(post_id)
data = await request.json() data = await request.json()
post_content = data.get("post_content", "").strip() post_content = sanitize_post_content(data.get("post_content", "").strip())
chat_history = data.get("chat_history", []) chat_history = data.get("chat_history", [])
if not post_content: if not post_content:
@@ -4641,7 +4693,7 @@ async def update_chat_post(request: Request, post_id: str):
for item in chat_history: for item in chat_history:
if 'ai' in item and item['ai']: if 'ai' in item and item['ai']:
writer_versions.append(item['ai']) writer_versions.append(sanitize_post_content(item['ai']))
# Store user feedback as "critic feedback" # Store user feedback as "critic feedback"
if 'user' in item and item['user']: if 'user' in item and item['user']:
critic_feedback_list.append({ critic_feedback_list.append({
@@ -4751,6 +4803,7 @@ async def company_chat_generate_post(request: Request):
company_strategy=company_strategy, company_strategy=company_strategy,
strategy_weight=post_type.strategy_weight strategy_weight=post_type.strategy_weight
) )
post_content = sanitize_post_content(post_content)
return JSONResponse({ return JSONResponse({
"success": True, "success": True,
@@ -4777,7 +4830,7 @@ async def company_chat_save_post(request: Request):
try: try:
data = await request.json() data = await request.json()
employee_id = data.get("employee_id") employee_id = data.get("employee_id")
post_content = data.get("post_content", "").strip() post_content = sanitize_post_content(data.get("post_content", "").strip())
post_type_id = data.get("post_type_id") post_type_id = data.get("post_type_id")
chat_history = data.get("chat_history", []) chat_history = data.get("chat_history", [])
topic_title = data.get("topic_title", post_content[:80] if post_content else "Chat Post") topic_title = data.get("topic_title", post_content[:80] if post_content else "Chat Post")
@@ -4794,7 +4847,7 @@ async def company_chat_save_post(request: Request):
if not perms.get("can_create_posts", True): if not perms.get("can_create_posts", True):
return JSONResponse({"success": False, "error": "Keine Berechtigung"}, status_code=403) return JSONResponse({"success": False, "error": "Keine Berechtigung"}, status_code=403)
writer_versions = [item['ai'] for item in chat_history if 'ai' in item and item['ai']] writer_versions = [sanitize_post_content(item['ai']) for item in chat_history if 'ai' in item and item['ai']]
critic_feedback_list = [] critic_feedback_list = []
for item in chat_history: for item in chat_history:
if 'user' in item and item['user']: if 'user' in item and item['user']: