Files
turf_saas/saas_api_v1.py
DevOps Engineer 41a9e36166 feat(sprint4-5): Landing page + onboarding SaaS — HRT-30
Frontend pages:
- landing.html: marketing page — hero, pricing (Free/9.90e/24.90e), features, FAQ, footer, mobile-first responsive, LCP < 2.5s friendly
- login.html: JWT auth login with JS validation, error handling, redirect-after-login
- register.html: registration with plan selection preview sidebar, password strength meter
- dashboard_saas.html: role-based dashboard (Free/Premium/Pro) with locked sections, race prediction cards, detailed table, stats row
- onboarding.html: 3-step wizard — plan confirm + Telegram alerts config + first prediction preview
- account.html: tabbed account management — profile, security (change-password, delete), plan upgrade, notification preferences

Backend:
- saas_auth.py: Flask Blueprint /api/v1/auth/* — register, login, token auth, profile/password/plan/preferences update, logout, delete-account
- saas_api_v1.py: Flask Blueprint /api/v1/* — stats/summary, predictions/today (plan-gated), value-bets (Premium+), CSV export (Pro)

Server:
- portal_server.py: register blueprints, serve all new SaaS routes at /login /register /dashboard /onboarding /account

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-25 18:04:19 +02:00

143 lines
5.4 KiB
Python

#!/usr/bin/env python3
"""
SaaS API v1 Blueprint — /api/v1/*
Stats, prédictions, résumés pour le dashboard SaaS.
Sprint 4-5 — HRT-30
"""
from flask import Blueprint, request, jsonify, Response
import sqlite3
import csv
import io
import os
from datetime import datetime
from saas_auth import require_auth
DB_PATH = os.environ.get("TURF_SAAS_DB", "/home/h3r7/turf_saas/turf_saas.db")
api_v1_bp = Blueprint("api_v1", __name__, url_prefix="/api/v1")
def get_db():
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
return conn
def plan_allows(user_plan, required):
order = {"free": 0, "premium": 1, "pro": 2}
return order.get(user_plan, 0) >= order.get(required, 0)
@api_v1_bp.route("/stats/summary", methods=["GET"])
@require_auth
def stats_summary():
today = datetime.now().strftime("%Y-%m-%d")
conn = get_db()
try:
courses_today = conn.execute(
"SELECT COUNT(DISTINCT num_reunion||'-'||num_course) FROM ml_predictions_cache WHERE date=?",
(today,)
).fetchone()[0] or 0
value_bets_today = conn.execute(
"SELECT COUNT(*) FROM ml_predictions_cache WHERE date=? AND is_value_bet=1",
(today,)
).fetchone()[0] or 0
acc_row = conn.execute("""
SELECT CAST(SUM(CASE WHEN p.ordre_arrivee BETWEEN 1 AND 3 AND m.recommendation='top3' THEN 1 ELSE 0 END) AS FLOAT)
/ NULLIF(COUNT(CASE WHEN m.recommendation='top3' THEN 1 END), 0) * 100 AS acc
FROM ml_predictions_cache m
JOIN pmu_partants p ON m.horse_name=p.nom AND m.date=p.date_programme
WHERE m.date >= date('now', '-30 days')
""").fetchone()
accuracy_top3 = round(acc_row[0], 1) if acc_row and acc_row[0] else None
next_race = conn.execute(
"SELECT heure, hippodrome FROM ml_predictions_cache WHERE date=? AND heure IS NOT NULL ORDER BY heure LIMIT 1",
(today,)
).fetchone()
conn.close()
return jsonify({
"courses_today": courses_today,
"value_bets_today": value_bets_today,
"accuracy_top3": accuracy_top3,
"next_race_time": next_race["heure"] if next_race else None,
"next_race_hippodrome": next_race["hippodrome"] if next_race else None,
}), 200
except Exception as e:
conn.close()
return jsonify({"error": str(e), "courses_today": 0, "value_bets_today": 0}), 200
@api_v1_bp.route("/predictions/today", methods=["GET"])
@require_auth
def predictions_today():
user = request.current_user
plan = user.get("plan", "free")
today = datetime.now().strftime("%Y-%m-%d")
conn = get_db()
try:
rows = conn.execute("""
SELECT horse_name, horse_number, odds, prob_top1, prob_top3,
ml_score, recommendation, is_value_bet, is_outlier,
race_label, race_name, hippodrome, discipline, distance,
heure, risque_label, risque_score, num_reunion, num_course
FROM ml_predictions_cache
WHERE date=?
ORDER BY num_reunion, num_course, ml_score DESC
""", (today,)).fetchall()
conn.close()
predictions = [dict(r) for r in rows]
if plan == "free" and predictions:
first = predictions[0]
first_key = (first["num_reunion"], first["num_course"])
predictions = [p for p in predictions if (p["num_reunion"], p["num_course"]) == first_key]
for p in predictions:
p["is_value_bet"] = 0
return jsonify({"date": today, "plan": plan, "count": len(predictions), "predictions": predictions}), 200
except Exception as e:
conn.close()
return jsonify({"error": str(e), "predictions": []}), 200
@api_v1_bp.route("/value-bets/today", methods=["GET"])
@require_auth
def value_bets_today():
user = request.current_user
plan = user.get("plan", "free")
if not plan_allows(plan, "premium"):
return jsonify({"error": "Cette fonctionnalité requiert un plan Premium ou Pro.", "upgrade_required": True}), 403
today = datetime.now().strftime("%Y-%m-%d")
conn = get_db()
rows = conn.execute("""
SELECT horse_name, race_label, race_name, hippodrome, odds, prob_top3, ml_score, risque_label, heure
FROM ml_predictions_cache WHERE date=? AND is_value_bet=1 ORDER BY ml_score DESC
""", (today,)).fetchall()
conn.close()
return jsonify({"value_bets": [dict(r) for r in rows], "count": len(rows)}), 200
@api_v1_bp.route("/export/csv", methods=["GET"])
@require_auth
def export_csv():
user = request.current_user
plan = user.get("plan", "free")
if not plan_allows(plan, "pro"):
return jsonify({"error": "L'export CSV requiert un plan Pro.", "upgrade_required": True}), 403
date_param = request.args.get("date", datetime.now().strftime("%Y-%m-%d"))
conn = get_db()
rows = conn.execute(
"SELECT * FROM ml_predictions_cache WHERE date=? ORDER BY num_reunion, num_course, ml_score DESC",
(date_param,)
).fetchall()
conn.close()
output = io.StringIO()
if rows:
writer = csv.DictWriter(output, fieldnames=rows[0].keys())
writer.writeheader()
writer.writerows([dict(r) for r in rows])
return Response(
output.getvalue(),
mimetype="text/csv",
headers={"Content-Disposition": f"attachment; filename=turf_ia_{date_param}.csv"}
)