diff --git a/aethas_balances.json b/aethas_balances.json deleted file mode 100644 index d8b69d3..0000000 --- a/aethas_balances.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "cagnotte_commune_vip": 34.988855 -} \ No newline at end of file diff --git a/aethas_ip_logs.json b/aethas_ip_logs.json index 536ddd8..3aca4a8 100644 --- a/aethas_ip_logs.json +++ b/aethas_ip_logs.json @@ -11650,5 +11650,179 @@ "action": "Acc\u00e8s \u00e0 /", "date": "2026-08-23 09:39:21", "timestamp": 1787470761.3066163 + }, + { + "ip": "127.0.0.1", + "action": "Acc\u00e8s \u00e0 /", + "date": "2026-08-23 10:29:03", + "timestamp": 1787473743.7729807 + }, + { + "ip": "127.0.0.1", + "action": "Acc\u00e8s \u00e0 /", + "date": "2026-08-23 10:29:07", + "timestamp": 1787473747.1458204 + }, + { + "ip": "127.0.0.1", + "action": "Acc\u00e8s \u00e0 /api/paypal/success", + "date": "2026-08-23 10:29:29", + "timestamp": 1787473769.173476 + }, + { + "ip": "127.0.0.1", + "action": "Acc\u00e8s \u00e0 /", + "date": "2026-08-23 10:29:29", + "timestamp": 1787473769.326424 + }, + { + "ip": "127.0.0.1", + "action": "Acc\u00e8s \u00e0 /", + "date": "2026-08-23 10:38:09", + "timestamp": 1787474289.483428 + }, + { + "ip": "127.0.0.1", + "action": "Acc\u00e8s \u00e0 /api/paypal/success", + "date": "2026-08-23 10:38:30", + "timestamp": 1787474310.2446322 + }, + { + "ip": "127.0.0.1", + "action": "Acc\u00e8s \u00e0 /", + "date": "2026-08-23 10:38:30", + "timestamp": 1787474310.3879251 + }, + { + "ip": "127.0.0.1", + "action": "Acc\u00e8s \u00e0 /", + "date": "2026-08-23 10:40:40", + "timestamp": 1787474440.6522577 + }, + { + "ip": "127.0.0.1", + "action": "Acc\u00e8s \u00e0 /api/paypal/success", + "date": "2026-08-23 10:41:17", + "timestamp": 1787474477.0745878 + }, + { + "ip": "127.0.0.1", + "action": "Acc\u00e8s \u00e0 /", + "date": "2026-08-23 10:45:53", + "timestamp": 1787474753.4862194 + }, + { + "ip": "127.0.0.1", + "action": "Acc\u00e8s \u00e0 /api/paypal/success", + "date": "2026-08-23 10:46:11", + "timestamp": 1787474771.704107 + }, + { + "ip": "127.0.0.1", + "action": "Acc\u00e8s \u00e0 /", + "date": "2026-08-23 10:46:11", + "timestamp": 1787474771.8661208 + }, + { + "ip": "127.0.0.1", + "action": "Acc\u00e8s \u00e0 /", + "date": "2026-08-23 11:09:04", + "timestamp": 1787476144.4969127 + }, + { + "ip": "127.0.0.1", + "action": "Acc\u00e8s \u00e0 /", + "date": "2026-08-23 11:09:05", + "timestamp": 1787476145.455936 + }, + { + "ip": "127.0.0.1", + "action": "Acc\u00e8s \u00e0 /", + "date": "2026-08-23 11:09:07", + "timestamp": 1787476147.144958 + }, + { + "ip": "127.0.0.1", + "action": "Acc\u00e8s \u00e0 /", + "date": "2026-08-23 11:09:20", + "timestamp": 1787476160.3453426 + }, + { + "ip": "127.0.0.1", + "action": "Acc\u00e8s \u00e0 /", + "date": "2026-08-23 11:13:18", + "timestamp": 1787476398.5733256 + }, + { + "ip": "127.0.0.1", + "action": "Acc\u00e8s \u00e0 /", + "date": "2026-08-23 11:13:25", + "timestamp": 1787476405.3547883 + }, + { + "ip": "127.0.0.1", + "action": "Acc\u00e8s \u00e0 /", + "date": "2026-08-23 11:16:45", + "timestamp": 1787476605.5372877 + }, + { + "ip": "127.0.0.1", + "action": "Acc\u00e8s \u00e0 /", + "date": "2026-08-23 11:16:54", + "timestamp": 1787476614.6163135 + }, + { + "ip": "127.0.0.1", + "action": "Acc\u00e8s \u00e0 /", + "date": "2026-08-23 11:17:17", + "timestamp": 1787476637.6742752 + }, + { + "ip": "127.0.0.1", + "action": "Acc\u00e8s \u00e0 /", + "date": "2026-08-23 11:17:28", + "timestamp": 1787476648.5071766 + }, + { + "ip": "127.0.0.1", + "action": "Acc\u00e8s \u00e0 /", + "date": "2026-08-23 11:21:45", + "timestamp": 1787476905.9197671 + }, + { + "ip": "127.0.0.1", + "action": "Acc\u00e8s \u00e0 /", + "date": "2026-08-23 11:23:15", + "timestamp": 1787476995.713237 + }, + { + "ip": "127.0.0.1", + "action": "Acc\u00e8s \u00e0 /", + "date": "2026-08-23 11:26:20", + "timestamp": 1787477180.2584555 + }, + { + "ip": "127.0.0.1", + "action": "Acc\u00e8s \u00e0 /", + "date": "2026-08-23 11:27:10", + "timestamp": 1787477230.102091 + }, + { + "ip": "127.0.0.1", + "action": "Acc\u00e8s \u00e0 /", + "date": "2026-08-23 11:27:39", + "timestamp": 1787477259.2473674 + }, + { + "ip": "127.0.0.1", + "action": "Acc\u00e8s \u00e0 /", + "date": "2026-08-23 11:32:08", + "timestamp": 1787477528.771442 + }, + { + "ip": "127.0.0.1", + "action": "Acc\u00e8s \u00e0 /", + "date": "2026-08-23 11:36:33", + "timestamp": 1787477793.7782843 } ] \ No newline at end of file diff --git a/main.py b/main.py index 05025da..8b47c29 100644 --- a/main.py +++ b/main.py @@ -1,4 +1,8 @@ +import os +from dotenv import load_dotenv +load_dotenv() # Force la lecture du fichier .env en local import uuid +import json from fasthtml.common import * from starlette.staticfiles import StaticFiles from starlette.responses import FileResponse, Response, HTMLResponse, RedirectResponse @@ -10,6 +14,8 @@ from modules.ui_components import render_main_page, render_sidebar, render_login from modules.chat_handler import handle_chat_request from modules.security import verifier_et_bloquer_vpn, journaliser_ip from modules.auth_manager import IDENTIFIANTS_ADMIN, verifier_mot_de_passe +from modules.logger import log_event +from modules.billing_manager import charger_soldes, sauvegarder_soldes # Lancement du module d'auto-mise à jour en arrière-plan import modules.model_updater diff --git a/modules/billing_manager.py b/modules/billing_manager.py index a048f83..b47edf4 100644 --- a/modules/billing_manager.py +++ b/modules/billing_manager.py @@ -16,7 +16,6 @@ TARIFS_FILE = os.path.join(BASE_DIR, "tarifs_openrouter.json") VIP_ADMIN_USERS = ["xavier", "céline", "celine", "anaëlle", "anaelle", "renan", "audric"] def obtenir_tarifs_dynamiques() -> dict: - """Récupère les tarifs via API toutes les 2 heures maximum pour éviter le hardcoding.""" maintenant = time.time() if os.path.exists(TARIFS_FILE): @@ -36,7 +35,6 @@ def obtenir_tarifs_dynamiques() -> dict: pricing = m.get("pricing", {}) p_prompt = float(pricing.get("prompt", 0.0)) p_comp = float(pricing.get("completion", 0.0)) - # On sécurise l'estimation en prenant la valeur la plus haute entre tokens in et out tarifs[m["id"]] = max(p_prompt, p_comp) with open(TARIFS_FILE, "w", encoding="utf-8") as f: @@ -46,7 +44,6 @@ def obtenir_tarifs_dynamiques() -> dict: except Exception as e: log_event("ERROR", "BILLING", f"Échec récupération des tarifs : {e}") - # Repli sur le cache si l'API est injoignable if os.path.exists(TARIFS_FILE): try: with open(TARIFS_FILE, "r", encoding="utf-8") as f: @@ -150,6 +147,31 @@ def verifier_et_valider_budget(prompt: str, modele_id: str, utilisateur_nom: str return True, prix_facture def obtenir_solde(utilisateur_nom: str) -> float: + # --- SYNCHRONISATION DIRECTE POUR L'ADMINISTRATEUR PRINCIPAL --- + if utilisateur_nom.lower() in ["xavier"]: + api_key = os.getenv("OPENROUTER_API_KEY") + if api_key: + try: + resp = requests.get("https://openrouter.ai/api/v1/auth/key", headers={"Authorization": f"Bearer {api_key}"}, timeout=3) + if resp.status_code == 200: + data = resp.json().get("data", {}) + + credit_total = data.get('usage', 0.0) + limite_restante = data.get('limit_remaining') + balance = data.get('balance', 0.0) + + vrai_solde = float(limite_restante) if limite_restante is not None else float(balance) + + if vrai_solde > 0 or limite_restante is not None: + soldes = charger_soldes() + soldes["cagnotte_commune_vip"] = vrai_solde + soldes["xavier"] = vrai_solde + sauvegarder_soldes(soldes) + return vrai_solde + except Exception as e: + log_event("WARNING", "BILLING", f"Échec de synchronisation OpenRouter : {e}") + + # --- COMPORTEMENT STANDARD POUR LES AUTRES UTILISATEURS --- soldes = charger_soldes() if utilisateur_nom.lower() in VIP_ADMIN_USERS: return soldes.get("cagnotte_commune_vip", 0.0) @@ -177,27 +199,4 @@ def generer_dossier_litige(utilisateur_nom: str, est_admin: bool = False) -> lis "tokens": item.get("tokens"), "montant_facture_ttc": item.get("prix_facture_client") }) - return dossier - -def calculer_prix_recharge_ttc(montant_credits_usd: float, methode: str = "paypal") -> float: - """ - Calcule le coût final pour l'utilisateur qui souhaite acheter une recharge de crédits. - Le coût final répercute les frais bancaires pour que l'entreprise soit à l'équilibre. - - Frais estimés (Variables selon devises, basés sur des profils Sandbox/Standard) : - - PayPal : 2.9% + 0.35$ fixes par transaction - - Revolut Business : 1.0% + 0.20$ fixes par transaction en ligne - - Formule d'absorption des frais : Montant_Final = (Credits + Frais_Fixes) / (1 - Pourcentage_Frais) - """ - if methode.lower() == "paypal": - frais_fixes = 0.35 - frais_variable = 0.029 - elif methode.lower() == "revolut": - frais_fixes = 0.20 - frais_variable = 0.010 - else: - return montant_credits_usd # Aucune méthode bancaire spécifiée - - montant_a_payer = (montant_credits_usd + frais_fixes) / (1 - frais_variable) - return round(montant_a_payer, 2) \ No newline at end of file + return dossier \ No newline at end of file diff --git a/modules/debug_mailer.py b/modules/debug_mailer.py index 7dbc580..e7e02b5 100644 --- a/modules/debug_mailer.py +++ b/modules/debug_mailer.py @@ -3,25 +3,24 @@ import smtplib from email.mime.text import MIMEText from email.mime.multipart import MIMEMultipart -# Option de désactivation depuis l'interface (Phase Debug) DEBUG_MAIL_ACTIVE = True def envoyer_alerte_debug(niveau, source, message_erreur): """ - Envoie un e-mail à l'administrateur en cas d'erreur serveur (Phase Debug). - Utilise le serveur SMTP défini dans le .env + Envoie un e-mail à l'administrateur. + Gère le chiffrement (Port 587) et le relai interne sans mot de passe (Port 25). """ if not DEBUG_MAIL_ACTIVE: return email_admin = os.getenv("ADMIN_EMAIL", "votre-email@domaine.com") - smtp_serveur = os.getenv("SMTP_SERVER") + smtp_serveur = os.getenv("SMTP_SERVER", "192.168.1.20") smtp_port = int(os.getenv("SMTP_PORT", 587)) - smtp_user = os.getenv("SMTP_USER") - smtp_pass = os.getenv("SMTP_PASS") + smtp_user = os.getenv("SMTP_USER", "") + smtp_pass = os.getenv("SMTP_PASS", "") - if not all([smtp_serveur, smtp_user, smtp_pass]): - print(f"[{niveau}] [DEBUG_MAILER] Configuration SMTP absente. Impossible d'envoyer l'alerte pour : {message_erreur}") + if not smtp_serveur: + print(f"[{niveau}] [DEBUG_MAILER] Serveur SMTP absent. Alerte annulée.") return sujet = f"[ALERTE {niveau}] Aethas38_Multi_IA - Erreur dans {source}" @@ -36,22 +35,32 @@ def envoyer_alerte_debug(niveau, source, message_erreur): {message_erreur}
-

