fix: resolve onboarding session not found warnings and frontend build OOM
- Use canonical Clerk user id (clerk_user_id) across all onboarding entrypoints to ensure consistent OnboardingSession.user_id lookup - Fix API key persistence in api_key_manager.py to use correct APIKey model columns (session_id, provider, key) - Increase Node heap for frontend build to 8GB and add build:nomap script to disable sourcemaps and reduce memory usage - Update onboarding endpoints (endpoints_core.py, onboarding_control_service.py, step_management_service.py) to prefer clerk_user_id over id - Fix frontend workflowStore.ts TypeScript error by returning WorkflowError instance - Add website_automation_service.py for onboarding automation
This commit is contained in:
@@ -222,6 +222,94 @@ async def update_business_info(business_info_id: int, business_info: dict):
|
||||
raise HTTPException(status_code=500, detail=f"Failed to update business info: {str(e)}")
|
||||
|
||||
|
||||
async def generate_website_preview(intake: Dict[str, Any], current_user: Dict[str, Any]):
|
||||
try:
|
||||
user_id = current_user.get("id")
|
||||
from services.onboarding.website_intake_service import website_intake_service
|
||||
from services.onboarding.website_style_service import website_style_service
|
||||
from api.onboarding_utils.website_automation_service import website_automation_service
|
||||
from services.user_website_service import user_website_service
|
||||
from models.user_website_request import UserWebsiteRequest, WebsiteStatus, TemplateType
|
||||
|
||||
existing = user_website_service.get_user_website_by_user(user_id)
|
||||
if not existing:
|
||||
user_website_service.create_user_website(
|
||||
UserWebsiteRequest(
|
||||
user_id=user_id,
|
||||
template_type=TemplateType(intake.get("template_type", "blog")),
|
||||
status=WebsiteStatus.PREVIEWING,
|
||||
business_name=intake.get("business_name"),
|
||||
business_description=intake.get("business_summary")
|
||||
)
|
||||
)
|
||||
|
||||
site_brief = website_intake_service.generate_site_brief(intake, user_id=str(user_id))
|
||||
if existing and existing.netlify_site_url:
|
||||
site_brief.setdefault("site_brief", {})
|
||||
site_brief["site_brief"]["canonical_url"] = existing.netlify_site_url
|
||||
tokens = website_style_service.generate_theme_tokens(site_brief, user_id=str(user_id))
|
||||
css = website_style_service.render_css(tokens) if tokens and not tokens.get("error") else ""
|
||||
preview = await website_automation_service.generate_preview_site(user_id, site_brief, css)
|
||||
|
||||
return {
|
||||
"site_brief": site_brief,
|
||||
"theme_tokens": tokens,
|
||||
"css": css,
|
||||
"preview_url": preview.get("preview_url"),
|
||||
"preview_root": preview.get("preview_root"),
|
||||
"preview_files": preview.get("preview_files"),
|
||||
"preview_html": preview.get("preview_html"),
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating website preview: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail="Failed to generate website preview")
|
||||
|
||||
|
||||
async def deploy_website(intake: Dict[str, Any], current_user: Dict[str, Any]):
|
||||
try:
|
||||
user_id = current_user.get("id")
|
||||
from api.onboarding_utils.website_automation_service import WebsiteAutomationService
|
||||
from services.user_website_service import user_website_service
|
||||
from services.onboarding.website_intake_service import website_intake_service
|
||||
from services.onboarding.website_style_service import website_style_service
|
||||
from models.user_website_request import WebsiteStatusUpdate
|
||||
|
||||
template = intake.get("template_type", "blog")
|
||||
business_name = intake.get("business_name") or intake.get("business_summary") or f"ALwrity Site {user_id}"
|
||||
business_info = {"name": business_name}
|
||||
site_brief = website_intake_service.generate_site_brief(intake, user_id=str(user_id))
|
||||
tokens = website_style_service.generate_theme_tokens(site_brief, user_id=str(user_id))
|
||||
css = website_style_service.render_css(tokens) if tokens and not tokens.get("error") else ""
|
||||
|
||||
service = WebsiteAutomationService()
|
||||
result = await service.generate_website(
|
||||
user_id,
|
||||
business_info,
|
||||
template,
|
||||
site_brief=site_brief,
|
||||
css=css
|
||||
)
|
||||
|
||||
user_website_service.update_user_website_status(
|
||||
user_id=user_id,
|
||||
status_update=WebsiteStatusUpdate(
|
||||
status=WebsiteStatus.DEPLOYED,
|
||||
github_repo_url=result.get("repo_url"),
|
||||
netlify_site_url=result.get("live_url"),
|
||||
netlify_admin_url=result.get("admin_url")
|
||||
)
|
||||
)
|
||||
return {
|
||||
**result,
|
||||
"site_brief": site_brief,
|
||||
"theme_tokens": tokens,
|
||||
"css": css
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error deploying website: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail="Failed to deploy website")
|
||||
|
||||
|
||||
__all__ = [name for name in globals().keys() if not name.startswith('_')]
|
||||
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ async def initialize_onboarding(current_user: Dict[str, Any] = Depends(get_curre
|
||||
logger.error("initialize_onboarding called without a valid current_user")
|
||||
raise HTTPException(status_code=401, detail="User not authenticated")
|
||||
|
||||
user_id = str(current_user.get('id'))
|
||||
user_id = str(current_user.get('clerk_user_id') or current_user.get('id'))
|
||||
progress_service = OnboardingProgressService()
|
||||
status = progress_service.get_onboarding_status(user_id)
|
||||
|
||||
@@ -96,7 +96,7 @@ async def initialize_onboarding(current_user: Dict[str, Any] = Depends(get_curre
|
||||
"email": current_user.get('email'),
|
||||
"first_name": current_user.get('first_name'),
|
||||
"last_name": current_user.get('last_name'),
|
||||
"clerk_user_id": user_id,
|
||||
"clerk_user_id": str(current_user.get('clerk_user_id') or user_id),
|
||||
},
|
||||
"onboarding": {
|
||||
"is_completed": status['is_completed'],
|
||||
|
||||
@@ -22,7 +22,7 @@ class OnboardingControlService:
|
||||
db_gen = get_db()
|
||||
db = next(db_gen)
|
||||
try:
|
||||
user_id = str(current_user.get('id'))
|
||||
user_id = str(current_user.get('clerk_user_id') or current_user.get('id'))
|
||||
|
||||
# Ensure user workspace exists when starting onboarding
|
||||
try:
|
||||
@@ -53,7 +53,7 @@ class OnboardingControlService:
|
||||
"""Reset the onboarding progress for a specific user."""
|
||||
try:
|
||||
from services.onboarding.progress_service import OnboardingProgressService
|
||||
user_id = str(current_user.get('id'))
|
||||
user_id = str(current_user.get('clerk_user_id') or current_user.get('id'))
|
||||
progress_service = OnboardingProgressService()
|
||||
success = progress_service.reset_onboarding(user_id)
|
||||
|
||||
|
||||
@@ -416,7 +416,7 @@ class StepManagementService:
|
||||
async def get_step_data(self, step_number: int, current_user: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Get data for a specific step."""
|
||||
try:
|
||||
user_id = str(current_user.get('id'))
|
||||
user_id = str(current_user.get('clerk_user_id') or current_user.get('id'))
|
||||
db = next(get_db(current_user))
|
||||
|
||||
# Use SSOT for reading step data
|
||||
@@ -492,7 +492,7 @@ class StepManagementService:
|
||||
"""Mark a step as completed."""
|
||||
try:
|
||||
logger.info(f"[complete_step] Completing step {step_number}")
|
||||
user_id = str(current_user.get('id'))
|
||||
user_id = str(current_user.get('clerk_user_id') or current_user.get('id'))
|
||||
|
||||
# Optional validation
|
||||
try:
|
||||
@@ -672,7 +672,7 @@ class StepManagementService:
|
||||
"""Skip a step (for optional steps)."""
|
||||
try:
|
||||
from services.onboarding.api_key_manager import get_onboarding_progress_for_user
|
||||
user_id = str(current_user.get('id'))
|
||||
user_id = str(current_user.get('clerk_user_id') or current_user.get('id'))
|
||||
progress = get_onboarding_progress_for_user(user_id)
|
||||
step = progress.get_step_data(step_number)
|
||||
|
||||
@@ -695,7 +695,7 @@ class StepManagementService:
|
||||
async def validate_step_access(self, step_number: int, current_user: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Validate if user can access a specific step."""
|
||||
try:
|
||||
user_id = str(current_user.get('id'))
|
||||
user_id = str(current_user.get('clerk_user_id') or current_user.get('id'))
|
||||
progress = get_onboarding_progress_for_user(user_id)
|
||||
|
||||
if not progress.can_proceed_to_step(step_number):
|
||||
|
||||
250
backend/api/onboarding_utils/website_automation_service.py
Normal file
250
backend/api/onboarding_utils/website_automation_service.py
Normal file
@@ -0,0 +1,250 @@
|
||||
"""Website Automation Service for API layer - orchestrates website creation."""
|
||||
from typing import Dict, Any, Optional
|
||||
from loguru import logger
|
||||
import os
|
||||
import tempfile
|
||||
import json
|
||||
from fastapi import HTTPException
|
||||
|
||||
# Import the actual automation service
|
||||
from services.onboarding.website_automation_service import WebsiteAutomationService as CoreAutomationService
|
||||
|
||||
|
||||
class WebsiteAutomationService:
|
||||
"""API layer service for website automation operations."""
|
||||
|
||||
def __init__(self):
|
||||
logger.info("🔄 Initializing WebsiteAutomationService (API layer)...")
|
||||
self.core_service = CoreAutomationService()
|
||||
|
||||
async def generate_preview_site(
|
||||
self,
|
||||
user_id: str,
|
||||
site_brief: Dict[str, Any],
|
||||
css: str
|
||||
) -> Dict[str, Any]:
|
||||
"""Generate a preview site for the user."""
|
||||
try:
|
||||
logger.info(f"Generating preview site for user {user_id}")
|
||||
|
||||
# For preview, we'll create a temporary HTML file
|
||||
# In production, this could be hosted on a preview server
|
||||
preview_html = self._generate_preview_html(site_brief, css)
|
||||
|
||||
# Save to temporary file (in production, use proper hosting)
|
||||
preview_url = f"/preview/{user_id}/index.html"
|
||||
|
||||
return {
|
||||
"preview_url": preview_url,
|
||||
"preview_root": f"/preview/{user_id}",
|
||||
"preview_files": ["index.html", "custom.css"],
|
||||
"preview_html": preview_html
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to generate preview site: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Preview generation failed: {str(e)}")
|
||||
|
||||
def _generate_preview_html(self, site_brief: Dict[str, Any], css: str) -> str:
|
||||
"""Generate HTML preview from site brief and CSS."""
|
||||
try:
|
||||
site_data = site_brief.get("site_brief", {})
|
||||
business_name = site_data.get("business_name", "Your Business")
|
||||
tagline = site_data.get("tagline", "Your tagline here")
|
||||
|
||||
# Get content plan
|
||||
content_plan = site_brief.get("content_plan", {})
|
||||
required_pages = content_plan.get("required_pages", [])
|
||||
|
||||
# Generate HTML
|
||||
html = f"""<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{business_name}</title>
|
||||
<style>
|
||||
{css}
|
||||
|
||||
/* Additional preview styles */
|
||||
.preview-banner {{
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
}}
|
||||
.preview-banner h1 {{
|
||||
margin: 0;
|
||||
font-size: 2.5rem;
|
||||
}}
|
||||
.preview-banner p {{
|
||||
margin: 0.5rem 0 0 0;
|
||||
font-size: 1.2rem;
|
||||
opacity: 0.9;
|
||||
}}
|
||||
.preview-content {{
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 2rem;
|
||||
}}
|
||||
.preview-section {{
|
||||
margin-bottom: 3rem;
|
||||
}}
|
||||
.preview-grid {{
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
}}
|
||||
.preview-card {{
|
||||
background: var(--color-surface, #f8fafc);
|
||||
padding: 1.5rem;
|
||||
border-radius: var(--border-radius-medium, 0.5rem);
|
||||
box-shadow: var(--shadow-small, 0 1px 2px 0 rgb(0 0 0 / 0.05));
|
||||
}}
|
||||
.preview-watermark {{
|
||||
position: fixed;
|
||||
bottom: 1rem;
|
||||
right: 1rem;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
color: white;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.875rem;
|
||||
z-index: 1000;
|
||||
}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="preview-banner">
|
||||
<h1>{business_name}</h1>
|
||||
<p>{tagline}</p>
|
||||
<div class="preview-watermark">ALwrity Preview</div>
|
||||
</div>
|
||||
|
||||
<div class="preview-content">
|
||||
{self._generate_page_content(required_pages)}
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Basic interactions for preview
|
||||
document.addEventListener('DOMContentLoaded', function() {{
|
||||
console.log('ALwrity website preview loaded');
|
||||
}});
|
||||
</script>
|
||||
</body>
|
||||
</html>"""
|
||||
|
||||
return html
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to generate preview HTML: {str(e)}")
|
||||
return f"<html><body><h1>Preview Error</h1><p>{str(e)}</p></body></html>"
|
||||
|
||||
def _generate_page_content(self, required_pages: list) -> str:
|
||||
"""Generate HTML content for pages."""
|
||||
if not required_pages:
|
||||
return """
|
||||
<div class="preview-section">
|
||||
<h2>Welcome to Your Website</h2>
|
||||
<p>This is a preview of your new website. The content will be generated based on your business information.</p>
|
||||
<div class="preview-grid">
|
||||
<div class="preview-card">
|
||||
<h3>About Us</h3>
|
||||
<p>Learn more about your business and what makes you unique.</p>
|
||||
</div>
|
||||
<div class="preview-card">
|
||||
<h3>Services</h3>
|
||||
<p>Discover the services and products you offer to your customers.</p>
|
||||
</div>
|
||||
<div class="preview-card">
|
||||
<h3>Contact</h3>
|
||||
<p>Get in touch with you through various contact methods.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
|
||||
content_parts = []
|
||||
|
||||
for page in required_pages:
|
||||
page_name = page.get("page", "page").title()
|
||||
goal = page.get("goal", "")
|
||||
key_points = page.get("key_points", [])
|
||||
cta = page.get("cta", "Get Started")
|
||||
|
||||
page_html = f"""
|
||||
<div class="preview-section">
|
||||
<h2>{page_name}</h2>
|
||||
<p>{goal}</p>
|
||||
"""
|
||||
|
||||
if key_points:
|
||||
page_html += "<div class='preview-grid'>"
|
||||
for point in key_points:
|
||||
page_html += f"""
|
||||
<div class="preview-card">
|
||||
<p>{point}</p>
|
||||
</div>
|
||||
"""
|
||||
page_html += "</div>"
|
||||
|
||||
if cta:
|
||||
page_html += f"""
|
||||
<div style="margin-top: 2rem;">
|
||||
<button class="btn btn-primary">{cta}</button>
|
||||
</div>
|
||||
"""
|
||||
|
||||
page_html += "</div>"
|
||||
content_parts.append(page_html)
|
||||
|
||||
return "".join(content_parts)
|
||||
|
||||
async def generate_website(
|
||||
self,
|
||||
user_id: str,
|
||||
business_info: Dict[str, Any],
|
||||
niche: str,
|
||||
site_brief: Optional[Dict[str, Any]] = None,
|
||||
css: Optional[str] = None
|
||||
) -> Dict[str, str]:
|
||||
"""Generate and deploy a full website."""
|
||||
try:
|
||||
logger.info(f"Generating website for user {user_id}")
|
||||
|
||||
# Use the core automation service
|
||||
result = await self.core_service.generate_website(
|
||||
user_id=user_id,
|
||||
business_info=business_info,
|
||||
niche=niche,
|
||||
site_brief=site_brief,
|
||||
css=css
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to generate website: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Website generation failed: {str(e)}")
|
||||
|
||||
def get_deployment_status(self, user_id: str) -> Dict[str, Any]:
|
||||
"""Get the status of website deployment."""
|
||||
try:
|
||||
# This would typically check the deployment status
|
||||
# For now, return a placeholder
|
||||
return {
|
||||
"status": "pending",
|
||||
"message": "Deployment status checking not yet implemented"
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get deployment status: {str(e)}")
|
||||
return {
|
||||
"status": "error",
|
||||
"message": str(e)
|
||||
}
|
||||
|
||||
|
||||
# Singleton instance
|
||||
website_automation_service = WebsiteAutomationService()
|
||||
Reference in New Issue
Block a user