""" Status Système - GMAO Collège Page de surveillance de l'état des systèmes avec vérification de connexion réelle. API JSON pour rafraîchissement asynchrone. """ from flask import Blueprint, render_template, jsonify from flask_login import login_required from datetime import datetime, timezone import os import requests status_bp = Blueprint('status', __name__, url_prefix='/status') def _find_pids_by_cmd(pattern): """Trouve les PID des processus dont la cmdline contient pattern.""" pids = [] try: for pid in os.listdir('/proc'): if not pid.isdigit(): continue try: cmdline_path = f'/proc/{pid}/cmdline' with open(cmdline_path, 'rb') as f: cmdline = f.read().decode('utf-8', errors='replace').replace('\x00', ' ') if pattern in cmdline and 'grep' not in cmdline: pids.append(pid) except (FileNotFoundError, PermissionError): continue except Exception: pass return pids def _read_last_run(path): """Lit et formate la dernière exécution depuis un fichier.""" if not os.path.exists(path): return None try: with open(path, 'r') as f: last_run = f.read().strip() dt = datetime.fromisoformat(last_run) return dt.strftime('%d/%m/%Y %H:%M:%S') except Exception: return None def check_database(): """Vérifie la connexion à la base de données MariaDB.""" result = { 'name': 'MariaDB', 'icon': 'bi-database', 'configured': True, 'connected': False, 'message': '' } try: from app_new.extensions import db result_sql = db.session.execute(db.text('SELECT 1')).scalar() if result_sql == 1: result['connected'] = True version = db.session.execute(db.text('SELECT VERSION()')).scalar() result['message'] = f'Connecté — {version}' else: result['message'] = 'Réponse inattendue' except Exception as e: result['message'] = f'Erreur: {str(e)[:80]}' return result def check_outlook(): """Vérifie la connexion Outlook/Microsoft Graph API.""" result = { 'name': 'Outlook / Microsoft', 'icon': 'bi-envelope', 'configured': False, 'connected': False, 'message': '' } try: from app_new.outlook.models import OutlookAccount from app_new.extensions import db accounts = OutlookAccount.query.filter_by(is_active=True).all() if not accounts: result['message'] = 'Aucun compte configuré' return result result['configured'] = True # Prendre le compte avec le refresh_token le plus récent account = max(accounts, key=lambda a: a.last_connected_at or datetime.min.replace(tzinfo=timezone.utc)) result['message'] = f'{account.email}' # Vérifier le statut de connexion stocké if account.connection_status == 'active': result['connected'] = True if account.last_connected_at: dt = account.last_connected_at if dt.tzinfo is None: dt = dt.replace(tzinfo=timezone.utc) result['message'] += f' — Dernière sync: {dt.strftime("%d/%m/%Y %H:%M")}' # Tester réellement l'API Graph try: from app_new.outlook.auth import get_access_token token = get_access_token(account) if token: resp = requests.get( 'https://graph.microsoft.com/v1.0/me', headers={'Authorization': f'Bearer {token}'}, timeout=10 ) if resp.status_code == 200: me = resp.json() result['connected'] = True result['message'] += f' — API OK ({me.get("displayName", "")})' elif resp.status_code == 401: result['connected'] = False result['message'] += ' — Token expiré, reconnexion nécessaire' else: result['connected'] = False result['message'] += f' — API erreur {resp.status_code}' else: result['connected'] = False result['message'] += ' — Impossible d\'obtenir un token' except Exception as e: result['message'] += f' — Test API: {str(e)[:40]}' else: result['connected'] = False status_txt = account.connection_status or 'inconnu' result['message'] += f' — Statut: {status_txt}' # Tenter quand même de rafraîchir le token try: from app_new.outlook.auth import get_access_token token = get_access_token(account) if token: result['connected'] = True result['message'] += ' — Token rafraîchi automatiquement' except Exception: pass except Exception as e: result['message'] = f'Erreur: {str(e)[:80]}' return result def check_ent(): """Vérifie la connexion ENT77.""" result = { 'name': 'ENT77', 'icon': 'bi-person-badge', 'configured': False, 'connected': False, 'message': '' } try: from app_new.ent.models import EntCredential from app_new.extensions import db cred = EntCredential.query.filter_by(is_active=True).first() if not cred: result['message'] = 'Non configuré' return result result['configured'] = True if cred.last_check: dt = cred.last_check if dt.tzinfo is None: dt = dt.replace(tzinfo=timezone.utc) result['message'] = f'Dernière vérification: {dt.strftime("%d/%m/%Y %H:%M")}' # Tester l'accessibilité du serveur ENT try: resp = requests.get('https://ent77.seine-et-marne.fr', timeout=10, allow_redirects=True) if resp.status_code in (200, 302, 301): result['connected'] = True result['message'] += ' — Serveur accessible' else: result['message'] += f' — HTTP {resp.status_code}' except requests.exceptions.Timeout: result['message'] += ' — Timeout' except Exception as e: result['message'] += f' — {str(e)[:40]}' except Exception as e: result['message'] = f'Erreur: {str(e)[:80]}' return result def check_pronote(): """Vérifie la connexion PRONOTE.""" result = { 'name': 'PRONOTE', 'icon': 'bi-calendar-week', 'configured': False, 'connected': False, 'message': '' } try: from app_new.pronote.models import PronoteSession from app_new.extensions import db session = PronoteSession.get() if not session or not session.session_url: result['message'] = 'Non configuré' return result result['configured'] = True result['message'] = f'{session.username or "N/A"}' # Tester l'URL PRONOTE try: resp = requests.get(session.session_url, timeout=10, allow_redirects=True) if resp.status_code == 200: result['connected'] = True result['message'] += ' — Serveur accessible' else: result['message'] += f' — HTTP {resp.status_code}' except requests.exceptions.Timeout: result['message'] += ' — Timeout' except Exception as e: result['message'] += f' — {str(e)[:40]}' if session.last_connection: dt = session.last_connection if dt.tzinfo is None: dt = dt.replace(tzinfo=timezone.utc) result['message'] += f', dernière connexion: {dt.strftime("%d/%m/%Y %H:%M")}' except Exception as e: result['message'] = f'Erreur: {str(e)[:80]}' return result def check_yeastar(): """Vérifie la connexion Yeastar P520 avec test de login API.""" result = { 'name': 'Yeastar P520', 'icon': 'bi-telephone', 'configured': False, 'connected': False, 'message': '' } try: from app_new.yeastar.models import YeastarConfig from app_new.extensions import db config = YeastarConfig.query.filter_by(is_active=True).first() if not config or not config.base_url: result['message'] = 'Non configuré' return result result['configured'] = True result['message'] = f'{config.base_url}' # Test de connexion API try: from app_new.lib_ext.api_client import YeastarClient username = config.get_username() password = config.get_password() client = YeastarClient(config.base_url, username, password) login_result = client.login() if login_result.get('success'): result['connected'] = True try: exts = client.get_extensions() result['message'] += f' — Connecté, {len(exts)} extensions' client.logout() except Exception: result['message'] += ' — Connecté (erreur liste extensions)' else: result['message'] += f' — Login échoué: {login_result.get("error", "inconnu")[:40]}' except Exception as e: result['message'] += f' — Erreur: {str(e)[:40]}' except Exception as e: result['message'] = f'Erreur: {str(e)[:80]}' return result def check_gmao_context(): """Vérifie la configuration GMAO (auto-interprétation, sync PRONOTE).""" result = { 'name': 'Contexte GMAO', 'icon': 'bi-gear', 'configured': False, 'connected': False, 'message': '' } try: from app_new.outlook.models import GmaoContext from app_new.extensions import db ctx = GmaoContext.get_for_user(2) if not ctx: result['message'] = 'Non configuré' return result result['configured'] = True result['connected'] = True parts = [] if ctx.auto_interpret_enabled: parts.append('Auto-interprétation ON') else: parts.append('Auto-interprétation OFF') if ctx.pronote_sync_enabled: parts.append(f'Sync PRONOTE ON (×{ctx.pronote_sync_interval or 360}min)') else: parts.append('Sync PRONOTE OFF') result['message'] = ' | '.join(parts) except Exception as e: result['message'] = f'Erreur: {str(e)[:80]}' return result def check_watchdog(name, pattern, last_run_path, label_sync=''): """Vérifie si un watchdog est en cours d'exécution.""" result = { 'name': name, 'running': False, 'pid': None, 'last_run': None, 'message': '' } try: pids = _find_pids_by_cmd(pattern) if pids: result['running'] = True result['pid'] = pids[0] result['last_run'] = _read_last_run(last_run_path) msg = f'Actif (PID: {result["pid"]})' if result['last_run']: msg += f', {label_sync}: {result["last_run"]}' result['message'] = msg else: result['message'] = 'Inactif' except Exception as e: result['message'] = f'Erreur: {str(e)[:30]}' return result @status_bp.route('/') @login_required def index(): """Page de status du système — rendu HTML rapide, données chargées en async.""" return render_template('status/index.html') @status_bp.route('/api/check') @login_required def api_check(): """API JSON pour rafraîchissement asynchrone — vérifie les connexions en temps réel.""" systems = [ check_database(), check_outlook(), check_ent(), check_pronote(), check_yeastar(), check_gmao_context(), ] configured_count = sum(1 for s in systems if s.get('configured')) connected_count = sum(1 for s in systems if s.get('connected')) watchdogs = [ check_watchdog('Watchdog DND Yeastar', 'watchdog_dnd.py', '/tmp/dnd_last_run.txt', 'dernière action'), check_watchdog('Watchdog GMAO Mail', 'gmao_watchdog.py', '/tmp/gmao_last_run.txt', 'dernière exécution'), check_watchdog('Watchdog ENT', 'ent_watchdog.py', '/tmp/ent_last_run.txt', 'dernière exécution'), check_watchdog('Watchdog PRONOTE', 'pronote_watchdog.py', '/tmp/pronote_last_run.txt', 'dernière sync'), ] return jsonify({ 'systems': systems, 'watchdogs': watchdogs, 'configured_count': configured_count, 'connected_count': connected_count, 'timestamp': datetime.now(timezone.utc).isoformat() })