Notification automatique de l'Orchestrateur (Phase Debug)

+

Notification automatique de l'Orchestrateur

""" msg = MIMEMultipart() - msg['From'] = smtp_user + # Si pas d'utilisateur défini, on utilise une adresse générique pour le relai interne + msg['From'] = smtp_user if smtp_user else "orchestrateur@aethas38.duckdns.org" msg['To'] = email_admin msg['Subject'] = sujet msg.attach(MIMEText(corps_html, 'html')) try: server = smtplib.SMTP(smtp_serveur, smtp_port) - server.starttls() - server.login(smtp_user, smtp_pass) + + # Logique de routage intelligent selon le port + if smtp_port == 587: + server.starttls() + if smtp_user and smtp_pass: + server.login(smtp_user, smtp_pass) + elif smtp_port == 25: + # Sur le port 25 en réseau local, Mailu s'attend souvent à un simple relai sans LOGIN + pass + server.send_message(msg) server.quit() + print(f"[{niveau}] Alerte e-mail envoyée avec succès à l'admin.") except Exception as e: print(f"ÉCHEC CRITIQUE : Impossible d'envoyer l'alerte e-mail : {e}") \ No newline at end of file diff --git a/modules/paypal.py b/modules/paypal.py index c8b53e7..7de96ab 100644 --- a/modules/paypal.py +++ b/modules/paypal.py @@ -1,19 +1,54 @@ import os from fasthtml.common import * -def render_paypal_donation_widget(): - """Génère le bouton de don PayPal pour l'interface sidebar avec le lien configuré.""" +def render_paypal_recharge_widget(montant_usd: float = 10.00): + client_id = os.getenv("PAYPAL_CLIENT_ID") - # Récupération depuis l'environnement ou utilisation directe de votre lien officiel - paypal_url = os.environ.get("PAYPAL_DONATION_LINK", "https://www.paypal.com/donate/?hosted_button_id=2B6T9UESCN2W6") + if not client_id: + return Div("Clé PayPal manquante dans .env", style="color: red;") + + script_paypal = Script(src=f"https://www.paypal.com/sdk/js?client-id={client_id}¤cy=USD") + container_id = f"paypal-button-container-{str(montant_usd).replace('.', '')}" - return Div( - A( - "☕ Soutenir l'hébergeur", - href=paypal_url, - target="_blank", - cls="export-btn", - style="text-align: center; color: #00e5ff; border-color: #00e5ff; display: block; text-decoration: none; margin-top: 10px;" - ), - style="margin-top: 10px;" - ) \ No newline at end of file + script_logique = Script(f""" + function chargerBoutonPayPal() {{ + if (typeof paypal !== 'undefined') {{ + paypal.Buttons({{ + createOrder: function(data, actions) {{ + return actions.order.create({{ + purchase_units: [{{ amount: {{ value: '{montant_usd}' }} }}] + }}); + }}, + onApprove: function(data, actions) {{ + return actions.order.capture().then(function(details) {{ + alert('Paiement Sandbox validé par ' + details.payer.name.given_name + ' !'); + // Fermer la fenêtre après paiement + document.getElementById('modal-recharge-paypal').style.display = 'none'; + }}); + }} + }}).render('#{container_id}'); + }} else {{ + setTimeout(chargerBoutonPayPal, 100); + }} + }} + chargerBoutonPayPal(); + """) + + # La boîte centrale de paiement + paypal_box = Div( + H3(f"Recharge de {montant_usd}$", style="color: #fff; margin-top: 0; margin-bottom: 15px; font-size: 1.2em; text-align: center;"), + Div(id=container_id, style="min-height: 150px; width: 100%;"), + Button("Fermer", type="button", onclick="document.getElementById('modal-recharge-paypal').style.display='none'", cls="btn-submit", style="background-color: #555; width: 100%; margin-top: 15px; border-radius: 8px; font-size: 0.9em;"), + style="background: #1a1a1a; padding: 25px; border-radius: 12px; border: 2px solid #00e5ff; width: 400px; max-width: 90vw; box-shadow: 0 10px 30px rgba(0,0,0,0.8);" + ) + + # L'arrière-plan sombre (overlay) + modal_overlay = Div( + script_paypal, + paypal_box, + script_logique, + id="modal-recharge-paypal", + style="display: none; position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0,0,0,0.8); align-items: center; justify-content: center; z-index: 10000;" + ) + + return modal_overlay \ No newline at end of file diff --git a/modules/paypal_checkout.py b/modules/paypal_checkout.py index 4f711e8..bc955f6 100644 --- a/modules/paypal_checkout.py +++ b/modules/paypal_checkout.py @@ -2,49 +2,66 @@ import os from fasthtml.common import * def render_paypal_recharge_widget(montant_usd: float = 10.00): - """ - Génère le bouton de paiement PayPal (Smart Button) pour recharger les crédits. - Utilise le Client ID Sandbox défini dans le fichier .env. - """ client_id = os.getenv("PAYPAL_CLIENT_ID") if not client_id: - return Div("Clé PayPal (Client ID) manquante dans .env", style="color: red;") + return Div("Clé PayPal manquante dans .env", style="color: red;") - # Le script officiel de PayPal injecte les boutons de paiement script_paypal = Script(src=f"https://www.paypal.com/sdk/js?client-id={client_id}¤cy=USD") - container_id = f"paypal-button-container-{str(montant_usd).replace('.', '')}" script_logique = Script(f""" - paypal.Buttons({{ - createOrder: function(data, actions) {{ - return actions.order.create({{ - purchase_units: [{{ - amount: {{ - value: '{montant_usd}' - }} - }}] - }}); - }}, - onApprove: function(data, actions) {{ - return actions.order.capture().then(function(details) {{ - alert('Paiement réussi par ' + details.payer.name.given_name + '! Les crédits seront ajoutés.'); - // Ici on fera un fetch vers ton backend python pour créditer l'utilisateur - fetch('/api/paypal/success', {{ - method: 'POST', - headers: {{ 'Content-Type': 'application/json' }}, - body: JSON.stringify({{ orderID: data.orderID }}) - }}).then(res => window.location.reload()); - }}); + function chargerBoutonPayPal() {{ + if (typeof paypal !== 'undefined') {{ + paypal.Buttons({{ + createOrder: function(data, actions) {{ + return actions.order.create({{ + purchase_units: [{{ amount: {{ value: '{montant_usd}' }} }}] + }}); + }}, + onApprove: function(data, actions) {{ + return actions.order.capture().then(function(details) {{ + alert('Paiement Sandbox validé par ' + details.payer.name.given_name + ' ! Vos crédits vont être ajoutés.'); + + // L'ajout de credentials: 'same-origin' est VITAL pour envoyer le cookie de session ! + fetch('/api/paypal/success', {{ + method: 'POST', + headers: {{ 'Content-Type': 'application/json' }}, + credentials: 'same-origin', + body: JSON.stringify({{ orderID: data.orderID }}) + }}).then(res => {{ + if(res.ok) {{ + document.getElementById('modal-recharge-paypal').style.display = 'none'; + window.location.reload(); + }} else {{ + alert("Erreur lors de la validation serveur."); + }} + }}); + }}); + }} + }}).render('#{container_id}'); + }} else {{ + setTimeout(chargerBoutonPayPal, 100); }} - }}).render('#{container_id}'); + }} + chargerBoutonPayPal(); """) - return Div( + # La boîte centrale de paiement + paypal_box = Div( + H3(f"Recharge de {montant_usd}$", style="color: #fff; margin-top: 0; margin-bottom: 15px; font-size: 1.2em; text-align: center;"), + Div(id=container_id, style="min-height: 150px; width: 100%;"), + Button("Fermer", type="button", onclick="document.getElementById('modal-recharge-paypal').style.display='none'", cls="btn-submit", style="background-color: #555; width: 100%; margin-top: 15px; border-radius: 8px; font-size: 0.9em;"), + style="background: #1a1a1a; padding: 25px; border-radius: 12px; border: 2px solid #00e5ff; width: 400px; max-width: 90vw; box-shadow: 0 10px 30px rgba(0,0,0,0.8);" + ) + + # L'arrière-plan sombre (overlay) + modal_overlay = Div( script_paypal, - H3(f"Recharge de {montant_usd}$"), - Div(id=container_id), + paypal_box, script_logique, - style="background: #1a1a1a; padding: 20px; border-radius: 8px; border: 1px solid #333; margin-top: 15px; text-align: center; max-width: 300px;" - ) \ No newline at end of file + id="modal-recharge-paypal", + style="display: none; position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0,0,0,0.8); align-items: center; justify-content: center; z-index: 10000;" + ) + + return modal_overlay \ No newline at end of file diff --git a/modules/ui_components.py b/modules/ui_components.py index 167a761..5d60b4a 100644 --- a/modules/ui_components.py +++ b/modules/ui_components.py @@ -1,44 +1,30 @@ import os import requests -from modules.paypal_checkout import render_paypal_recharge_widget from fasthtml.common import * from modules.database import load_history -from modules.paypal import render_paypal_donation_widget from modules.billing_manager import obtenir_solde def get_openrouter_stats(): api_key = os.getenv("OPENROUTER_API_KEY") - if not api_key: - return {} + if not api_key: return {} try: - resp = requests.get( - "https://openrouter.ai/api/v1/auth/key", - headers={"Authorization": f"Bearer {api_key}"}, - timeout=3 - ) + resp = requests.get("https://openrouter.ai/api/v1/auth/key", headers={"Authorization": f"Bearer {api_key}"}, timeout=3) if resp.status_code == 200: return resp.json().get("data", {}) - except: - pass + except: pass return {} def render_user_msg(text): - return Div( - P(text, style="margin:0; font-weight:bold; white-space: pre-wrap;"), - cls="user-bubble" - ) + return Div(P(text, style="margin:0; font-weight:bold; white-space: pre-wrap;"), cls="user-bubble") def render_ai_msg(text, intention, nom_fournisseur, nom_modele, couleur, execution_time=None): time_badge = Span(f"⏱️ {execution_time}s", style="margin-right: 10px; color:#aaa; font-size:0.8em;") if execution_time else "" - encart_info = Div( Span(f"🎯 {intention}", style="margin-right: 10px; color:#aaa; font-size:0.8em;"), Span(f"🏢 {nom_fournisseur}", style="margin-right: 10px; color:#aaa; font-size:0.8em;"), Span(f"🧠 {nom_modele}", style=f"margin-right: 10px; color:{couleur}; font-weight:bold; font-size:0.8em;"), - time_badge, - style="border-bottom: 1px solid #333; padding-bottom: 10px; margin-bottom: 15px; display: flex; flex-wrap: wrap; gap: 5px;" + time_badge, style="border-bottom: 1px solid #333; padding-bottom: 10px; margin-bottom: 15px; display: flex; flex-wrap: wrap; gap: 5px;" ) - actions = Div( Button("📄 PDF", onclick="downloadResponsePDF(this)", cls="msg-action-btn", title="Télécharger en PDF"), Button("📘 DOC", onclick="downloadResponseDOC(this)", cls="msg-action-btn", title="Télécharger en Word"), @@ -48,15 +34,7 @@ def render_ai_msg(text, intention, nom_fournisseur, nom_modele, couleur, executi Button("📧 Email", onclick="shareEmail(this)", cls="msg-action-btn", title="Envoyer par courriel"), cls="msg-actions" ) - - return Div( - encart_info, - Div(text, cls="markdown-raw", style="display:none;"), - Div(cls="markdown-rendered", style="line-height: 1.6;"), - actions, - cls="ai-bubble", - style=f"border-left: 4px solid {couleur};" - ) + return Div(encart_info, Div(text, cls="markdown-raw", style="display:none;"), Div(cls="markdown-rendered", style="line-height: 1.6;"), actions, cls="ai-bubble", style=f"border-left: 4px solid {couleur};") def render_sidebar(): history = load_history() @@ -64,50 +42,24 @@ def render_sidebar(): items = [] for chat_id, data in sorted_chats: pin_icon = "📌" if data.get('pinned', False) else "📍" - items.append( - Div( - A(Span(data['title'], style="color:inherit;"), href=f"/load_chat/{chat_id}", cls="history-link"), - Button(pin_icon, cls="history-btn", hx_post=f"/pin/{chat_id}", hx_target="#sidebar-content", hx_swap="outerHTML"), - Button("✏️", cls="history-btn", hx_post=f"/rename/{chat_id}", hx_prompt="Nouveau nom :", hx_target="#sidebar-content", hx_swap="outerHTML"), - Button("🗑️", cls="history-btn", hx_post=f"/delete/{chat_id}", hx_target="#sidebar-content", hx_swap="outerHTML"), - cls="history-item", - id=f"chat-item-{chat_id}" - ) - ) + items.append(Div(A(Span(data['title'], style="color:inherit;"), href=f"/load_chat/{chat_id}", cls="history-link"), Button(pin_icon, cls="history-btn", hx_post=f"/pin/{chat_id}", hx_target="#sidebar-content", hx_swap="outerHTML"), Button("✏️", cls="history-btn", hx_post=f"/rename/{chat_id}", hx_prompt="Nouveau nom :", hx_target="#sidebar-content", hx_swap="outerHTML"), Button("🗑️", cls="history-btn", hx_post=f"/delete/{chat_id}", hx_target="#sidebar-content", hx_swap="outerHTML"), cls="history-item", id=f"chat-item-{chat_id}")) return Div(*items, id="sidebar-content", cls="sidebar-history", hx_swap_oob="true") def render_main_page(chat_id, utilisateur_nom="xavier", role="Super Admin"): history = load_history() messages_ui = [] - if chat_id in history: for msg in history[chat_id].get('messages', []): - if msg['role'] == 'user': - messages_ui.append(render_user_msg(msg['content'])) - else: - messages_ui.append(render_ai_msg( - msg['content'], - msg.get('intention', ''), - msg.get('fournisseur', ''), - msg.get('modele', ''), - msg.get('couleur', '#333'), - msg.get('execution_time', '') - )) - - if messages_ui: - messages_ui.append(Script("setTimeout(renderMarkdown, 150);")) + if msg['role'] == 'user': messages_ui.append(render_user_msg(msg['content'])) + else: messages_ui.append(render_ai_msg(msg['content'], msg.get('intention', ''), msg.get('fournisseur', ''), msg.get('modele', ''), msg.get('couleur', '#333'), msg.get('execution_time', ''))) + if messages_ui: messages_ui.append(Script("setTimeout(renderMarkdown, 150);")) auto_refresh_script = Script(''' let serverDown = false; setInterval(() => { fetch('/ping', {cache: 'no-store'}) - .then(response => { - if (serverDown && response.ok) { window.location.reload(); } - }) - .catch(() => { - serverDown = true; - console.log("Serveur hors ligne (Mise à jour en cours)..."); - }); + .then(response => { if (serverDown && response.ok) { window.location.reload(); } }) + .catch(() => { serverDown = true; console.log("Serveur hors ligne..."); }); }, 5000); ''') @@ -115,34 +67,23 @@ def render_main_page(chat_id, utilisateur_nom="xavier", role="Super Admin"): if role.lower() in ["super admin", "admin"]: stats_or = get_openrouter_stats() - limit_rem = stats_or.get('limit_remaining') - limit_rem = float(limit_rem) if limit_rem is not None else 0.0 - usage_daily = stats_or.get('usage_daily') - usage_daily = float(usage_daily) if usage_daily is not None else 0.0 - usage_monthly = stats_or.get('usage_monthly') - usage_monthly = float(usage_monthly) if usage_monthly is not None else 0.0 + limit_rem = float(stats_or.get('limit_remaining') or 0.0) + usage_daily = float(stats_or.get('usage_daily') or 0.0) + usage_monthly = float(stats_or.get('usage_monthly') or 0.0) badge_finance = Div( P(f"👤 {utilisateur_nom.capitalize()} ({role})", style="color: #00e5ff; margin: 0; font-weight: bold; font-size: 0.9em;"), - P(f"💳 Solde personnel : {solde_perso:.2f} $", style="color: #aaa; margin: 2px 0; font-size: 0.8em;"), + P(f"💳 Solde personnel : {solde_perso:.2f} $", style="color: #00e5ff; margin: 2px 0; font-size: 0.85em; font-weight: bold;"), P(f"🏦 Cagnotte globale : {obtenir_solde('xavier'):.2f} $", style="color: #4caf50; margin: 0; font-size: 0.8em; font-weight: bold;"), P(f"🌐 OR Restant : {limit_rem:.4f} $", style="color: #ff9800; margin: 2px 0 0; font-size: 0.8em; font-weight: bold;"), P(f"📊 OR Conso (Jour/Mois) : {usage_daily:.4f}$ / {usage_monthly:.4f}$", style="color: #8bc34a; margin: 0; font-size: 0.75em;"), - # AJOUT DU BOUTON PAYPAL POUR TESTER L'ACHAT DE 10$ - render_paypal_recharge_widget(10.00), style="padding: 10px; background: #181818; border-radius: 6px; border: 1px solid #333;" ) else: - badge_finance = Div( - P(f"👤 {utilisateur_nom.capitalize()} ({role})", style="color: #fff; margin: 0; font-weight: bold; font-size: 0.9em;"), - P(f"💳 Solde disponible : {solde_perso:.2f} $", style="color: #00e5ff; margin: 2px 0 0; font-size: 0.85em;"), - style="padding: 10px; background: #181818; border-radius: 6px; border: 1px solid #333;" - ) + badge_finance = Div(P(f"👤 {utilisateur_nom.capitalize()} ({role})", style="color: #fff; margin: 0; font-weight: bold; font-size: 0.9em;"), P(f"💳 Solde disponible : {solde_perso:.2f} $", style="color: #00e5ff; margin: 2px 0 0; font-size: 0.85em;"), style="padding: 10px; background: #181818; border-radius: 6px; border: 1px solid #333;") return Title("Aethas38 - Core"), Body( auto_refresh_script, - - # --- SIDEBAR FIXE (BUREAU) & MASQUABLE (MOBILE) --- Div( Div( Img(src="/Logo/logo_aethas.svg", style="max-width: 80%; height: auto; display: block; margin: 0 auto 20px;"), @@ -150,88 +91,35 @@ def render_main_page(chat_id, utilisateur_nom="xavier", role="Super Admin"): A("+ Nouvelle discussion", href="/new_chat", cls="new-chat-btn"), A("💾 Exporter JSON", href="/export_json", target="_blank", cls="export-btn"), A("🚪 Déconnexion", href="/logout", cls="export-btn", style="background-color: #8b0000; color: white; border-color: #8b0000; margin-top: 5px;"), - - Div( - render_paypal_donation_widget(), - A("❤️ Soutenir le créateur", href="https://paypal.me/VotreLienIci", target="_blank", style="font-size: 0.8em; padding: 6px 12px; background-color: transparent; color: #00e5ff; border: 1px solid #00e5ff; border-radius: 6px; text-decoration: none; transition: 0.3s; font-weight: bold; text-align: center;"), - style="display: flex; flex-direction: column; gap: 12px; align-items: center; margin-top: 20px;" - ), - cls="nav-buttons", - style="display: flex; flex-direction: column; gap: 10px;" - ), - cls="sidebar-top" + cls="nav-buttons", style="display: flex; flex-direction: column; gap: 10px;" + ), cls="sidebar-top" ), - render_sidebar(), - Div(badge_finance, cls="sidebar-footer"), - cls="sidebar" + render_sidebar(), Div(badge_finance, cls="sidebar-footer"), cls="sidebar" ), - - # --- CONTENU CENTRAL --- Div( - # HEADER MOBILE (VISIBLE SEULEMENT SUR SMARTPHONE) - Div( - Button("☰", onclick="toggleSidebar()", cls="hamburger-btn"), - Img(src="/Logo/logo_aethas.svg", style="height: 30px;"), - Div(style="width: 30px;"), # Espaceur pour centrer le logo - cls="mobile-header" - ), - + Div(Button("☰", onclick="toggleSidebar()", cls="hamburger-btn"), Img(src="/Logo/logo_aethas.svg", style="height: 30px;"), Div(style="width: 30px;"), cls="mobile-header"), Div(*messages_ui, id="chat-history", cls="chat-container"), - - Div( - Div(Div(cls="tech-spinner"), Span("Analyse en cours... (~5s à 10s)"), cls="tech-loader-container"), - id="global-loader", - cls="htmx-indicator" - ), - - # --- LAYOUT ZONE DE SAISIE RESTRUCTURÉ --- + Div(Div(Div(cls="tech-spinner"), Span("Analyse en cours... (~5s à 10s)"), cls="tech-loader-container"), id="global-loader", cls="htmx-indicator"), Div( Form( Input(type="hidden", name="chat_id", value=chat_id), Div( Div(id="file-preview-list"), - - # RANGÉE 1 : BOUTON +, TEXTAREA ET BOUTON ENVOI Div( Label("+", _for="file-upload", cls="file-btn"), Input(type="file", name="files", id="file-upload", multiple=True, style="display:none;", onchange="handleFileSelection(this)"), Textarea(name="prompt", id="prompt-input", placeholder="Requête, lien ou glissez vos fichiers ici...", cls="input-field", rows="1", oninput="autoResize(this)"), - Button("↑", type="submit", cls="btn-submit"), - cls="textarea-wrapper" + Button("↑", type="submit", cls="btn-submit"), cls="textarea-wrapper" ), - - # RANGÉE 2 : LES OPTIONS (SOUS LA TEXTAREA) Div( - Label( - Input(type="checkbox", name="mode_education", id="mode-education-toggle", style="margin-right: 6px; accent-color: #8bc34a; cursor: pointer;"), - "🎓 Éducation", - style="color: #aaa; font-size: 0.85em; display: flex; align-items: center; cursor: pointer; white-space: nowrap;" - ), - Label( - Input(type="checkbox", name="mode_payant", id="mode-payant-toggle", style="margin-right: 6px; accent-color: #00e5ff; cursor: pointer;"), - "👑 Payant", - style="color: #aaa; font-size: 0.85em; display: flex; align-items: center; cursor: pointer; white-space: nowrap;" - ), - Label( - Input(type="checkbox", name="mode_multi_ia", id="mode-multi-toggle", style="margin-right: 6px; accent-color: #d97757; cursor: pointer;"), - "⚡ Multi-IA", - style="color: #aaa; font-size: 0.85em; display: flex; align-items: center; cursor: pointer; white-space: nowrap;" - ), + Label(Input(type="checkbox", name="mode_education", id="mode-education-toggle", style="margin-right: 6px; accent-color: #8bc34a; cursor: pointer;"), "🎓 Éducation", style="color: #aaa; font-size: 0.85em; display: flex; align-items: center; cursor: pointer; white-space: nowrap;"), + Label(Input(type="checkbox", name="mode_payant", id="mode-payant-toggle", style="margin-right: 6px; accent-color: #00e5ff; cursor: pointer;"), "👑 Payant", style="color: #aaa; font-size: 0.85em; display: flex; align-items: center; cursor: pointer; white-space: nowrap;"), + Label(Input(type="checkbox", name="mode_multi_ia", id="mode-multi-toggle", style="margin-right: 6px; accent-color: #d97757; cursor: pointer;"), "⚡ Multi-IA", style="color: #aaa; font-size: 0.85em; display: flex; align-items: center; cursor: pointer; white-space: nowrap;"), cls="options-row" - ), - cls="input-container-box" - ), - hx_post="/chat", - hx_target="#chat-history", - hx_swap="beforeend", - hx_indicator="#global-loader", - hx_on__after_request="this.reset(); document.getElementById('file-preview-list').innerHTML=''; document.getElementById('prompt-input').style.height='36px';", - enctype="multipart/form-data", - style="width: 100%; display: flex; justify-content: center;" - ), - cls="input-area" - ), - cls="main-content" + ), cls="input-container-box" + ), hx_post="/chat", hx_target="#chat-history", hx_swap="beforeend", hx_indicator="#global-loader", hx_on__after_request="this.reset(); document.getElementById('file-preview-list').innerHTML=''; document.getElementById('prompt-input').style.height='36px';", enctype="multipart/form-data", style="width: 100%; display: flex; justify-content: center;" + ), cls="input-area" + ), cls="main-content" ) ) @@ -241,29 +129,10 @@ def render_admin_moderation_panel(utilisateur_cible): P("Loi applicable : Aucun utilisateur n'est dispensé du respect de la législation en vigueur."), Form( Input(type="hidden", name="cible", value=utilisateur_cible), - Div( - Label("Action décidée :", style="font-weight: bold;"), - Select( - Option("Avertissement simple", value="Avertissement"), - Option("Bannissement 1 jour", value="Ban 1J"), - Option("Bannissement définitif et signalement PHAROS", value="Ban Definitif + Pharos"), - name="decision", - cls="input-field" - ), - style="margin-bottom: 15px;" - ), - Div( - Label("Commentaire (Preuve d'audit) :", style="font-weight: bold;"), - Textarea(name="commentaire", rows="4", cls="input-field", placeholder="Raison..."), - style="margin-bottom: 15px;" - ), - Button("Valider la décision et archiver", type="submit", cls="btn-submit", style="background-color: #ff4444; color: white; width: 100%;"), - hx_post="/admin_decision", - hx_target="#admin-result", - style="background: #1a1a1a; padding: 20px; border-radius: 8px; border: 1px solid #333;" - ), - Div(id="admin-result", style="margin-top: 15px; color: #00e5ff;"), - cls="admin-panel" + Div(Label("Action décidée :", style="font-weight: bold;"), Select(Option("Avertissement simple", value="Avertissement"), Option("Bannissement 1 jour", value="Ban 1J"), Option("Bannissement définitif et signalement PHAROS", value="Ban Definitif + Pharos"), name="decision", cls="input-field"), style="margin-bottom: 15px;"), + Div(Label("Commentaire (Preuve d'audit) :", style="font-weight: bold;"), Textarea(name="commentaire", rows="4", cls="input-field", placeholder="Raison..."), style="margin-bottom: 15px;"), + Button("Valider la décision et archiver", type="submit", cls="btn-submit", style="background-color: #ff4444; color: white; width: 100%;"), hx_post="/admin_decision", hx_target="#admin-result", style="background: #1a1a1a; padding: 20px; border-radius: 8px; border: 1px solid #333;" + ), Div(id="admin-result", style="margin-top: 15px; color: #00e5ff;"), cls="admin-panel" ) def render_login_page(error=None): @@ -274,64 +143,45 @@ def render_login_page(error=None): H2("Accès Restreint", style="text-align: center; color: #00e5ff;"), error_msg, Form( - Div( - Label("Email :", style="color: #aaa; font-size: 0.9em;"), - Input(type="email", name="email", placeholder="admin@mail.aethas38.duckdns.org", cls="input-field", required=True), - style="margin-bottom: 15px;" - ), - Div( - Label("Mot de passe :", style="color: #aaa; font-size: 0.9em;"), - Input(type="password", name="password", placeholder="••••••••", cls="input-field", required=True), - style="margin-bottom: 20px;" - ), - Button("Déverrouiller le système", type="submit", cls="btn-submit", style="width: 100%;"), - method="post", action="/login", - ), - style="max-width: 400px; margin: 100px auto; padding: 30px; background: #1a1a1a; border-radius: 10px; border: 1px solid #333; box-shadow: 0 4px 15px rgba(0,229,255,0.1);" - ), - style="background-color: #0d0d0d; color: #fff; font-family: monospace; height: 100vh; display: flex; align-items: center; justify-content: center;" + Div(Label("Email :", style="color: #aaa; font-size: 0.9em;"), Input(type="email", name="email", placeholder="admin@mail.aethas38.duckdns.org", cls="input-field", required=True), style="margin-bottom: 15px;"), + Div(Label("Mot de passe :", style="color: #aaa; font-size: 0.9em;"), Input(type="password", name="password", placeholder="••••••••", cls="input-field", required=True), style="margin-bottom: 20px;"), + Button("Déverrouiller le système", type="submit", cls="btn-submit", style="width: 100%;"), method="post", action="/login", + ), style="max-width: 400px; margin: 100px auto; padding: 30px; background: #1a1a1a; border-radius: 10px; border: 1px solid #333; box-shadow: 0 4px 15px rgba(0,229,255,0.1);" + ), style="background-color: #0d0d0d; color: #fff; font-family: monospace; height: 100vh; display: flex; align-items: center; justify-content: center;" ) def render_popup_paiement(cout_estime, prompt_sauvegarde, chat_id, multi_ia_state=False, edu_state=False): - multi_ia_val = "on" if multi_ia_state else "off" + """ + Modale de rejet financier. + Version sans système de paiement (en attendant la structure juridique). + """ edu_val = "on" if edu_state else "off" + return Div( Div( - H3("⚠️ Validation Financière Requise", style="color: #ff9800; margin-top: 0;"), - P("Cette requête nécessite l'utilisation d'un modèle d'IA payant.", style="color: #ccc;"), - P(f"Coût estimé API (TTC) : {cout_estime} $", style="font-size: 1.2em; font-weight: bold; color: #fff; background: #333; padding: 10px; border-radius: 5px; text-align: center;"), + H3("⚠️ Crédits insuffisants", style="color: #ff4444; margin-top: 0;"), + P("Cette requête nécessite un modèle d'IA dont le coût dépasse votre solde actuel.", style="color: #ccc;"), + P(f"Coût estimé de la requête : {cout_estime} $", style="font-size: 1.1em; font-weight: bold; color: #fff; background: #333; padding: 10px; border-radius: 5px; text-align: center;"), Div( - Button("Annuler la requête", type="button", onclick="document.getElementById('popup-paiement').remove();", cls="btn-submit", style="background-color: #555; width: 30%; font-size: 0.8em; border-radius: 8px;"), + Button("Annuler la requête", type="button", onclick="document.getElementById('popup-paiement').remove();", cls="btn-submit", style="background-color: #555; width: 48%; font-size: 0.85em; border-radius: 8px;"), + # Bouton de secours pour forcer un modèle gratuit local (Ollama) Form( Input(type="hidden", name="prompt", value=prompt_sauvegarde), Input(type="hidden", name="chat_id", value=chat_id), Input(type="hidden", name="mode_payant", value="off"), - Input(type="hidden", name="mode_multi_ia", value=multi_ia_val), + Input(type="hidden", name="mode_multi_ia", value="off"), Input(type="hidden", name="mode_education", value=edu_val), Input(type="hidden", name="validation_acquise", value="true"), Input(type="hidden", name="choix_utilisateur", value="gratuit_force"), - Button("Tenter en Gratuit ⚠️", type="submit", onclick="document.getElementById('popup-paiement').remove();", cls="btn-submit", style="background-color: #00e5ff; width: 100%; color: black; font-size: 0.8em; border-radius: 8px;"), - hx_post="/chat", hx_target="#chat-history", hx_swap="beforeend", hx_indicator="#global-loader", style="width: 30%;" + Button("Tenter en Gratuit (Local)", type="submit", onclick="document.getElementById('popup-paiement').remove();", cls="btn-submit", style="background-color: #00e5ff; width: 100%; color: black; font-size: 0.85em; border-radius: 8px;"), + hx_post="/chat", hx_target="#chat-history", hx_swap="beforeend", hx_indicator="#global-loader", style="width: 48%;" ), - - Form( - Input(type="hidden", name="prompt", value=prompt_sauvegarde), - Input(type="hidden", name="chat_id", value=chat_id), - Input(type="hidden", name="mode_payant", value="on"), - Input(type="hidden", name="mode_multi_ia", value=multi_ia_val), - Input(type="hidden", name="mode_education", value=edu_val), - Input(type="hidden", name="validation_acquise", value="true"), - Input(type="hidden", name="choix_utilisateur", value="paiement_accepte"), - Button("Payer l'API", type="submit", onclick="document.getElementById('popup-paiement').remove();", cls="btn-submit", style="background-color: #4caf50; width: 100%; font-size: 0.8em; border-radius: 8px;"), - hx_post="/chat", hx_target="#chat-history", hx_swap="beforeend", hx_indicator="#global-loader", style="width: 30%;" - ), - style="display: flex; justify-content: space-between; margin-top: 20px; gap: 10px;" ), - style="background: #1a1a1a; padding: 25px; border-radius: 12px; border: 2px solid #ff9800; width: 500px; max-width: 90vw; box-shadow: 0 10px 30px rgba(0,0,0,0.8);" + style="background: #1a1a1a; padding: 25px; border-radius: 12px; border: 2px solid #ff4444; width: 450px; max-width: 90vw; box-shadow: 0 10px 30px rgba(0,0,0,0.8);" ), id="popup-paiement", - style="position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0,0,0,0.7); display: flex; align-items: center; justify-content: center; z-index: 9999;" + style="position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0,0,0,0.8); display: flex; align-items: center; justify-content: center; z-index: 9999;" ) \ No newline at end of file