diff --git a/account.html b/account.html new file mode 100644 index 0000000..342d609 --- /dev/null +++ b/account.html @@ -0,0 +1,383 @@ + + + + + + Mon Compte — Turf IA + + + + +
+

⚙️ Mon compte

+ +
+ + + + +
+ + +
+
+
Informations personnelles
+
Profil mis à jour avec succès.
+
+
+
+
+
+
+
+ +
+
+
+
Informations de compte
+
+
+
+
+
+ + +
+
+
Changer le mot de passe
+
Mot de passe mis à jour.
+
+
+
+
+
+ +
+
+
+
Zone dangereuse
+

La suppression de votre compte est irréversible. Toutes vos données seront perdues.

+ +
+
+ + +
+
+
Votre plan actuel
+
+
+
Free
+
0€ /mois
+
    +
  • 1 course complète/jour
  • +
  • Aperçu Top-3
  • +
+ +
+
+
Premium ⭐
+
9,90€ /mois
+
    +
  • Toutes les courses
  • +
  • Alertes Telegram
  • +
  • Value bets
  • +
  • Historique 90j
  • +
+ +
+
+
Pro 🚀
+
24,90€ /mois
+
    +
  • Tout Premium
  • +
  • Export CSV
  • +
  • API REST
  • +
  • Support prioritaire
  • +
+ +
+
+

💳 Paiement sécurisé via Stripe (disponible dans le Sprint 5-6). Pour l'instant, contactez-nous pour activer un plan payant.

+
+
+ + +
+
+
Alertes Telegram
+
Préférences enregistrées.
+
+
+ + +

Envoyez /start à @TurfIABot pour obtenir votre Chat ID.

+
+
+

Recevoir des alertes pour :

