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:
15
core/analytics/dashboard/__init__.py
Normal file
15
core/analytics/dashboard/__init__.py
Normal file
@@ -0,0 +1,15 @@
|
||||
"""Analytics dashboard module."""
|
||||
|
||||
from .dashboard_manager import (
|
||||
DashboardManager,
|
||||
Dashboard,
|
||||
DashboardWidget,
|
||||
DashboardTemplate
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
'DashboardManager',
|
||||
'Dashboard',
|
||||
'DashboardWidget',
|
||||
'DashboardTemplate'
|
||||
]
|
||||
468
core/analytics/dashboard/dashboard_manager.py
Normal file
468
core/analytics/dashboard/dashboard_manager.py
Normal file
@@ -0,0 +1,468 @@
|
||||
"""Dashboard management for analytics."""
|
||||
|
||||
import logging
|
||||
import json
|
||||
import uuid
|
||||
from typing import Dict, List, Optional, Any
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class DashboardWidget:
|
||||
"""Dashboard widget configuration."""
|
||||
widget_id: str
|
||||
widget_type: str # chart, table, metric, insight
|
||||
title: str
|
||||
config: Dict[str, Any]
|
||||
position: Dict[str, int] # x, y, width, height
|
||||
|
||||
def to_dict(self) -> Dict:
|
||||
"""Convert to dictionary."""
|
||||
return {
|
||||
'widget_id': self.widget_id,
|
||||
'widget_type': self.widget_type,
|
||||
'title': self.title,
|
||||
'config': self.config,
|
||||
'position': self.position
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict) -> 'DashboardWidget':
|
||||
"""Create from dictionary."""
|
||||
return cls(**data)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Dashboard:
|
||||
"""Dashboard configuration."""
|
||||
dashboard_id: str
|
||||
name: str
|
||||
description: str
|
||||
owner: str
|
||||
widgets: List[DashboardWidget] = field(default_factory=list)
|
||||
layout: str = 'grid' # grid, flex
|
||||
refresh_interval: int = 30 # seconds
|
||||
is_public: bool = False
|
||||
shared_with: List[str] = field(default_factory=list)
|
||||
created_at: datetime = field(default_factory=datetime.now)
|
||||
updated_at: datetime = field(default_factory=datetime.now)
|
||||
|
||||
def to_dict(self) -> Dict:
|
||||
"""Convert to dictionary."""
|
||||
return {
|
||||
'dashboard_id': self.dashboard_id,
|
||||
'name': self.name,
|
||||
'description': self.description,
|
||||
'owner': self.owner,
|
||||
'widgets': [w.to_dict() for w in self.widgets],
|
||||
'layout': self.layout,
|
||||
'refresh_interval': self.refresh_interval,
|
||||
'is_public': self.is_public,
|
||||
'shared_with': self.shared_with,
|
||||
'created_at': self.created_at.isoformat(),
|
||||
'updated_at': self.updated_at.isoformat()
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict) -> 'Dashboard':
|
||||
"""Create from dictionary."""
|
||||
data = data.copy()
|
||||
data['widgets'] = [DashboardWidget.from_dict(w) for w in data.get('widgets', [])]
|
||||
data['created_at'] = datetime.fromisoformat(data['created_at'])
|
||||
data['updated_at'] = datetime.fromisoformat(data['updated_at'])
|
||||
return cls(**data)
|
||||
|
||||
|
||||
@dataclass
|
||||
class DashboardTemplate:
|
||||
"""Pre-built dashboard template."""
|
||||
template_id: str
|
||||
name: str
|
||||
description: str
|
||||
category: str
|
||||
widgets: List[DashboardWidget]
|
||||
|
||||
def to_dict(self) -> Dict:
|
||||
"""Convert to dictionary."""
|
||||
return {
|
||||
'template_id': self.template_id,
|
||||
'name': self.name,
|
||||
'description': self.description,
|
||||
'category': self.category,
|
||||
'widgets': [w.to_dict() for w in self.widgets]
|
||||
}
|
||||
|
||||
|
||||
class DashboardManager:
|
||||
"""Manage analytics dashboards."""
|
||||
|
||||
def __init__(self, storage_dir: str = "data/analytics/dashboards"):
|
||||
"""
|
||||
Initialize dashboard manager.
|
||||
|
||||
Args:
|
||||
storage_dir: Directory for dashboard storage
|
||||
"""
|
||||
self.storage_dir = Path(storage_dir)
|
||||
self.storage_dir.mkdir(parents=True, exist_ok=True)
|
||||
self.dashboards: Dict[str, Dashboard] = {}
|
||||
self.templates: Dict[str, DashboardTemplate] = {}
|
||||
self._load_dashboards()
|
||||
self._init_templates()
|
||||
logger.info("DashboardManager initialized")
|
||||
|
||||
def create_dashboard(
|
||||
self,
|
||||
name: str,
|
||||
description: str,
|
||||
owner: str,
|
||||
template_id: Optional[str] = None
|
||||
) -> Dashboard:
|
||||
"""
|
||||
Create a new dashboard.
|
||||
|
||||
Args:
|
||||
name: Dashboard name
|
||||
description: Dashboard description
|
||||
owner: Owner username
|
||||
template_id: Optional template to use
|
||||
|
||||
Returns:
|
||||
Created dashboard
|
||||
"""
|
||||
dashboard_id = str(uuid.uuid4())
|
||||
|
||||
# Create from template if specified
|
||||
if template_id and template_id in self.templates:
|
||||
template = self.templates[template_id]
|
||||
widgets = [
|
||||
DashboardWidget(
|
||||
widget_id=str(uuid.uuid4()),
|
||||
widget_type=w.widget_type,
|
||||
title=w.title,
|
||||
config=w.config.copy(),
|
||||
position=w.position.copy()
|
||||
)
|
||||
for w in template.widgets
|
||||
]
|
||||
else:
|
||||
widgets = []
|
||||
|
||||
dashboard = Dashboard(
|
||||
dashboard_id=dashboard_id,
|
||||
name=name,
|
||||
description=description,
|
||||
owner=owner,
|
||||
widgets=widgets
|
||||
)
|
||||
|
||||
self.dashboards[dashboard_id] = dashboard
|
||||
self._save_dashboard(dashboard)
|
||||
|
||||
logger.info(f"Created dashboard: {dashboard_id}")
|
||||
return dashboard
|
||||
|
||||
def get_dashboard(self, dashboard_id: str) -> Optional[Dashboard]:
|
||||
"""Get dashboard by ID."""
|
||||
return self.dashboards.get(dashboard_id)
|
||||
|
||||
def list_dashboards(
|
||||
self,
|
||||
owner: Optional[str] = None,
|
||||
include_shared: bool = True
|
||||
) -> List[Dashboard]:
|
||||
"""
|
||||
List dashboards.
|
||||
|
||||
Args:
|
||||
owner: Filter by owner (None = all)
|
||||
include_shared: Include dashboards shared with owner
|
||||
|
||||
Returns:
|
||||
List of dashboards
|
||||
"""
|
||||
dashboards = list(self.dashboards.values())
|
||||
|
||||
if owner:
|
||||
dashboards = [
|
||||
d for d in dashboards
|
||||
if d.owner == owner or
|
||||
(include_shared and (d.is_public or owner in d.shared_with))
|
||||
]
|
||||
|
||||
return dashboards
|
||||
|
||||
def update_dashboard(
|
||||
self,
|
||||
dashboard_id: str,
|
||||
updates: Dict[str, Any]
|
||||
) -> Optional[Dashboard]:
|
||||
"""
|
||||
Update dashboard configuration.
|
||||
|
||||
Args:
|
||||
dashboard_id: Dashboard identifier
|
||||
updates: Dictionary of updates
|
||||
|
||||
Returns:
|
||||
Updated dashboard or None
|
||||
"""
|
||||
dashboard = self.dashboards.get(dashboard_id)
|
||||
if not dashboard:
|
||||
return None
|
||||
|
||||
# Apply updates
|
||||
for key, value in updates.items():
|
||||
if hasattr(dashboard, key):
|
||||
setattr(dashboard, key, value)
|
||||
|
||||
dashboard.updated_at = datetime.now()
|
||||
self._save_dashboard(dashboard)
|
||||
|
||||
logger.info(f"Updated dashboard: {dashboard_id}")
|
||||
return dashboard
|
||||
|
||||
def delete_dashboard(self, dashboard_id: str) -> bool:
|
||||
"""
|
||||
Delete a dashboard.
|
||||
|
||||
Args:
|
||||
dashboard_id: Dashboard identifier
|
||||
|
||||
Returns:
|
||||
True if deleted, False if not found
|
||||
"""
|
||||
if dashboard_id not in self.dashboards:
|
||||
return False
|
||||
|
||||
del self.dashboards[dashboard_id]
|
||||
|
||||
# Delete file
|
||||
filepath = self.storage_dir / f"{dashboard_id}.json"
|
||||
if filepath.exists():
|
||||
filepath.unlink()
|
||||
|
||||
logger.info(f"Deleted dashboard: {dashboard_id}")
|
||||
return True
|
||||
|
||||
def add_widget(
|
||||
self,
|
||||
dashboard_id: str,
|
||||
widget_type: str,
|
||||
title: str,
|
||||
config: Dict[str, Any],
|
||||
position: Dict[str, int]
|
||||
) -> Optional[DashboardWidget]:
|
||||
"""
|
||||
Add widget to dashboard.
|
||||
|
||||
Args:
|
||||
dashboard_id: Dashboard identifier
|
||||
widget_type: Widget type
|
||||
title: Widget title
|
||||
config: Widget configuration
|
||||
position: Widget position
|
||||
|
||||
Returns:
|
||||
Created widget or None
|
||||
"""
|
||||
dashboard = self.dashboards.get(dashboard_id)
|
||||
if not dashboard:
|
||||
return None
|
||||
|
||||
widget = DashboardWidget(
|
||||
widget_id=str(uuid.uuid4()),
|
||||
widget_type=widget_type,
|
||||
title=title,
|
||||
config=config,
|
||||
position=position
|
||||
)
|
||||
|
||||
dashboard.widgets.append(widget)
|
||||
dashboard.updated_at = datetime.now()
|
||||
self._save_dashboard(dashboard)
|
||||
|
||||
logger.info(f"Added widget to dashboard {dashboard_id}")
|
||||
return widget
|
||||
|
||||
def remove_widget(
|
||||
self,
|
||||
dashboard_id: str,
|
||||
widget_id: str
|
||||
) -> bool:
|
||||
"""
|
||||
Remove widget from dashboard.
|
||||
|
||||
Args:
|
||||
dashboard_id: Dashboard identifier
|
||||
widget_id: Widget identifier
|
||||
|
||||
Returns:
|
||||
True if removed, False if not found
|
||||
"""
|
||||
dashboard = self.dashboards.get(dashboard_id)
|
||||
if not dashboard:
|
||||
return False
|
||||
|
||||
dashboard.widgets = [w for w in dashboard.widgets if w.widget_id != widget_id]
|
||||
dashboard.updated_at = datetime.now()
|
||||
self._save_dashboard(dashboard)
|
||||
|
||||
logger.info(f"Removed widget from dashboard {dashboard_id}")
|
||||
return True
|
||||
|
||||
def share_dashboard(
|
||||
self,
|
||||
dashboard_id: str,
|
||||
username: str
|
||||
) -> bool:
|
||||
"""
|
||||
Share dashboard with a user.
|
||||
|
||||
Args:
|
||||
dashboard_id: Dashboard identifier
|
||||
username: Username to share with
|
||||
|
||||
Returns:
|
||||
True if shared, False if not found
|
||||
"""
|
||||
dashboard = self.dashboards.get(dashboard_id)
|
||||
if not dashboard:
|
||||
return False
|
||||
|
||||
if username not in dashboard.shared_with:
|
||||
dashboard.shared_with.append(username)
|
||||
dashboard.updated_at = datetime.now()
|
||||
self._save_dashboard(dashboard)
|
||||
|
||||
logger.info(f"Shared dashboard {dashboard_id} with {username}")
|
||||
return True
|
||||
|
||||
def make_public(
|
||||
self,
|
||||
dashboard_id: str,
|
||||
is_public: bool = True
|
||||
) -> bool:
|
||||
"""
|
||||
Make dashboard public or private.
|
||||
|
||||
Args:
|
||||
dashboard_id: Dashboard identifier
|
||||
is_public: Whether dashboard should be public
|
||||
|
||||
Returns:
|
||||
True if updated, False if not found
|
||||
"""
|
||||
dashboard = self.dashboards.get(dashboard_id)
|
||||
if not dashboard:
|
||||
return False
|
||||
|
||||
dashboard.is_public = is_public
|
||||
dashboard.updated_at = datetime.now()
|
||||
self._save_dashboard(dashboard)
|
||||
|
||||
logger.info(f"Dashboard {dashboard_id} public: {is_public}")
|
||||
return True
|
||||
|
||||
def get_templates(self) -> List[DashboardTemplate]:
|
||||
"""Get all dashboard templates."""
|
||||
return list(self.templates.values())
|
||||
|
||||
def _load_dashboards(self) -> None:
|
||||
"""Load dashboards from storage."""
|
||||
for filepath in self.storage_dir.glob('*.json'):
|
||||
try:
|
||||
with open(filepath, 'r') as f:
|
||||
data = json.load(f)
|
||||
dashboard = Dashboard.from_dict(data)
|
||||
self.dashboards[dashboard.dashboard_id] = dashboard
|
||||
except Exception as e:
|
||||
logger.error(f"Error loading dashboard {filepath}: {e}")
|
||||
|
||||
logger.info(f"Loaded {len(self.dashboards)} dashboards")
|
||||
|
||||
def _save_dashboard(self, dashboard: Dashboard) -> None:
|
||||
"""Save dashboard to storage."""
|
||||
filepath = self.storage_dir / f"{dashboard.dashboard_id}.json"
|
||||
with open(filepath, 'w') as f:
|
||||
json.dump(dashboard.to_dict(), f, indent=2)
|
||||
|
||||
def _init_templates(self) -> None:
|
||||
"""Initialize default dashboard templates."""
|
||||
# Performance Overview Template
|
||||
self.templates['performance'] = DashboardTemplate(
|
||||
template_id='performance',
|
||||
name='Performance Overview',
|
||||
description='Overview of workflow performance metrics',
|
||||
category='performance',
|
||||
widgets=[
|
||||
DashboardWidget(
|
||||
widget_id='perf_chart',
|
||||
widget_type='chart',
|
||||
title='Execution Duration Trend',
|
||||
config={
|
||||
'chart_type': 'line',
|
||||
'metric': 'duration',
|
||||
'time_range': '7d'
|
||||
},
|
||||
position={'x': 0, 'y': 0, 'width': 6, 'height': 4}
|
||||
),
|
||||
DashboardWidget(
|
||||
widget_id='success_rate',
|
||||
widget_type='metric',
|
||||
title='Success Rate',
|
||||
config={
|
||||
'metric': 'success_rate',
|
||||
'format': 'percentage'
|
||||
},
|
||||
position={'x': 6, 'y': 0, 'width': 3, 'height': 2}
|
||||
),
|
||||
DashboardWidget(
|
||||
widget_id='bottlenecks',
|
||||
widget_type='table',
|
||||
title='Top Bottlenecks',
|
||||
config={
|
||||
'metric': 'bottlenecks',
|
||||
'limit': 10
|
||||
},
|
||||
position={'x': 0, 'y': 4, 'width': 9, 'height': 4}
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
# Anomaly Detection Template
|
||||
self.templates['anomalies'] = DashboardTemplate(
|
||||
template_id='anomalies',
|
||||
name='Anomaly Detection',
|
||||
description='Real-time anomaly detection and alerts',
|
||||
category='monitoring',
|
||||
widgets=[
|
||||
DashboardWidget(
|
||||
widget_id='anomaly_chart',
|
||||
widget_type='chart',
|
||||
title='Anomalies Over Time',
|
||||
config={
|
||||
'chart_type': 'scatter',
|
||||
'metric': 'anomalies',
|
||||
'time_range': '24h'
|
||||
},
|
||||
position={'x': 0, 'y': 0, 'width': 8, 'height': 4}
|
||||
),
|
||||
DashboardWidget(
|
||||
widget_id='anomaly_list',
|
||||
widget_type='table',
|
||||
title='Recent Anomalies',
|
||||
config={
|
||||
'metric': 'anomalies',
|
||||
'limit': 20
|
||||
},
|
||||
position={'x': 0, 'y': 4, 'width': 12, 'height': 4}
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
logger.info(f"Initialized {len(self.templates)} dashboard templates")
|
||||
Reference in New Issue
Block a user