#!/usr/bin/env python3 """Watchdog pour l'interprétation automatique des emails GMAO - Pattern DND.""" import sys import os import time import json import signal import requests from datetime import datetime, timedelta, timezone # Définir le répertoire de travail WORKDIR = os.environ.get('DOCKER_GMAO_WORKDIR', '/root/hermes-workspace/gmao-college') os.chdir(WORKDIR) sys.path.insert(0, WORKDIR) # Activer le venv venv_site_packages = os.path.join(WORKDIR, 'venv', 'lib', 'python3.13', 'site-packages') if os.path.exists(venv_site_packages): sys.path.insert(0, venv_site_packages) # Charger les variables d'environnement depuis .env from dotenv import load_dotenv env_file = os.environ.get('DOCKER_GMAO_ENVFILE', os.path.join(WORKDIR, '.env')) load_dotenv(env_file) # Fichier de log et de dernière exécution LOG_FILE = '/tmp/gmao_watchdog.log' LAST_RUN_FILE = '/tmp/gmao_last_run.txt' running = True def log(message, level='info'): """Écrit un message dans le log et dans la base centralisee.""" timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S') print(f'[{timestamp}] {message}') with open(LOG_FILE, 'a') as f: f.write(f'[{timestamp}] {message}\n') # Log centralise try: sys.path.insert(0, '/app') from app_new.watchdog_logger import log_watchdog log_watchdog('gmao_watchdog', message, level=level) except Exception: pass def get_last_run(): """Retourne la date de dernière exécution.""" if os.path.exists(LAST_RUN_FILE): try: with open(LAST_RUN_FILE, 'r') as f: return datetime.fromisoformat(f.read().strip()) except: return None return None def save_last_run(): """Sauvegarde la date de dernière exécution.""" with open(LAST_RUN_FILE, 'w') as f: f.write(datetime.now().isoformat()) def should_run(gmao_context): """Vérifie si le watchdog doit s'exécuter selon l'intervalle configuré.""" if not gmao_context or not gmao_context.auto_interpret_enabled: return False last_run = get_last_run() if not last_run: return True interval_minutes = gmao_context.auto_interpret_interval or 10 next_run = last_run + timedelta(minutes=interval_minutes) return datetime.now() >= next_run def sync_inbox(): """Synchronise la boîte de réception Outlook.""" from app_new import create_app, db from app_new.outlook.models import OutlookAccount, OutlookFolder, OutlookMail, OutlookAttachment from app_new.outlook.auth import get_access_token app = create_app() with app.app_context(): # Récupérer le compte Outlook account = OutlookAccount.query.filter_by(is_active=True).first() if not account: log("Aucun compte Outlook actif") return {'success': False, 'error': 'Aucun compte Outlook actif'} # Obtenir le token access_token, error = get_access_token(account) if error: log(f"Erreur token: {error}") return {'success': False, 'error': error} # Trouver le dossier inbox par nom (Boîte de réception) folder = OutlookFolder.query.filter_by(account_id=account.id, name='Boîte de réception').first() if not folder: # Fallback: chercher par folder_type folder = OutlookFolder.query.filter_by(account_id=account.id, folder_type='inbox').first() if not folder: log("Dossier inbox non trouvé") return {'success': False, 'error': 'Dossier inbox non trouvé'} # Paramètres de synchronisation max_messages = 50 # Limite pour le watchdog headers = {'Authorization': f'Bearer {access_token}'} # Récupérer les messages récents url = f'https://outlook.office365.com/api/v2.0/me/MailFolders/{folder.id}/messages?$top={max_messages}&$orderby=ReceivedDateTime desc&$select=Id,Subject,From,ReceivedDateTime,IsRead,HasAttachments,BodyPreview,Body,ToRecipients,CcRecipients,Importance' try: resp = requests.get(url, headers=headers, timeout=30) if resp.status_code != 200: error_msg = resp.json().get('error', {}).get('message', resp.text) if resp.json() else resp.text log(f"Erreur API Outlook: {error_msg}") return {'success': False, 'error': error_msg} data = resp.json() mails_data = data.get('value', []) saved_count = 0 updated_count = 0 for mail_data in mails_data: mail_id = mail_data.get('Id') existing = OutlookMail.query.filter_by(id=mail_id).first() from_addr = mail_data.get('From', {}).get('EmailAddress', {}) body = mail_data.get('Body', {}) received_str = mail_data.get('ReceivedDateTime', '') try: received_at = datetime.fromisoformat(received_str.replace('Z', '+00:00')) except: received_at = datetime.utcnow() # Destinataires to_recipients = json.dumps([r.get('EmailAddress', {}).get('Address', '') for r in mail_data.get('ToRecipients', [])]) cc_recipients = json.dumps([r.get('EmailAddress', {}).get('Address', '') for r in mail_data.get('CcRecipients', [])]) if not existing: mail = OutlookMail( id=mail_id, account_id=account.id, folder_id=folder.id, subject=mail_data.get('Subject', ''), from_name=from_addr.get('Name', ''), from_email=from_addr.get('Address', ''), to_recipients=to_recipients, cc_recipients=cc_recipients, received_at=received_at, is_read=mail_data.get('IsRead', False), has_attachments=mail_data.get('HasAttachments', False), body_preview=mail_data.get('BodyPreview', ''), body_content=body.get('content', '') if body.get('contentType') == 'html' else '', body_type=body.get('contentType', 'text'), importance=mail_data.get('Importance', 'normal') ) db.session.add(mail) saved_count += 1 else: # Mettre à jour si nécessaire existing.is_read = mail_data.get('IsRead', existing.is_read) existing.has_attachments = mail_data.get('HasAttachments', existing.has_attachments) updated_count += 1 db.session.commit() # Télécharger les pièces jointes PDF pour tous les emails avec pièces jointes pdf_count = 0 emails_with_attachments = [m for m in mails_data if m.get('HasAttachments')] for mail_data in emails_with_attachments: mail_id = mail_data.get('Id') try: # Récupérer les pièces jointes att_url = f'https://outlook.office365.com/api/v2.0/me/messages/{mail_id}/attachments' att_resp = requests.get(att_url, headers=headers, timeout=30) if att_resp.status_code == 200: attachments_data = att_resp.json().get('value', []) for att in attachments_data: att_id = att.get('Id') att_name = att.get('Name', '') att_content_type = att.get('ContentType', '') # Vérifier si c'est un PDF if att_name.lower().endswith('.pdf') or 'pdf' in att_content_type.lower(): # Vérifier si déjà téléchargé existing_att = OutlookAttachment.query.filter_by(id=att_id).first() if not existing_att: # Télécharger le contenu content_url = f'https://outlook.office365.com/api/v2.0/me/messages/{mail_id}/attachments/{att_id}/$value' content_resp = requests.get(content_url, headers=headers, timeout=60) if content_resp.status_code == 200: new_att = OutlookAttachment( id=att_id, mail_id=mail_id, name=att_name, content_type=att_content_type, size=len(content_resp.content), content=content_resp.content ) db.session.add(new_att) pdf_count += 1 log(f" PDF téléchargé: {att_name}") except Exception as e: log(f" Erreur téléchargement pièce jointe: {str(e)[:50]}") if pdf_count > 0: db.session.commit() log(f"PDFs téléchargés: {pdf_count}") # Mettre à jour la date de dernière connexion Outlook account.connection_status = 'active' account.last_connected_at = datetime.now(timezone.utc) db.session.commit() log(f"Sync: {saved_count} nouveaux, {updated_count} mis à jour") return {'success': True, 'saved': saved_count, 'updated': updated_count} except Exception as e: log(f"Erreur sync: {str(e)}") return {'success': False, 'error': str(e)} def run_watchdog(): """Exécute un cycle du watchdog - retourne False si désactivé.""" global running from app_new import create_app, db from app_new.outlook.models import GmaoContext from app_new.outlook.auto_interpret import run_auto_interpret app = create_app() with app.app_context(): # Récupérer le contexte GMAO (tous les contextes actifs) gmao_context = GmaoContext.query.filter_by(auto_interpret_enabled=True).first() # Vérifier si activé if not gmao_context or not gmao_context.auto_interpret_enabled: log("Watchdog désactivé dans la config") return False # Arrêter le watchdog if not should_run(gmao_context): # Pas le moment, attendre return True interval = gmao_context.auto_interpret_interval or 10 log(f"Exécution de l'interprétation (intervalle: {interval} min)...") try: log("Démarrage synchronisation...") # 1. D'abord synchroniser les emails sync_result = sync_inbox() log(f"Sync result: {sync_result}") if sync_result.get('success'): log(f"Sync: {sync_result.get('saved', 0)} nouveaux, {sync_result.get('updated', 0)} mis à jour") else: log(f"Erreur sync inbox: {sync_result.get('error', 'inconnue')}") log("Démarrage interprétation...") # 2. Ensuite interpréter result = run_auto_interpret() log(f"Interpret result: processed={result.get('processed', 0)}, created={result.get('created', 0)}") if result.get('errors'): for err in result.get('errors', []): log(f" ⚠ Erreur email: {err[:100]}") save_last_run() log("save_last_run done") # Commit final pour libérer la session db.session.commit() log("db.session.commit done") except Exception as e: db.session.rollback() import traceback log(f"ERREUR dans le cycle: {str(e)}") log(f"Traceback: {traceback.format_exc()[:500]}") # Continuer quand même try: db.session.rollback() except: pass return True # Continuer def main(): """Point d'entrée principal - pattern DND avec redémarrage automatique.""" global running log("="*50) log("Watchdog GMAO démarré (pattern DND)") log("="*50) # Capturer le signal d'arrêt def signal_handler(signum, frame): global running running = False log("Signal reçu, arrêt du watchdog...") signal.signal(signal.SIGTERM, signal_handler) signal.signal(signal.SIGINT, signal_handler) while running: result = None try: result = run_watchdog() if result is False: log("Watchdog désactivé - arrêt") break # Sortir proprement si désactivé except Exception as e: import traceback log(f"Crash: {str(e)}", level='error') log(f"Traceback: {traceback.format_exc()[-500:]}", level='error') # Redémarrer automatiquement après crash # Attendre avant la prochaine vérification # Si le watchdog est désactivé, on attend plus longtemps pour éviter les redémarrages incessants wait_seconds = 300 if result is False else 30 for _ in range(wait_seconds): if not running: break time.sleep(1) log("Watchdog GMAO arrêté") if __name__ == '__main__': main()