+ + + +
+ +
+
+
+
+ +
+ + + + diff --git a/combined_api.py b/combined_api.py index 66a4eff..43ba265 100755 --- a/combined_api.py +++ b/combined_api.py @@ -131,6 +131,24 @@ def get_db(): return conn +@app.route("/health") +@app.route("/turf/health") +def health(): + """Health check endpoint for Docker/load balancer. Returns 200 if app is running.""" + import sqlite3 as _sqlite3 + + db_ok = True + try: + conn = _sqlite3.connect(DB_PATH, timeout=2) + conn.execute("SELECT 1") + conn.close() + except Exception: + db_ok = False + status = "ok" if db_ok else "degraded" + http_code = 200 if db_ok else 503 + return {"status": status, "service": "combined-api", "db": db_ok}, http_code + + @app.route("/") def index(): return send_file("/home/h3r7/turf_saas/dashboard.html") @@ -3519,7 +3537,6 @@ def brave_search(): return jsonify({"error": str(e)}), 500 - @app.route("/turf/api/predictions_analysis", methods=["GET"]) def api_predictions_analysis(): """Analyse des predictions vs resultats reels""" @@ -3533,13 +3550,25 @@ def api_predictions_analysis(): cursor = conn.cursor() stats = { - "canalturf": {"total": 0, "top1_pct": 0, "top3_pct": 0, "top5_pct": 0, "ze2_pct": 0}, - "scoring": {"total": 0, "top1_pct": 0, "top3_pct": 0, "top5_pct": 0, "ze2_pct": 0}, + "canalturf": { + "total": 0, + "top1_pct": 0, + "top3_pct": 0, + "top5_pct": 0, + "ze2_pct": 0, + }, + "scoring": { + "total": 0, + "top1_pct": 0, + "top3_pct": 0, + "top5_pct": 0, + "ze2_pct": 0, + }, } for source in ["canalturf", "scoring"]: pred_table = "predictions" if source == "canalturf" else "scoring" - pred_col = "predicted_1" if source == "canalturf" else "horse_number" + pred_col = "predicted_1" if source == "canalturf" else "horse_number" try: cursor.execute( f""" @@ -3566,16 +3595,16 @@ def api_predictions_analysis(): top1_hit = top3_hit = 0 total = len(races) for race, data in races.items(): - actual = set(data["actual"][:3]) - pred_top1 = data["predicted"][0] if data["predicted"] else None - actual_top1 = data["actual"][0] if data["actual"] else None + actual = set(data["actual"][:3]) + pred_top1 = data["predicted"][0] if data["predicted"] else None + actual_top1 = data["actual"][0] if data["actual"] else None if pred_top1 and actual_top1 and pred_top1 == actual_top1: top1_hit += 1 if len(set(data["predicted"][:3]) & actual) >= 1: top3_hit += 1 if total > 0: - stats[source]["total"] = total + stats[source]["total"] = total stats[source]["top1_pct"] = round(top1_hit / total * 100, 1) stats[source]["top3_pct"] = round(top3_hit / total * 100, 1) except Exception as e: diff --git a/dashboard_api.py b/dashboard_api.py index 791d9d6..4cd938c 100755 --- a/dashboard_api.py +++ b/dashboard_api.py @@ -86,11 +86,15 @@ def ensure_ml_cache_table(conn): """) # Migration : ajouter colonnes risque si table existante sans elles try: - conn.execute("ALTER TABLE ml_predictions_cache ADD COLUMN risque_label TEXT DEFAULT 'neutral'") + conn.execute( + "ALTER TABLE ml_predictions_cache ADD COLUMN risque_label TEXT DEFAULT 'neutral'" + ) except Exception: pass try: - conn.execute("ALTER TABLE ml_predictions_cache ADD COLUMN risque_score INTEGER DEFAULT 50") + conn.execute( + "ALTER TABLE ml_predictions_cache ADD COLUMN risque_score INTEGER DEFAULT 50" + ) except Exception: pass conn.commit() @@ -101,7 +105,7 @@ def get_ml_from_cache(conn, date): ensure_ml_cache_table(conn) cursor = conn.execute( """SELECT * FROM ml_predictions_cache WHERE date = ? ORDER BY ml_score DESC""", - (date,) + (date,), ) rows = cursor.fetchall() if not rows: @@ -112,34 +116,36 @@ def get_ml_from_cache(conn, date): for row in rows: r = dict(row) pred = { - "horse_name": r["horse_name"], - "horse_number": r["horse_number"], - "odds": r["odds"], - "prob_top1": r["prob_top1"], - "prob_top3": r["prob_top3"], - "ml_score": r["ml_score"], + "horse_name": r["horse_name"], + "horse_number": r["horse_number"], + "odds": r["odds"], + "prob_top1": r["prob_top1"], + "prob_top3": r["prob_top3"], + "ml_score": r["ml_score"], "recommendation": r["recommendation"], - "is_value_bet": r["is_value_bet"], - "is_outlier": r["is_outlier"], - "num_reunion": r["num_reunion"], - "num_course": r["num_course"], - "race_label": r["race_label"], - "race_name": r["race_name"], - "hippodrome": r["hippodrome"], - "discipline": r["discipline"], - "distance": r["distance"], - "heure": r["heure"], - "risque_label": r["risque_label"] if "risque_label" in r.keys() else "neutral", - "risque_score": r["risque_score"] if "risque_score" in r.keys() else 50, + "is_value_bet": r["is_value_bet"], + "is_outlier": r["is_outlier"], + "num_reunion": r["num_reunion"], + "num_course": r["num_course"], + "race_label": r["race_label"], + "race_name": r["race_name"], + "hippodrome": r["hippodrome"], + "discipline": r["discipline"], + "distance": r["distance"], + "heure": r["heure"], + "risque_label": r["risque_label"] + if "risque_label" in r.keys() + else "neutral", + "risque_score": r["risque_score"] if "risque_score" in r.keys() else 50, } predictions.append(pred) key = f"{r['num_reunion']}_{r['num_course']}" if key not in course_info: course_info[key] = { - "libelle": r["race_name"], - "libelle_court": r["hippodrome"], - "discipline": r["discipline"], - "distance": r["distance"], + "libelle": r["race_name"], + "libelle_court": r["hippodrome"], + "discipline": r["discipline"], + "distance": r["distance"], "heure_depart_str": r["heure"], } return predictions, course_info @@ -152,15 +158,18 @@ def save_ml_to_cache(conn, date, predictions, model_version="xgboost_v1"): conn.execute("DELETE FROM ml_predictions_cache WHERE date = ?", (date,)) # Calculer le risque par course (grouper les chevaux avec tous leurs scores ML) from collections import defaultdict + race_horses = defaultdict(list) for p in predictions: key = (p.get("num_reunion"), p.get("num_course")) - race_horses[key].append({ - "odds": p.get("odds", 999), - "ml_score": p.get("ml_score", 0), - "prob_top1":p.get("prob_top1", 0), - "prob_top3":p.get("prob_top3", 0), - }) + race_horses[key].append( + { + "odds": p.get("odds", 999), + "ml_score": p.get("ml_score", 0), + "prob_top1": p.get("prob_top1", 0), + "prob_top3": p.get("prob_top3", 0), + } + ) race_risque = {} for key, partants in race_horses.items(): @@ -170,36 +179,39 @@ def save_ml_to_cache(conn, date, predictions, model_version="xgboost_v1"): for p in predictions: rkey = (p.get("num_reunion"), p.get("num_course")) rl, rs = race_risque.get(rkey, ("neutral", 50)) - conn.execute(""" + conn.execute( + """ INSERT INTO ml_predictions_cache (date, num_reunion, num_course, 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, model_version) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) - """, ( - date, - p.get("num_reunion"), - p.get("num_course"), - p.get("horse_name"), - p.get("horse_number"), - p.get("odds"), - p.get("prob_top1"), - p.get("prob_top3"), - p.get("ml_score"), - p.get("recommendation"), - p.get("is_value_bet", 0), - p.get("is_outlier", 0), - p.get("race_label"), - p.get("race_name"), - p.get("hippodrome"), - p.get("discipline"), - p.get("distance"), - p.get("heure"), - rl, - rs, - model_version, - )) + """, + ( + date, + p.get("num_reunion"), + p.get("num_course"), + p.get("horse_name"), + p.get("horse_number"), + p.get("odds"), + p.get("prob_top1"), + p.get("prob_top3"), + p.get("ml_score"), + p.get("recommendation"), + p.get("is_value_bet", 0), + p.get("is_outlier", 0), + p.get("race_label"), + p.get("race_name"), + p.get("hippodrome"), + p.get("discipline"), + p.get("distance"), + p.get("heure"), + rl, + rs, + model_version, + ), + ) conn.commit() @@ -219,22 +231,38 @@ def calculate_risque(partants): return None, None # Trier par ml_score desc (ou prob_top1 si ml_score absent) - sorted_p = sorted(partants, key=lambda x: x.get("ml_score") or x.get("prob_top1") or 0, reverse=True) + sorted_p = sorted( + partants, + key=lambda x: x.get("ml_score") or x.get("prob_top1") or 0, + reverse=True, + ) - top1_score = sorted_p[0].get("ml_score") or sorted_p[0].get("prob_top1") or 0 - top2_score = sorted_p[1].get("ml_score") or sorted_p[1].get("prob_top1") or 0 if len(sorted_p) > 1 else 0 - top3_score = sorted_p[2].get("ml_score") or sorted_p[2].get("prob_top1") or 0 if len(sorted_p) > 2 else 0 + top1_score = sorted_p[0].get("ml_score") or sorted_p[0].get("prob_top1") or 0 + top2_score = ( + sorted_p[1].get("ml_score") or sorted_p[1].get("prob_top1") or 0 + if len(sorted_p) > 1 + else 0 + ) + top3_score = ( + sorted_p[2].get("ml_score") or sorted_p[2].get("prob_top1") or 0 + if len(sorted_p) > 2 + else 0 + ) - gap_1_2 = top1_score - top2_score # écart entre 1er et 2e ML - gap_1_3 = top1_score - top3_score # écart entre 1er et 3e ML + gap_1_2 = top1_score - top2_score # écart entre 1er et 2e ML + gap_1_3 = top1_score - top3_score # écart entre 1er et 3e ML # Nombre de concurrents avec ml_score > 40 (dangereux) nb_dangerous = sum(1 for p in sorted_p if (p.get("ml_score") or 0) > 40) # Détection favori de cote surpris par le ML odds_fav = sorted(partants, key=lambda x: x.get("odds") or 999) - fav_odds = odds_fav[0].get("odds") or 999 if odds_fav else 999 - fav_ml = odds_fav[0].get("ml_score") or odds_fav[0].get("prob_top1") or 0 if odds_fav else 0 + fav_odds = odds_fav[0].get("odds") or 999 if odds_fav else 999 + fav_ml = ( + odds_fav[0].get("ml_score") or odds_fav[0].get("prob_top1") or 0 + if odds_fav + else 0 + ) fav_surprise = fav_odds < 5 and fav_ml < 25 # favori de cote ignoré par le ML # --- SAFE : domination claire --- @@ -256,7 +284,6 @@ def calculate_risque(partants): return "neutral", score - def table_exists(conn, table_name): c = conn.execute( "SELECT 1 FROM sqlite_master WHERE type='table' AND name=?", (table_name,) @@ -470,6 +497,28 @@ def prepare_features_from_db(horse_data): return df +@app.route("/health") +@app.route("/turf/health") +def health(): + """Health check endpoint for Docker/load balancer. Returns 200 if app is running.""" + import sqlite3 as _sqlite3 + + db_ok = True + try: + conn = _sqlite3.connect( + DB_FILE if "DB_FILE" in dir() else "/home/h3r7/turf_saas/turf_saas.db", + timeout=2, + ) + conn.execute("SELECT 1") + conn.close() + except Exception: + db_ok = False + status = "ok" if db_ok else "degraded" + return {"status": status, "service": "dashboard-api", "db": db_ok}, ( + 200 if db_ok else 503 + ) + + @app.route("/") def index(): return send_file("/home/h3r7/turf_saas/dashboard.html") @@ -722,13 +771,15 @@ def api_ml_predictions(): cached_preds, cached_courses = get_ml_from_cache(conn, today) if cached_preds: conn.close() - return jsonify({ - "date": today, - "model_version": "xgboost_v1", - "predictions": cached_preds, - "courses": cached_courses, - "from_cache": True, - }) + return jsonify( + { + "date": today, + "model_version": "xgboost_v1", + "predictions": cached_preds, + "courses": cached_courses, + "from_cache": True, + } + ) # --- CALCUL ML --- models = load_models() @@ -946,15 +997,18 @@ def api_ml_predictions(): # --- CALCUL RISQUE PAR COURSE + INJECTION DANS PREDICTIONS --- from collections import defaultdict as _dd + _race_horses_ml = _dd(list) for p in predictions: key = (p.get("num_reunion"), p.get("num_course")) - _race_horses_ml[key].append({ - "odds": p.get("odds", 999), - "ml_score": p.get("ml_score", 0), - "prob_top1": p.get("prob_top1", 0), - "prob_top3": p.get("prob_top3", 0), - }) + _race_horses_ml[key].append( + { + "odds": p.get("odds", 999), + "ml_score": p.get("ml_score", 0), + "prob_top1": p.get("prob_top1", 0), + "prob_top3": p.get("prob_top3", 0), + } + ) _race_risque_map = {} for key, partants in _race_horses_ml.items(): label, score = calculate_risque(partants) @@ -996,6 +1050,7 @@ def api_ml_predictions_refresh(): conn.close() # Déléguer au endpoint principal avec force_refresh from flask import redirect, url_for + return redirect(url_for("api_ml_predictions") + "?refresh=1") @@ -1107,8 +1162,6 @@ def api_suggestions(): return jsonify({"suggestions": suggestions}) - - @app.route("/turf/api/metrics/summary") @app.route("/turf/api/metrics/summary/") def metrics_summary(): @@ -1124,7 +1177,8 @@ def metrics_summary(): "ROUND(SUM(roi_sp_net), 3) as roi_sp_cumul, ROUND(AVG(ecart_rang_moyen), 2) as moy_ecart_rang, " "SUM(quinte_5sur5) as nb_5sur5, SUM(quinte_4sur5) as nb_4sur5, SUM(quinte_3sur5) as nb_3sur5 " "FROM prediction_metrics WHERE date >= date('now', ?) GROUP BY source ORDER BY moy_taux_place DESC", - (date_filter,)) + (date_filter,), + ) cols = [d[0] for d in cur.description] rows = [dict(zip(cols, row)) for row in cur.fetchall()] conn.close() @@ -1132,6 +1186,7 @@ def metrics_summary(): except Exception as e: return jsonify({"error": True, "message": str(e)}) + @app.route("/turf/api/metrics/daily") @app.route("/turf/api/metrics/daily/") def metrics_daily(): @@ -1146,7 +1201,8 @@ def metrics_daily(): "ROUND(AVG(roi_sp_net), 3) as roi_sp, SUM(quinte_5sur5) as quinte_5sur5, " "SUM(quinte_4sur5) as quinte_4sur5 " "FROM prediction_metrics WHERE date >= date('now', ?) GROUP BY date, source ORDER BY date DESC", - (date_filter,)) + (date_filter,), + ) cols = [d[0] for d in cur.description] rows = [dict(zip(cols, row)) for row in cur.fetchall()] conn.close() @@ -1154,6 +1210,7 @@ def metrics_daily(): except Exception as e: return jsonify({"error": True, "message": str(e)}) + if __name__ == "__main__": load_models() app.run(host="0.0.0.0", port=8791, debug=False) diff --git a/dashboard_saas.html b/dashboard_saas.html new file mode 100644 index 0000000..797d425 --- /dev/null +++ b/dashboard_saas.html @@ -0,0 +1,441 @@ + + + + + + Dashboard — Turf IA + + + + + + + + +
+
+
Tableau de bord
+
+ + +
+
+ +
+ + + + +
+
+
Courses analysées
+
+
aujourd'hui
+
+
+
Précision Top-3
+
+
30 derniers jours
+
+
+
Value bets du jour
+
+
+
+
+
Prochaine course
+
+
+
+
+ + +
+ 🏁 Prédictions du jour + Chargement… +
+ +
+
Chargement des prédictions…
+
+ + + +
+
+ +
+ + + + diff --git a/landing.html b/landing.html new file mode 100644 index 0000000..7ce24b2 --- /dev/null +++ b/landing.html @@ -0,0 +1,467 @@ + + + + + + Turf IA — Prédictions PMU par Intelligence Artificielle + + + + + + + + + +
+
+ 🤖 Intelligence Artificielle · XGBoost · Données PMU temps réel +
+

