"""Outlook Dashboard Routes - GMAO Collège""" from flask import Blueprint, render_template, redirect, url_for, request, flash, jsonify, send_file from flask_login import login_required, current_user from app_new.extensions import db from app_new.core.system_security import system_admin_required from ..outlook.models import OutlookAccount, OutlookMail, OutlookFolder, OutlookAttachment, OutlookMailInterpretation, GmaoContext import os import subprocess import json import tempfile dashboard_bp = Blueprint('outlook_dashboard', __name__, template_folder='templates') # ─── Interprétation Hermes ────────────────────────────────────────────────── @dashboard_bp.route('/api/interpret/', methods=['POST']) @login_required def api_interpret_mail(mail_id): """Interprète un email avec Hermes en utilisant le contexte GMAO configuré.""" mail = OutlookMail.query.get_or_404(mail_id) # Récupérer le contexte GMAO global de l'utilisateur gmao_context = GmaoContext.get_for_user(current_user.id) # Vérifier si l'expéditeur est exclu if mail.from_email and gmao_context.is_sender_excluded(mail.from_email): return jsonify({ 'error': 'Expéditeur exclu', 'message': f"L'expéditeur '{mail.from_email}' est dans la liste des expéditeurs exclus. Aucune interprétation effectuée.", 'excluded': True }), 400 global_context = gmao_context.get_full_context() # Contexte additionnel fourni pour cet email additional_context = request.json.get('context', '') # Préparer le contenu de l'email email_content = f"""Sujet: {mail.subject or '(sans sujet)'} De: {mail.from_name or 'Inconnu'} <{mail.from_email or ''}> À: {mail.to_recipients or ''} Date: {mail.received_at.strftime('%d/%m/%Y %H:%M') if mail.received_at else '-'} Contenu: {mail.body_content or mail.body_preview or '(pas de contenu)'} """ # Ajouter les pièces jointes si présentes if mail.attachments: from .models import OutlookAttachment attachments = OutlookAttachment.query.filter_by(mail_id=mail.id).all() if attachments: email_content += "\n=== PIÈCES JOINTES ===\n" for att in attachments: email_content += f"- {att.name} ({att.content_type})\n" # Extraire le texte des PDF if att.name and att.name.lower().endswith('.pdf'): try: import requests access_token, _ = get_access_token(account) headers = {'Authorization': f'Bearer {access_token}'} resp = requests.get( f'https://outlook.office365.com/api/v2.0/me/messages/{mail.id}/attachments/{att.id}/$value', headers=headers, timeout=30 ) if resp.status_code == 200: import tempfile import pdfplumber # Sauvegarder temporairement temp_path = tempfile.mktemp(suffix='.pdf') with open(temp_path, 'wb') as f: f.write(resp.content) # Extraire le texte text_content = [] with pdfplumber.open(temp_path) as pdf: for page in pdf.pages[:5]: # Limiter à 5 pages text = page.extract_text() if text: text_content.append(text.strip()) # Nettoyer import os os.remove(temp_path) if text_content: email_content += f" [CONTENU PDF]:\n" email_content += " " + "\n ".join("\n".join(text_content).split("\n")[:50])[:2000] + "\n" # Limiter à 2000 chars else: email_content += " [PDF sans texte extractible]\n" except Exception as e: email_content += f" [Erreur extraction PDF: {str(e)[:50]}]\n" email_content += "=== FIN DES PIÈCES JOINTES ===\n" # Construire le prompt avec le contexte GMAO prompt_parts = [] # En-tête avec rôle et instructions JSON prompt_parts.append("""Tu es un assistant pour la GMAO (Gestion de Maintenance Assistée par Ordinaire) d'un établissement scolaire. Analyse cet email et EXTRAIS les informations pour créer une ou plusieurs interventions/tâches. IMPORTANT: Tu DOIS répondre UNIQUEMENT avec un JSON valide, sans texte avant ni après. Types possibles: - "curative": Panne, problème, réparation nécessaire (ex: fuite, équipement cassé) - "preventive": Maintenance planifiée, contrôle périodique (ex: inspection, entretien) - "administrative": Tâche administrative à effectuer (ex: déposer des documents, remplir un formulaire, rappel) - "formation": Formation à suivre, session de formation (ex: formation sécurité, recyclage) - "information": Information simple, pas d'action requise Actions possibles: - "create": Créer une nouvelle intervention/tâche - "update": Mettre à jour une intervention existante - "inform": Juste informer, pas d'action ATTENTION: Un email peut contenir PLUSIEURS tâches différentes (ex: plusieurs dates, plusieurs actions). Dans ce cas, crée une entrée pour chaque tâche identifiée. Format de réponse obligatoire: { "analyses": [ { "type": "curative|preventive|administrative|formation|information", "action": "create|update|inform", "confidence": 0.0-1.0, "intervention": { "titre": "Titre court et descriptif", "description": "Description détaillée du problème ou de la tâche", "urgence": "basse|normale|haute|urgente", "lieu": "Localisation précise (bâtiment, salle, étage...)", "equipement": "Équipement concerné si applicable", "intervenant_suggere": "Type d'intervenant suggéré (plombier, électricien, etc.)", "date_intervention": "YYYY-MM-DD ou null si non spécifiée" }, "date_suggestion": "YYYY-MM-DD ou null", "resume_court": "Résumé en une phrase" } ], "nb_taches": 1, "resume_global": "Résumé de l'email en une phrase" } Règles: - Si l'email mentionne plusieurs dates ou plusieurs actions, crée plusieurs entrées dans "analyses" - Chaque entrée dans "analyses" représente UNE tâche/intervention - type "curative": panne, problème, demande de réparation - type "preventive": maintenance planifiée, contrôle périodique - type "administrative": tâche administrative (dépôt de documents, formulaire, rappel) - type "formation": formation à suivre, session de formation - type "information": demande d'info, question sans intervention - action "create": nouvelle intervention/tâche à créer - action "update": mise à jour d'une intervention existante - action "inform": juste de l'information, pas d'intervention - confidence: ton niveau de confiance dans l'analyse (0.0 à 1.0) - Si tu ne peux pas extraire certaines informations, mets null""") # Ajouter le contexte GMAO global s'il existe if global_context and global_context.strip(): prompt_parts.append(f""" === CONTEXTE GMAO DE L'ÉTABLISSEMENT === {global_context} === FIN DU CONTEXTE ===""") # Ajouter le contexte additionnel s'il existe if additional_context and additional_context.strip(): prompt_parts.append(f""" === CONTEXTE ADDITIONNEL POUR CET EMAIL === {additional_context} === FIN DU CONTEXTE ADDITIONNEL ===""") # Ajouter l'email à analyser prompt_parts.append(f""" === EMAIL À ANALYSER === {email_content} === FIN DE L'EMAIL === Fournis ton analyse:""") prompt = "\n".join(prompt_parts) try: # Utiliser directement l'API OpenRouter (plus fiable que Hermes CLI) import requests as api_requests from ..core.models.settings import AppSettings # Récupérer la clé API: 1. DB, 2. Config Hermes, 3. Environnement api_key = AppSettings.get_openrouter_api_key() if not api_key: # Chercher dans la config Hermes hermes_config_path = os.path.expanduser('~/.hermes/config.yaml') try: import yaml with open(hermes_config_path, 'r') as f: config = yaml.safe_load(f) if 'providers' in config: for provider in config['providers']: if provider.get('name') == 'openrouter' or 'openrouter' in str(provider.get('base_url', '')): api_key = provider.get('api_key') break if not api_key and 'secrets' in config: api_key = config['secrets'].get('openrouter_api_key') except Exception: pass # Essayer depuis la base de données GMAO if not api_key: from ..core.models.settings import AppSettings api_key = AppSettings.get('openrouter_api_key') if not api_key: # Essayer la variable d'environnement api_key = os.environ.get('OPENROUTER_API_KEY') if not api_key: return jsonify({'error': 'Clé API OpenRouter non configurée. Allez dans Admin > Paramètres pour la configurer.'}), 500 # Limiter la taille du prompt max_prompt_len = 8000 if len(prompt) > max_prompt_len: email_start = prompt.find('=== EMAIL À ANALYSER ===') if email_start > 0: context_part = prompt[:email_start] email_part = prompt[email_start:] allowed_context = max_prompt_len - len(email_part) if allowed_context > 500: context_part = context_part[:allowed_context] + "\n... [contexte tronqué]" prompt = context_part + email_part # Appel direct à OpenRouter API response = api_requests.post( 'https://openrouter.ai/api/v1/chat/completions', headers={ 'Authorization': f'Bearer {api_key}', 'Content-Type': 'application/json', 'HTTP-Referer': 'http://localhost:5000', 'X-Title': 'GMAO College' }, json={ 'model': 'poolside/laguna-m.1:free', 'messages': [{'role': 'user', 'content': prompt}], 'max_tokens': 2000 }, timeout=90 ) # Si erreur, essayer avec un autre modèle if response.status_code != 200: error_info = response.json() if response.json() else {} error_msg = error_info.get('error', {}).get('message', response.text[:200]) # Essayer avec un modèle alternatif response = api_requests.post( 'https://openrouter.ai/api/v1/chat/completions', headers={ 'Authorization': f'Bearer {api_key}', 'Content-Type': 'application/json', 'HTTP-Referer': 'http://localhost:5000', 'X-Title': 'GMAO College' }, json={ 'model': 'poolside/laguna-m.1:free', 'messages': [{'role': 'user', 'content': prompt}], 'max_tokens': 2000 }, timeout=90 ) if response.status_code != 200: error_info = response.json() if response.json() else {} error_msg = error_info.get('error', {}).get('message', response.text[:200]) if response.json() else response.text[:200] current_app.logger.error(f"OpenRouter API error: {error_msg}") return jsonify({'error': f'Erreur API: {error_msg[:300]}'}), 500 data = response.json() if not data: return jsonify({'error': 'Erreur API: réponse vide'}), 500 choices = data.get('choices', []) if not choices or len(choices) == 0: return jsonify({'error': 'Erreur API: aucune réponse générée'}), 500 message = choices[0].get('message', {}) interpretation_text = message.get('content', '') if message else '' if not interpretation_text: interpretation_text = "Aucune réponse reçue du modèle." # Parser le JSON de la réponse import json parsed_data = None try: # Nettoyer la réponse (enlever les blocs markdown si présents) cleaned_text = interpretation_text.strip() if cleaned_text.startswith('```json'): cleaned_text = cleaned_text[7:] if cleaned_text.startswith('```'): cleaned_text = cleaned_text[3:] if cleaned_text.endswith('```'): cleaned_text = cleaned_text[:-3] cleaned_text = cleaned_text.strip() parsed_data = json.loads(cleaned_text) except json.JSONDecodeError as e: # Si le parsing échoue, on garde le texte brut parsed_data = None # Sauvegarder l'interprétation avec le contexte utilisé full_context = "" if global_context: full_context += "[CONTEXTE GMAO]\n" + global_context if additional_context: if full_context: full_context += "\n\n" full_context += "[CONTEXTE ADDITIONNEL]\n" + additional_context # Parser le nouveau format avec plusieurs analyses interpretations_created = [] if parsed_data and 'analyses' in parsed_data: # Format multi-interventions for idx, analysis in enumerate(parsed_data.get('analyses', [])): interpretation = OutlookMailInterpretation( mail_id=mail_id, user_id=current_user.id, interpretation=interpretation_text, user_context=full_context if full_context else None ) interpretation.set_from_parsed(analysis) if analysis.get('confidence'): interpretation.confidence_score = float(analysis['confidence']) # Marquer le numéro d'analyse si plusieurs if len(parsed_data['analyses']) > 1: interpretation.interpretation = f"[Analyse {idx+1}/{len(parsed_data['analyses'])}]\n{interpretation_text}" db.session.add(interpretation) interpretations_created.append(interpretation) elif parsed_data: # Format ancien (rétrocompatibilité) interpretation = OutlookMailInterpretation( mail_id=mail_id, user_id=current_user.id, interpretation=interpretation_text, user_context=full_context if full_context else None ) interpretation.set_from_parsed(parsed_data) if parsed_data.get('confidence'): interpretation.confidence_score = float(parsed_data['confidence']) db.session.add(interpretation) interpretations_created.append(interpretation) else: # JSON invalide, créer une interprétation vide interpretation = OutlookMailInterpretation( mail_id=mail_id, user_id=current_user.id, interpretation=interpretation_text, user_context=full_context if full_context else None ) db.session.add(interpretation) interpretations_created.append(interpretation) db.session.commit() # Préparer la réponse avec les données structurées response_data = { 'success': True, 'interpretation': interpretation_text, 'ids': [i.id for i in interpretations_created], 'nb_analyses': len(interpretations_created), 'context_used': bool(global_context) or bool(additional_context), 'parsed': parsed_data is not None } if parsed_data: if 'analyses' in parsed_data: response_data['analyses'] = parsed_data['analyses'] response_data['resume_global'] = parsed_data.get('resume_global', '') else: response_data['analysis'] = { 'type': parsed_data.get('type'), 'action': parsed_data.get('action'), 'confidence': parsed_data.get('confidence'), 'intervention': parsed_data.get('intervention', {}), 'date_suggestion': parsed_data.get('date_suggestion'), 'resume_court': parsed_data.get('resume_court') } return jsonify(response_data) except api_requests.exceptions.Timeout: return jsonify({'error': 'Timeout: le modèle a pris trop de temps'}), 500 except api_requests.exceptions.RequestException as e: return jsonify({'error': f'Erreur réseau: {str(e)}'}), 500 except Exception as e: import traceback traceback.print_exc() return jsonify({'error': f'Erreur: {str(e)}'}), 500 @dashboard_bp.route('/api/interpretations/') @login_required def api_get_interpretations(mail_id): """Récupère les interprétations d'un email.""" interpretations = OutlookMailInterpretation.query.filter_by( mail_id=mail_id ).order_by(OutlookMailInterpretation.created_at.desc()).all() return jsonify({ 'interpretations': [{ 'id': i.id, 'interpretation': i.interpretation, 'user_context': i.user_context, 'created_at': i.created_at.strftime('%d/%m/%Y %H:%M') } for i in interpretations] }) @dashboard_bp.route('/api/interpretation/', methods=['DELETE']) @login_required def api_delete_interpretation(interp_id): """Supprime une interprétation.""" interpretation = OutlookMailInterpretation.query.get_or_404(interp_id) if interpretation.user_id != current_user.id: return jsonify({'error': 'Non autorisé'}), 403 db.session.delete(interpretation) db.session.commit() return jsonify({'success': True}) @dashboard_bp.route('/api/interpretation/', methods=['GET', 'PUT']) @login_required def api_get_interpretation(interp_id): """Récupère ou met à jour une interprétation.""" interpretation = OutlookMailInterpretation.query.get_or_404(interp_id) if interpretation.user_id != current_user.id: return jsonify({'error': 'Non autorisé'}), 403 if request.method == 'PUT': data = request.get_json() if 'status' in data: interpretation.status = data['status'] db.session.commit() return jsonify({ 'success': True, 'id': interpretation.id, 'status': interpretation.status }) return jsonify({ 'success': True, 'id': interpretation.id, 'interpretation': interpretation.interpretation, 'analysis_type': interpretation.analysis_type, 'analysis_action': interpretation.analysis_action, 'suggested_title': interpretation.suggested_title, 'suggested_description': interpretation.suggested_description, 'suggested_urgency': interpretation.suggested_urgency, 'suggested_location': interpretation.suggested_location, 'suggested_equipment': interpretation.suggested_equipment, 'suggested_assignee': interpretation.suggested_assignee, 'raw_json': interpretation.raw_json, 'confidence_score': interpretation.confidence_score }) @dashboard_bp.route('/interpretation//create-intervention') @login_required def create_intervention_from_interpretation(interp_id): """Crée une intervention depuis une interprétation.""" interpretation = OutlookMailInterpretation.query.get_or_404(interp_id) if interpretation.user_id != current_user.id: flash('Non autorisé', 'error') return redirect(url_for('outlook.index')) # Rediriger vers le formulaire d'intervention avec les données pré-remplies return redirect(url_for('interventions.create', from_interpretation=interp_id, source='outlook')) @dashboard_bp.route('/interpretation//reject', methods=['POST']) @login_required def reject_interpretation(interpretation_id): """Rejette une interprétation, déplace le mail vers 'Traités' et supprime de la base.""" interpretation = OutlookMailInterpretation.query.get_or_404(interpretation_id) if interpretation.user_id != current_user.id: return jsonify({'success': False, 'error': 'Non autorisé'}), 403 mail = interpretation.mail move_result = None # Déplacer le mail vers le dossier "Traités" if mail: try: move_result = move_mail_to_processed(mail) except Exception as e: move_result = {'success': False, 'error': str(e)} # Supprimer l'interprétation db.session.delete(interpretation) # Supprimer le mail de la base GMAO if mail: db.session.delete(mail) db.session.commit() if move_result and move_result.get('success'): return jsonify({'success': True, 'message': 'Interprétation rejetée. Mail déplacé et supprimé de la base.'}) elif move_result: return jsonify({'success': True, 'warning': f'Interprétation rejetée. Attention: {move_result.get("error")}'}) else: return jsonify({'success': True, 'message': 'Interprétation rejetée.'}) def move_mail_to_processed(mail): """Déplace un mail vers le dossier 'Traités' via l'API Outlook.""" import requests import logging logger = logging.getLogger(__name__) account = mail.account if not account: logger.error("move_mail_to_processed: Pas de compte associé au mail") return {'success': False, 'error': 'Pas de compte associé'} # Obtenir le token access_token, error = get_access_token(account) if error: logger.error(f"move_mail_to_processed: Erreur token: {error}") return {'success': False, 'error': error} headers = { 'Authorization': f'Bearer {access_token}', 'Content-Type': 'application/json' } # Chercher ou créer le dossier "Traités" processed_folder_id = get_or_create_processed_folder(account, access_token) if not processed_folder_id: logger.error("move_mail_to_processed: Impossible de trouver/créer le dossier Traités") return {'success': False, 'error': 'Impossible de trouver/créer le dossier Traités'} logger.info(f"move_mail_to_processed: Dossier Traités ID: {processed_folder_id}") # Déplacer le mail move_url = f'https://outlook.office365.com/api/v2.0/me/messages/{mail.id}/move' move_data = {'DestinationId': processed_folder_id} try: logger.info(f"move_mail_to_processed: Appel API {move_url}") resp = requests.post(move_url, headers=headers, json=move_data, timeout=30) logger.info(f"move_mail_to_processed: Réponse {resp.status_code}: {resp.text[:200]}") if resp.status_code in [200, 201]: # Mettre à jour le folder_id dans la base mail.folder_id = processed_folder_id return {'success': True} else: return {'success': False, 'error': f'API erreur {resp.status_code}: {resp.text[:100]}'} except Exception as e: logger.error(f"move_mail_to_processed: Exception {e}") return {'success': False, 'error': str(e)} def get_or_create_processed_folder(account, access_token): """Récupère l'ID du dossier 'Traités' depuis la configuration.""" from .processed_folder_config import ProcessedFolderConfig import logging logger = logging.getLogger(__name__) config = ProcessedFolderConfig.get_config() # Utiliser l'ID configuré if config.outlook_folder_id: logger.info(f"get_or_create_processed_folder: Using configured folder ID: {config.outlook_folder_id}") return config.outlook_folder_id # Fallback: chercher dans la liste des dossiers headers = {'Authorization': f'Bearer {access_token}'} folders_url = 'https://outlook.office365.com/api/v2.0/me/MailFolders' try: resp = requests.get(folders_url, headers=headers, timeout=30) if resp.status_code == 200: folders = resp.json().get('value', []) for folder in folders: if folder.get('DisplayName', '').lower() in ['traités', 'traites', 'processed']: logger.info(f"get_or_create_processed_folder: Found folder '{folder.get('DisplayName')}' -> {folder.get('Id')}") return folder.get('Id') # Créer le dossier "Traités" create_url = 'https://outlook.office365.com/api/v2.0/me/MailFolders' create_data = {'DisplayName': 'Traités'} resp = requests.post(create_url, headers=headers, json=create_data, timeout=30) if resp.status_code == 201: logger.info(f"get_or_create_processed_folder: Created folder -> {resp.json().get('Id')}") return resp.json().get('Id') except Exception as e: logger.error(f"get_or_create_processed_folder: Exception {e}") return None # ─── Configuration du Contexte GMAO ───────────────────────────────────────── @dashboard_bp.route('/context') @login_required def context_config(): """Page de configuration du contexte GMAO.""" gmao_context = GmaoContext.get_for_user(current_user.id) return render_template('outlook/context_config.html', gmao_context=gmao_context) @dashboard_bp.route('/api/context', methods=['GET']) @login_required def api_get_context(): """API pour récupérer le contexte GMAO.""" gmao_context = GmaoContext.get_for_user(current_user.id) return jsonify({ 'id': gmao_context.id, 'establishment_name': gmao_context.establishment_name or '', 'establishment_type': gmao_context.establishment_type or '', 'address': gmao_context.address or '', 'technical_context': gmao_context.technical_context or '', 'equipment_context': gmao_context.equipment_context or '', 'procedure_context': gmao_context.procedure_context or '', 'staff_info': gmao_context.staff_info or '', 'contact_info': gmao_context.contact_info or '', 'hermes_instructions': gmao_context.hermes_instructions or '', 'excluded_senders': gmao_context.excluded_senders or '', 'full_context': gmao_context.get_full_context(), 'updated_at': gmao_context.updated_at.strftime('%d/%m/%Y %H:%M') if gmao_context.updated_at else '' }) @dashboard_bp.route('/api/context', methods=['POST']) @login_required def api_save_context(): """API pour sauvegarder le contexte GMAO.""" gmao_context = GmaoContext.get_for_user(current_user.id) data = request.json # Mise à jour des champs gmao_context.establishment_name = data.get('establishment_name', '') gmao_context.establishment_type = data.get('establishment_type', '') gmao_context.address = data.get('address', '') gmao_context.technical_context = data.get('technical_context', '') gmao_context.equipment_context = data.get('equipment_context', '') gmao_context.procedure_context = data.get('procedure_context', '') gmao_context.staff_info = data.get('staff_info', '') gmao_context.contact_info = data.get('contact_info', '') gmao_context.hermes_instructions = data.get('hermes_instructions', '') gmao_context.excluded_senders = data.get('excluded_senders', '') db.session.commit() return jsonify({ 'success': True, 'message': 'Contexte GMAO sauvegardé', 'full_context': gmao_context.get_full_context() }) @dashboard_bp.route('/api/auto-interpret', methods=['GET']) @login_required def api_get_auto_interpret(): """API pour récupérer la configuration d'interprétation automatique.""" gmao_context = GmaoContext.get_for_user(current_user.id) return jsonify({ 'auto_interpret_enabled': gmao_context.auto_interpret_enabled or False, 'auto_interpret_interval': gmao_context.auto_interpret_interval or 10, 'auto_interpret_folder': gmao_context.auto_interpret_folder or 'inbox', 'auto_interpret_max_emails': gmao_context.auto_interpret_max_emails or 10, 'auto_interpret_lookback_hours': gmao_context.auto_interpret_lookback_hours or 24 }) @dashboard_bp.route('/api/auto-interpret', methods=['POST']) @system_admin_required def api_save_auto_interpret(): """API pour sauvegarder la configuration d'interprétation automatique. Contrôle les DEUX watchdogs (GMAO et ENT) avec le même bouton. Pattern DND: tue les processus existants puis les relance si activé. """ import subprocess gmao_context = GmaoContext.get_for_user(current_user.id) data = request.json gmao_context.auto_interpret_enabled = data.get('auto_interpret_enabled', False) gmao_context.auto_interpret_interval = data.get('auto_interpret_interval', 10) gmao_context.auto_interpret_folder = data.get('auto_interpret_folder', 'inbox') gmao_context.auto_interpret_max_emails = data.get('auto_interpret_max_emails', 10) gmao_context.auto_interpret_lookback_hours = data.get('auto_interpret_lookback_hours', 24) db.session.commit() # Redémarrer les DEUX watchdogs (pattern DND) # 1. Tuer les processus existants try: subprocess.run(['pkill', '-f', 'gmao_watchdog'], capture_output=True) except: pass try: subprocess.run(['pkill', '-f', 'ent_watchdog'], capture_output=True) except: pass # 2. Relancer si activé if gmao_context.auto_interpret_enabled: # Relancer GMAO watchdog gmao_script = '/app/gmao_watchdog.py' subprocess.Popen(['python3', '/app/watchdog_gate.py', 'outlook', gmao_script], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) # Relancer ENT watchdog ent_script = '/app/ent_watchdog.py' subprocess.Popen(['python3', '/app/watchdog_gate.py', 'ent', ent_script], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) return jsonify({ 'success': True, 'message': 'Configuration sauvegardée', 'watchdogs': { 'gmao': 'restarted' if gmao_context.auto_interpret_enabled else 'stopped', 'ent': 'restarted' if gmao_context.auto_interpret_enabled else 'stopped' } }) @dashboard_bp.route('/auto-interpret') @login_required def auto_interpret_page(): """Page de configuration de l'interprétation automatique.""" return render_template('outlook/auto_interpret.html') @dashboard_bp.route('/api/auto-interpret/run', methods=['POST']) @login_required def api_run_auto_interpret(): """API pour exécuter l'interprétation automatique manuellement.""" from .auto_interpret import run_auto_interpret result = run_auto_interpret(current_user.id) return jsonify(result) @dashboard_bp.route('/interpretations') @login_required def all_interpretations(): """Page affichant toutes les interprétations.""" from .models import OutlookMailInterpretation, OutlookMail from app_new.core.models import User from flask import request # Filtres status_filter = request.args.get('status', 'pending') type_filter = request.args.get('type', '') # Requête de base query = OutlookMailInterpretation.query # Filtrer par statut if status_filter == 'pending': query = query.filter_by(status='pending') elif status_filter == 'rejected': query = query.filter_by(status='rejected') elif status_filter == 'accepted': query = query.filter_by(status='accepted') # 'all' ne filtre pas # Filtrer par type if type_filter: query = query.filter_by(analysis_type=type_filter) # Ordonner par date décroissante interpretations = query.order_by(OutlookMailInterpretation.created_at.desc()).all() return render_template('outlook/interpretations.html', interpretations=interpretations, status_filter=status_filter, type_filter=type_filter) # ─── Configuration des dossiers traités ───────────────────────────────────── @dashboard_bp.route('/processed-folders') @login_required def processed_folders_config(): """Page de configuration des dossiers 'Traités'.""" from .processed_folder_config import ProcessedFolderConfig config = ProcessedFolderConfig.get_config() return render_template('outlook/processed_folders.html', config=config) @dashboard_bp.route('/api/processed-folders', methods=['GET']) @login_required def api_get_processed_folders(): """API pour récupérer la configuration des dossiers traités.""" from .processed_folder_config import ProcessedFolderConfig config = ProcessedFolderConfig.get_config() return jsonify({ 'outlook_folder_id': config.outlook_folder_id or '', 'outlook_folder_name': config.outlook_folder_name or '', 'ent_folder_id': config.ent_folder_id or '', 'ent_folder_name': config.ent_folder_name or '', 'updated_at': config.updated_at.strftime('%d/%m/%Y %H:%M') if config.updated_at else '' }) @dashboard_bp.route('/api/processed-folders', methods=['POST']) @login_required def api_save_processed_folders(): """API pour sauvegarder la configuration des dossiers traités.""" from .processed_folder_config import ProcessedFolderConfig config = ProcessedFolderConfig.get_config() data = request.json config.outlook_folder_id = data.get('outlook_folder_id', '') config.outlook_folder_name = data.get('outlook_folder_name', '') config.ent_folder_id = data.get('ent_folder_id', '') config.ent_folder_name = data.get('ent_folder_name', '') db.session.commit() return jsonify({'success': True}) # ─── Configuration PRONOTE ───────────────────────────────────── @dashboard_bp.route('/api/pronote-config', methods=['GET']) @login_required def api_get_pronote_config(): """API pour récupérer la configuration PRONOTE.""" gmao_context = GmaoContext.get_for_user(current_user.id) # Lire la dernière synchronisation import os last_run_file = '/tmp/pronote_last_run.txt' last_sync = None if os.path.exists(last_run_file): try: from datetime import datetime, timezone with open(last_run_file, 'r') as f: last_sync = datetime.fromisoformat(f.read().strip()).strftime('%d/%m/%Y %H:%M') except: pass return jsonify({ 'pronote_sync_enabled': gmao_context.pronote_sync_enabled or False, 'pronote_sync_interval': gmao_context.pronote_sync_interval or 360, 'pronote_sync_weeks': gmao_context.pronote_sync_weeks or 4, 'last_sync': last_sync }) @dashboard_bp.route('/api/pronote-config', methods=['POST']) @login_required def api_save_pronote_config(): """API pour sauvegarder la configuration PRONOTE.""" gmao_context = GmaoContext.get_for_user(current_user.id) data = request.json gmao_context.pronote_sync_enabled = data.get('pronote_sync_enabled', False) gmao_context.pronote_sync_interval = data.get('pronote_sync_interval', 360) gmao_context.pronote_sync_weeks = data.get('pronote_sync_weeks', 4) db.session.commit() return jsonify({'success': True}) @dashboard_bp.route('/api/pronote-watchdog-status', methods=['GET']) @login_required def api_pronote_watchdog_status(): """API pour vérifier le statut du watchdog PRONOTE.""" import subprocess try: result = subprocess.run(['pgrep', '-f', 'pronote_watchdog.py'], capture_output=True) running = result.returncode == 0 return jsonify({'running': running}) except: return jsonify({'running': False}) @dashboard_bp.route('/api/pronote-watchdog-restart', methods=['POST']) @system_admin_required def api_pronote_watchdog_restart(): """API pour redémarrer le watchdog PRONOTE.""" import subprocess import os workdir = '/app' # Tuer l'ancien processus subprocess.run(['pkill', '-f', 'pronote_watchdog.py'], capture_output=True) # Démarrer le nouveau subprocess.Popen( ['python3', '/app/watchdog_gate.py', 'pronote', '/app/pronote_watchdog.py'], cwd=workdir, stdout=open('/tmp/pronote_watchdog.out', 'a'), stderr=open('/tmp/pronote_watchdog.err', 'a') ) return jsonify({'success': True, 'message': 'Watchdog PRONOTE redémarré'})