All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m20s
Translation quality & format preservation: - Word: merge adjacent same-format runs into one unit (sentence-level coherence like inline-tag handling); translate comments/balloons; dedupe textbox collection (was translated twice); RTL no longer overrides center/justify alignment; CJK/Arabic font hints (eastAsia/cs) - PPTX: chart translations now actually reach the output file (ChartPart.blob is read-only — rewrite chart XML in the saved ZIP); CJK typeface hints (a:ea) - Excel: sheet renames no longer break references — rewrite cell formulas (3D/quoted), defined names, data validations, cond. formats - PDF: bold/italic honored (hebo/heit/hebi); table cells never merge; unchanged blocks left untouched (typography preserved, fixes duplicate hyperlinks); attempted/changed stats + route gate now cover PDF; CJK font paths; scanned PDFs via Mistral OCR (detection + admin settings) Features: - formality param (formal/informal) + automatic regional-variant prompts - output_mode=bilingual docx (source above translation) - per-user translation memory on Redis (falls back to LRU), context-hashed - QA report + 0-100 confidence score in job status; L0 on by default - OpenAI-compatible providers: whole chunk in ONE numbered-JSON request (~15x fewer calls) with per-item fallback; base prompt always present (custom prompt no longer replaces translation instructions) Infra & marketing alignment: - plan-based engine gating + vision gating (closes paid-engine leak); /providers/available filtered per plan; 107 languages exposed - zh-CN/zh-TW validation fixed; libmagic disabled on Windows (native crash) - admin: Mistral OCR settings + engine status dashboard; httpx<0.28 pin (TestClient breakage); Prometheus test fixture fixed - marketing docs aligned with code (PDF+OCR, retention, engines, pricing) - security: .env.ionos/.env.production/provider_settings.json removed Tests: 1173 passed / 0 failed (6 network tests deselected: free Google endpoint temporarily blocked from this machine)
99 lines
2.8 KiB
Python
99 lines
2.8 KiB
Python
"""
|
|
Waitlist / email-capture routes for the marketing landing page.
|
|
|
|
Public endpoint: POST /api/v1/waitlist (no auth) — deduplicated by email,
|
|
persisted in data/waitlist.json (same JSON-file pattern as provider settings).
|
|
"""
|
|
|
|
import json
|
|
import logging
|
|
import threading
|
|
from datetime import datetime, timezone
|
|
from typing import List, Optional
|
|
|
|
from fastapi import APIRouter
|
|
from fastapi.responses import JSONResponse
|
|
from pydantic import BaseModel, EmailStr
|
|
|
|
from config import config
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
router = APIRouter(prefix="/api/v1/waitlist", tags=["Waitlist"])
|
|
|
|
WAITLIST_FILE = config.BASE_DIR / "data" / "waitlist.json"
|
|
_write_lock = threading.Lock()
|
|
|
|
|
|
class WaitlistEntry(BaseModel):
|
|
email: EmailStr
|
|
interest: Optional[str] = None
|
|
|
|
|
|
def _load_waitlist() -> List[dict]:
|
|
if not WAITLIST_FILE.exists():
|
|
return []
|
|
try:
|
|
with open(WAITLIST_FILE, encoding="utf-8") as f:
|
|
data = json.load(f)
|
|
return data if isinstance(data, list) else []
|
|
except Exception as e:
|
|
logger.warning(f"Failed to load waitlist: {e}")
|
|
return []
|
|
|
|
|
|
def _save_waitlist(entries: List[dict]) -> None:
|
|
WAITLIST_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
with open(WAITLIST_FILE, "w", encoding="utf-8") as f:
|
|
json.dump(entries, f, indent=2)
|
|
|
|
|
|
@router.post("", status_code=201, summary="Join the waitlist")
|
|
async def join_waitlist(entry: WaitlistEntry):
|
|
email = entry.email.lower().strip()
|
|
now = datetime.now(timezone.utc).isoformat()
|
|
|
|
with _write_lock:
|
|
entries = _load_waitlist()
|
|
for existing in entries:
|
|
if existing.get("email", "").lower() == email:
|
|
existing["updated_at"] = now
|
|
if entry.interest:
|
|
existing["interest"] = entry.interest
|
|
_save_waitlist(entries)
|
|
logger.info(f"Waitlist rejoin: {email}")
|
|
return JSONResponse(
|
|
status_code=200,
|
|
content={
|
|
"data": {"email": email, "status": "already_joined"},
|
|
"message": "You are already on the list.",
|
|
},
|
|
)
|
|
|
|
entries.append(
|
|
{
|
|
"email": email,
|
|
"interest": entry.interest,
|
|
"joined_at": now,
|
|
"updated_at": now,
|
|
}
|
|
)
|
|
_save_waitlist(entries)
|
|
logger.info(f"Waitlist join: {email} (total={len(entries)})")
|
|
|
|
return JSONResponse(
|
|
status_code=201,
|
|
content={
|
|
"data": {"email": email, "status": "joined"},
|
|
"message": "Welcome aboard!",
|
|
},
|
|
)
|
|
|
|
|
|
@router.get("/count", summary="Waitlist count")
|
|
async def waitlist_count():
|
|
return JSONResponse(
|
|
status_code=200,
|
|
content={"data": {"count": len(_load_waitlist())}},
|
|
)
|