diff --git a/README.md b/README.md index 5f6bff0..dc176cf 100644 --- a/README.md +++ b/README.md @@ -288,6 +288,30 @@ curl http://localhost:8080/health --- +### **Phase 0: UI/UX Simplification (IMMEDIATE - 1 week)** + +#### 0.1 Interface Simplification +- [ ] **Agrupar validações similares** em um único card +- [ ] **Mostrar apenas o essencial** na visão principal +- [ ] **Detalhes técnicos** em modal ou seção expandível +- [ ] **Código de cores**: 🔴 Crítico, 🟡 Aviso, 🔵 Info +- [ ] **Ícones específicos**: ⚡ CPU, 💾 Memory, 📊 Ratio +- [ ] **Cards colapsáveis** para reduzir poluição visual + +#### 0.2 Melhorar Hierarquia Visual +- [ ] **Modo "Simples"** vs "Técnico" +- [ ] **Ações diretas**: "Fix CPU Ratio" button +- [ ] **Progress bars** para mostrar saúde do namespace +- [ ] **Timeline** de melhorias implementadas +- [ ] **Comparação** entre namespaces + +#### 0.3 Funcionalidades Avançadas +- [ ] **Botão "Apply Fix"** para ajustes automáticos +- [ ] **Histórico de melhorias** implementadas +- [ ] **Comparação entre namespaces** + +--- + ### **Phase 1: Enhanced Validation & Categorization (IMMEDIATE - 1-2 weeks)** #### 1.1 Smart Resource Detection diff --git a/app/api/routes.py b/app/api/routes.py index 544f316..350aec9 100644 --- a/app/api/routes.py +++ b/app/api/routes.py @@ -9,7 +9,8 @@ from fastapi.responses import FileResponse from app.models.resource_models import ( ClusterReport, NamespaceReport, ExportRequest, - ApplyRecommendationRequest, WorkloadCategory, SmartRecommendation + ApplyRecommendationRequest, WorkloadCategory, SmartRecommendation, + PodHealthScore, SimplifiedValidation ) from app.services.validation_service import ValidationService from app.services.report_service import ReportService @@ -802,6 +803,48 @@ async def get_resource_quotas( logger.error(f"Error getting resource quotas: {e}") raise HTTPException(status_code=500, detail=str(e)) +@api_router.get("/pod-health-scores") +async def get_pod_health_scores( + namespace: Optional[str] = None, + k8s_client=Depends(get_k8s_client) +): + """Get simplified pod health scores with grouped validations""" + try: + # Get pods + pods = await k8s_client.get_all_pods() + + if namespace: + pods = [pod for pod in pods if pod.namespace == namespace] + + health_scores = [] + + for pod in pods: + # Get validations for this pod + pod_validations = validation_service.validate_pod_resources(pod) + + # Calculate health score + health_score = validation_service.calculate_pod_health_score(pod, pod_validations) + health_scores.append(health_score) + + # Sort by health score (worst first) + health_scores.sort(key=lambda x: x.health_score) + + return { + "pods": health_scores, + "total_pods": len(health_scores), + "summary": { + "excellent": len([h for h in health_scores if h.health_score >= 9]), + "good": len([h for h in health_scores if 7 <= h.health_score < 9]), + "medium": len([h for h in health_scores if 5 <= h.health_score < 7]), + "poor": len([h for h in health_scores if 3 <= h.health_score < 5]), + "critical": len([h for h in health_scores if h.health_score < 3]) + } + } + + except Exception as e: + logger.error(f"Error getting pod health scores: {e}") + raise HTTPException(status_code=500, detail=str(e)) + @api_router.get("/health") async def health_check(): """API health check""" diff --git a/app/models/resource_models.py b/app/models/resource_models.py index c0cb9d4..281ba2f 100644 --- a/app/models/resource_models.py +++ b/app/models/resource_models.py @@ -141,6 +141,43 @@ class ResourceQuota(BaseModel): usage_percentage: float = 0.0 recommended_quota: Optional[Dict[str, str]] = None +class PodHealthScore(BaseModel): + """Pod health score and simplified status""" + pod_name: str + namespace: str + health_score: int # 0-10 + health_status: str # "Excellent", "Good", "Medium", "Poor", "Critical" + status_color: str # "green", "yellow", "orange", "red" + status_icon: str # "✅", "🟡", "🟠", "🔴" + + # Simplified resource display + cpu_display: str # "100m → 500m (5:1 ratio)" + memory_display: str # "128Mi → 256Mi (2:1 ratio)" + cpu_status: str # "✅", "⚠️", "🔴" + memory_status: str # "✅", "⚠️", "🔴" + + # Grouped validations + critical_issues: List[str] = [] + warnings: List[str] = [] + info_items: List[str] = [] + + # Actions available + available_actions: List[str] = [] # "fix_cpu_ratio", "add_requests", "add_limits" + oc_commands: List[str] = [] + +class SimplifiedValidation(BaseModel): + """Simplified validation for UI display""" + pod_name: str + namespace: str + validation_group: str # "cpu_ratio", "memory_ratio", "missing_requests", "missing_limits" + severity: str # "critical", "warning", "info" + title: str # "CPU Ratio Issue" + description: str # "CPU ratio 5:1 exceeds recommended 3:1" + current_value: str # "5:1" + recommended_value: str # "3:1" + action_required: str # "Adjust CPU limits to 300m" + oc_command: Optional[str] = None + class ClusterHealth(BaseModel): """Cluster health overview""" total_pods: int diff --git a/app/services/validation_service.py b/app/services/validation_service.py index 76c7f15..6be0302 100644 --- a/app/services/validation_service.py +++ b/app/services/validation_service.py @@ -12,7 +12,9 @@ from app.models.resource_models import ( NamespaceResources, QoSClassification, ResourceQuota, - ClusterHealth + ClusterHealth, + PodHealthScore, + SimplifiedValidation ) from app.core.config import settings from app.services.historical_analysis import HistoricalAnalysisService @@ -808,3 +810,227 @@ class ValidationService: # For now, return a simple calculation based on namespace count # In a real implementation, this would check actual ResourceQuota objects return min(len(namespaces) * 0.2, 1.0) # 20% per namespace, max 100% + + def calculate_pod_health_score(self, pod: PodResource, validations: List[ResourceValidation]) -> PodHealthScore: + """Calculate pod health score and create simplified display""" + # Calculate health score (0-10) + health_score = 10 + + # Deduct points for issues + for validation in validations: + if validation.severity == "critical": + health_score -= 3 + elif validation.severity == "error": + health_score -= 2 + elif validation.severity == "warning": + health_score -= 1 + + # Ensure score is between 0-10 + health_score = max(0, min(10, health_score)) + + # Determine health status and visual indicators + if health_score >= 9: + health_status = "Excellent" + status_color = "green" + status_icon = "✅" + elif health_score >= 7: + health_status = "Good" + status_color = "green" + status_icon = "✅" + elif health_score >= 5: + health_status = "Medium" + status_color = "yellow" + status_icon = "🟡" + elif health_score >= 3: + health_status = "Poor" + status_color = "orange" + status_icon = "🟠" + else: + health_status = "Critical" + status_color = "red" + status_icon = "🔴" + + # Create simplified resource display + cpu_display, cpu_status = self._create_cpu_display(pod) + memory_display, memory_status = self._create_memory_display(pod) + + # Group validations by severity + critical_issues = [] + warnings = [] + info_items = [] + + for validation in validations: + if validation.severity == "critical": + critical_issues.append(validation.message) + elif validation.severity in ["error", "warning"]: + warnings.append(validation.message) + else: + info_items.append(validation.message) + + # Determine available actions + available_actions = self._determine_available_actions(validations) + oc_commands = self._generate_oc_commands(pod, validations) + + return PodHealthScore( + pod_name=pod.name, + namespace=pod.namespace, + health_score=health_score, + health_status=health_status, + status_color=status_color, + status_icon=status_icon, + cpu_display=cpu_display, + memory_display=memory_display, + cpu_status=cpu_status, + memory_status=memory_status, + critical_issues=critical_issues, + warnings=warnings, + info_items=info_items, + available_actions=available_actions, + oc_commands=oc_commands + ) + + def _create_cpu_display(self, pod: PodResource) -> tuple[str, str]: + """Create CPU display string and status""" + if pod.cpu_requests == 0 and pod.cpu_limits == 0: + return "No CPU resources defined", "🔴" + + # Format CPU values + cpu_req_str = self._format_cpu_value(pod.cpu_requests) + cpu_lim_str = self._format_cpu_value(pod.cpu_limits) + + # Calculate ratio + if pod.cpu_requests > 0: + ratio = pod.cpu_limits / pod.cpu_requests + ratio_str = f"({ratio:.1f}:1 ratio)" + else: + ratio_str = "(no requests)" + + display = f"{cpu_req_str} → {cpu_lim_str} {ratio_str}" + + # Determine status + if pod.cpu_requests == 0: + status = "🔴" # No requests + elif pod.cpu_limits == 0: + status = "🟡" # No limits + elif pod.cpu_requests > 0 and pod.cpu_limits > 0: + ratio = pod.cpu_limits / pod.cpu_requests + if ratio > 5: + status = "🔴" # Very high ratio + elif ratio > 3: + status = "🟡" # High ratio + else: + status = "✅" # Good ratio + else: + status = "🔴" + + return display, status + + def _create_memory_display(self, pod: PodResource) -> tuple[str, str]: + """Create memory display string and status""" + if pod.memory_requests == 0 and pod.memory_limits == 0: + return "No memory resources defined", "🔴" + + # Format memory values + mem_req_str = self._format_memory_value(pod.memory_requests) + mem_lim_str = self._format_memory_value(pod.memory_limits) + + # Calculate ratio + if pod.memory_requests > 0: + ratio = pod.memory_limits / pod.memory_requests + ratio_str = f"({ratio:.1f}:1 ratio)" + else: + ratio_str = "(no requests)" + + display = f"{mem_req_str} → {mem_lim_str} {ratio_str}" + + # Determine status + if pod.memory_requests == 0: + status = "🔴" # No requests + elif pod.memory_limits == 0: + status = "🟡" # No limits + elif pod.memory_requests > 0 and pod.memory_limits > 0: + ratio = pod.memory_limits / pod.memory_requests + if ratio > 5: + status = "🔴" # Very high ratio + elif ratio > 3: + status = "🟡" # High ratio + else: + status = "✅" # Good ratio + else: + status = "🔴" + + return display, status + + def _format_cpu_value(self, value: float) -> str: + """Format CPU value for display""" + if value >= 1.0: + return f"{value:.1f} cores" + else: + return f"{int(value * 1000)}m" + + def _format_memory_value(self, value_bytes: float) -> str: + """Format memory value for display""" + if value_bytes >= 1024 * 1024 * 1024: # >= 1 GiB + return f"{value_bytes / (1024 * 1024 * 1024):.1f} GiB" + else: + return f"{int(value_bytes / (1024 * 1024))} MiB" + + def _determine_available_actions(self, validations: List[ResourceValidation]) -> List[str]: + """Determine available actions based on validations""" + actions = [] + + for validation in validations: + if validation.validation_type == "missing_requests": + actions.append("add_requests") + elif validation.validation_type == "missing_limits": + actions.append("add_limits") + elif validation.validation_type == "cpu_ratio": + actions.append("fix_cpu_ratio") + elif validation.validation_type == "memory_ratio": + actions.append("fix_memory_ratio") + + return list(set(actions)) # Remove duplicates + + def _generate_oc_commands(self, pod: PodResource, validations: List[ResourceValidation]) -> List[str]: + """Generate oc commands for fixing issues""" + commands = [] + + # Generate commands for each validation + for validation in validations: + if validation.validation_type == "missing_requests": + cmd = self._generate_add_requests_command(pod, validation) + if cmd: + commands.append(cmd) + elif validation.validation_type == "missing_limits": + cmd = self._generate_add_limits_command(pod, validation) + if cmd: + commands.append(cmd) + elif validation.validation_type in ["cpu_ratio", "memory_ratio"]: + cmd = self._generate_fix_ratio_command(pod, validation) + if cmd: + commands.append(cmd) + + return commands + + def _generate_add_requests_command(self, pod: PodResource, validation: ResourceValidation) -> str: + """Generate oc command to add requests""" + # This would need to be implemented based on specific container + return f"oc patch pod {pod.name} -n {pod.namespace} --type='merge' -p='{{\"spec\":{{\"containers\":[{{\"name\":\"{validation.container_name}\",\"resources\":{{\"requests\":{{\"cpu\":\"100m\",\"memory\":\"128Mi\"}}}}}}]}}}}'" + + def _generate_add_limits_command(self, pod: PodResource, validation: ResourceValidation) -> str: + """Generate oc command to add limits""" + return f"oc patch pod {pod.name} -n {pod.namespace} --type='merge' -p='{{\"spec\":{{\"containers\":[{{\"name\":\"{validation.container_name}\",\"resources\":{{\"limits\":{{\"cpu\":\"500m\",\"memory\":\"512Mi\"}}}}}}]}}}}'" + + def _generate_fix_ratio_command(self, pod: PodResource, validation: ResourceValidation) -> str: + """Generate oc command to fix ratio""" + # Calculate recommended limits based on 3:1 ratio + if validation.validation_type == "cpu_ratio": + recommended_limit = pod.cpu_requests * 3 + limit_str = self._format_cpu_value(recommended_limit) + return f"oc patch pod {pod.name} -n {pod.namespace} --type='merge' -p='{{\"spec\":{{\"containers\":[{{\"name\":\"{validation.container_name}\",\"resources\":{{\"limits\":{{\"cpu\":\"{limit_str}\"}}}}}}]}}}}'" + elif validation.validation_type == "memory_ratio": + recommended_limit = pod.memory_requests * 3 + limit_str = self._format_memory_value(recommended_limit) + return f"oc patch pod {pod.name} -n {pod.namespace} --type='merge' -p='{{\"spec\":{{\"containers\":[{{\"name\":\"{validation.container_name}\",\"resources\":{{\"limits\":{{\"memory\":\"{limit_str}\"}}}}}}]}}}}'" + + return "" diff --git a/app/static/index.html b/app/static/index.html index b750dbb..f8d1459 100644 --- a/app/static/index.html +++ b/app/static/index.html @@ -1278,6 +1278,336 @@ gap: 0.5rem; } } + + /* Simplified View Styles */ + .view-mode-toggle { + display: flex; + gap: 0.5rem; + margin-bottom: 2rem; + padding: 0.5rem; + background: #f8f9fa; + border-radius: 8px; + } + + .mode-btn { + padding: 0.75rem 1.5rem; + border: 2px solid #e9ecef; + background: white; + color: #6c757d; + border-radius: 6px; + cursor: pointer; + font-weight: 500; + transition: all 0.2s ease; + } + + .mode-btn.active { + background: #007bff; + color: white; + border-color: #007bff; + } + + .mode-btn:hover:not(.active) { + background: #f8f9fa; + border-color: #007bff; + color: #007bff; + } + + .health-summary { + margin-bottom: 2rem; + } + + .summary-stats { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); + gap: 1rem; + } + + .stat-card { + display: flex; + align-items: center; + padding: 1rem; + border-radius: 8px; + border: 2px solid; + background: white; + } + + .stat-card.excellent { + border-color: #28a745; + background: #f8fff9; + } + + .stat-card.good { + border-color: #28a745; + background: #f8fff9; + } + + .stat-card.medium { + border-color: #ffc107; + background: #fffdf5; + } + + .stat-card.poor { + border-color: #fd7e14; + background: #fff8f5; + } + + .stat-card.critical { + border-color: #dc3545; + background: #fff5f5; + } + + .stat-icon { + font-size: 2rem; + margin-right: 1rem; + } + + .stat-content { + flex: 1; + } + + .stat-number { + font-size: 2rem; + font-weight: 700; + line-height: 1; + } + + .stat-label { + font-size: 0.9rem; + color: #6c757d; + margin-top: 0.25rem; + } + + .pod-cards-container { + display: grid; + gap: 1rem; + } + + .pod-health-card { + background: white; + border: 2px solid #e9ecef; + border-radius: 12px; + padding: 1.5rem; + transition: all 0.2s ease; + } + + .pod-health-card:hover { + box-shadow: 0 4px 12px rgba(0,0,0,0.1); + transform: translateY(-2px); + } + + .pod-health-card.excellent { + border-color: #28a745; + } + + .pod-health-card.good { + border-color: #28a745; + } + + .pod-health-card.medium { + border-color: #ffc107; + } + + .pod-health-card.poor { + border-color: #fd7e14; + } + + .pod-health-card.critical { + border-color: #dc3545; + } + + .pod-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 1rem; + } + + .pod-name { + font-size: 1.2rem; + font-weight: 600; + color: #333; + } + + .pod-namespace { + font-size: 0.9rem; + color: #6c757d; + margin-top: 0.25rem; + } + + .health-score { + display: flex; + align-items: center; + gap: 0.5rem; + } + + .health-score-value { + font-size: 1.5rem; + font-weight: 700; + } + + .health-status { + font-size: 0.9rem; + color: #6c757d; + } + + .resource-display { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 1rem; + margin-bottom: 1rem; + } + + .resource-item { + display: flex; + align-items: center; + gap: 0.5rem; + } + + .resource-icon { + font-size: 1.2rem; + } + + .resource-text { + flex: 1; + } + + .resource-label { + font-size: 0.9rem; + color: #6c757d; + margin-bottom: 0.25rem; + } + + .resource-value { + font-size: 1rem; + font-weight: 500; + } + + .pod-actions { + display: flex; + gap: 0.5rem; + margin-top: 1rem; + } + + .action-btn { + padding: 0.5rem 1rem; + border: 1px solid #007bff; + background: white; + color: #007bff; + border-radius: 6px; + cursor: pointer; + font-size: 0.9rem; + transition: all 0.2s ease; + } + + .action-btn:hover { + background: #007bff; + color: white; + } + + .action-btn.primary { + background: #007bff; + color: white; + } + + .action-btn.primary:hover { + background: #0056b3; + } + + .technical-details { + margin-top: 2rem; + } + + .technical-details.hidden { + display: none; + } + + .technical-card { + background: white; + border: 1px solid #e9ecef; + border-radius: 8px; + padding: 1.5rem; + margin-bottom: 1rem; + } + + .technical-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1rem; + padding-bottom: 0.5rem; + border-bottom: 1px solid #e9ecef; + } + + .validation-groups { + display: grid; + gap: 1rem; + } + + .validation-group { + padding: 1rem; + border-radius: 6px; + border-left: 4px solid; + } + + .validation-group.critical { + background: #fff5f5; + border-left-color: #dc3545; + } + + .validation-group.warning { + background: #fffdf5; + border-left-color: #ffc107; + } + + .validation-group.info { + background: #f8f9fa; + border-left-color: #6c757d; + } + + .validation-group-title { + font-weight: 600; + margin-bottom: 0.5rem; + } + + .validation-list { + list-style: none; + padding: 0; + } + + .validation-item { + padding: 0.5rem 0; + border-bottom: 1px solid #e9ecef; + } + + .validation-item:last-child { + border-bottom: none; + } + + .oc-command { + background: #f8f9fa; + border: 1px solid #e9ecef; + border-radius: 4px; + padding: 0.75rem; + font-family: 'Courier New', monospace; + font-size: 0.9rem; + margin-top: 0.5rem; + word-break: break-all; + } + + .copy-btn { + background: #28a745; + color: white; + border: none; + padding: 0.25rem 0.5rem; + border-radius: 4px; + cursor: pointer; + font-size: 0.8rem; + margin-left: 0.5rem; + } + + .copy-btn:hover { + background: #218838; + } @@ -1291,6 +1621,10 @@ 🏠 Cluster Health + + 🎯 + Simplified View + 📊 Workload Analysis @@ -1561,6 +1895,70 @@
+ + +