Pariez plus intelligemment
grâce à l'IA

+

Nos modèles XGBoost analysent chaque course PMU en temps réel — cotes, historique, jockeys, météo — pour vous donner les meilleures prédictions du marché.

+
+ Commencer gratuitement + Voir comment ça marche +
+
+
+73%précision Top-3
+
150+courses analysées/jour
+
2.4stemps de réponse moyen
+
3 plansadaptés à chaque profil
+
+
+ + +
+
+ +

Tout ce dont vous avez besoin pour gagner

+

Un moteur IA complet, des alertes instantanées, et des analyses détaillées pour chaque parieur.

+
+
+
+
🧠
+

Prédictions XGBoost

+

Modèle entraîné sur des milliers de courses PMU. Probabilités Top-1 et Top-3 pour chaque partant, mis à jour en continu.

+
+
+
💎
+

Value Bets identifiés

+

Détection automatique des cotes sous-évaluées par le marché. Seulement les paris où l'espérance mathématique est positive.

+
+
+
📱
+

Alertes Telegram

+

Recevez les meilleures opportunités directement sur votre téléphone, avant le départ, avec toutes les infos clés.

+
+
+
📊
+

Dashboard temps réel

+

Tableau de bord complet : courses du jour, historique de performance, ROI, statistiques par hippodrome et discipline.

