v2 - new interface, many new page for fine tuning
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
"""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)
|
||||
Reference in New Issue
Block a user