gmao/gmao_watchdog.py

334 lines
14 KiB
Python

#!/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()