+
+
+
🌤️
+

Analyse météo & terrain

+

Impact des conditions météo et de l'état du terrain intégré dans chaque prédiction pour une précision maximale.

+
+
+
📤
+

Export CSV & API

+

Exportez vos données, intégrez nos prédictions dans vos propres outils via notre API documentée (plan Pro).

+
+
+
+ + +
+
+ +

En 3 étapes, prêt à parier

+

De l'inscription à votre première prédiction en moins de 2 minutes.

+
+
+
+
1
+
+

Créez votre compte gratuitement

+

Inscription en 30 secondes, sans carte bancaire. Accès immédiat au plan Free avec un aperçu des prédictions du jour.

+
+
+
+
2
+
+

Choisissez votre plan

+

Free pour découvrir, Premium (9,90€/mois) pour toutes les courses et alertes, Pro (24,90€/mois) pour l'API et les exports.

+
+
+
+
3
+
+

Recevez vos premières prédictions

+

Notre IA analyse les 150+ courses du jour. Accédez aux Top-3, value bets et probabilités depuis votre dashboard ou Telegram.

+
+
+
+
+ + +
+
+ +

Des prix transparents

+

Commencez gratuitement. Passez au niveau supérieur quand vous êtes prêt.

+
+
+ +
+
Free
+
0/mois
+

