gmao/app_new/status/routes.py

380 lines
13 KiB
Python
Raw Normal View History

2026-08-14 18:02:25 +02:00
"""
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()
})