Sales Trainer v0.1: corporate sales-training simulator (Flask+Vue, 15 personas, chat simulator, judge, analytics)
- Auth/roles (no self-reg), admin user provision, JWT - Analyze: sales kit + initial pain-fit from form/upload - Persona generator: 15 personas (5/tier) w/ pain variety, negotiation, init mode, channel, latent/revealable, wrong_text special - Chat simulator: per-mode initiation, one-shot, hidden signals, judge-LLM debrief+coaching - Trainee loop: win/lose board, weak-areas, user-generated personas - Admin analytics; EN+TH Vue SPA served by Flask - Deploy: Dockerfile, docker-compose, README, eng-log + HANDOFF - Tests (mock LLM): m0/m1/routes/e2e all pass
This commit is contained in:
103
backend/app/factory.py
Normal file
103
backend/app/factory.py
Normal file
@@ -0,0 +1,103 @@
|
||||
"""Flask application factory."""
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from flask import Flask
|
||||
from flask_cors import CORS
|
||||
|
||||
from .auth.users import AuthError, UserStore
|
||||
from .config import Config
|
||||
|
||||
|
||||
def bootstrap_admin(users: UserStore) -> None:
|
||||
"""Ensure a default org + super-admin exists on first run (no self-registration)."""
|
||||
email = "admin@salestrainer.local"
|
||||
org = users.orgs.get_or_none("org-default")
|
||||
if org is None:
|
||||
org = users.create_org("Default Organization", org_id="org-default")
|
||||
if users.get_user_or_none(email) is None:
|
||||
users.create_user(
|
||||
org_id=org["id"],
|
||||
email=email,
|
||||
password="admin123",
|
||||
name="Super Admin",
|
||||
role="super_admin",
|
||||
)
|
||||
print("[bootstrap] created default super-admin:", email, "/ admin123")
|
||||
|
||||
|
||||
def create_app() -> Flask:
|
||||
Config.ensure_dirs()
|
||||
app = Flask(__name__)
|
||||
app.config["SECRET_KEY"] = Config.SECRET_KEY
|
||||
CORS(app, resources={r"/api/*": {"origins": "*"}})
|
||||
|
||||
from .api.auth_routes import auth_bp
|
||||
from .api.admin_routes import admin_bp
|
||||
from .api.group_routes import groups_bp
|
||||
from .api.chat_routes import chat_bp
|
||||
from .api.me_routes import me_bp
|
||||
from .api.analytics_routes import analytics_bp
|
||||
from .api.helpers import register_error_handlers
|
||||
|
||||
app.register_blueprint(auth_bp, url_prefix="/api/auth")
|
||||
app.register_blueprint(admin_bp, url_prefix="/api/admin")
|
||||
app.register_blueprint(groups_bp, url_prefix="/api/groups")
|
||||
app.register_blueprint(chat_bp, url_prefix="/api/chat")
|
||||
app.register_blueprint(me_bp, url_prefix="/api/me")
|
||||
app.register_blueprint(analytics_bp, url_prefix="/api/analytics")
|
||||
|
||||
register_error_handlers(app)
|
||||
|
||||
from .auth.users import UserStore
|
||||
from .llm import LLMClient
|
||||
from .llm import LLMError
|
||||
from .services.groups import GroupStore
|
||||
from .services.sessions import SessionStore
|
||||
from .services.trainee import MyPersonaStore
|
||||
|
||||
app.extensions["user_store"] = UserStore(Config.DATA_DIR)
|
||||
app.extensions["group_store"] = GroupStore(Config.DATA_DIR)
|
||||
app.extensions["session_store"] = SessionStore(Config.DATA_DIR)
|
||||
app.extensions["my_persona_store"] = MyPersonaStore(Config.DATA_DIR)
|
||||
try:
|
||||
app.extensions["llm"] = LLMClient()
|
||||
except LLMError as exc:
|
||||
print(f"[warn] LLM not configured yet: {exc}")
|
||||
app.extensions["llm"] = None
|
||||
|
||||
bootstrap_admin(app.extensions["user_store"])
|
||||
|
||||
@app.get("/health")
|
||||
def health():
|
||||
return {"status": "ok", "service": Config.APP_NAME}
|
||||
|
||||
# Serve built Vue frontend if present (production single-app mode).
|
||||
_register_frontend(app)
|
||||
return app
|
||||
|
||||
|
||||
def _register_frontend(app: Flask) -> None:
|
||||
from flask import send_from_directory
|
||||
|
||||
# repo-root frontend/dist (factory.py -> app/ -> backend/ -> repo root)
|
||||
dist = Path(__file__).resolve().parent.parent.parent / "frontend" / "dist"
|
||||
if not (dist / "index.html").exists():
|
||||
print(f"[info] frontend build not found at {dist}; API-only mode")
|
||||
return
|
||||
|
||||
@app.route("/")
|
||||
def index():
|
||||
return send_from_directory(dist, "index.html")
|
||||
|
||||
@app.route("/<path:path>", methods=["GET", "HEAD", "OPTIONS", "POST", "PUT", "DELETE", "PATCH"])
|
||||
def assets(path: str):
|
||||
# Never let the SPA fallback shadow API/auth routes: return 404 for them.
|
||||
if path.startswith("api/") or path.startswith("health"):
|
||||
return ("not found", 404)
|
||||
candidate = dist / path
|
||||
if candidate.is_file():
|
||||
return send_from_directory(dist, path)
|
||||
# SPA fallback for client-side routes
|
||||
return send_from_directory(dist, "index.html")
|
||||
Reference in New Issue
Block a user