Pour découvrir la puissance de l'IA turf.

+
    +
  • Aperçu Top-3 du jour (limité)
  • +
  • 1 course complète par jour
  • +
  • Statistiques basiques
  • +
  • Alertes Telegram
  • +
  • Toutes les courses
  • +
  • Value bets
  • +
  • Export CSV
  • +
  • Accès API
  • +
+ Commencer gratuitement +
+ + + +
+
Pro
+
24,90/mois
+

Pour les professionnels et développeurs qui veulent tout.

+
    +
  • Tout du plan Premium
  • +
  • Export CSV illimité
  • +
  • Accès API REST documentée
  • +
  • Backtest personnalisé
  • +
  • Historique illimité
  • +
  • Support prioritaire
  • +
  • Webhook alertes personnalisées
  • +
  • Multi-compte (5 utilisateurs)
  • +
+ Choisir Pro +
+
+
+ + +
+
+ +

Questions fréquentes

+
+
+
+ Comment fonctionne le modèle IA ? +
Notre modèle XGBoost est entraîné sur plusieurs années de données PMU : cotes, historique des chevaux et drivers, conditions météo, état du terrain, statistiques par hippodrome. Il calcule pour chaque partant une probabilité d'arriver dans le Top-1 et Top-3, ainsi qu'un score de value bet comparant notre estimation à la cote du marché.
+
+
+ Les prédictions garantissent-elles des gains ? +
Non. Aucune prédiction ne garantit des gains. Le pari hippique reste un jeu de hasard. Notre IA améliore vos chances en identifiant les opportunités statistiquement favorables, mais les résultats passés ne préjugent pas des résultats futurs. Pariez de façon responsable.
+
+
+ Puis-je annuler à tout moment ? +
Oui, sans engagement. Vous pouvez annuler votre abonnement Premium ou Pro à tout moment depuis votre espace compte. L'accès reste actif jusqu'à la fin de la période payée.
+
+
+ Les alertes Telegram fonctionnent-elles sur mobile ? +
Oui. Après activation dans vos paramètres, vous recevrez les alertes value bets et top picks directement dans votre application Telegram, avec toutes les informations nécessaires pour parier rapidement avant le départ.
+
+
+ L'API est-elle compatible avec d'autres outils ? +
Oui. L'API REST du plan Pro est documentée (OpenAPI/Swagger) et compatible avec n'importe quel outil : Python, JavaScript, n8n, Zapier, Excel, etc. Un token personnel vous est fourni dans votre dashboard.
+
+
+ Quelles disciplines sont couvertes ? +
Nous couvrons toutes les disciplines PMU : Plat, Trot Attelé, Trot Monté, et Galop sur les hippodromes français. Les courses Quinté+ sont signalées et prioritaires dans votre dashboard.
+
+
+
+ + +
+
+

