v1.0 - Version stable: multi-PC, détection UI-DETR-1, 3 modes exécution

- Frontend v4 accessible sur réseau local (192.168.1.40)
- Ports ouverts: 3002 (frontend), 5001 (backend), 5004 (dashboard)
- Ollama GPU fonctionnel
- Self-healing interactif
- Dashboard confiance

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Dom
2026-01-29 11:23:51 +01:00
parent 21bfa3b337
commit a27b74cf22
1595 changed files with 412691 additions and 400 deletions

View File

@@ -0,0 +1,5 @@
"""Real-time analytics components."""
from .realtime_analytics import RealtimeAnalytics
__all__ = ['RealtimeAnalytics']

View File

@@ -0,0 +1,283 @@
"""Real-time analytics for active workflows."""
import logging
import threading
from typing import Dict, Any, Optional, List, Callable
from datetime import datetime
from dataclasses import dataclass, field
from ..collection.metrics_collector import MetricsCollector, ExecutionMetrics
logger = logging.getLogger(__name__)
@dataclass
class LiveExecution:
"""Live execution tracking."""
execution_id: str
workflow_id: str
started_at: datetime
current_step: int = 0
total_steps: int = 0
steps_completed: int = 0
steps_failed: int = 0
current_node_id: Optional[str] = None
last_update: datetime = field(default_factory=datetime.now)
@property
def progress_percent(self) -> float:
"""Calculate progress percentage."""
if self.total_steps == 0:
return 0.0
return (self.steps_completed / self.total_steps) * 100
@property
def estimated_completion(self) -> Optional[datetime]:
"""Estimate completion time."""
if self.steps_completed == 0 or self.total_steps == 0:
return None
elapsed = (datetime.now() - self.started_at).total_seconds()
avg_time_per_step = elapsed / self.steps_completed
remaining_steps = self.total_steps - self.steps_completed
estimated_remaining = avg_time_per_step * remaining_steps
from datetime import timedelta
return datetime.now() + timedelta(seconds=estimated_remaining)
def to_dict(self) -> Dict[str, Any]:
"""Convert to dictionary."""
return {
'execution_id': self.execution_id,
'workflow_id': self.workflow_id,
'started_at': self.started_at.isoformat(),
'current_step': self.current_step,
'total_steps': self.total_steps,
'steps_completed': self.steps_completed,
'steps_failed': self.steps_failed,
'current_node_id': self.current_node_id,
'progress_percent': self.progress_percent,
'estimated_completion': self.estimated_completion.isoformat() if self.estimated_completion else None,
'last_update': self.last_update.isoformat()
}
class RealtimeAnalytics:
"""Real-time analytics for active workflows."""
def __init__(self, metrics_collector: Optional[MetricsCollector] = None):
"""
Initialize real-time analytics.
Args:
metrics_collector: Metrics collector instance
"""
self.collector = metrics_collector
self.active_executions: Dict[str, LiveExecution] = {}
self.subscribers: Dict[str, List[Callable]] = {}
self._lock = threading.Lock()
logger.info("RealtimeAnalytics initialized")
def track_execution(
self,
execution_id: str,
workflow_id: str,
total_steps: int = 0
) -> None:
"""
Start tracking an execution in real-time.
Args:
execution_id: Execution identifier
workflow_id: Workflow identifier
total_steps: Total number of steps
"""
with self._lock:
self.active_executions[execution_id] = LiveExecution(
execution_id=execution_id,
workflow_id=workflow_id,
started_at=datetime.now(),
total_steps=total_steps
)
# Notify subscribers
self._notify_subscribers(execution_id, 'started')
logger.info(f"Tracking execution: {execution_id}")
def update_progress(
self,
execution_id: str,
current_step: int,
total_steps: Optional[int] = None,
current_node_id: Optional[str] = None
) -> None:
"""
Update execution progress.
Args:
execution_id: Execution identifier
current_step: Current step number
total_steps: Total steps (updates if provided)
current_node_id: Current node ID
"""
with self._lock:
if execution_id not in self.active_executions:
logger.warning(f"Execution not tracked: {execution_id}")
return
execution = self.active_executions[execution_id]
execution.current_step = current_step
if total_steps is not None:
execution.total_steps = total_steps
if current_node_id is not None:
execution.current_node_id = current_node_id
execution.last_update = datetime.now()
# Notify subscribers
self._notify_subscribers(execution_id, 'progress')
def record_step_complete(
self,
execution_id: str,
success: bool
) -> None:
"""
Record step completion.
Args:
execution_id: Execution identifier
success: Whether step succeeded
"""
with self._lock:
if execution_id not in self.active_executions:
return
execution = self.active_executions[execution_id]
if success:
execution.steps_completed += 1
else:
execution.steps_failed += 1
execution.last_update = datetime.now()
# Notify subscribers
self._notify_subscribers(execution_id, 'step_complete')
def complete_execution(
self,
execution_id: str,
status: str
) -> None:
"""
Mark execution as complete.
Args:
execution_id: Execution identifier
status: Final status
"""
with self._lock:
if execution_id in self.active_executions:
del self.active_executions[execution_id]
# Notify subscribers
self._notify_subscribers(execution_id, 'completed', {'status': status})
logger.info(f"Execution completed: {execution_id} ({status})")
def get_live_metrics(self, execution_id: str) -> Optional[Dict[str, Any]]:
"""
Get live metrics for an execution.
Args:
execution_id: Execution identifier
Returns:
Live metrics dictionary or None
"""
with self._lock:
execution = self.active_executions.get(execution_id)
if not execution:
return None
return execution.to_dict()
def get_all_active(self) -> List[Dict[str, Any]]:
"""
Get all active executions.
Returns:
List of active execution metrics
"""
with self._lock:
return [e.to_dict() for e in self.active_executions.values()]
def subscribe(
self,
execution_id: str,
callback: Callable[[str, Dict], None]
) -> None:
"""
Subscribe to real-time updates for an execution.
Args:
execution_id: Execution identifier
callback: Callback function (event_type, data)
"""
with self._lock:
if execution_id not in self.subscribers:
self.subscribers[execution_id] = []
self.subscribers[execution_id].append(callback)
logger.debug(f"Subscriber added for {execution_id}")
def unsubscribe(
self,
execution_id: str,
callback: Optional[Callable] = None
) -> None:
"""
Unsubscribe from updates.
Args:
execution_id: Execution identifier
callback: Specific callback to remove (None = remove all)
"""
with self._lock:
if execution_id not in self.subscribers:
return
if callback is None:
del self.subscribers[execution_id]
else:
self.subscribers[execution_id] = [
cb for cb in self.subscribers[execution_id] if cb != callback
]
def _notify_subscribers(
self,
execution_id: str,
event_type: str,
data: Optional[Dict] = None
) -> None:
"""Notify subscribers of an event."""
with self._lock:
callbacks = self.subscribers.get(execution_id, []).copy()
if not callbacks:
return
# Get current metrics
metrics = self.get_live_metrics(execution_id)
event_data = {
'event_type': event_type,
'execution_id': execution_id,
'metrics': metrics,
**(data or {})
}
# Call subscribers (outside lock)
for callback in callbacks:
try:
callback(event_type, event_data)
except Exception as e:
logger.error(f"Subscriber callback error: {e}")