gmao/app_new/outlook/dashboard.py
2026-08-14 16:02:25 +00:00

922 lines
37 KiB
Python

"""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/<mail_id>', 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/<mail_id>')
@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/<int:interp_id>', 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/<int:interp_id>', 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/<int:interp_id>/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/<int:interpretation_id>/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', gmao_script],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL)
# Relancer ENT watchdog
ent_script = '/app/ent_watchdog.py'
subprocess.Popen(['python3', 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', '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é'})