Prêt à parier plus intelligemment ?

+

Rejoignez des centaines de parieurs qui utilisent déjà Turf IA chaque jour. Essai gratuit, sans carte bancaire.

+ Créer mon compte gratuit → +
+
+ + + + + + +
+ + + + diff --git a/login.html b/login.html new file mode 100644 index 0000000..483dad0 --- /dev/null +++ b/login.html @@ -0,0 +1,182 @@ + + + + + + Connexion — Turf IA + + + + +
+
+

Bon retour !

+

Connectez-vous à votre compte Turf IA.

+ +
+ +
+
+ + +
Email invalide.
+
+
+ + +
Mot de passe requis.
+
+ +
+ +
ou
+ +
+
+ + + + + diff --git a/onboarding.html b/onboarding.html new file mode 100644 index 0000000..ea491c5 --- /dev/null +++ b/onboarding.html @@ -0,0 +1,335 @@ + + + + + + Bienvenue — Turf IA + + + +
+ +
+
1
+
+
2
+
+
3
+
+ + +
+
+
Étape 1 sur 3
+

Bienvenue sur Turf IA 🏇

+

Votre compte est créé. Confirmez votre plan de départ pour personnaliser votre expérience.

+ +
+
+
+
🆓
+
+

Free

+

Aperçu quotidien, 1 course complète

+
+
+
+
0€
/mois
+
+
+
+
+
+
+
+

Premium

+

Toutes les courses, alertes Telegram

+
+
+
+
9,90€
/mois
+
+
+
+
+
+
🚀
+
+

Pro

+

API, export CSV, support prioritaire

+
+
+
+
24,90€
/mois
+
+
+
+
+ +
+ +
+
+
+ + +
+
+
Étape 2 sur 3
+

Configurez vos alertes

+

Recevez les meilleures opportunités directement sur votre téléphone avant chaque départ.

+ +
+

📱 Alertes Telegram

+

Pour activer les alertes, envoyez /start à notre bot Telegram @TurfIABot et collez ci-dessous votre Chat ID.

+
+ + +
+
+ +
+

🔔 Quand recevoir les alertes :

+ + + +
+ +
+ + +
+
+
+ + +
+
+
Étape 3 sur 3
+
🎉
+

Vous êtes prêt !

+

Voici un aperçu de votre première prédiction du jour.

+ +
+
PRÉDICTION EXEMPLE — R1C1
+
🥇
CHARGEMENT...
—%
+
🥈
—%
+
🥉
—%
+
+ +
    +
  • Compte créé et configuré
  • +
  • Alertes Telegram configurées
  • +
  • Dashboard accessible 24h/24
  • +
