"""Journal admin Alfred Glass V2 : lecture seule du journal systemd d'Alfred. Les donnees sont protegees par la meme session admin que /config. Les sorties Hermes independantes ne sont PAS inventees/aggregees artificiellement. """ from __future__ import annotations from datetime import datetime, timezone import json import re import subprocess from flask import Blueprint, jsonify, render_template, request from services import admin_console # Redaction defensive : le journal applicatif peut contenir des erreurs # avec des secrets. Aucune methode ne peut promettre une suppression parfaite. _SECRET = re.compile( r'(?i)\b((?:PROXMOX_TOKEN_VALUE|HA_TOKEN|OPENAI_API_KEY|API_KEY|PASSWORD|SECRET|' r'ACCESS_TOKEN|REFRESH_TOKEN|TOKEN|AUTHORIZATION|API[-_]?KEY|BEARER)\s*[:=]\s*)' r'(?:["\']?[^\s,;"\']+["\']?)' ) _BEARER = re.compile(r'(?i)\bBearer\s+[a-z0-9._~+/=-]{8,}') _URI_SECRET = re.compile(r'(?i)([?&](?:token|key|api_key|password|secret)=)[^\s&#]+') def redact(message: str) -> str: result = str(message or '')[:2200] result = _SECRET.sub(r'\1[MASQUÉ]', result) result = _BEARER.sub('Bearer [MASQUÉ]', result) result = _URI_SECRET.sub(r'\1[MASQUÉ]', result) return result def component(message: str) -> str: upper = message.upper() if any(k in upper for k in ('[ALFRED STT]', 'WHISPER', 'VOSK', 'WAKE WORD', 'WAKEWORD', 'TRANSCRIPTION')): return 'Vocal' if any(k in upper for k in ('[ALFRED PIPER]', '[ALFRED TTS]', 'WYOMING PIPER', 'SPEAK_ASYNC')): return 'Piper' if any(k in upper for k in ('MCP_', 'MCP ', 'TOOL CALL', 'LIST_NODES', 'CREATE_TIMER', 'HA_LIST')): return 'MCP' if any(k in upper for k in ('[ALFRED] ENVOI À HERMES', '[ALFRED] RÉPONSE HERMES', 'HERMES', 'AGENT')): return 'Hermes' return 'Système' def level(message: str, priority: str) -> str: if priority and priority.isdigit() and int(priority) <= 3: return 'Erreur' upper = message.upper() if 'TRACEBACK' in upper or 'EXCEPTION' in upper or '[ERROR]' in upper or ' ERREUR ' in upper: return 'Erreur' if priority and priority.isdigit() and int(priority) == 4 or 'WARNING' in upper or 'ATTENTION' in upper: return 'Avertissement' return 'Information' def read_journal(limit: int = 220): cmd = ['journalctl', '--no-pager', '--quiet', '--output=json', '--unit=alfred.service', '--lines='+str(limit)] try: result = subprocess.run(cmd, capture_output=True, text=True, timeout=7, check=False) except (OSError, subprocess.TimeoutExpired) as exc: raise RuntimeError('Lecture du journal indisponible : '+type(exc).__name__) from exc if result.returncode != 0: raise RuntimeError('Lecture du journal refusée. Vérifier les droits systemd.') items = [] for raw in result.stdout.splitlines()[-limit:]: try: record = json.loads(raw) msg = str(record.get('MESSAGE') or '') if not msg: continue # Certaines traces Flask contiennent des en-tetes d'authentification : prudence. micros = str(record.get('__REALTIME_TIMESTAMP') or '0') iso = datetime.fromtimestamp(int(micros)/1_000_000,timezone.utc).isoformat() items.append({'at': iso, 'component':component(msg), 'level':level(msg,str(record.get('PRIORITY') or '')), 'message':redact(msg)}) except (TypeError, ValueError, OverflowError, json.JSONDecodeError, OSError): continue return items def register_logs_page(app): bp = Blueprint('alfred_glass_logs_v2', __name__) @bp.get('/logs') @bp.get('/logs/') def logs_page(): # Le HTML ne contient jamais les journaux; l'API exige la session admin. return render_template('alfred_logs_v2.html') @bp.get('/api/alfred/logs') def logs_api(): if not admin_console._authenticated(): return jsonify(error='Authentification administrateur nécessaire.', login_url='/config'), 401 try: limit = min(400,max(20,int(request.args.get('limit',220)))) except (TypeError,ValueError): return jsonify(error='Limite invalide.'),400 try: records=read_journal(limit) except RuntimeError as exc: return jsonify(error=str(exc)),503 response = jsonify(ok=True,source='journalctl -u alfred.service',items=records, warning='Lecture du journal Alfred uniquement ; les logs Hermes autonomes peuvent être absents.') response.headers['Cache-Control'] = 'no-store, private' return response app.register_blueprint(bp)