feat: add landing page with beta signup + raise KB node limit to 100

Landing page at /landing with full marketing content: hero, features,
pricing, testimonials, and beta email signup form. Beta signups email
beta@resolutionflow.com via new public endpoint. Unauthenticated users
redirect to landing instead of login. Also raises KB Accelerator node
limit from 50 to 100 to accommodate dense troubleshooting articles.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
chihlasm
2026-03-12 00:23:29 -04:00
parent 92c86cab80
commit 042a12b190
9 changed files with 1756 additions and 4 deletions

View File

@@ -0,0 +1,31 @@
"""Public beta signup endpoint — no auth required."""
import logging
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel, EmailStr
from app.core.email import EmailService
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/beta-signup", tags=["beta"])
class BetaSignupRequest(BaseModel):
email: EmailStr
class BetaSignupResponse(BaseModel):
success: bool
message: str
@router.post("", response_model=BetaSignupResponse)
async def beta_signup(data: BetaSignupRequest):
"""Collect beta interest — sends notification to beta@resolutionflow.com."""
sent = await EmailService.send_beta_signup_notification(data.email)
if not sent:
logger.warning("Beta signup recorded (email delivery skipped): %s", data.email)
return BetaSignupResponse(
success=True,
message="Thanks! We'll be in touch with beta access details.",
)

View File

@@ -15,6 +15,7 @@ from app.api.endpoints import admin_survey
from app.api.endpoints import tree_transfer
from app.api.endpoints import ai_suggestions
from app.api.endpoints import kb_accelerator
from app.api.endpoints import beta_signup
api_router = APIRouter()
@@ -54,3 +55,4 @@ api_router.include_router(admin_survey.router)
api_router.include_router(tree_transfer.router)
api_router.include_router(ai_suggestions.router)
api_router.include_router(kb_accelerator.router)
api_router.include_router(beta_signup.router)

View File

@@ -151,9 +151,9 @@ def validate_generated_tree(tree: dict[str, Any]) -> list[str]:
errors.append(
f"Tree has only {node_count} nodes. Minimum 5 required for a useful tree."
)
if node_count > 50:
if node_count > 100:
errors.append(
f"Tree has {node_count} nodes. Maximum 50 allowed."
f"Tree has {node_count} nodes. Maximum 100 allowed."
)
if solution_count < 2:
errors.append(

View File

@@ -418,6 +418,72 @@ class EmailService:
logger.exception("Failed to send survey copy email to %s", to_email)
return False
@staticmethod
async def send_beta_signup_notification(
signup_email: str,
notify_email: str = "beta@resolutionflow.com",
) -> bool:
"""Notify beta@resolutionflow.com about a new beta signup. Fire-and-forget."""
if not settings.email_enabled:
logger.warning("Beta signup email not sent — RESEND_API_KEY not configured")
return False
try:
import resend
import html as html_mod
from datetime import datetime, timezone
resend.api_key = settings.RESEND_API_KEY
date_str = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
safe_email = html_mod.escape(signup_email)
subject = f"[ResolutionFlow Beta] New signup — {safe_email}"
email_html = f"""<!DOCTYPE html>
<html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width"></head>
<body style="margin:0;padding:0;background:#101114;font-family:'Inter',Helvetica,Arial,sans-serif;">
<table width="100%" cellpadding="0" cellspacing="0" style="background:#101114;padding:40px 0;">
<tr><td align="center">
<table width="560" cellpadding="0" cellspacing="0" style="background:#14161a;border:1px solid rgba(255,255,255,0.06);border-radius:16px;">
<tr><td style="padding:40px 40px 24px;text-align:center;">
<h1 style="margin:0;color:#f8fafc;font-size:24px;font-weight:600;">Resolution<span style="color:#06b6d4;">Flow</span></h1>
<p style="margin:8px 0 0;color:#5a6170;font-size:14px;">New Beta Signup</p>
</td></tr>
<tr><td style="padding:0 40px 24px;">
<p style="margin:0;color:#8891a0;font-size:16px;line-height:1.6;">
A new user has requested beta access:
</p>
</td></tr>
<tr><td style="padding:0 40px 24px;text-align:center;">
<div style="background:rgba(0,0,0,0.3);border:1px solid rgba(255,255,255,0.06);border-radius:12px;padding:20px;">
<p style="margin:0 0 4px;color:#5a6170;font-size:12px;text-transform:uppercase;letter-spacing:1px;">Email</p>
<p style="margin:0;color:#22d3ee;font-size:18px;font-weight:600;">{safe_email}</p>
</div>
</td></tr>
<tr><td style="padding:0 40px 32px;">
<p style="margin:0;color:#5a6170;font-size:12px;text-align:center;">
Submitted at {date_str}
</p>
</td></tr>
</table>
</td></tr>
</table>
</body></html>"""
resend.Emails.send({
"from": settings.FROM_EMAIL,
"to": [notify_email],
"reply_to": signup_email,
"subject": subject,
"html": email_html,
})
logger.info("Beta signup notification sent for %s", signup_email)
return True
except Exception:
logger.exception("Failed to send beta signup notification for %s", signup_email)
return False
@staticmethod
async def send_survey_invite_email(
to_email: str,

View File

@@ -139,7 +139,7 @@ Return a JSON object with this structure:
4. The first node is the root of the decision tree.
5. All `next_node_id` and option `next_node_id` references must point to existing node IDs.
6. Detect implicit branching logic (e.g., "If X, do Y; otherwise Z") and create decision nodes.
7. Produce at least 3 nodes. Maximum 50 nodes.
7. Produce at least 3 nodes. Maximum 100 nodes.
8. Use high confidence (0.9+) for directly stated steps, medium (0.7-0.89) for reasonable inferences, low (<0.7) for significant interpretation.
9. Return ONLY valid JSON — no markdown fences, no explanation text."""