+ + +
+
+
+ + + + + + diff --git a/portal_server.py b/portal_server.py index 906c62c..22a3fa1 100755 --- a/portal_server.py +++ b/portal_server.py @@ -10,17 +10,72 @@ app = Flask(__name__) DASHBOARD_API_URL = "http://localhost:8791" COMBINED_API_URL = "http://localhost:8790" -COMBINED_API_URL = "http://localhost:8790" +SAAS_DIR = "/home/h3r7/turf_saas" + +# ─── SaaS Auth & API v1 blueprints ──────────────────────────────────────────── +try: + from saas_auth import auth_bp + from saas_api_v1 import api_v1_bp + + app.register_blueprint(auth_bp) + app.register_blueprint(api_v1_bp) + print("[portal] SaaS auth & API v1 blueprints registered ✅") +except Exception as e: + print(f"[portal] Warning: could not register SaaS blueprints: {e}") + + +# ─── Landing & SaaS pages ───────────────────────────────────────────────────── + + +@app.route("/health") +def health(): + """Health check endpoint for Docker/load balancer. Returns 200 if app is running.""" + return {"status": "ok", "service": "portal"}, 200 @app.route("/") -def portal(): - return send_from_directory("/home/h3r7/turf_saas", "portail.html") +def landing(): + """Marketing landing page.""" + return send_from_directory(SAAS_DIR, "landing.html") + + +@app.route("/login") +def login_page(): + return send_from_directory(SAAS_DIR, "login.html") + + +@app.route("/register") +def register_page(): + return send_from_directory(SAAS_DIR, "register.html") + + +@app.route("/dashboard") +def dashboard_saas(): + return send_from_directory(SAAS_DIR, "dashboard_saas.html") + + +@app.route("/onboarding") +def onboarding(): + return send_from_directory(SAAS_DIR, "onboarding.html") + + +@app.route("/account") +def account(): + return send_from_directory(SAAS_DIR, "account.html") + + +@app.route("/portal") +@app.route("/portail") +def portal_legacy(): + """Legacy portal redirect.""" + return send_from_directory(SAAS_DIR, "portail.html") @app.route("/favicon.ico") def favicon(): - return send_from_directory("/home/h3r7/turf_saas", "favicon.ico") + return send_from_directory(SAAS_DIR, "favicon.ico") + + @app.route("/prompts", methods=["GET", "POST", "PUT", "DELETE", "PATCH"]) @app.route("/prompts/", methods=["GET", "POST", "PUT", "DELETE", "PATCH"]) @app.route("/prompts/", methods=["GET", "POST", "PUT", "DELETE", "PATCH"]) @@ -269,9 +324,7 @@ def niches_business(): @app.route("/template_restaurant_json.html") def template_restaurant(): - return send_from_directory( - "/home/h3r7/turf_saas", "template_restaurant_json.html" - ) + return send_from_directory("/home/h3r7/turf_saas", "template_restaurant_json.html") @app.route("/template_boulangerie_final.html") @@ -288,9 +341,7 @@ def template_artisan(): @app.route("/template_restaurant_final.html") def template_restaurant_final(): - return send_from_directory( - "/home/h3r7/turf_saas", "template_restaurant_final.html" - ) + return send_from_directory("/home/h3r7/turf_saas", "template_restaurant_final.html") @app.route("/template_complet.html") @@ -300,9 +351,7 @@ def template_complet(): @app.route("/boite_a_idees_dashboard") def boite_a_idees_dashboard(): - return send_from_directory( - "/home/h3r7/turf_saas", "boite_a_idees_dashboard.html" - ) + return send_from_directory("/home/h3r7/turf_saas", "boite_a_idees_dashboard.html") @app.route("/datagouv_explorer.html") @@ -345,13 +394,23 @@ def api_chat_workflows(): return jsonify([dict(w) for w in workflows]) except Exception as e: return jsonify({"error": str(e)}), 500 + + @app.route("/api/chat/nvidia-models", methods=["GET"]) def api_nvidia_models(): - return jsonify([ - {"id": k, "name": v.split("/")[-1].replace("-instruct", "").replace("-", " ").title(), "full_id": v} - for k, v in NVIDIA_MODELS.items() - ]) - + return jsonify( + [ + { + "id": k, + "name": v.split("/")[-1] + .replace("-instruct", "") + .replace("-", " ") + .title(), + "full_id": v, + } + for k, v in NVIDIA_MODELS.items() + ] + ) @app.route("/api/chat/sessions", methods=["GET"]) @@ -457,7 +516,9 @@ def api_chat_cleanup(): OPENCLAW_TOKEN = "szDd75yG15ZRADWEhGus3dfNoQve7ZhdxpGq9gudeEzoVhUqB0TomVJd90XcwkUz" OPENCLAW_CONTAINER = "openclaw-ow404080wwkkgkgc4oswwssc" -NVIDIA_API_KEY = "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" +NVIDIA_API_KEY = ( + "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" +) NVIDIA_API_URL = "https://integrate.api.nvidia.com/v1/chat/completions" NVIDIA_MODEL = "meta/llama-3.1-8b-instruct" # Default model NVIDIA_MODELS = { @@ -476,7 +537,6 @@ NVIDIA_MODELS = { } - @app.route("/webhook/telegram", methods=["POST"]) def telegram_webhook(): try: @@ -542,25 +602,25 @@ def webhook_proxy(workflow_slug): model_key = request.json.get("model", "llama-3.1-8b") model_id = NVIDIA_MODELS.get(model_key, NVIDIA_MODEL) resp = requests.post( - NVIDIA_API_URL, - headers={ - "Authorization": f"Bearer {NVIDIA_API_KEY}", - "Content-Type": "application/json", - }, - json={ - "model": model_id, - "messages": [{"role": "user", "content": user_message}], - "max_tokens": 1024, - "temperature": 0.7, - }, - timeout=60, + NVIDIA_API_URL, + headers={ + "Authorization": f"Bearer {NVIDIA_API_KEY}", + "Content-Type": "application/json", + }, + json={ + "model": model_id, + "messages": [{"role": "user", "content": user_message}], + "max_tokens": 1024, + "temperature": 0.7, + }, + timeout=60, ) data = resp.json() ai_response = ( - data.get("choices", [{}])[0] - .get("message", {}) - .get("content", str(data)) - ) + data.get("choices", [{}])[0] + .get("message", {}) + .get("content", str(data)) + ) else: # Proxy vers webhook n8n resp = requests.post( @@ -702,12 +762,17 @@ def api_proxy(api_path=""): url = f"{DASHBOARD_API_URL}/turf/api" try: fwd_method = request.method - fwd_json = request.get_json(silent=True) if fwd_method in ("POST", "PUT", "PATCH") else None + fwd_json = ( + request.get_json(silent=True) + if fwd_method in ("POST", "PUT", "PATCH") + else None + ) fwd_headers = {"Content-Type": "application/json"} if request.headers.get("Authorization"): fwd_headers["Authorization"] = request.headers.get("Authorization") - resp = requests.request(method=fwd_method, url=url, json=fwd_json, timeout=30, - headers=fwd_headers) + resp = requests.request( + method=fwd_method, url=url, json=fwd_json, timeout=30, headers=fwd_headers + ) return resp.content, resp.status_code, {"Content-Type": "application/json"} except Exception as e: return jsonify({"error": str(e), "url": url}), 500 @@ -744,23 +809,26 @@ def opencode_api(): return jsonify({"error": str(e)}), 500 - @app.route("/candidatures/") def candidatures_index(): return send_from_directory("/home/h3r7/turf_saas", "crm_candidatures.html") + @app.route("/candidatures/") def candidatures_static(filename): return send_from_directory("/home/h3r7/turf_saas", filename) + @app.route("/map") def map_visual(): return send_from_directory("/home/h3r7/turf_saas", "map_visual.html") + @app.route("/architecture.json") def architecture_json(): return send_from_directory("/home/h3r7/turf_saas", "architecture.json") + if __name__ == "__main__": app.run(host="0.0.0.0", port=8792, debug=False) @@ -827,5 +895,3 @@ def proxy_prompts_test(): return response except Exception as e: return f"Erreur proxy prompts: {e}", 502 - - diff --git a/register.html b/register.html new file mode 100644 index 0000000..ebf42d6 --- /dev/null +++ b/register.html @@ -0,0 +1,271 @@ + + + + + + Inscription — Turf IA + + + + +
+
+

