Improve the frontend

This commit is contained in:
Urtzi Alfaro
2025-10-21 19:50:07 +02:00
parent 05da20357d
commit 8d30172483
105 changed files with 14699 additions and 4630 deletions

View File

@@ -0,0 +1,7 @@
"""
Alert Processor Repositories
"""
from .analytics_repository import AlertAnalyticsRepository
__all__ = ['AlertAnalyticsRepository']

View File

@@ -0,0 +1,382 @@
"""
Alert Analytics Repository
Handles all database operations for alert analytics
"""
from typing import List, Dict, Any, Optional
from datetime import datetime, timedelta
from uuid import UUID
from sqlalchemy import select, func, and_, extract, case
from sqlalchemy.ext.asyncio import AsyncSession
import structlog
from app.models.alerts import Alert, AlertInteraction, AlertSeverity, AlertStatus
logger = structlog.get_logger()
class AlertAnalyticsRepository:
"""Repository for alert analytics operations"""
def __init__(self, session: AsyncSession):
self.session = session
async def create_interaction(
self,
tenant_id: UUID,
alert_id: UUID,
user_id: UUID,
interaction_type: str,
metadata: Optional[Dict[str, Any]] = None
) -> AlertInteraction:
"""Create a new alert interaction"""
# Get alert to calculate response time
alert_query = select(Alert).where(Alert.id == alert_id)
result = await self.session.execute(alert_query)
alert = result.scalar_one_or_none()
if not alert:
raise ValueError(f"Alert {alert_id} not found")
# Calculate response time
now = datetime.utcnow()
response_time_seconds = int((now - alert.created_at).total_seconds())
# Create interaction
interaction = AlertInteraction(
tenant_id=tenant_id,
alert_id=alert_id,
user_id=user_id,
interaction_type=interaction_type,
interacted_at=now,
response_time_seconds=response_time_seconds,
interaction_metadata=metadata or {}
)
self.session.add(interaction)
# Update alert status if applicable
if interaction_type == 'acknowledged' and alert.status == AlertStatus.ACTIVE:
alert.status = AlertStatus.ACKNOWLEDGED
elif interaction_type == 'resolved':
alert.status = AlertStatus.RESOLVED
alert.resolved_at = now
elif interaction_type == 'dismissed':
alert.status = AlertStatus.IGNORED
await self.session.commit()
await self.session.refresh(interaction)
logger.info(
"Alert interaction created",
alert_id=str(alert_id),
interaction_type=interaction_type,
response_time=response_time_seconds
)
return interaction
async def create_interactions_batch(
self,
tenant_id: UUID,
interactions: List[Dict[str, Any]]
) -> List[AlertInteraction]:
"""Create multiple interactions in batch"""
created_interactions = []
for interaction_data in interactions:
try:
interaction = await self.create_interaction(
tenant_id=tenant_id,
alert_id=UUID(interaction_data['alert_id']),
user_id=UUID(interaction_data['user_id']),
interaction_type=interaction_data['interaction_type'],
metadata=interaction_data.get('metadata')
)
created_interactions.append(interaction)
except Exception as e:
logger.error(
"Failed to create interaction in batch",
error=str(e),
alert_id=interaction_data.get('alert_id')
)
continue
return created_interactions
async def get_analytics_trends(
self,
tenant_id: UUID,
days: int = 7
) -> List[Dict[str, Any]]:
"""Get alert trends for the last N days"""
start_date = datetime.utcnow() - timedelta(days=days)
# Query alerts grouped by date and severity
query = (
select(
func.date(Alert.created_at).label('date'),
func.count(Alert.id).label('total_count'),
func.sum(
case((Alert.severity == AlertSeverity.URGENT, 1), else_=0)
).label('urgent_count'),
func.sum(
case((Alert.severity == AlertSeverity.HIGH, 1), else_=0)
).label('high_count'),
func.sum(
case((Alert.severity == AlertSeverity.MEDIUM, 1), else_=0)
).label('medium_count'),
func.sum(
case((Alert.severity == AlertSeverity.LOW, 1), else_=0)
).label('low_count')
)
.where(
and_(
Alert.tenant_id == tenant_id,
Alert.created_at >= start_date
)
)
.group_by(func.date(Alert.created_at))
.order_by(func.date(Alert.created_at))
)
result = await self.session.execute(query)
rows = result.all()
# Fill in missing dates with zeros
trends = []
current_date = start_date.date()
end_date = datetime.utcnow().date()
# Create a dict for quick lookup
data_by_date = {row.date: row for row in rows}
while current_date <= end_date:
date_str = current_date.isoformat()
row = data_by_date.get(current_date)
trends.append({
'date': date_str,
'count': int(row.total_count) if row else 0,
'urgentCount': int(row.urgent_count) if row else 0,
'highCount': int(row.high_count) if row else 0,
'mediumCount': int(row.medium_count) if row else 0,
'lowCount': int(row.low_count) if row else 0,
})
current_date += timedelta(days=1)
return trends
async def get_average_response_time(
self,
tenant_id: UUID,
days: int = 7
) -> int:
"""Get average response time in minutes for acknowledged alerts"""
start_date = datetime.utcnow() - timedelta(days=days)
query = (
select(func.avg(AlertInteraction.response_time_seconds))
.where(
and_(
AlertInteraction.tenant_id == tenant_id,
AlertInteraction.interaction_type == 'acknowledged',
AlertInteraction.interacted_at >= start_date,
AlertInteraction.response_time_seconds < 86400 # Less than 24 hours
)
)
)
result = await self.session.execute(query)
avg_seconds = result.scalar_one_or_none()
if avg_seconds is None:
return 0
# Convert to minutes
return round(avg_seconds / 60)
async def get_top_categories(
self,
tenant_id: UUID,
days: int = 7,
limit: int = 3
) -> List[Dict[str, Any]]:
"""Get top alert categories"""
start_date = datetime.utcnow() - timedelta(days=days)
query = (
select(
Alert.alert_type,
func.count(Alert.id).label('count')
)
.where(
and_(
Alert.tenant_id == tenant_id,
Alert.created_at >= start_date
)
)
.group_by(Alert.alert_type)
.order_by(func.count(Alert.id).desc())
.limit(limit)
)
result = await self.session.execute(query)
rows = result.all()
# Calculate total for percentages
total_query = (
select(func.count(Alert.id))
.where(
and_(
Alert.tenant_id == tenant_id,
Alert.created_at >= start_date
)
)
)
total_result = await self.session.execute(total_query)
total = total_result.scalar_one() or 1
categories = []
for row in rows:
percentage = round((row.count / total) * 100) if total > 0 else 0
categories.append({
'category': row.alert_type,
'count': row.count,
'percentage': percentage
})
return categories
async def get_resolution_stats(
self,
tenant_id: UUID,
days: int = 7
) -> Dict[str, Any]:
"""Get resolution statistics"""
start_date = datetime.utcnow() - timedelta(days=days)
# Total alerts
total_query = (
select(func.count(Alert.id))
.where(
and_(
Alert.tenant_id == tenant_id,
Alert.created_at >= start_date
)
)
)
total_result = await self.session.execute(total_query)
total_alerts = total_result.scalar_one() or 0
# Resolved alerts
resolved_query = (
select(func.count(Alert.id))
.where(
and_(
Alert.tenant_id == tenant_id,
Alert.created_at >= start_date,
Alert.status == AlertStatus.RESOLVED
)
)
)
resolved_result = await self.session.execute(resolved_query)
resolved_alerts = resolved_result.scalar_one() or 0
# Active alerts
active_query = (
select(func.count(Alert.id))
.where(
and_(
Alert.tenant_id == tenant_id,
Alert.created_at >= start_date,
Alert.status == AlertStatus.ACTIVE
)
)
)
active_result = await self.session.execute(active_query)
active_alerts = active_result.scalar_one() or 0
resolution_rate = round((resolved_alerts / total_alerts) * 100) if total_alerts > 0 else 0
return {
'totalAlerts': total_alerts,
'resolvedAlerts': resolved_alerts,
'activeAlerts': active_alerts,
'resolutionRate': resolution_rate
}
async def get_busiest_day(
self,
tenant_id: UUID,
days: int = 7
) -> str:
"""Get busiest day of week"""
start_date = datetime.utcnow() - timedelta(days=days)
query = (
select(
extract('dow', Alert.created_at).label('day_of_week'),
func.count(Alert.id).label('count')
)
.where(
and_(
Alert.tenant_id == tenant_id,
Alert.created_at >= start_date
)
)
.group_by(extract('dow', Alert.created_at))
.order_by(func.count(Alert.id).desc())
.limit(1)
)
result = await self.session.execute(query)
row = result.first()
if not row:
return 'N/A'
day_names = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']
return day_names[int(row.day_of_week)]
async def get_predicted_daily_average(
self,
tenant_id: UUID,
days: int = 7
) -> int:
"""Calculate predicted daily average based on trends"""
trends = await self.get_analytics_trends(tenant_id, days)
if not trends:
return 0
total_count = sum(trend['count'] for trend in trends)
return round(total_count / len(trends))
async def get_full_analytics(
self,
tenant_id: UUID,
days: int = 7
) -> Dict[str, Any]:
"""Get complete analytics data"""
trends = await self.get_analytics_trends(tenant_id, days)
avg_response_time = await self.get_average_response_time(tenant_id, days)
top_categories = await self.get_top_categories(tenant_id, days)
resolution_stats = await self.get_resolution_stats(tenant_id, days)
busiest_day = await self.get_busiest_day(tenant_id, days)
predicted_avg = await self.get_predicted_daily_average(tenant_id, days)
return {
'trends': trends,
'averageResponseTime': avg_response_time,
'topCategories': top_categories,
'totalAlerts': resolution_stats['totalAlerts'],
'resolvedAlerts': resolution_stats['resolvedAlerts'],
'activeAlerts': resolution_stats['activeAlerts'],
'resolutionRate': resolution_stats['resolutionRate'],
'predictedDailyAverage': predicted_avg,
'busiestDay': busiest_day
}