Implement simplified UI/UX with health scores and grouped validations

This commit is contained in:
2025-09-30 09:37:49 -03:00
parent 021ce06323
commit fa8f3a41e5
5 changed files with 930 additions and 14 deletions

View File

@@ -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

View File

@@ -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"""

View File

@@ -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

View File

@@ -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 ""

View File

@@ -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;
}
</style>
</head>
<body>
@@ -1291,6 +1621,10 @@
<span class="nav-icon">🏠</span>
<span class="nav-text">Cluster Health</span>
</a>
<a href="#" class="nav-item" data-section="simplified-view">
<span class="nav-icon">🎯</span>
<span class="nav-text">Simplified View</span>
</a>
<a href="#" class="nav-item" data-section="workload-categories">
<span class="nav-icon">📊</span>
<span class="nav-text">Workload Analysis</span>
@@ -1561,6 +1895,70 @@
<div id="vpaList"></div>
</div>
<!-- Simplified View -->
<div class="card" id="simplifiedViewCard" style="display: none;">
<h2>🎯 Simplified Pod Health View</h2>
<!-- View Mode Toggle -->
<div class="view-mode-toggle">
<button class="mode-btn active" id="simpleModeBtn" onclick="switchViewMode('simple')">Simple Mode</button>
<button class="mode-btn" id="technicalModeBtn" onclick="switchViewMode('technical')">Technical Mode</button>
</div>
<!-- Health Summary -->
<div class="health-summary" id="healthSummary">
<div class="summary-stats">
<div class="stat-card excellent">
<div class="stat-icon"></div>
<div class="stat-content">
<div class="stat-number" id="excellentCount">0</div>
<div class="stat-label">Excellent</div>
</div>
</div>
<div class="stat-card good">
<div class="stat-icon"></div>
<div class="stat-content">
<div class="stat-number" id="goodCount">0</div>
<div class="stat-label">Good</div>
</div>
</div>
<div class="stat-card medium">
<div class="stat-icon">🟡</div>
<div class="stat-content">
<div class="stat-number" id="mediumCount">0</div>
<div class="stat-label">Medium</div>
</div>
</div>
<div class="stat-card poor">
<div class="stat-icon">🟠</div>
<div class="stat-content">
<div class="stat-number" id="poorCount">0</div>
<div class="stat-label">Poor</div>
</div>
</div>
<div class="stat-card critical">
<div class="stat-icon">🔴</div>
<div class="stat-content">
<div class="stat-number" id="criticalCount">0</div>
<div class="stat-label">Critical</div>
</div>
</div>
</div>
</div>
<!-- Pod Health Cards (Simple Mode) -->
<div id="simplePodCards" class="pod-cards-container">
<!-- Will be populated by JavaScript -->
</div>
<!-- Technical Details (Technical Mode) -->
<div id="technicalDetails" class="technical-details hidden">
<div id="technicalPodCards" class="technical-cards-container">
<!-- Will be populated by JavaScript -->
</div>
</div>
</div>
<!-- Loading -->
<div class="loading hidden" id="loading">
<p>Loading data...</p>
@@ -3422,6 +3820,7 @@
// Show selected section
const sectionMap = {
'dashboard': 'validationsCard',
'simplified-view': 'simplifiedViewCard',
'historical-analysis': 'historicalCard',
'smart-recommendations': 'smartRecommendationsCard',
'workload-categories': 'workloadCategoriesCard',
@@ -3441,6 +3840,9 @@
case 'dashboard':
loadClusterHealth();
break;
case 'simplified-view':
loadSimplifiedView();
break;
case 'historical-analysis':
loadHistoricalValidations();
break;
@@ -3456,6 +3858,190 @@
}
}
// Simplified View Functions
let currentViewMode = 'simple';
let healthScoresData = null;
async function loadSimplifiedView() {
try {
showLoading();
const response = await fetch('/api/v1/pod-health-scores');
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
healthScoresData = await response.json();
updateHealthSummary(healthScoresData.summary);
updatePodCards(healthScoresData.pods);
hideLoading();
} catch (error) {
console.error('Error loading simplified view:', error);
showError('Error loading simplified view: ' + error.message);
}
}
function updateHealthSummary(summary) {
document.getElementById('excellentCount').textContent = summary.excellent;
document.getElementById('goodCount').textContent = summary.good;
document.getElementById('mediumCount').textContent = summary.medium;
document.getElementById('poorCount').textContent = summary.poor;
document.getElementById('criticalCount').textContent = summary.critical;
}
function updatePodCards(pods) {
const container = document.getElementById('simplePodCards');
const technicalContainer = document.getElementById('technicalPodCards');
let simpleHtml = '';
let technicalHtml = '';
pods.forEach(pod => {
// Simple mode card
simpleHtml += `
<div class="pod-health-card ${pod.status_color}">
<div class="pod-header">
<div>
<div class="pod-name">${pod.pod_name}</div>
<div class="pod-namespace">${pod.namespace}</div>
</div>
<div class="health-score">
<span class="health-score-value">${pod.health_score}/10</span>
<span class="health-status">${pod.health_status}</span>
</div>
</div>
<div class="resource-display">
<div class="resource-item">
<span class="resource-icon">⚡</span>
<div class="resource-text">
<div class="resource-label">CPU</div>
<div class="resource-value">${pod.cpu_display} ${pod.cpu_status}</div>
</div>
</div>
<div class="resource-item">
<span class="resource-icon">💾</span>
<div class="resource-text">
<div class="resource-label">Memory</div>
<div class="resource-value">${pod.memory_display} ${pod.memory_status}</div>
</div>
</div>
</div>
<div class="pod-actions">
<button class="action-btn primary" onclick="viewPodDetails('${pod.pod_name}')">View Details</button>
${pod.available_actions.includes('fix_cpu_ratio') ? '<button class="action-btn" onclick="fixPodIssue(\'' + pod.pod_name + '\', \'cpu_ratio\')">Fix CPU Ratio</button>' : ''}
${pod.available_actions.includes('add_requests') ? '<button class="action-btn" onclick="fixPodIssue(\'' + pod.pod_name + '\', \'add_requests\')">Add Requests</button>' : ''}
</div>
</div>
`;
// Technical mode card
technicalHtml += `
<div class="technical-card">
<div class="technical-header">
<div>
<div class="pod-name">${pod.pod_name}</div>
<div class="pod-namespace">${pod.namespace}</div>
</div>
<div class="health-score">
<span class="health-score-value">${pod.health_score}/10</span>
<span class="health-status">${pod.health_status}</span>
</div>
</div>
<div class="validation-groups">
${pod.critical_issues.length > 0 ? `
<div class="validation-group critical">
<div class="validation-group-title">🔴 Critical Issues (${pod.critical_issues.length})</div>
<ul class="validation-list">
${pod.critical_issues.map(issue => `<li class="validation-item">${issue}</li>`).join('')}
</ul>
</div>
` : ''}
${pod.warnings.length > 0 ? `
<div class="validation-group warning">
<div class="validation-group-title">🟡 Warnings (${pod.warnings.length})</div>
<ul class="validation-list">
${pod.warnings.map(warning => `<li class="validation-item">${warning}</li>`).join('')}
</ul>
</div>
` : ''}
${pod.info_items.length > 0 ? `
<div class="validation-group info">
<div class="validation-group-title">🔵 Information (${pod.info_items.length})</div>
<ul class="validation-list">
${pod.info_items.map(info => `<li class="validation-item">${info}</li>`).join('')}
</ul>
</div>
` : ''}
</div>
${pod.oc_commands.length > 0 ? `
<div class="oc-commands">
<h4>Available Commands:</h4>
${pod.oc_commands.map(cmd => `
<div class="oc-command">
${cmd}
<button class="copy-btn" onclick="copyToClipboard('${cmd.replace(/'/g, "\\'")}')">Copy</button>
</div>
`).join('')}
</div>
` : ''}
</div>
`;
});
container.innerHTML = simpleHtml;
technicalContainer.innerHTML = technicalHtml;
}
function switchViewMode(mode) {
currentViewMode = mode;
// Update button states
document.getElementById('simpleModeBtn').classList.toggle('active', mode === 'simple');
document.getElementById('technicalModeBtn').classList.toggle('active', mode === 'technical');
// Show/hide appropriate content
document.getElementById('simplePodCards').style.display = mode === 'simple' ? 'block' : 'none';
document.getElementById('technicalDetails').classList.toggle('hidden', mode === 'simple');
}
function viewPodDetails(podName) {
// Switch to technical mode and scroll to pod
switchViewMode('technical');
const podCard = document.querySelector(`[data-pod="${podName}"]`);
if (podCard) {
podCard.scrollIntoView({ behavior: 'smooth' });
}
}
function fixPodIssue(podName, issueType) {
// This would implement the fix logic
alert(`Fixing ${issueType} for pod ${podName}`);
}
function copyToClipboard(text) {
navigator.clipboard.writeText(text).then(() => {
// Show success feedback
const btn = event.target;
const originalText = btn.textContent;
btn.textContent = 'Copied!';
btn.style.background = '#28a745';
setTimeout(() => {
btn.textContent = originalText;
btn.style.background = '#28a745';
}, 2000);
}).catch(err => {
console.error('Failed to copy: ', err);
alert('Failed to copy to clipboard');
});
}
// Add click handlers for navigation
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('.nav-item').forEach(item => {