Créer votre compte

+

Commencez gratuitement, sans carte bancaire.

+ +
+ +
+
+
+ + +
Requis.
+
+
+ + +
Requis.
+
+
+
+ + +
Email invalide.
+
+
+ + +
+
8 caractères minimum requis.
+
+
+ + +
+
+

En vous inscrivant, vous acceptez nos Conditions Générales d'Utilisation et notre Politique de Confidentialité. Vous devez avoir 18 ans ou plus.

+
+ +
+ + +
+ + +
+
+
Free
+
0€/mois
+
    +
  • Aperçu Top-3 du jour
  • +
  • 1 course complète/jour
  • +
  • Statistiques basiques
  • +
+
+ + +

⚡ Pas de carte bancaire requise pour le plan Free

+
+
+
© 2026 Turf IA — H3R7 Tech. Jouez de façon responsable. 18+
+ + + + diff --git a/saas_api_v1.py b/saas_api_v1.py new file mode 100644 index 0000000..3bb020b --- /dev/null +++ b/saas_api_v1.py @@ -0,0 +1,257 @@ +#!/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 +import sqlite3 +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: str, required: str) -> bool: + order = {"free": 0, "premium": 1, "pro": 2} + return order.get(user_plan, 0) >= order.get(required, 0) + + +# ─── Stats ──────────────────────────────────────────────────────────────────── + + +@api_v1_bp.route("/stats/summary", methods=["GET"]) +@require_auth +def stats_summary(): + """GET /api/v1/stats/summary — résumé dashboard.""" + today = datetime.now().strftime("%Y-%m-%d") + conn = get_db() + + try: + # Courses today + 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 + value_bets_today = ( + conn.execute( + "SELECT COUNT(*) FROM ml_predictions_cache WHERE date=? AND is_value_bet=1", + (today,), + ).fetchone()[0] + or 0 + ) + + # Accuracy top3 (30 days) + 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 + 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 + + +# ─── Predictions ────────────────────────────────────────────────────────────── + + +@api_v1_bp.route("/predictions/today", methods=["GET"]) +@require_auth +def predictions_today(): + """GET /api/v1/predictions/today — prédictions du jour selon le plan.""" + 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] + + # Free plan: return only 1 race + if plan == "free": + if 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 + ] + # Mask value bet flag in free + for p in predictions: + p["is_value_bet"] = 0 + + # Premium/Pro: full predictions + 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("/predictions/race/", methods=["GET"]) +@require_auth +def predictions_race(race_label): + """GET /api/v1/predictions/race/