From e39668e480214420aed1cd765baa2414dbdf35f5 Mon Sep 17 00:00:00 2001 From: andersonid Date: Thu, 2 Oct 2025 08:17:22 -0300 Subject: [PATCH] Implement Smart Recommendations Engine with dashboard and modals --- app/api/routes.py | 62 +++++++++ app/static/index.html | 288 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 348 insertions(+), 2 deletions(-) diff --git a/app/api/routes.py b/app/api/routes.py index 74db50c..b8e501a 100644 --- a/app/api/routes.py +++ b/app/api/routes.py @@ -15,6 +15,7 @@ from app.models.resource_models import ( from app.services.validation_service import ValidationService from app.services.report_service import ReportService from app.services.historical_analysis import HistoricalAnalysisService +from app.services.smart_recommendations import SmartRecommendationsService from app.core.prometheus_client import PrometheusClient logger = logging.getLogger(__name__) @@ -25,6 +26,7 @@ api_router = APIRouter() # Initialize services validation_service = ValidationService() report_service = ReportService() +smart_recommendations_service = SmartRecommendationsService() def get_k8s_client(request: Request): """Dependency to get Kubernetes client""" @@ -1137,6 +1139,66 @@ async def get_pod_health_scores( logger.error(f"Error getting pod health scores: {e}") raise HTTPException(status_code=500, detail=str(e)) +@api_router.get("/smart-recommendations") +async def get_smart_recommendations( + namespace: Optional[str] = None, + priority: Optional[str] = None, + k8s_client=Depends(get_k8s_client) +): + """Get smart recommendations for resource optimization""" + try: + # Get all pods + pods = await k8s_client.get_all_pods() + + if namespace: + pods = [pod for pod in pods if pod.namespace == namespace] + + # Categorize workloads + categories = await smart_recommendations_service.categorize_workloads(pods) + + # Generate smart recommendations + recommendations = await smart_recommendations_service.generate_smart_recommendations(pods, categories) + + # Filter by priority if specified + if priority: + recommendations = [r for r in recommendations if r.priority == priority] + + # Group by namespace + recommendations_by_namespace = {} + for rec in recommendations: + if rec.namespace not in recommendations_by_namespace: + recommendations_by_namespace[rec.namespace] = [] + recommendations_by_namespace[rec.namespace].append(rec) + + # Calculate summary + summary = { + "total_recommendations": len(recommendations), + "by_priority": { + "critical": len([r for r in recommendations if r.priority == "critical"]), + "high": len([r for r in recommendations if r.priority == "high"]), + "medium": len([r for r in recommendations if r.priority == "medium"]), + "low": len([r for r in recommendations if r.priority == "low"]) + }, + "by_type": { + "resource_config": len([r for r in recommendations if r.recommendation_type == "resource_config"]), + "vpa_activation": len([r for r in recommendations if r.recommendation_type == "vpa_activation"]), + "ratio_adjustment": len([r for r in recommendations if r.recommendation_type == "ratio_adjustment"]) + }, + "namespaces_affected": len(recommendations_by_namespace) + } + + return { + "recommendations": recommendations, + "categories": categories, + "grouped_by_namespace": recommendations_by_namespace, + "summary": summary, + "timestamp": datetime.now().isoformat() + } + + except Exception as e: + logger.error(f"Error getting smart recommendations: {e}") + raise HTTPException(status_code=500, detail=str(e)) + @api_router.get("/health") async def health_check(): """API health check""" diff --git a/app/static/index.html b/app/static/index.html index 4be5a1d..f32e132 100644 --- a/app/static/index.html +++ b/app/static/index.html @@ -1450,7 +1450,8 @@ // Load smart recommendations async function loadSmartRecommendations() { try { - const response = await fetch('/api/recommendations'); + showLoading(); + const response = await fetch('/api/v1/smart-recommendations'); const data = await response.json(); updateSmartRecommendations(data); } catch (error) { @@ -1463,7 +1464,155 @@ // Update smart recommendations function updateSmartRecommendations(data) { const content = document.getElementById('smartRecommendationsContent'); - content.innerHTML = '

Smart recommendations feature coming soon...

'; + + if (!data || !data.recommendations) { + content.innerHTML = '

No recommendations data available

'; + return; + } + + const recommendations = data.recommendations; + const summary = data.summary; + + let html = ` + +
+
+

${summary.total_recommendations}

+

Total Recommendations

+
+
+

${summary.by_priority.critical}

+

Critical Issues

+
+
+

${summary.by_priority.high}

+

High Priority

+
+
+

${summary.namespaces_affected}

+

Namespaces Affected

+
+
+ + +
+
+ + +
+
+ + +
+ +
+ + +
+ `; + + if (recommendations.length === 0) { + html += ` +
+

🎉 No Recommendations Needed

+

All workloads are properly configured with optimal resource settings.

+
+ `; + } else { + recommendations.forEach((rec, index) => { + const priorityColor = { + 'critical': '#e74c3c', + 'high': '#f39c12', + 'medium': '#3498db', + 'low': '#95a5a6' + }[rec.priority] || '#95a5a6'; + + const priorityIcon = { + 'critical': '🔴', + 'high': '🟠', + 'medium': '🔵', + 'low': '⚪' + }[rec.priority] || '⚪'; + + html += ` +
+
+
+

${priorityIcon} ${rec.title}

+

+ Workload: ${rec.workload_name} | + Namespace: ${rec.namespace} | + Type: ${rec.recommendation_type.replace('_', ' ').toUpperCase()} +

+
+
+ + ${rec.priority.toUpperCase()} + + ${rec.confidence_level ? `
Confidence: ${Math.round(rec.confidence_level * 100)}%
` : ''} +
+
+
+

${rec.description}

+ + ${rec.implementation_steps ? ` +
+

Implementation Steps:

+
    + ${rec.implementation_steps.map(step => `
  1. ${step}
  2. `).join('')} +
+
+ ` : ''} + + ${rec.kubectl_commands && rec.kubectl_commands.length > 0 ? ` +
+

Kubectl Commands:

+
+ ${rec.kubectl_commands.map(cmd => `
${cmd}
`).join('')} +
+
+ ` : ''} + +
+ + ${rec.vpa_yaml ? ` + + ` : ''} +
+
+
+ `; + }); + } + + html += ` +
+ `; + + content.innerHTML = html; + + // Store recommendations data for modal access + window.currentRecommendations = recommendations; } // Load VPA management @@ -1837,6 +1986,141 @@ } } + // Smart Recommendations Functions + function filterRecommendations() { + const priorityFilter = document.getElementById('priorityFilter').value; + const typeFilter = document.getElementById('typeFilter').value; + const cards = document.querySelectorAll('.recommendation-card'); + + cards.forEach(card => { + const priority = card.getAttribute('data-priority'); + const type = card.getAttribute('data-type'); + + let show = true; + + if (priorityFilter && priority !== priorityFilter) { + show = false; + } + + if (typeFilter && type !== typeFilter) { + show = false; + } + + card.style.display = show ? 'block' : 'none'; + }); + } + + function refreshRecommendations() { + loadSmartRecommendations(); + } + + function showRecommendationDetails(index) { + const rec = window.currentRecommendations[index]; + if (!rec) return; + + let modalContent = ` + + + `; + + showModal(modalContent); + } + + function showVPAYaml(index) { + const rec = window.currentRecommendations[index]; + if (!rec || !rec.vpa_yaml) return; + + let modalContent = ` + + + `; + + showModal(modalContent); + } + + function copyRecommendationCommands(index) { + const rec = window.currentRecommendations[index]; + if (!rec || !rec.kubectl_commands) return; + + const commands = rec.kubectl_commands.join('\n'); + copyToClipboard(commands); + } + + function copyVPAYaml(index) { + const rec = window.currentRecommendations[index]; + if (!rec || !rec.vpa_yaml) return; + + copyToClipboard(rec.vpa_yaml); + } + + function getPriorityColor(priority) { + const colors = { + 'critical': '#e74c3c', + 'high': '#f39c12', + 'medium': '#3498db', + 'low': '#95a5a6' + }; + return colors[priority] || '#95a5a6'; + } + // Utility functions function showLoading() { document.getElementById('problemTableBody').innerHTML =