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:
ajaysi
2026-03-09 12:35:13 +05:30
parent a25ec8302c
commit b3cc83ed6e
13 changed files with 1708 additions and 20 deletions

View File

@@ -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('_')]

View File

@@ -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'],

View File

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

View File

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

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