Files
openshift-resource-governance/app/static/index.html
andersonid 3a5af8ce67 Feat: implementar dashboard de cluster health com QoS e Resource Quotas
- Adicionar modelos para QoSClassification, ResourceQuota e ClusterHealth
- Implementar classificação automática de QoS (Guaranteed, Burstable, BestEffort)
- Criar análise de Resource Quotas com recomendações automáticas
- Adicionar dashboard principal com visão geral do cluster
- Implementar análise de overcommit com métricas visuais
- Adicionar top resource consumers com ranking
- Criar distribuição de QoS com estatísticas
- Adicionar novos endpoints API para cluster health e QoS
- Melhorar interface com design responsivo e intuitivo
- Alinhar com práticas Red Hat para gerenciamento de recursos
2025-09-29 16:35:07 -03:00

2951 lines
104 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>OpenShift Resource Governance Tool</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background-color: #f5f5f5;
color: #333;
display: flex;
min-height: 100vh;
}
/* Sidebar Styles */
.sidebar {
width: 250px;
background: #2c3e50;
color: white;
position: fixed;
height: 100vh;
left: 0;
top: 0;
z-index: 1000;
overflow-y: auto;
transition: transform 0.3s ease;
}
.sidebar-header {
padding: 1.5rem 1rem;
border-bottom: 1px solid #34495e;
background: #34495e;
}
.sidebar-header h2 {
font-size: 1.2rem;
font-weight: 600;
}
.sidebar-nav {
padding: 1rem 0;
}
.nav-item {
display: flex;
align-items: center;
padding: 0.75rem 1rem;
color: #bdc3c7;
text-decoration: none;
transition: all 0.3s ease;
border-left: 3px solid transparent;
}
.nav-item:hover {
background: #34495e;
color: white;
border-left-color: #3498db;
}
.nav-item.active {
background: #3498db;
color: white;
border-left-color: #2980b9;
}
.nav-icon {
font-size: 1.2rem;
margin-right: 0.75rem;
width: 20px;
text-align: center;
}
.nav-text {
font-weight: 500;
}
/* Main Content */
.main-content {
flex: 1;
margin-left: 250px;
min-height: 100vh;
}
/* Export Button */
.export-button {
position: fixed;
top: 1rem;
right: 1rem;
background: #28a745;
color: white;
border: none;
padding: 0.75rem 1rem;
border-radius: 4px;
cursor: pointer;
font-size: 0.9rem;
font-weight: 500;
z-index: 100;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
transition: background 0.3s ease;
}
.export-button:hover {
background: #218838;
}
/* Export Modal Styles */
.form-group {
margin-bottom: 1rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
color: #333;
}
.form-group select,
.form-group input {
width: 100%;
padding: 0.5rem;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 0.9rem;
}
.checkbox-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
margin-bottom: 1.5rem;
}
.checkbox-group label {
display: flex;
align-items: center;
gap: 0.5rem;
font-weight: normal;
margin-bottom: 0;
}
.modal-actions {
display: flex;
gap: 1rem;
justify-content: flex-end;
margin-top: 1.5rem;
}
.time-range-selector {
display: flex;
gap: 0.5rem;
margin-bottom: 1rem;
padding: 1rem;
background: #f8f9fa;
border-radius: 0.5rem;
}
.time-range-btn {
padding: 0.5rem 1rem;
border: 1px solid #ddd;
background: white;
border-radius: 0.25rem;
cursor: pointer;
transition: all 0.2s;
}
.time-range-btn:hover {
background: #e9ecef;
}
.time-range-btn.active {
background: #007bff;
color: white;
border-color: #007bff;
}
.page-header {
padding: 0 1rem;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
}
.card {
background: white;
border-radius: 8px;
padding: 1.5rem;
margin-bottom: 1.5rem;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.card h2 {
color: #cc0000;
margin-bottom: 1rem;
font-size: 1.3rem;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
margin-bottom: 2rem;
}
.stat-card {
background: white;
padding: 1.5rem;
border-radius: 8px;
text-align: center;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.stat-number {
font-size: 2rem;
font-weight: bold;
color: #cc0000;
}
.stat-label {
color: #666;
margin-top: 0.5rem;
}
.btn {
background: #cc0000;
color: white;
border: none;
padding: 0.75rem 1.5rem;
border-radius: 4px;
cursor: pointer;
font-size: 1rem;
transition: background-color 0.2s;
}
.btn:hover {
background: #8b0000;
}
.btn:disabled {
background: #ccc;
cursor: not-allowed;
}
.btn-secondary {
background: #6c757d;
}
.btn-secondary:hover {
background: #545b62;
}
.loading {
text-align: center;
padding: 2rem;
color: #666;
}
.error {
background: #f8d7da;
color: #721c24;
padding: 1rem;
border-radius: 4px;
margin: 1rem 0;
}
.success {
background: #d4edda;
color: #155724;
padding: 1rem;
border-radius: 4px;
margin: 1rem 0;
}
.validation-item {
padding: 1rem;
border-left: 4px solid #ccc;
margin: 0.75rem 0;
background: #f8f9fa;
border-radius: 6px;
border: 1px solid #dee2e6;
}
.validation-item.error {
border-left-color: #dc3545;
background: #f8d7da;
}
.validation-item.warning {
border-left-color: #ffc107;
background: #fff3cd;
}
.validation-item.critical {
border-left-color: #dc3545;
background: #f8d7da;
font-weight: bold;
}
.validation-header {
font-weight: bold;
margin-bottom: 0.5rem;
}
.validation-message {
color: #666;
margin-bottom: 0.5rem;
}
.validation-recommendation {
font-style: italic;
color: #007bff;
}
.export-section {
display: flex;
gap: 1rem;
align-items: center;
flex-wrap: wrap;
}
.export-section select,
.export-section input {
padding: 0.5rem;
border: 1px solid #ddd;
border-radius: 4px;
}
.table {
width: 100%;
border-collapse: collapse;
margin-top: 1rem;
}
.table th,
.table td {
padding: 0.75rem;
text-align: left;
border-bottom: 1px solid #ddd;
}
.table th {
background: #f8f9fa;
font-weight: 600;
}
.severity-badge {
padding: 0.25rem 0.5rem;
border-radius: 12px;
font-size: 0.8rem;
font-weight: bold;
}
.severity-error {
background: #f8d7da;
color: #721c24;
}
.severity-warning {
background: #fff3cd;
color: #856404;
}
.severity-critical {
background: #f8d7da;
color: #721c24;
}
.hidden {
display: none;
}
/* Accordion Styles */
.accordion {
border: 1px solid #ddd;
border-radius: 8px;
margin-bottom: 1rem;
overflow: visible;
}
.accordion-header {
background: #f8f9fa;
padding: 1rem 1.5rem;
cursor: pointer;
display: flex;
align-items: center;
border-bottom: 1px solid #ddd;
transition: all 0.2s;
position: relative;
gap: 1rem;
}
.accordion-title {
flex: 0 0 auto;
font-weight: 600;
font-size: 1.1rem;
color: #333;
min-width: 0;
}
.accordion-stats {
display: flex;
gap: 1rem;
align-items: center;
flex: 0 0 auto;
margin-left: auto;
}
.accordion-stat {
font-size: 0.9rem;
font-weight: 500;
white-space: nowrap;
text-align: right;
}
.accordion-arrow {
font-size: 1.2rem;
transition: transform 0.3s ease;
color: #6c757d;
flex: 0 0 auto;
margin-left: 0.5rem;
}
.accordion-header:hover {
background: #e9ecef;
}
.accordion-header.active .accordion-arrow {
transform: rotate(90deg);
}
.accordion-header.active {
background: #495057;
color: white;
}
.accordion-title {
font-weight: 600;
font-size: 1.1rem;
}
.accordion-stats {
display: flex;
gap: 1rem;
font-size: 0.9rem;
}
.accordion-actions {
display: flex;
align-items: center;
gap: 0.5rem;
}
.accordion-stat {
background: rgba(255,255,255,0.2);
padding: 0.25rem 0.5rem;
border-radius: 4px;
}
.accordion-content {
padding: 1rem 1.5rem;
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease;
background: white;
border-top: 1px solid #dee2e6;
}
.accordion-content.active {
max-height: none;
overflow: visible;
padding: 1rem 1.5rem;
}
/* Garantir que o conteúdo não seja cortado */
.accordion-content .validation-item:last-child {
margin-bottom: 0;
}
.accordion-content .historical-validation:last-child {
margin-bottom: 0;
}
.pod-list {
padding: 1rem 1.5rem;
}
.pod-item {
background: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 6px;
padding: 1rem;
margin-bottom: 0.5rem;
}
.pod-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.5rem;
}
.pod-name {
font-weight: 600;
color: #cc0000;
}
.pod-validations-count {
background: #6c757d;
color: white;
padding: 0.25rem 0.5rem;
border-radius: 12px;
font-size: 0.8rem;
}
.deployment-list {
padding: 1rem 1.5rem;
}
.deployment-item {
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 8px;
margin-bottom: 1rem;
overflow: hidden;
}
.deployment-header {
background: #e3f2fd;
padding: 1rem;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #dee2e6;
}
.deployment-title {
font-weight: bold;
color: #1976d2;
font-size: 1.1rem;
}
.deployment-stats {
display: flex;
gap: 1rem;
align-items: center;
}
.deployment-stat {
font-size: 0.9rem;
color: #666;
}
.deployment-actions {
display: flex;
align-items: center;
gap: 0.5rem;
}
.validation-list {
margin-top: 0.5rem;
}
.validation-item {
padding: 0.75rem;
border-left: 4px solid #ccc;
margin: 0.25rem 0;
background: white;
border-radius: 4px;
}
.validation-item.error {
border-left-color: #dc3545;
}
.validation-item.warning {
border-left-color: #ffc107;
}
.validation-item.critical {
border-left-color: #dc3545;
font-weight: bold;
}
/* Pagination Styles */
.pagination {
display: flex;
justify-content: center;
align-items: center;
gap: 0.5rem;
margin: 2rem 0;
}
.pagination button {
padding: 0.5rem 1rem;
border: 1px solid #ddd;
background: white;
cursor: pointer;
border-radius: 4px;
transition: all 0.2s;
}
.pagination button:hover:not(:disabled) {
background: #f8f9fa;
}
.pagination button:disabled {
background: #f8f9fa;
color: #6c757d;
cursor: not-allowed;
}
/* Historical Analysis Styles */
.historical-summary {
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 8px;
padding: 1rem;
margin-bottom: 1rem;
}
.historical-summary h3 {
margin: 0 0 1rem 0;
color: #495057;
}
.historical-stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
}
.historical-stat {
background: white;
padding: 0.75rem;
border-radius: 6px;
border-left: 4px solid #007bff;
}
.historical-stat h4 {
margin: 0 0 0.5rem 0;
font-size: 0.9rem;
color: #6c757d;
}
.historical-stat .value {
font-size: 1.5rem;
font-weight: bold;
color: #007bff;
}
.historical-validation {
background: #fff3cd;
border: 1px solid #ffeaa7;
border-radius: 6px;
padding: 1rem;
margin-bottom: 0.75rem;
}
.historical-validation.error {
background: #f8d7da;
border-color: #f5c6cb;
}
.historical-validation.warning {
background: #fff3cd;
border-color: #ffeaa7;
}
.historical-validation.info {
background: #d1ecf1;
border-color: #bee5eb;
}
.historical-validation.critical {
background: #f8d7da;
border-color: #f5c6cb;
border-left: 4px solid #dc3545;
}
.pagination button.active {
background: #cc0000;
color: white;
border-color: #cc0000;
}
.pagination-info {
color: #666;
font-size: 0.9rem;
}
/* Modal Styles */
.modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
z-index: 1000;
display: none;
align-items: center;
justify-content: center;
}
.modal.show {
display: flex;
}
.modal-content {
background: white;
border-radius: 8px;
max-width: 800px;
width: 90%;
max-height: 80vh;
overflow-y: auto;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 1.5rem;
border-bottom: 1px solid #dee2e6;
background: #f8f9fa;
}
.modal-header h2 {
margin: 0;
color: #cc0000;
}
.modal-close {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: #6c757d;
padding: 0;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
}
.modal-close:hover {
color: #dc3545;
}
.modal-body {
padding: 1.5rem;
}
/* Filters */
.filters {
display: flex;
gap: 1rem;
margin-bottom: 1rem;
flex-wrap: wrap;
align-items: center;
}
.filter-group {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.filter-group label {
font-size: 0.9rem;
color: #666;
}
.filter-group select,
.filter-group input {
padding: 0.5rem;
border: 1px solid #ddd;
border-radius: 4px;
}
.checkbox-label {
display: flex !important;
flex-direction: row !important;
align-items: center;
gap: 0.5rem;
cursor: pointer;
}
.checkbox-label input[type="checkbox"] {
margin: 0;
width: auto;
}
/* Cluster Health Dashboard Styles */
.cluster-health-section {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 2rem;
border-radius: 12px;
margin-bottom: 2rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.health-status {
display: flex;
align-items: center;
gap: 1rem;
}
.health-indicator {
font-size: 3rem;
animation: pulse 2s infinite;
}
@keyframes pulse {
0% { transform: scale(1); }
50% { transform: scale(1.1); }
100% { transform: scale(1); }
}
.health-text h3 {
margin: 0;
font-size: 1.5rem;
font-weight: 600;
}
.health-text p {
margin: 0.5rem 0 0 0;
opacity: 0.9;
}
.health-metrics {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 2rem;
}
.metric {
text-align: center;
}
.metric-label {
display: block;
font-size: 0.9rem;
opacity: 0.8;
margin-bottom: 0.5rem;
}
.metric-value {
display: block;
font-size: 1.5rem;
font-weight: 700;
}
.metric-value.critical {
color: #ff6b6b;
}
.resource-overview {
margin-bottom: 2rem;
}
.resource-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1.5rem;
margin-top: 1rem;
}
.resource-card {
background: #f8f9fa;
padding: 1.5rem;
border-radius: 8px;
border-left: 4px solid #007bff;
}
.resource-card h4 {
margin: 0 0 1rem 0;
color: #333;
}
.resource-bar {
background: #e9ecef;
height: 8px;
border-radius: 4px;
overflow: hidden;
margin-bottom: 0.5rem;
}
.resource-fill {
height: 100%;
background: linear-gradient(90deg, #28a745, #ffc107, #dc3545);
transition: width 0.3s ease;
}
.resource-text {
display: flex;
justify-content: space-between;
font-size: 0.9rem;
color: #666;
}
.top-consumers {
margin-bottom: 2rem;
}
.consumers-list {
display: grid;
gap: 0.5rem;
margin-top: 1rem;
}
.consumer-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem;
background: #f8f9fa;
border-radius: 6px;
border-left: 4px solid #007bff;
}
.consumer-info {
display: flex;
align-items: center;
gap: 1rem;
}
.consumer-rank {
font-weight: 700;
color: #007bff;
}
.consumer-name {
font-weight: 600;
}
.consumer-namespace {
color: #666;
font-size: 0.9rem;
}
.consumer-resources {
text-align: right;
font-size: 0.9rem;
}
.qos-distribution {
margin-bottom: 2rem;
}
.qos-stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1rem;
margin-top: 1rem;
}
.qos-stat {
padding: 1rem;
border-radius: 6px;
text-align: center;
}
.qos-stat.guaranteed {
background: #d4edda;
border: 1px solid #c3e6cb;
}
.qos-stat.burstable {
background: #fff3cd;
border: 1px solid #ffeaa7;
}
.qos-stat.besteffort {
background: #f8d7da;
border: 1px solid #f5c6cb;
}
.qos-label {
display: block;
font-weight: 600;
margin-bottom: 0.5rem;
}
.qos-value {
display: block;
font-size: 1.5rem;
font-weight: 700;
}
.resource-analysis-section {
margin-top: 2rem;
padding-top: 2rem;
border-top: 2px solid #e9ecef;
}
/* Smart Recommendations Styles */
.validation-details {
display: flex;
gap: 1rem;
margin: 0.5rem 0;
flex-wrap: wrap;
}
.detail-item {
font-size: 0.9rem;
color: #666;
}
.implementation-steps {
margin: 1rem 0;
padding: 1rem;
background: #f8f9fa;
border-radius: 6px;
border-left: 4px solid #007bff;
}
.implementation-steps ol {
margin: 0.5rem 0 0 1rem;
}
.implementation-steps li {
margin: 0.25rem 0;
}
.kubectl-commands {
margin: 1rem 0;
padding: 1rem;
background: #f8f9fa;
border-radius: 6px;
border-left: 4px solid #28a745;
}
.kubectl-commands pre {
margin: 0.5rem 0 0 0;
background: #2d3748;
color: #e2e8f0;
padding: 0.75rem;
border-radius: 4px;
overflow-x: auto;
font-size: 0.85rem;
}
.vpa-yaml {
margin: 1rem 0;
padding: 1rem;
background: #f8f9fa;
border-radius: 6px;
border-left: 4px solid #ffc107;
}
.vpa-yaml pre {
margin: 0.5rem 0 0 0;
background: #2d3748;
color: #e2e8f0;
padding: 0.75rem;
border-radius: 4px;
overflow-x: auto;
font-size: 0.85rem;
}
/* Workload Categories Styles */
.workload-list {
padding: 1rem 0;
}
.workload-item {
background: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 6px;
padding: 1rem;
margin-bottom: 0.75rem;
}
.workload-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.5rem;
}
.workload-name {
font-weight: 600;
color: #cc0000;
font-size: 1.1rem;
}
.workload-namespace {
color: #666;
font-size: 0.9rem;
}
.workload-details {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 0.75rem;
}
.workload-stat {
font-size: 0.9rem;
}
.badge {
padding: 0.25rem 0.5rem;
border-radius: 12px;
font-size: 0.8rem;
font-weight: bold;
text-transform: uppercase;
}
.badge.success {
background: #d4edda;
color: #155724;
}
.badge.info {
background: #d1ecf1;
color: #0c5460;
}
.badge.warning {
background: #fff3cd;
color: #856404;
}
.badge.error {
background: #f8d7da;
color: #721c24;
}
.badge.critical {
background: #f8d7da;
color: #721c24;
font-weight: bold;
}
/* Severity Info */
.severity-info {
background: #d1ecf1;
color: #0c5460;
}
.severity-badge.severity-info {
background: #d1ecf1;
color: #0c5460;
}
@media (max-width: 768px) {
.container {
padding: 1rem;
}
.stats-grid {
grid-template-columns: 1fr;
}
.export-section {
flex-direction: column;
align-items: stretch;
}
.filters {
flex-direction: column;
align-items: stretch;
}
.accordion-stats {
flex-direction: column;
gap: 0.25rem;
}
.pod-header {
flex-direction: column;
align-items: flex-start;
gap: 0.5rem;
}
}
</style>
</head>
<body>
<!-- Sidebar -->
<div class="sidebar" id="sidebar">
<div class="sidebar-header">
<h2>Resource Governance</h2>
</div>
<nav class="sidebar-nav">
<a href="#" class="nav-item active" data-section="dashboard">
<span class="nav-icon">🏠</span>
<span class="nav-text">Cluster Health</span>
</a>
<a href="#" class="nav-item" data-section="historical-analysis">
<span class="nav-icon">📈</span>
<span class="nav-text">Historical Resource Usage</span>
</a>
<a href="#" class="nav-item" data-section="smart-recommendations">
<span class="nav-icon">🎯</span>
<span class="nav-text">Smart Recommendations</span>
</a>
<a href="#" class="nav-item" data-section="workload-categories">
<span class="nav-icon">📊</span>
<span class="nav-text">Workload Analysis</span>
</a>
<a href="#" class="nav-item" data-section="vpa-recommendations">
<span class="nav-icon">⚙️</span>
<span class="nav-text">VPA Management</span>
</a>
</nav>
</div>
<!-- Main Content -->
<div class="main-content">
<div class="page-header">
<h1 style="color: #cc0000; margin: 1rem 0;">OpenShift Resource Governance Tool</h1>
</div>
<!-- Export Button -->
<button class="export-button" onclick="showExportModal()">Export</button>
<div class="container">
<!-- Cluster Statistics -->
<div class="stats-grid" id="statsGrid">
<div class="stat-card">
<div class="stat-number" id="totalPods">-</div>
<div class="stat-label">Total Pods</div>
</div>
<div class="stat-card">
<div class="stat-number" id="totalNamespaces">-</div>
<div class="stat-label">Namespaces</div>
</div>
<div class="stat-card">
<div class="stat-number" id="totalNodes">-</div>
<div class="stat-label">Nodes</div>
</div>
<div class="stat-card">
<div class="stat-number" id="criticalIssues">-</div>
<div class="stat-label">Critical Issues</div>
</div>
</div>
<!-- Controls -->
<!-- Export Reports -->
<!-- Historical Analysis -->
<div class="card" id="historicalCard" style="display: none;">
<h2>Historical Analysis with Prometheus</h2>
<!-- Historical analysis filters -->
<div class="filters">
<div class="filter-group">
<label for="timeRangeFilter">Period:</label>
<select id="timeRangeFilter">
<option value="1h">1 hour</option>
<option value="6h">6 hours</option>
<option value="24h" selected>24 hours</option>
<option value="7d">7 days</option>
<option value="30d">30 days</option>
</select>
</div>
<div class="filter-group">
<label for="historicalSeverityFilter">Severity:</label>
<select id="historicalSeverityFilter">
<option value="">All</option>
<option value="error">Error</option>
<option value="warning">Warning</option>
<option value="info">Info</option>
<option value="critical">Critical</option>
</select>
</div>
<button class="btn" onclick="loadHistoricalValidations()">Apply Analysis</button>
</div>
<div id="historicalSummary" class="historical-summary"></div>
<div id="historicalValidationsList"></div>
</div>
<!-- Cluster Health Dashboard -->
<div class="card" id="validationsCard" style="display: block;">
<h2>🏠 Cluster Health Overview</h2>
<!-- Cluster Health Status -->
<div class="cluster-health-section">
<div class="health-status" id="clusterHealthStatus">
<div class="health-indicator" id="healthIndicator">🟢</div>
<div class="health-text">
<h3 id="healthTitle">Cluster Healthy</h3>
<p id="healthSubtitle">All systems operational</p>
</div>
</div>
<div class="health-metrics">
<div class="metric">
<span class="metric-label">Pods:</span>
<span class="metric-value" id="totalPods">-</span>
</div>
<div class="metric">
<span class="metric-label">Namespaces:</span>
<span class="metric-value" id="totalNamespaces">-</span>
</div>
<div class="metric">
<span class="metric-label">Critical Issues:</span>
<span class="metric-value critical" id="criticalIssues">-</span>
</div>
<div class="metric">
<span class="metric-label">Overcommit:</span>
<span class="metric-value" id="overcommitStatus">-</span>
</div>
</div>
</div>
<!-- Resource Overview -->
<div class="resource-overview">
<h3>📊 Resource Consumption</h3>
<div class="resource-grid">
<div class="resource-card">
<h4>CPU</h4>
<div class="resource-bar">
<div class="resource-fill" id="cpuUsageBar" style="width: 0%"></div>
</div>
<div class="resource-text">
<span id="cpuUsageText">0 / 0 cores</span>
<span id="cpuOvercommitText">0% overcommit</span>
</div>
</div>
<div class="resource-card">
<h4>Memory</h4>
<div class="resource-bar">
<div class="resource-fill" id="memoryUsageBar" style="width: 0%"></div>
</div>
<div class="resource-text">
<span id="memoryUsageText">0 / 0 GiB</span>
<span id="memoryOvercommitText">0% overcommit</span>
</div>
</div>
</div>
</div>
<!-- Top Resource Consumers -->
<div class="top-consumers">
<h3>🥇 Top Resource Consumers</h3>
<div id="topConsumersList" class="consumers-list">
<!-- Will be populated by JavaScript -->
</div>
</div>
<!-- QoS Distribution -->
<div class="qos-distribution">
<h3>⚡ QoS Distribution</h3>
<div class="qos-stats">
<div class="qos-stat guaranteed">
<span class="qos-label">Guaranteed:</span>
<span class="qos-value" id="guaranteedCount">0</span>
</div>
<div class="qos-stat burstable">
<span class="qos-label">Burstable:</span>
<span class="qos-value" id="burstableCount">0</span>
</div>
<div class="qos-stat besteffort">
<span class="qos-label">BestEffort:</span>
<span class="qos-value" id="besteffortCount">0</span>
</div>
</div>
</div>
<!-- Resource Analysis (Original) -->
<div class="resource-analysis-section">
<h3>🔍 Detailed Resource Analysis</h3>
<!-- Filters -->
<div class="filters">
<div class="filter-group">
<label for="severityFilter">Severity:</label>
<select id="severityFilter">
<option value="">All</option>
<option value="error">Error</option>
<option value="warning">Warning</option>
<option value="critical">Critical</option>
</select>
</div>
<div class="filter-group">
<label for="pageSizeFilter">Per page:</label>
<select id="pageSizeFilter">
<option value="10">10</option>
<option value="20" selected>20</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</div>
<div class="filter-group">
<label class="checkbox-label">
<input type="checkbox" id="includeSystemNamespaces" onchange="loadValidationsByNamespace()">
Include system namespaces
</label>
</div>
<button class="btn" onclick="loadValidationsByNamespace()">Apply Filters</button>
</div>
<div id="validationsList"></div>
<div id="pagination" class="pagination"></div>
</div>
<!-- Smart Recommendations -->
<div class="card" id="smartRecommendationsCard" style="display: none;">
<h2>Smart Recommendations</h2>
<!-- Filters -->
<div class="filters">
<div class="filter-group">
<label for="recommendationPriorityFilter">Priority:</label>
<select id="recommendationPriorityFilter">
<option value="">All</option>
<option value="critical">Critical</option>
<option value="high">High</option>
<option value="medium">Medium</option>
<option value="low">Low</option>
</select>
</div>
<div class="filter-group">
<label for="recommendationTypeFilter">Type:</label>
<select id="recommendationTypeFilter">
<option value="">All</option>
<option value="resource_config">Resource Configuration</option>
<option value="vpa_activation">VPA Activation</option>
<option value="ratio_adjustment">Ratio Adjustment</option>
</select>
</div>
<button class="btn" onclick="loadSmartRecommendations()">Apply Filters</button>
</div>
<div id="smartRecommendationsList"></div>
</div>
<!-- Workload Categories -->
<div class="card" id="workloadCategoriesCard" style="display: none;">
<h2>Workload Analysis</h2>
<!-- Filters -->
<div class="filters">
<div class="filter-group">
<label for="categoryFilter">Category:</label>
<select id="categoryFilter">
<option value="">All</option>
<option value="new">New Workloads</option>
<option value="established">Established</option>
<option value="outlier">Outliers</option>
<option value="compliant">Compliant</option>
</select>
</div>
<button class="btn" onclick="loadWorkloadCategories()">Apply Filters</button>
</div>
<div id="workloadCategoriesList"></div>
</div>
<!-- VPA Management -->
<div class="card" id="vpaCard" style="display: none;">
<h2>VPA Management</h2>
<div id="vpaList"></div>
</div>
<!-- Loading -->
<div class="loading hidden" id="loading">
<p>Loading data...</p>
</div>
<!-- Error -->
<div class="error hidden" id="error"></div>
<!-- Success -->
<div class="success hidden" id="success"></div>
<!-- Export Modal -->
<div class="modal hidden" id="exportModal">
<div class="modal-content">
<div class="modal-header">
<h2>Export Report</h2>
<button class="modal-close" onclick="closeExportModal()">&times;</button>
</div>
<div class="modal-body">
<div class="export-section">
<div class="form-group">
<label for="exportFormat">Format:</label>
<select id="exportFormat">
<option value="xlsx">Excel (XLSX)</option>
<option value="csv">CSV</option>
<option value="pdf">PDF</option>
</select>
</div>
<div class="form-group">
<label for="exportNamespaces">Namespaces (optional):</label>
<input type="text" id="exportNamespaces" placeholder="Comma-separated list">
</div>
<div class="checkbox-group">
<label>
<input type="checkbox" id="includeVPA" checked> Include VPA
</label>
<label>
<input type="checkbox" id="includeValidations" checked> Include Analysis
</label>
</div>
<div class="modal-actions">
<button class="btn btn-secondary" onclick="closeExportModal()">Cancel</button>
<button class="btn" onclick="exportReport()">Export</button>
</div>
</div>
</div>
</div>
</div>
<!-- Historical Analysis Modal -->
<div class="modal hidden" id="historicalModal">
<div class="modal-content">
<div class="modal-header">
<h2 id="historicalModalTitle">Historical Analysis</h2>
<button class="modal-close" onclick="closeHistoricalModal()">&times;</button>
</div>
<div class="modal-body" id="historicalModalBody">
<div class="time-range-selector">
<button class="time-range-btn active" data-range="24h">1 Day</button>
<button class="time-range-btn" data-range="7d">7 Days</button>
<button class="time-range-btn" data-range="30d">30 Days</button>
</div>
<div id="historicalAnalysisContent">
<p>Loading historical analysis...</p>
</div>
</div>
</div>
</div>
</div>
<script>
let currentData = null;
let currentPage = 1;
let currentPageSize = 20;
let currentSeverity = '';
// Carregar status inicial
document.addEventListener('DOMContentLoaded', function() {
loadClusterStatus();
});
async function loadClusterStatus() {
showLoading();
hideMessages();
try {
const response = await fetch('/api/v1/cluster/status');
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
currentData = data;
updateStats(data);
showSuccess('Cluster status loaded successfully. Loading analysis...');
// Automatically load validations after initial scan
await loadValidationsByNamespace();
} catch (error) {
showError('Error loading cluster status: ' + error.message);
} finally {
hideLoading();
}
}
async function loadValidations() {
if (!currentData) {
showError('Carregue o status do cluster primeiro');
return;
}
showLoading();
try {
const response = await fetch('/api/v1/validations');
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
displayValidations(data.validations || data);
document.getElementById('validationsCard').style.display = 'block';
} catch (error) {
showError('Error loading analysis: ' + error.message);
} finally {
hideLoading();
}
}
async function loadValidationsByNamespace() {
showLoading();
try {
const severity = document.getElementById('severityFilter').value;
const pageSize = parseInt(document.getElementById('pageSizeFilter').value);
const includeSystem = document.getElementById('includeSystemNamespaces').checked;
currentSeverity = severity;
currentPageSize = pageSize;
currentPage = 1;
const params = new URLSearchParams({
page: currentPage,
page_size: currentPageSize,
include_system_namespaces: includeSystem
});
if (severity) {
params.append('severity', severity);
}
const response = await fetch(`/api/v1/validations/by-namespace?${params}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
displayValidationsByNamespace(data);
document.getElementById('validationsCard').style.display = 'block';
} catch (error) {
showError('Error loading analysis by namespace: ' + error.message);
} finally {
hideLoading();
}
}
async function loadPage(page) {
currentPage = page;
showLoading();
try {
const params = new URLSearchParams({
page: currentPage,
page_size: currentPageSize
});
if (currentSeverity) {
params.append('severity', currentSeverity);
}
const response = await fetch(`/api/v1/validations/by-namespace?${params}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
displayValidationsByNamespace(data);
} catch (error) {
showError('Error loading page: ' + error.message);
} finally {
hideLoading();
}
}
async function loadVPARecommendations() {
showLoading();
try {
const response = await fetch('/api/v1/vpa/recommendations');
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const recommendations = await response.json();
displayVPARecommendations(recommendations);
document.getElementById('vpaCard').style.display = 'block';
} catch (error) {
showError('Error loading VPA recommendations: ' + error.message);
} finally {
hideLoading();
}
}
async function exportReport() {
showLoading();
try {
const format = document.getElementById('exportFormat').value;
const namespaces = document.getElementById('namespaces').value;
const includeVPA = document.getElementById('includeVPA').checked;
const includeValidations = document.getElementById('includeValidations').checked;
const requestBody = {
format: format,
includeVPA: includeVPA,
includeValidations: includeValidations
};
if (namespaces.trim()) {
requestBody.namespaces = namespaces.split(',').map(n => n.trim());
}
const response = await fetch('/api/v1/export', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(requestBody)
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
// Get filename from Content-Disposition header
const contentDisposition = response.headers.get('Content-Disposition');
let filename = 'report.csv';
if (contentDisposition) {
const filenameMatch = contentDisposition.match(/filename="(.+)"/);
if (filenameMatch) {
filename = filenameMatch[1];
}
}
// Download the file
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
showSuccess(`Report exported: ${filename}`);
} catch (error) {
showError('Error exporting report: ' + error.message);
} finally {
hideLoading();
}
}
function updateStats(data) {
document.getElementById('totalPods').textContent = data.total_pods || 0;
document.getElementById('totalNamespaces').textContent = data.total_namespaces || 0;
document.getElementById('totalNodes').textContent = data.total_nodes || 0;
document.getElementById('criticalIssues').textContent = data.summary?.critical_issues || 0;
}
function displayValidations(validations) {
const container = document.getElementById('validationsList');
if (validations.length === 0) {
container.innerHTML = '<p>No validations found.</p>';
return;
}
let html = '<table class="table"><thead><tr><th>Pod</th><th>Namespace</th><th>Container</th><th>Type</th><th>Severity</th><th>Message</th></tr></thead><tbody>';
validations.forEach(validation => {
const severityClass = `severity-${validation.severity}`;
html += `
<tr>
<td>${validation.pod_name}</td>
<td>${validation.namespace}</td>
<td>${validation.container_name}</td>
<td>${validation.validation_type}</td>
<td><span class="severity-badge ${severityClass}">${validation.severity}</span></td>
<td>${validation.message}</td>
</tr>
`;
});
html += '</tbody></table>';
container.innerHTML = html;
}
function displayValidationsByNamespace(data) {
const container = document.getElementById('validationsList');
const paginationContainer = document.getElementById('pagination');
if (!data.namespaces || data.namespaces.length === 0) {
container.innerHTML = '<p>No validations found.</p>';
paginationContainer.innerHTML = '';
return;
}
let html = '';
data.namespaces.forEach((namespace, index) => {
const pods = Object.values(namespace.pods);
const totalValidations = namespace.total_validations;
const errorCount = namespace.severity_breakdown.error || 0;
const warningCount = namespace.severity_breakdown.warning || 0;
// Agrupar pods por deployment
const deployments = {};
pods.forEach(pod => {
if (pod.validations && pod.validations.length > 0) {
// Extrair nome do deployment do nome do pod
const deploymentName = pod.pod_name.split('-').slice(0, -2).join('-') || 'unknown';
if (!deployments[deploymentName]) {
deployments[deploymentName] = [];
}
deployments[deploymentName].push(pod);
}
});
html += `
<div class="accordion">
<div class="accordion-header" onclick="toggleAccordion(${index})">
<div class="accordion-title">${namespace.namespace}</div>
<div class="accordion-stats">
<div class="accordion-stat">${pods.length} pods</div>
<div class="accordion-stat">${totalValidations} analysis</div>
<div class="accordion-stat" style="color: #dc3545;">${errorCount} errors</div>
<div class="accordion-stat" style="color: #ffc107;">${warningCount} warnings</div>
</div>
<div class="accordion-actions">
<button class="btn btn-secondary" style="padding: 0.25rem 0.5rem; font-size: 0.8rem; margin-right: 0.5rem;" onclick="loadNamespaceHistoricalAnalysis('${namespace.namespace}')">Namespace Analysis</button>
<div class="accordion-arrow">▶</div>
</div>
</div>
<div class="accordion-content" id="accordion-${index}">
<div class="deployment-list">
`;
// Renderizar cada deployment
Object.keys(deployments).forEach(deploymentName => {
const deploymentPods = deployments[deploymentName];
const deploymentValidations = deploymentPods.reduce((sum, pod) => sum + pod.validations.length, 0);
const deploymentErrors = deploymentPods.reduce((sum, pod) => sum + pod.validations.filter(v => v.severity === 'error').length, 0);
const deploymentWarnings = deploymentPods.reduce((sum, pod) => sum + pod.validations.filter(v => v.severity === 'warning').length, 0);
html += `
<div class="deployment-item">
<div class="deployment-header">
<div class="deployment-title">🚀 ${deploymentName}</div>
<div class="deployment-stats">
<div class="deployment-stat">${deploymentPods.length} pods</div>
<div class="deployment-stat">${deploymentValidations} analysis</div>
<div class="deployment-stat" style="color: #dc3545;">${deploymentErrors} errors</div>
<div class="deployment-stat" style="color: #ffc107;">${deploymentWarnings} warnings</div>
</div>
<div class="deployment-actions">
<button class="btn btn-secondary" style="padding: 0.25rem 0.5rem; font-size: 0.8rem;" onclick="loadWorkloadHistoricalAnalysis('${namespace.namespace}', '${deploymentName}')">Deployment Analysis</button>
</div>
</div>
<div class="pod-list">
`;
// Renderizar pods do deployment
deploymentPods.forEach(pod => {
html += `
<div class="pod-item">
<div class="pod-header">
<div class="pod-name">${pod.pod_name}</div>
<div class="pod-validations-count">${pod.validations.length} validations</div>
</div>
<div class="validation-list">
`;
pod.validations.forEach(validation => {
const severityClass = validation.severity;
html += `
<div class="validation-item ${severityClass}">
<div class="validation-header">
<span class="severity-badge severity-${severityClass}">${validation.severity}</span>
${validation.validation_type} - ${validation.container_name}
</div>
<div class="validation-message">${validation.message}</div>
<div class="validation-recommendation">${validation.recommendation}</div>
</div>
`;
});
html += `
</div>
</div>
`;
});
html += `
</div>
</div>
`;
});
html += `
</div>
</div>
</div>
`;
});
container.innerHTML = html;
// Renderizar paginação
renderPagination(data.pagination);
}
function renderPagination(pagination) {
const container = document.getElementById('pagination');
if (!pagination || pagination.total_pages <= 1) {
container.innerHTML = '';
return;
}
let html = '';
// Botão anterior
html += `<button onclick="loadPage(${pagination.page - 1})" ${pagination.page <= 1 ? 'disabled' : ''}>Previous</button>`;
// Pages
const startPage = Math.max(1, pagination.page - 2);
const endPage = Math.min(pagination.total_pages, pagination.page + 2);
if (startPage > 1) {
html += `<button onclick="loadPage(1)">1</button>`;
if (startPage > 2) {
html += `<span>...</span>`;
}
}
for (let i = startPage; i <= endPage; i++) {
const activeClass = i === pagination.page ? 'active' : '';
html += `<button class="${activeClass}" onclick="loadPage(${i})">${i}</button>`;
}
if (endPage < pagination.total_pages) {
if (endPage < pagination.total_pages - 1) {
html += `<span>...</span>`;
}
html += `<button onclick="loadPage(${pagination.total_pages})">${pagination.total_pages}</button>`;
}
// Botão próximo
html += `<button onclick="loadPage(${pagination.page + 1})" ${pagination.page >= pagination.total_pages ? 'disabled' : ''}>Next</button>`;
// Informações da paginação
html += `<div class="pagination-info">
Page ${pagination.page} of ${pagination.total_pages}
(${pagination.total} namespaces)
</div>`;
container.innerHTML = html;
}
function toggleAccordion(index) {
const accordion = document.querySelectorAll('.accordion')[index];
const header = accordion.querySelector('.accordion-header');
const content = accordion.querySelector('.accordion-content');
const isActive = header.classList.contains('active');
// Fechar todos os acordeões
document.querySelectorAll('.accordion-header').forEach(h => h.classList.remove('active'));
document.querySelectorAll('.accordion-content').forEach(c => c.classList.remove('active'));
// Abrir o selecionado se não estava ativo
if (!isActive) {
header.classList.add('active');
content.classList.add('active');
}
}
function displayVPARecommendations(recommendations) {
const container = document.getElementById('vpaList');
if (recommendations.length === 0) {
container.innerHTML = '<p>No VPA recommendations found.</p>';
return;
}
let html = '<table class="table"><thead><tr><th>Name</th><th>Namespace</th><th>Target</th><th>Recommendations</th></tr></thead><tbody>';
recommendations.forEach(rec => {
html += `
<tr>
<td>${rec.name}</td>
<td>${rec.namespace}</td>
<td>${rec.target_ref?.kind}/${rec.target_ref?.name || 'N/A'}</td>
<td>${JSON.stringify(rec.recommendations, null, 2)}</td>
</tr>
`;
});
html += '</tbody></table>';
container.innerHTML = html;
}
function showLoading() {
document.getElementById('loading').classList.remove('hidden');
}
function hideLoading() {
document.getElementById('loading').classList.add('hidden');
}
function showError(message) {
const errorDiv = document.getElementById('error');
errorDiv.textContent = message;
errorDiv.classList.remove('hidden');
setTimeout(() => errorDiv.classList.add('hidden'), 5000);
}
function showSuccess(message) {
const successDiv = document.getElementById('success');
successDiv.textContent = message;
successDiv.classList.remove('hidden');
setTimeout(() => successDiv.classList.add('hidden'), 3000);
}
function hideMessages() {
document.getElementById('error').classList.add('hidden');
document.getElementById('success').classList.add('hidden');
}
// Historical analysis functions
async function loadHistoricalValidations() {
showLoading();
try {
const timeRange = document.getElementById('timeRangeFilter').value;
const severity = document.getElementById('historicalSeverityFilter').value;
// Carregar resumo histórico
const summaryResponse = await fetch(`/api/v1/cluster/historical-summary?time_range=${timeRange}`);
if (summaryResponse.ok) {
const summaryData = await summaryResponse.json();
displayHistoricalSummary(summaryData.summary);
}
// Load historical validations
const params = new URLSearchParams({
time_range: timeRange
});
if (severity) {
params.append('severity', severity);
}
const response = await fetch(`/api/v1/validations/historical?${params}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
displayHistoricalValidations(data);
document.getElementById('historicalCard').style.display = 'block';
} catch (error) {
showError('Error loading historical analysis: ' + error.message);
} finally {
hideLoading();
}
}
function displayHistoricalSummary(summary) {
const container = document.getElementById('historicalSummary');
if (!summary || Object.keys(summary).length === 0) {
container.innerHTML = '<p>Unable to get historical data from Prometheus.</p>';
return;
}
const cpuUtilization = summary.cpu_utilization || 0;
const memoryUtilization = summary.memory_utilization || 0;
container.innerHTML = `
<h3>Historical Cluster Summary (${summary.time_range})</h3>
<div class="historical-stats">
<div class="historical-stat">
<h4>CPU Utilization</h4>
<div class="value">${cpuUtilization.toFixed(1)}%</div>
</div>
<div class="historical-stat">
<h4>Memory Utilization</h4>
<div class="value">${memoryUtilization.toFixed(1)}%</div>
</div>
<div class="historical-stat">
<h4>CPU Usage</h4>
<div class="value">${summary.cpu_usage ? summary.cpu_usage.toFixed(3) : '0'} cores</div>
</div>
<div class="historical-stat">
<h4>Memory Usage</h4>
<div class="value">${summary.memory_usage ? (summary.memory_usage / (1024*1024*1024)).toFixed(2) : '0'} GiB</div>
</div>
</div>
`;
}
function displayHistoricalValidations(data) {
const container = document.getElementById('historicalValidationsList');
if (!data.validations || data.validations.length === 0) {
container.innerHTML = '<p>No historical validations found.</p>';
return;
}
// Filtrar por severidade se especificado
let validations = data.validations;
const severity = document.getElementById('historicalSeverityFilter').value;
if (severity) {
validations = validations.filter(v => v.severity === severity);
}
// Agrupar por namespace
const groupedByNamespace = {};
validations.forEach(validation => {
if (!groupedByNamespace[validation.namespace]) {
groupedByNamespace[validation.namespace] = [];
}
groupedByNamespace[validation.namespace].push(validation);
});
let html = '';
Object.keys(groupedByNamespace).forEach(namespace => {
const namespaceValidations = groupedByNamespace[namespace];
const errorCount = namespaceValidations.filter(v => v.severity === 'error').length;
const warningCount = namespaceValidations.filter(v => v.severity === 'warning').length;
const infoCount = namespaceValidations.filter(v => v.severity === 'info').length;
const criticalCount = namespaceValidations.filter(v => v.severity === 'critical').length;
html += `
<div class="accordion">
<div class="accordion-header" onclick="toggleAccordion(this)">
<div>
<strong>${namespace}</strong>
<span class="badge">${namespaceValidations.length} validations</span>
</div>
<div class="severity-badges">
${criticalCount > 0 ? `<span class="badge severity-critical">${criticalCount} critical</span>` : ''}
${errorCount > 0 ? `<span class="badge severity-error">${errorCount} error</span>` : ''}
${warningCount > 0 ? `<span class="badge severity-warning">${warningCount} warning</span>` : ''}
${infoCount > 0 ? `<span class="badge severity-info">${infoCount} info</span>` : ''}
</div>
<span class="accordion-icon">▼</span>
</div>
<div class="accordion-content">
${namespaceValidations.map(validation => `
<div class="historical-validation ${validation.severity}">
<div class="validation-header">
<strong>${validation.pod_name}</strong> - ${validation.container_name}
<span class="severity-badge severity-${validation.severity}">${validation.severity}</span>
</div>
<div class="validation-message">${validation.message}</div>
<div class="validation-recommendation">
<strong>Recommendation:</strong> ${validation.recommendation}
</div>
</div>
`).join('')}
</div>
</div>
`;
});
container.innerHTML = html;
}
// Historical Analysis Modal Functions
let currentNamespace = null;
let currentWorkload = null;
async function loadWorkloadHistoricalAnalysis(namespace, workload, timeRange = '24h') {
showLoading();
currentNamespace = namespace;
currentWorkload = workload;
try {
const response = await fetch(`/api/v1/namespace/${namespace}/workload/${workload}/historical-analysis?time_range=${timeRange}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
displayWorkloadHistoricalAnalysis(data);
showHistoricalModal(`${namespace}/${workload}`);
} catch (error) {
showError('Error loading historical analysis: ' + error.message);
} finally {
hideLoading();
}
}
async function loadPodHistoricalAnalysis(namespace, podName, timeRange = '24h') {
showLoading();
currentNamespace = namespace;
currentWorkload = podName; // For backward compatibility
try {
const response = await fetch(`/api/v1/namespace/${namespace}/pod/${podName}/historical-analysis?time_range=${timeRange}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
displayPodHistoricalAnalysis(data);
showHistoricalModal(`${namespace}/${podName}`);
} catch (error) {
showError('Error loading historical analysis: ' + error.message);
} finally {
hideLoading();
}
}
async function loadNamespaceHistoricalAnalysis(namespace) {
showLoading();
try {
const timeRange = '24h'; // Default time range
const response = await fetch(`/api/v1/namespace/${namespace}/historical-analysis?time_range=${timeRange}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
displayNamespaceHistoricalAnalysis(data);
showHistoricalModal(namespace);
} catch (error) {
showError('Error loading historical analysis: ' + error.message);
} finally {
hideLoading();
}
}
function displayWorkloadHistoricalAnalysis(data) {
const container = document.getElementById('historicalAnalysisContent');
if (data.analysis.error) {
container.innerHTML = `
<div class="error">
<h3>Error loading historical data</h3>
<p>${data.analysis.error}</p>
</div>
`;
return;
}
const analysis = data.analysis;
const recommendations = analysis.recommendations || [];
let html = `
<div class="historical-summary">
<h3>Workload: ${analysis.workload}</h3>
<p><strong>Namespace:</strong> ${analysis.namespace}</p>
<p><strong>Time Range:</strong> ${analysis.time_range}</p>
</div>
<div class="historical-stats">
<div class="historical-stat">
<h4>CPU Usage</h4>
<div class="value">${analysis.cpu_usage.toFixed(3)} cores</div>
</div>
<div class="historical-stat">
<h4>Memory Usage</h4>
<div class="value">${(analysis.memory_usage / (1024*1024*1024)).toFixed(2)} GiB</div>
</div>
<div class="historical-stat">
<h4>CPU Utilization</h4>
<div class="value" style="color: ${analysis.cpu_utilization > 80 ? '#dc3545' : analysis.cpu_utilization < 20 ? '#28a745' : '#007bff'}">${analysis.cpu_utilization.toFixed(1)}%</div>
</div>
<div class="historical-stat">
<h4>Memory Utilization</h4>
<div class="value" style="color: ${analysis.memory_utilization > 80 ? '#dc3545' : analysis.memory_utilization < 20 ? '#28a745' : '#007bff'}">${analysis.memory_utilization.toFixed(1)}%</div>
</div>
<div class="historical-stat">
<h4>CPU Requests</h4>
<div class="value">${analysis.cpu_requests.toFixed(3)} cores</div>
</div>
<div class="historical-stat">
<h4>Memory Requests</h4>
<div class="value">${(analysis.memory_requests / (1024*1024*1024)).toFixed(2)} GiB</div>
</div>
<div class="historical-stat">
<h4>CPU Limits</h4>
<div class="value">${analysis.cpu_limits.toFixed(3)} cores</div>
</div>
<div class="historical-stat">
<h4>Memory Limits</h4>
<div class="value">${(analysis.memory_limits / (1024*1024*1024)).toFixed(2)} GiB</div>
</div>
</div>
`;
if (recommendations.length > 0) {
html += `
<div class="historical-summary">
<h3>Recommendations</h3>
`;
recommendations.forEach(rec => {
const severityClass = rec.severity === 'error' ? 'error' : rec.severity === 'warning' ? 'warning' : 'info';
html += `
<div class="recommendation ${severityClass}">
<span class="badge ${severityClass}">${rec.severity}</span>
<strong>${rec.message}</strong>
<p>${rec.recommendation}</p>
</div>
`;
});
html += `</div>`;
}
container.innerHTML = html;
}
function displayPodHistoricalAnalysis(data) {
const container = document.getElementById('historicalAnalysisContent');
if (data.analysis.error) {
container.innerHTML = `
<div class="error">
<h3>Error loading historical data</h3>
<p>${data.analysis.error}</p>
</div>
`;
return;
}
const analysis = data.analysis;
const recommendations = analysis.recommendations || [];
let html = `
<div class="historical-summary">
<h3>Pod: ${analysis.pod_name}</h3>
<p><strong>Namespace:</strong> ${analysis.namespace}</p>
<p><strong>Time Range:</strong> ${analysis.time_range}</p>
<p><strong>Containers:</strong> ${analysis.container_count}</p>
</div>
<div class="historical-stats">
<div class="historical-stat">
<h4>CPU Usage</h4>
<div class="value">${analysis.cpu_usage.toFixed(3)} cores</div>
</div>
<div class="historical-stat">
<h4>Memory Usage</h4>
<div class="value">${(analysis.memory_usage / (1024*1024*1024)).toFixed(2)} GiB</div>
</div>
<div class="historical-stat">
<h4>CPU Utilization</h4>
<div class="value" style="color: ${analysis.cpu_utilization > 80 ? '#dc3545' : analysis.cpu_utilization < 20 ? '#28a745' : '#007bff'}">${analysis.cpu_utilization.toFixed(1)}%</div>
</div>
<div class="historical-stat">
<h4>Memory Utilization</h4>
<div class="value" style="color: ${analysis.memory_utilization > 80 ? '#dc3545' : analysis.memory_utilization < 20 ? '#28a745' : '#007bff'}">${analysis.memory_utilization.toFixed(1)}%</div>
</div>
</div>
`;
if (recommendations.length > 0) {
html += `
<div class="historical-summary">
<h3>Recommendations</h3>
`;
recommendations.forEach(rec => {
const severityClass = rec.severity;
html += `
<div class="historical-validation ${severityClass}">
<div class="validation-header">
<span class="severity-badge severity-${severityClass}">${severityClass}</span>
${rec.message}
</div>
<div class="validation-recommendation">
<strong>Recommendation:</strong> ${rec.recommendation}
</div>
</div>
`;
});
html += `</div>`;
} else {
html += `
<div class="historical-summary">
<h3>Recommendations</h3>
<p>No specific recommendations at this time. Resource utilization appears to be within normal ranges.</p>
</div>
`;
}
container.innerHTML = html;
}
function displayNamespaceHistoricalAnalysis(data) {
const container = document.getElementById('historicalAnalysisContent');
if (data.analysis.error) {
container.innerHTML = `
<div class="error">
<h3>Error loading historical data</h3>
<p>${data.analysis.error}</p>
</div>
`;
return;
}
const analysis = data.analysis;
const recommendations = analysis.recommendations || [];
let html = `
<div class="historical-summary">
<h3>Namespace: ${analysis.namespace}</h3>
<p><strong>Time Range:</strong> ${analysis.time_range}</p>
<p><strong>Pods:</strong> ${analysis.pod_count}</p>
</div>
<div class="historical-stats">
<div class="historical-stat">
<h4>CPU Usage</h4>
<div class="value">${analysis.cpu_usage.toFixed(3)} cores</div>
</div>
<div class="historical-stat">
<h4>Memory Usage</h4>
<div class="value">${(analysis.memory_usage / (1024*1024*1024)).toFixed(2)} GiB</div>
</div>
<div class="historical-stat">
<h4>CPU Utilization</h4>
<div class="value" style="color: ${analysis.cpu_utilization > 80 ? '#dc3545' : analysis.cpu_utilization < 20 ? '#28a745' : '#007bff'}">${analysis.cpu_utilization.toFixed(1)}%</div>
</div>
<div class="historical-stat">
<h4>Memory Utilization</h4>
<div class="value" style="color: ${analysis.memory_utilization > 80 ? '#dc3545' : analysis.memory_utilization < 20 ? '#28a745' : '#007bff'}">${analysis.memory_utilization.toFixed(1)}%</div>
</div>
</div>
`;
if (recommendations.length > 0) {
html += `
<div class="historical-summary">
<h3>Recommendations</h3>
`;
recommendations.forEach(rec => {
const severityClass = rec.severity;
html += `
<div class="historical-validation ${severityClass}">
<div class="validation-header">
<span class="severity-badge severity-${severityClass}">${severityClass}</span>
${rec.message}
</div>
<div class="validation-recommendation">
<strong>Recommendation:</strong> ${rec.recommendation}
</div>
</div>
`;
});
html += `</div>`;
} else {
html += `
<div class="historical-summary">
<h3>Recommendations</h3>
<p>No specific recommendations at this time. Resource utilization appears to be within normal ranges.</p>
</div>
`;
}
container.innerHTML = html;
}
function showHistoricalModal(namespace) {
document.getElementById('historicalModalTitle').textContent = `Historical Analysis - ${namespace}`;
const modal = document.getElementById('historicalModal');
modal.classList.remove('hidden');
modal.classList.add('show');
}
function closeHistoricalModal() {
const modal = document.getElementById('historicalModal');
modal.classList.remove('show');
modal.classList.add('hidden');
}
// Close modal when clicking outside
document.getElementById('historicalModal').addEventListener('click', function(e) {
if (e.target === this) {
closeHistoricalModal();
}
});
// Time range button event listeners
document.addEventListener('click', function(e) {
if (e.target.classList.contains('time-range-btn')) {
const timeRange = e.target.getAttribute('data-range');
// Update active button
document.querySelectorAll('.time-range-btn').forEach(btn => {
btn.classList.remove('active');
});
e.target.classList.add('active');
// Reload data with new time range
if (currentNamespace && currentWorkload) {
loadWorkloadHistoricalAnalysis(currentNamespace, currentWorkload, timeRange);
}
}
});
// Export Modal Functions
function showExportModal() {
document.getElementById('exportModal').classList.add('show');
}
function closeExportModal() {
document.getElementById('exportModal').classList.remove('show');
}
// Close export modal when clicking outside
document.getElementById('exportModal').addEventListener('click', function(e) {
if (e.target === this) {
closeExportModal();
}
});
// Cluster Health Functions
async function loadClusterHealth() {
showLoading();
try {
// Load cluster health data
const healthResponse = await fetch('/api/cluster-health');
if (!healthResponse.ok) {
throw new Error(`HTTP ${healthResponse.status}: ${healthResponse.statusText}`);
}
const healthData = await healthResponse.json();
// Load QoS classification
const qosResponse = await fetch('/api/qos-classification');
if (!qosResponse.ok) {
throw new Error(`HTTP ${qosResponse.status}: ${qosResponse.statusText}`);
}
const qosData = await qosResponse.json();
// Update cluster health display
updateClusterHealthDisplay(healthData, qosData);
// Also load detailed validations
loadValidationsByNamespace();
} catch (error) {
showError('Error loading cluster health: ' + error.message);
} finally {
hideLoading();
}
}
function updateClusterHealthDisplay(healthData, qosData) {
// Update health status
const healthIndicator = document.getElementById('healthIndicator');
const healthTitle = document.getElementById('healthTitle');
const healthSubtitle = document.getElementById('healthSubtitle');
if (healthData.overall_health === 'Critical') {
healthIndicator.textContent = '🔴';
healthTitle.textContent = 'Cluster Critical';
healthSubtitle.textContent = 'Immediate attention required';
} else if (healthData.overall_health === 'Warning') {
healthIndicator.textContent = '🟡';
healthTitle.textContent = 'Cluster Warning';
healthSubtitle.textContent = 'Some issues detected';
} else {
healthIndicator.textContent = '🟢';
healthTitle.textContent = 'Cluster Healthy';
healthSubtitle.textContent = 'All systems operational';
}
// Update metrics
document.getElementById('totalPods').textContent = healthData.total_pods;
document.getElementById('totalNamespaces').textContent = healthData.total_namespaces;
document.getElementById('criticalIssues').textContent = healthData.critical_issues;
// Update overcommit status
const cpuOvercommit = healthData.cpu_overcommit_percentage;
const memoryOvercommit = healthData.memory_overcommit_percentage;
const maxOvercommit = Math.max(cpuOvercommit, memoryOvercommit);
let overcommitText = '';
if (maxOvercommit > 150) {
overcommitText = '🔴 Critical';
} else if (maxOvercommit > 120) {
overcommitText = '🟡 High';
} else {
overcommitText = '🟢 Normal';
}
document.getElementById('overcommitStatus').textContent = overcommitText;
// Update resource consumption
updateResourceConsumption(healthData);
// Update top consumers
updateTopConsumers(healthData.top_resource_consumers);
// Update QoS distribution
updateQoSDistribution(qosData.distribution);
}
function updateResourceConsumption(healthData) {
// CPU
const cpuUsagePercent = (healthData.cluster_cpu_requests / healthData.cluster_cpu_capacity) * 100;
document.getElementById('cpuUsageBar').style.width = Math.min(cpuUsagePercent, 100) + '%';
document.getElementById('cpuUsageText').textContent =
`${healthData.cluster_cpu_requests.toFixed(1)} / ${healthData.cluster_cpu_capacity.toFixed(1)} cores`;
document.getElementById('cpuOvercommitText').textContent =
`${healthData.cpu_overcommit_percentage.toFixed(1)}% overcommit`;
// Memory
const memoryUsagePercent = (healthData.cluster_memory_requests / healthData.cluster_memory_capacity) * 100;
document.getElementById('memoryUsageBar').style.width = Math.min(memoryUsagePercent, 100) + '%';
document.getElementById('memoryUsageText').textContent =
`${healthData.cluster_memory_requests.toFixed(1)} / ${healthData.cluster_memory_capacity.toFixed(1)} GiB`;
document.getElementById('memoryOvercommitText').textContent =
`${healthData.memory_overcommit_percentage.toFixed(1)}% overcommit`;
}
function updateTopConsumers(consumers) {
const container = document.getElementById('topConsumersList');
container.innerHTML = '';
consumers.slice(0, 5).forEach((consumer, index) => {
const item = document.createElement('div');
item.className = 'consumer-item';
const rank = ['🥇', '🥈', '🥉', '4⃣', '5⃣'][index];
item.innerHTML = `
<div class="consumer-info">
<span class="consumer-rank">${rank}</span>
<div>
<div class="consumer-name">${consumer.name}</div>
<div class="consumer-namespace">${consumer.namespace}</div>
</div>
</div>
<div class="consumer-resources">
<div>CPU: ${consumer.cpu_requests.toFixed(1)} cores</div>
<div>Memory: ${consumer.memory_requests.toFixed(1)} GiB</div>
<div class="qos-badge qos-${consumer.qos_class.toLowerCase()}">${consumer.qos_class}</div>
</div>
`;
container.appendChild(item);
});
}
function updateQoSDistribution(distribution) {
document.getElementById('guaranteedCount').textContent = distribution.Guaranteed || 0;
document.getElementById('burstableCount').textContent = distribution.Burstable || 0;
document.getElementById('besteffortCount').textContent = distribution.BestEffort || 0;
}
// Smart Recommendations Functions
async function loadSmartRecommendations() {
showLoading();
try {
const priority = document.getElementById('recommendationPriorityFilter').value;
const type = document.getElementById('recommendationTypeFilter').value;
const params = new URLSearchParams();
if (priority) params.append('priority', priority);
const response = await fetch(`/api/v1/smart-recommendations?${params}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
displaySmartRecommendations(data, type);
document.getElementById('smartRecommendationsCard').style.display = 'block';
} catch (error) {
showError('Error loading smart recommendations: ' + error.message);
} finally {
hideLoading();
}
}
function displaySmartRecommendations(data, typeFilter) {
const container = document.getElementById('smartRecommendationsList');
if (!data.recommendations || data.recommendations.length === 0) {
container.innerHTML = '<p>No smart recommendations found.</p>';
return;
}
let recommendations = data.recommendations;
// Filter by type if specified
if (typeFilter) {
recommendations = recommendations.filter(r => r.recommendation_type === typeFilter);
}
if (recommendations.length === 0) {
container.innerHTML = '<p>No recommendations match the selected filters.</p>';
return;
}
let html = '';
recommendations.forEach(rec => {
const priorityClass = `severity-${rec.priority}`;
const confidenceLevel = rec.confidence_level ? `${(rec.confidence_level * 100).toFixed(0)}%` : 'N/A';
html += `
<div class="validation-item ${rec.priority}">
<div class="validation-header">
<span class="severity-badge ${priorityClass}">${rec.priority}</span>
<strong>${rec.title}</strong>
<span class="badge">${rec.recommendation_type}</span>
</div>
<div class="validation-message">
<strong>Workload:</strong> ${rec.workload_name} (${rec.namespace})
</div>
<div class="validation-recommendation">
<strong>Description:</strong> ${rec.description}
</div>
<div class="validation-details">
<div class="detail-item">
<strong>Confidence:</strong> ${confidenceLevel}
</div>
<div class="detail-item">
<strong>Impact:</strong> ${rec.estimated_impact || 'N/A'}
</div>
</div>
`;
if (rec.implementation_steps && rec.implementation_steps.length > 0) {
html += `
<div class="implementation-steps">
<strong>Implementation Steps:</strong>
<ol>
${rec.implementation_steps.map(step => `<li>${step}</li>`).join('')}
</ol>
</div>
`;
}
if (rec.kubectl_commands && rec.kubectl_commands.length > 0) {
html += `
<div class="kubectl-commands">
<strong>Kubectl Commands:</strong>
<pre><code>${rec.kubectl_commands.join('\n')}</code></pre>
</div>
`;
}
if (rec.vpa_yaml) {
html += `
<div class="vpa-yaml">
<strong>VPA Configuration:</strong>
<pre><code>${rec.vpa_yaml}</code></pre>
</div>
`;
}
html += '</div>';
});
container.innerHTML = html;
}
// Workload Categories Functions
async function loadWorkloadCategories() {
showLoading();
try {
const category = document.getElementById('categoryFilter').value;
const params = new URLSearchParams();
if (category) params.append('category', category);
const response = await fetch(`/api/v1/workload-categories?${params}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
displayWorkloadCategories(data);
document.getElementById('workloadCategoriesCard').style.display = 'block';
} catch (error) {
showError('Error loading workload categories: ' + error.message);
} finally {
hideLoading();
}
}
function displayWorkloadCategories(data) {
const container = document.getElementById('workloadCategoriesList');
if (!data.categories || Object.keys(data.categories).length === 0) {
container.innerHTML = '<p>No workload categories found.</p>';
return;
}
let html = `
<div class="stats-grid">
<div class="stat-card">
<div class="stat-number">${data.total_workloads}</div>
<div class="stat-label">Total Workloads</div>
</div>
</div>
`;
Object.keys(data.categories).forEach(categoryType => {
const category = data.categories[categoryType];
const categoryClass = categoryType === 'outlier' ? 'error' :
categoryType === 'new' ? 'warning' :
categoryType === 'compliant' ? 'success' : 'info';
html += `
<div class="accordion">
<div class="accordion-header" onclick="toggleAccordion(this)">
<div class="accordion-title">
<span class="badge ${categoryClass}">${categoryType}</span>
${category.count} workloads
</div>
<div class="accordion-stats">
<div class="accordion-stat">Avg Priority: ${category.average_priority_score?.toFixed(1) || 'N/A'}</div>
<div class="accordion-stat">VPA Candidates: ${category.workloads.filter(w => w.vpa_candidate).length}</div>
</div>
<div class="accordion-arrow">▶</div>
</div>
<div class="accordion-content">
<div class="workload-list">
`;
category.workloads.forEach(workload => {
const impactClass = workload.estimated_impact === 'critical' ? 'critical' :
workload.estimated_impact === 'high' ? 'error' :
workload.estimated_impact === 'medium' ? 'warning' : 'info';
html += `
<div class="workload-item">
<div class="workload-header">
<div class="workload-name">${workload.name}</div>
<div class="workload-namespace">${workload.namespace}</div>
</div>
<div class="workload-details">
<div class="workload-stat">
<strong>Priority Score:</strong> ${workload.priority_score}/10
</div>
<div class="workload-stat">
<strong>Impact:</strong>
<span class="badge ${impactClass}">${workload.estimated_impact}</span>
</div>
<div class="workload-stat">
<strong>VPA Candidate:</strong>
${workload.vpa_candidate ? '✅ Yes' : '❌ No'}
</div>
</div>
</div>
`;
});
html += `
</div>
</div>
</div>
`;
});
container.innerHTML = html;
}
// Navigation Functions
function showSection(sectionName) {
// Hide all sections
document.querySelectorAll('.card').forEach(card => {
card.style.display = 'none';
});
// Remove active class from all nav items
document.querySelectorAll('.nav-item').forEach(item => {
item.classList.remove('active');
});
// Show selected section
const sectionMap = {
'dashboard': 'validationsCard',
'historical-analysis': 'historicalCard',
'smart-recommendations': 'smartRecommendationsCard',
'workload-categories': 'workloadCategoriesCard',
'vpa-recommendations': 'vpaCard'
};
const cardId = sectionMap[sectionName];
if (cardId) {
document.getElementById(cardId).style.display = 'block';
}
// Add active class to clicked nav item
document.querySelector(`[data-section="${sectionName}"]`).classList.add('active');
// Load data for the section
switch(sectionName) {
case 'dashboard':
loadClusterHealth();
break;
case 'historical-analysis':
loadHistoricalValidations();
break;
case 'smart-recommendations':
loadSmartRecommendations();
break;
case 'workload-categories':
loadWorkloadCategories();
break;
case 'vpa-recommendations':
loadVPARecommendations();
break;
}
}
// Add click handlers for navigation
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('.nav-item').forEach(item => {
item.addEventListener('click', function(e) {
e.preventDefault();
const section = this.getAttribute('data-section');
showSection(section);
});
});
});
</script>
</div> <!-- Close main-content -->
</div> <!-- Close container -->
</body>
</html>