Files
openshift-resource-governance/app/static/index.html

2296 lines
90 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.
This file contains Unicode characters that might be confused with other characters. 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-divider {
height: 1px;
background-color: #34495e;
margin: 0.5rem 1rem;
}
.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: #27ae60;
color: white;
border: none;
padding: 0.75rem 1.5rem;
border-radius: 6px;
cursor: pointer;
font-weight: 500;
z-index: 1001;
transition: background 0.3s ease;
}
.export-button:hover {
background: #229954;
}
/* Header */
.header {
background: white;
padding: 2rem;
border-bottom: 1px solid #e0e0e0;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.header h1 {
font-size: 2rem;
font-weight: 700;
color: #2c3e50;
margin-bottom: 0.5rem;
}
/* Content */
.content {
padding: 2rem;
}
/* Cards */
.card {
background: white;
border-radius: 8px;
padding: 1.5rem;
margin-bottom: 1.5rem;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
border: 1px solid #e0e0e0;
}
.card h2 {
font-size: 1.5rem;
font-weight: 600;
color: #2c3e50;
margin-bottom: 1rem;
}
.card h3 {
font-size: 1.2rem;
font-weight: 600;
color: #34495e;
margin-bottom: 0.75rem;
}
/* Metrics Grid */
.metrics-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
margin-bottom: 2rem;
}
.metric-card {
background: #f8f9fa;
padding: 1.5rem;
border-radius: 8px;
text-align: center;
border-left: 4px solid #3498db;
}
.metric-value {
font-size: 2rem;
font-weight: 700;
color: #2c3e50;
margin-bottom: 0.5rem;
}
.metric-label {
font-size: 0.9rem;
color: #7f8c8d;
font-weight: 500;
}
.metric-card.warning {
border-left-color: #f39c12;
}
.metric-card.error {
border-left-color: #e74c3c;
}
.metric-card.success {
border-left-color: #27ae60;
}
.info-icon {
cursor: pointer;
color: #3498db;
font-size: 14px;
margin-left: 5px;
display: inline-block;
}
.info-icon:hover {
color: #2980b9;
}
.overcommit-details {
padding: 1rem;
}
.overcommit-details h3 {
color: #2c3e50;
margin-bottom: 1rem;
border-bottom: 2px solid #3498db;
padding-bottom: 0.5rem;
}
.metric-detail {
margin: 0.75rem 0;
padding: 0.5rem;
background: #f8f9fa;
border-left: 3px solid #3498db;
border-radius: 4px;
}
.metric-detail strong {
color: #2c3e50;
}
/* Status Overview */
.status-overview {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 2rem;
padding: 1.5rem;
background: #f8f9fa;
border-radius: 8px;
}
.status-icon {
font-size: 3rem;
}
.status-content h3 {
margin-bottom: 0.5rem;
font-size: 1.5rem;
}
.status-content p {
color: #7f8c8d;
font-size: 1rem;
}
/* Modal Styles */
.modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.5);
}
.modal-content {
background-color: #fefefe;
margin: 5% auto;
padding: 0;
border: 1px solid #888;
border-radius: 8px;
width: 80%;
max-width: 1000px;
max-height: 80vh;
overflow-y: auto;
}
.modal-header {
background: #f8f9fa;
padding: 20px;
border-bottom: 1px solid #dee2e6;
border-radius: 8px 8px 0 0;
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-header h2 {
margin: 0;
color: #495057;
}
.modal-body {
padding: 20px;
}
.close {
color: #aaa;
float: right;
font-size: 28px;
font-weight: bold;
cursor: pointer;
}
.close:hover {
color: black;
}
/* Namespace Details Styles */
.namespace-details {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
.namespace-summary {
background: #f8f9fa;
padding: 15px;
border-radius: 5px;
margin-bottom: 20px;
}
.namespace-summary ul {
margin: 10px 0;
padding-left: 20px;
}
.pod-detail {
border: 1px solid #dee2e6;
border-radius: 5px;
margin: 15px 0;
padding: 15px;
background: #fff;
}
.pod-detail h5 {
color: #495057;
margin-bottom: 10px;
border-bottom: 1px solid #dee2e6;
padding-bottom: 5px;
}
.containers-detail {
margin: 15px 0;
}
.container-detail {
background: #f8f9fa;
padding: 10px;
margin: 10px 0;
border-radius: 3px;
border-left: 4px solid #007bff;
}
.validations-detail {
margin: 15px 0;
}
.validation-item {
padding: 10px;
margin: 8px 0;
border-radius: 3px;
border-left: 4px solid #6c757d;
}
.validation-item.severity-error {
background: #f8d7da;
border-left-color: #dc3545;
}
.validation-item.severity-warning {
background: #fff3cd;
border-left-color: #ffc107;
}
.validation-item.severity-info {
background: #d1ecf1;
border-left-color: #17a2b8;
}
.validation-item p {
margin: 5px 0;
}
.validation-item strong {
color: #495057;
}
.validation-item em {
color: #6c757d;
font-size: 0.9em;
}
/* Fix Modal Styles */
.fix-details {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
.fix-info {
background: #f8f9fa;
padding: 15px;
border-radius: 5px;
margin: 15px 0;
}
.fix-info ul {
margin: 10px 0;
padding-left: 20px;
}
.fix-actions {
margin-top: 20px;
text-align: right;
}
.workload-selector {
margin-bottom: 20px;
padding: 15px;
background: #f8f9fa;
border-radius: 8px;
}
.workload-selector select {
margin: 0 10px;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
.chart-container {
margin: 20px 0;
padding: 15px;
border: 1px solid #ddd;
border-radius: 8px;
background: white;
}
.chart-container canvas {
border: 1px solid #eee;
border-radius: 4px;
}
.cluster-stats {
display: flex;
gap: 20px;
margin-bottom: 30px;
}
.stat-card {
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 8px;
padding: 20px;
text-align: center;
flex: 1;
}
.stat-card h4 {
margin: 0 0 10px 0;
color: #495057;
font-size: 14px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.stat-value {
font-size: 24px;
font-weight: bold;
color: #007bff;
}
.metrics-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 30px;
}
.metric-section {
background: white;
border: 1px solid #dee2e6;
border-radius: 8px;
padding: 20px;
}
.metric-section h4 {
margin: 0 0 20px 0;
color: #495057;
border-bottom: 2px solid #e9ecef;
padding-bottom: 10px;
}
.metric-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 0;
border-bottom: 1px solid #f8f9fa;
}
.metric-row:last-child {
border-bottom: none;
}
.metric-label {
font-weight: 500;
color: #6c757d;
min-width: 120px;
}
.metric-value {
font-weight: bold;
color: #212529;
font-family: 'Courier New', monospace;
}
.metric-percent {
color: #6c757d;
font-size: 12px;
font-style: italic;
}
@media (max-width: 768px) {
.metrics-grid {
grid-template-columns: 1fr;
}
.cluster-stats {
flex-direction: column;
}
}
/* Problem Summary Table */
.problem-summary {
margin-bottom: 2rem;
}
.filters {
display: flex;
gap: 1rem;
margin-bottom: 1rem;
align-items: center;
flex-wrap: wrap;
}
.filter-group {
display: flex;
align-items: center;
gap: 0.5rem;
}
.filter-group label {
font-weight: 500;
color: #34495e;
}
.filter-group select,
.filter-group input {
padding: 0.5rem;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 0.9rem;
}
.filter-checkbox {
display: flex;
align-items: center;
gap: 0.5rem;
}
.filter-checkbox input[type="checkbox"] {
width: 16px;
height: 16px;
}
.apply-filters-btn {
background: #3498db;
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 4px;
cursor: pointer;
font-weight: 500;
transition: background 0.3s ease;
}
.apply-filters-btn:hover {
background: #2980b9;
}
/* Table Styles */
.table-container {
overflow-x: auto;
border-radius: 8px;
border: 1px solid #e0e0e0;
}
table {
width: 100%;
border-collapse: collapse;
background: white;
}
th {
background: #f8f9fa;
padding: 1rem;
text-align: left;
font-weight: 600;
color: #2c3e50;
border-bottom: 2px solid #e0e0e0;
}
td {
padding: 1rem;
border-bottom: 1px solid #f0f0f0;
}
tr:hover {
background: #f8f9fa;
}
/* Severity Badges */
.severity-badge {
padding: 0.25rem 0.75rem;
border-radius: 12px;
font-size: 0.8rem;
font-weight: 600;
text-transform: uppercase;
}
.severity-critical {
background: #ffebee;
color: #c62828;
}
.severity-error {
background: #ffebee;
color: #d32f2f;
}
.severity-warning {
background: #fff3e0;
color: #f57c00;
}
.severity-info {
background: #e3f2fd;
color: #1976d2;
}
/* Action Buttons */
.action-buttons {
display: flex;
gap: 0.5rem;
}
.btn {
padding: 0.5rem 1rem;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.9rem;
font-weight: 500;
transition: all 0.3s ease;
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
.btn-primary {
background: #3498db;
color: white;
}
.btn-primary:hover {
background: #2980b9;
}
.btn-success {
background: #27ae60;
color: white;
}
.btn-success:hover {
background: #229954;
}
.btn-warning {
background: #f39c12;
color: white;
}
.btn-warning:hover {
background: #e67e22;
}
.btn-sm {
padding: 0.25rem 0.75rem;
font-size: 0.8rem;
}
/* Quick Actions */
.quick-actions {
display: flex;
gap: 1rem;
margin-top: 2rem;
flex-wrap: wrap;
}
.quick-action-btn {
background: #34495e;
color: white;
border: none;
padding: 1rem 1.5rem;
border-radius: 8px;
cursor: pointer;
font-weight: 500;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 0.75rem;
}
.quick-action-btn:hover {
background: #2c3e50;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
/* Loading States */
.loading {
text-align: center;
padding: 2rem;
color: #7f8c8d;
}
.loading::after {
content: '';
display: inline-block;
width: 20px;
height: 20px;
border: 2px solid #f3f3f3;
border-top: 2px solid #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-left: 0.5rem;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Responsive */
@media (max-width: 768px) {
.sidebar {
transform: translateX(-100%);
}
.main-content {
margin-left: 0;
}
.metrics-grid {
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
}
.filters {
flex-direction: column;
align-items: stretch;
}
.quick-actions {
flex-direction: column;
}
}
/* PromQL Queries Styles */
.promql-queries {
margin-top: 30px;
padding: 20px;
background: #f8f9fa;
border-radius: 8px;
border: 1px solid #dee2e6;
}
.promql-queries h4 {
color: #495057;
margin-bottom: 15px;
font-size: 1.1rem;
}
.promql-queries h5 {
color: #6c757d;
margin: 20px 0 10px 0;
font-size: 1rem;
font-weight: 600;
}
.query-section {
margin-bottom: 25px;
}
.query-item {
margin-bottom: 15px;
}
.query-item label {
display: block;
font-weight: 600;
color: #495057;
margin-bottom: 5px;
font-size: 0.9rem;
}
.query-box {
display: flex;
align-items: center;
background: white;
border: 1px solid #ced4da;
border-radius: 4px;
padding: 8px 12px;
gap: 10px;
}
.query-box code {
flex: 1;
background: none;
border: none;
padding: 0;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 0.85rem;
color: #e83e8c;
word-break: break-all;
white-space: pre-wrap;
}
.copy-btn {
background: #007bff;
color: white;
border: none;
border-radius: 4px;
padding: 6px 12px;
font-size: 0.8rem;
cursor: pointer;
transition: background-color 0.2s;
white-space: nowrap;
}
.copy-btn:hover {
background: #0056b3;
}
.copy-btn:active {
background: #004085;
}
</style>
</head>
<body>
<!-- Sidebar -->
<div class="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">Dashboard</span>
</a>
<div class="nav-divider"></div>
<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="vpa-management">
<span class="nav-icon">⚙️</span>
<span class="nav-text">VPA Management</span>
</a>
<a href="#" class="nav-item" data-section="historical-analysis">
<span class="nav-icon">📈</span>
<span class="nav-text">Historical Analysis</span>
</a>
</nav>
</div>
<!-- Main Content -->
<div class="main-content">
<!-- Export Button -->
<button class="export-button" onclick="exportData()">Export</button>
<!-- Header -->
<div class="header">
<h1>OpenShift Resource Governance Tool</h1>
</div>
<!-- Content -->
<div class="content">
<!-- Dashboard Section -->
<div id="dashboardSection">
<!-- Cluster Overview -->
<div class="card">
<h2>🎯 Resource Governance Dashboard</h2>
<!-- Status Overview -->
<div class="status-overview">
<div class="status-icon" id="clusterStatusIcon">🟢</div>
<div class="status-content">
<h3 id="clusterStatus">Cluster Healthy</h3>
<p id="clusterStatusMessage">All systems operational</p>
</div>
</div>
<!-- Metrics Grid -->
<div class="metrics-grid">
<div class="metric-card">
<div class="metric-value" id="totalPods">-</div>
<div class="metric-label">Total Pods</div>
</div>
<div class="metric-card">
<div class="metric-value" id="totalNamespaces">-</div>
<div class="metric-label">Namespaces</div>
</div>
<div class="metric-card">
<div class="metric-value" id="totalNodes">-</div>
<div class="metric-label">Nodes</div>
</div>
<div class="metric-card error" id="criticalIssuesCard">
<div class="metric-value" id="criticalIssues">-</div>
<div class="metric-label">Critical Issues</div>
</div>
</div>
<!-- Overcommit Summary -->
<div class="card">
<h3>📊 Cluster Overcommit Summary</h3>
<div class="metrics-grid">
<div class="metric-card">
<div class="metric-value" id="cpuOvercommit">-</div>
<div class="metric-label">CPU Overcommit <span class="info-icon" onclick="showOvercommitDetails('cpu')"></span></div>
</div>
<div class="metric-card">
<div class="metric-value" id="memoryOvercommit">-</div>
<div class="metric-label">Memory Overcommit <span class="info-icon" onclick="showOvercommitDetails('memory')"></span></div>
</div>
<div class="metric-card">
<div class="metric-value" id="namespacesInOvercommit">-</div>
<div class="metric-label">Namespaces in Overcommit</div>
</div>
<div class="metric-card">
<div class="metric-value" id="resourceUtilization">-</div>
<div class="metric-label">Resource Utilization <span class="info-icon" onclick="showResourceUtilizationDetails()"></span></div>
</div>
</div>
</div>
</div>
<!-- Problem Summary -->
<div class="card problem-summary">
<h2>🔍 Problem Summary</h2>
<p style="margin-bottom: 1.5rem; color: #7f8c8d;">Identify namespaces with resource configuration issues and take action</p>
<!-- Filters -->
<div class="filters">
<div class="filter-group">
<label for="severityFilter">Severity:</label>
<select id="severityFilter">
<option value="all">All</option>
<option value="critical">Critical</option>
<option value="error">Error</option>
<option value="warning">Warning</option>
<option value="info">Info</option>
</select>
</div>
<div class="filter-group">
<label for="perPageFilter">Per page:</label>
<select id="perPageFilter">
<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-checkbox">
<input type="checkbox" id="includeSystemNamespaces">
<label for="includeSystemNamespaces">Include system namespaces</label>
</div>
<button class="apply-filters-btn" onclick="applyFilters()">Apply Filters</button>
</div>
<!-- Problem Table -->
<div class="table-container">
<table>
<thead>
<tr>
<th>Namespace</th>
<th>Pods</th>
<th>Issues</th>
<th>Severity</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="problemTableBody">
<tr>
<td colspan="5" class="loading">Loading data...</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- Quick Actions -->
<div class="card">
<h2>⚡ Quick Actions</h2>
<div class="quick-actions">
<button class="quick-action-btn" onclick="generateVPARecommendations()">
<span>🎯</span>
<span>Generate VPA Recommendations</span>
</button>
<button class="quick-action-btn" onclick="showHistoricalAnalysis()">
<span>📈</span>
<span>Historical Analysis</span>
</button>
<button class="quick-action-btn" onclick="exportComplianceReport()">
<span>📊</span>
<span>Export Compliance Report</span>
</button>
<button class="quick-action-btn" onclick="fixAllIssues()">
<span>🔧</span>
<span>Fix All Issues</span>
</button>
</div>
</div>
</div>
<!-- Smart Recommendations Section -->
<div id="smartRecommendationsSection" style="display: none;">
<div class="card">
<h2>🎯 Smart Recommendations</h2>
<div id="smartRecommendationsContent">
<p class="loading">Loading recommendations...</p>
</div>
</div>
</div>
<!-- VPA Management Section -->
<div id="vpaManagementSection" style="display: none;">
<div class="card">
<h2>⚙️ VPA Management</h2>
<div id="vpaManagementContent">
<p class="loading">Loading VPA data...</p>
</div>
</div>
</div>
<!-- Historical Analysis Section -->
<div id="historicalAnalysisSection" style="display: none;">
<div class="card">
<h2>📈 Historical Analysis</h2>
<div id="historicalAnalysisContent">
<p class="loading">Loading historical data...</p>
</div>
</div>
</div>
</div>
</div>
<script>
// Global variables
let currentData = null;
let currentFilters = {
severity: 'all',
perPage: 20,
includeSystem: false
};
// Initialize the application
document.addEventListener('DOMContentLoaded', function() {
loadDashboard();
setupEventListeners();
});
// Setup event listeners
function setupEventListeners() {
// Navigation
document.querySelectorAll('.nav-item').forEach(item => {
item.addEventListener('click', function(e) {
e.preventDefault();
const section = this.dataset.section;
showSection(section);
});
});
// Filter changes
document.getElementById('severityFilter').addEventListener('change', function() {
currentFilters.severity = this.value;
});
document.getElementById('perPageFilter').addEventListener('change', function() {
currentFilters.perPage = parseInt(this.value);
});
document.getElementById('includeSystemNamespaces').addEventListener('change', function() {
currentFilters.includeSystem = this.checked;
});
}
// Show specific section
function showSection(section) {
// Hide all sections
document.querySelectorAll('[id$="Section"]').forEach(sec => {
sec.style.display = 'none';
});
// Remove active class from all nav items
document.querySelectorAll('.nav-item').forEach(item => {
item.classList.remove('active');
});
// Show selected section
const sectionElement = document.getElementById(section + 'Section');
if (sectionElement) {
sectionElement.style.display = 'block';
}
const navElement = document.querySelector(`[data-section="${section}"]`);
if (navElement) {
navElement.classList.add('active');
}
// Load section data
switch(section) {
case 'dashboard':
loadDashboard();
break;
case 'smart-recommendations':
loadSmartRecommendations();
break;
case 'vpa-management':
loadVPAManagement();
break;
case 'historical-analysis':
loadHistoricalAnalysis();
break;
}
}
// Load dashboard data
async function loadDashboard() {
try {
showLoading();
const response = await fetch('/api/v1/cluster/status');
const data = await response.json();
currentData = data;
updateDashboard(data);
} catch (error) {
console.error('Error loading dashboard:', error);
showError('Failed to load dashboard data');
}
}
// Update dashboard with data
function updateDashboard(data) {
// Update metrics
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.critical_issues || 0;
// Update overcommit metrics
if (data.overcommit) {
document.getElementById('cpuOvercommit').textContent = `${data.overcommit.cpu_overcommit_percent}%`;
document.getElementById('memoryOvercommit').textContent = `${data.overcommit.memory_overcommit_percent}%`;
document.getElementById('namespacesInOvercommit').textContent = data.overcommit.namespaces_in_overcommit || 0;
// Calculate resource utilization (usage vs requests)
const resourceUtilization = data.overcommit.resource_utilization || 0;
document.getElementById('resourceUtilization').textContent = `${resourceUtilization}%`;
// Store overcommit data for modal display
window.overcommitData = data.overcommit;
} else {
document.getElementById('cpuOvercommit').textContent = '0%';
document.getElementById('memoryOvercommit').textContent = '0%';
document.getElementById('namespacesInOvercommit').textContent = '0';
document.getElementById('resourceUtilization').textContent = '0%';
}
// Update status
const statusIcon = document.getElementById('clusterStatusIcon');
const statusText = document.getElementById('clusterStatus');
const statusMessage = document.getElementById('clusterStatusMessage');
if (data.critical_issues > 0) {
statusIcon.textContent = '🔴';
statusText.textContent = 'Critical Issues Found';
statusMessage.textContent = `${data.critical_issues} critical issues need attention`;
} else if (data.total_warnings > 0) {
statusIcon.textContent = '🟡';
statusText.textContent = 'Issues Found';
statusMessage.textContent = `${data.total_warnings} warnings found`;
} else {
statusIcon.textContent = '🟢';
statusText.textContent = 'Cluster Healthy';
statusMessage.textContent = 'All systems operational';
}
// Update problem table
updateProblemTable(data);
}
// Update problem table
function updateProblemTable(data) {
const tbody = document.getElementById('problemTableBody');
if (!data.namespaces || data.namespaces.length === 0) {
tbody.innerHTML = '<tr><td colspan="5" style="text-align: center; color: #7f8c8d;">No data available</td></tr>';
return;
}
let html = '';
data.namespaces.forEach(namespace => {
const severity = getHighestSeverity(namespace);
const severityClass = `severity-${severity}`;
html += `
<tr>
<td><strong>${namespace.namespace}</strong></td>
<td>${Object.keys(namespace.pods || {}).length}</td>
<td>${namespace.total_validations || 0}</td>
<td><span class="severity-badge ${severityClass}">${severity}</span></td>
<td>
<div class="action-buttons">
<button class="btn btn-primary btn-sm" onclick="analyzeNamespace('${namespace.namespace}')">Analyze</button>
<button class="btn btn-success btn-sm" onclick="fixNamespace('${namespace.namespace}')">Fix</button>
</div>
</td>
</tr>
`;
});
tbody.innerHTML = html;
}
// Get highest severity from namespace
function getHighestSeverity(namespace) {
const breakdown = namespace.severity_breakdown || {};
if (breakdown.error > 0) return 'error';
if (breakdown.warning > 0) return 'warning';
if (breakdown.info > 0) return 'info';
return 'info';
}
// Analyze namespace - show detailed issues
function analyzeNamespace(namespaceName) {
if (!currentData || !currentData.namespaces) return;
const namespace = currentData.namespaces.find(ns => ns.namespace === namespaceName);
if (!namespace) return;
// Show details in modal
showNamespaceDetailsSimple(namespaceName);
}
// Show namespace details in modal
function showNamespaceDetailsSimple(namespaceName) {
// Create modal if it doesn't exist
let modal = document.getElementById('namespaceModal');
if (!modal) {
modal = document.createElement('div');
modal.id = 'namespaceModal';
modal.className = 'modal';
modal.innerHTML = `
<div class="modal-content">
<div class="modal-header">
<h2>📋 Namespace Analysis</h2>
<span class="close">&times;</span>
</div>
<div class="modal-body" id="modalBody"></div>
</div>
`;
document.body.appendChild(modal);
// Add close functionality
modal.querySelector('.close').onclick = () => modal.style.display = 'none';
modal.onclick = (e) => {
if (e.target === modal) modal.style.display = 'none';
};
}
// Create detailed content
const namespace = currentData.namespaces.find(ns => ns.namespace === namespaceName);
if (!namespace) return;
let content = `
<div class="namespace-details">
<h3>📋 ${namespaceName} - Detailed Analysis</h3>
<div class="namespace-summary">
<p><strong>Pods:</strong> ${Object.keys(namespace.pods || {}).length}</p>
<p><strong>Total Issues:</strong> ${namespace.total_validations || 0}</p>
<p><strong>Severity Breakdown:</strong></p>
<ul>
<li>Errors: ${namespace.severity_breakdown?.error || 0}</li>
<li>Warnings: ${namespace.severity_breakdown?.warning || 0}</li>
<li>Info: ${namespace.severity_breakdown?.info || 0}</li>
</ul>
</div>
<div class="pods-details">
<h4>🔍 Pod Analysis</h4>
`;
// Add details for each pod
Object.values(namespace.pods || {}).forEach(pod => {
content += `
<div class="pod-detail">
<h5>📦 ${pod.pod_name}</h5>
<p><strong>Status:</strong> ${pod.phase}</p>
<p><strong>Node:</strong> ${pod.node_name}</p>
<div class="containers-detail">
<h6>Containers:</h6>
`;
pod.containers.forEach(container => {
const hasRequests = Object.keys(container.resources?.requests || {}).length > 0;
const hasLimits = Object.keys(container.resources?.limits || {}).length > 0;
content += `
<div class="container-detail">
<p><strong>${container.name}</strong> (${container.image})</p>
<p>Requests: ${hasRequests ? JSON.stringify(container.resources.requests) : '❌ Not defined'}</p>
<p>Limits: ${hasLimits ? JSON.stringify(container.resources.limits) : '❌ Not defined'}</p>
</div>
`;
});
content += `
</div>
<div class="validations-detail">
<h6>Issues Found:</h6>
`;
if (pod.validations && pod.validations.length > 0) {
pod.validations.forEach(validation => {
const severityClass = `severity-${validation.severity}`;
content += `
<div class="validation-item ${severityClass}">
<p><strong>${validation.rule_name}:</strong> ${validation.message}</p>
<p><em>Recommendation:</em> ${validation.recommendation}</p>
</div>
`;
});
} else {
content += `<p>No issues found for this pod.</p>`;
}
content += `
</div>
</div>
`;
});
content += `
</div>
</div>
`;
// Populate and show modal
document.getElementById('modalBody').innerHTML = content;
modal.style.display = 'block';
}
// Fix namespace - placeholder for now
function fixNamespace(namespaceName) {
// Create modal if it doesn't exist
let modal = document.getElementById('fixModal');
if (!modal) {
modal = document.createElement('div');
modal.id = 'fixModal';
modal.className = 'modal';
modal.innerHTML = `
<div class="modal-content">
<div class="modal-header">
<h2>🔧 Fix Namespace</h2>
<span class="close">&times;</span>
</div>
<div class="modal-body" id="fixModalBody"></div>
</div>
`;
document.body.appendChild(modal);
// Add close functionality
modal.querySelector('.close').onclick = () => modal.style.display = 'none';
modal.onclick = (e) => {
if (e.target === modal) modal.style.display = 'none';
};
}
// Create content
let content = `
<div class="fix-details">
<h3>🔧 Fix Namespace: ${namespaceName}</h3>
<div class="fix-info">
<p><strong>Status:</strong> Coming Soon in Phase 2</p>
<p>This functionality will include:</p>
<ul>
<li>Auto-generating resource recommendations</li>
<li>Creating YAML patches</li>
<li>Applying fixes via OpenShift API</li>
<li>Bulk fixes for multiple namespaces</li>
</ul>
<div class="fix-actions">
<button class="btn btn-primary" onclick="document.getElementById('fixModal').style.display='none'">Close</button>
</div>
</div>
</div>
`;
// Populate and show modal
document.getElementById('fixModalBody').innerHTML = content;
modal.style.display = 'block';
}
// Apply filters
function applyFilters() {
if (!currentData) return;
// Filter namespaces based on current filters
let filteredNamespaces = currentData.namespaces || [];
if (!currentFilters.includeSystem) {
filteredNamespaces = filteredNamespaces.filter(ns =>
!ns.name.startsWith('kube-') &&
!ns.name.startsWith('openshift-') &&
!ns.name.startsWith('default')
);
}
if (currentFilters.severity !== 'all') {
filteredNamespaces = filteredNamespaces.filter(ns => {
const severity = getHighestSeverity(ns);
return severity === currentFilters.severity;
});
}
// Update table with filtered data
const filteredData = { ...currentData, namespaces: filteredNamespaces };
updateProblemTable(filteredData);
}
// Load smart recommendations
async function loadSmartRecommendations() {
try {
showLoading();
const response = await fetch('/api/v1/smart-recommendations');
const data = await response.json();
console.log('Smart recommendations data:', data);
updateSmartRecommendations(data);
} catch (error) {
console.error('Error loading recommendations:', error);
document.getElementById('smartRecommendationsContent').innerHTML =
'<p style="color: #e74c3c;">Failed to load recommendations</p>';
}
}
// Update smart recommendations
function updateSmartRecommendations(data) {
const content = document.getElementById('smartRecommendationsContent');
if (!data || !data.recommendations) {
content.innerHTML = '<p style="color: #e74c3c;">No recommendations data available</p>';
return;
}
const recommendations = data.recommendations;
const summary = {
total_recommendations: data.total || recommendations.length,
by_priority: {
critical: recommendations.filter(r => r.priority === 'critical').length,
high: recommendations.filter(r => r.priority === 'high').length,
medium: recommendations.filter(r => r.priority === 'medium').length,
low: recommendations.filter(r => r.priority === 'low').length
},
by_type: {
resource_config: recommendations.filter(r => r.recommendation_type === 'resource_config').length,
vpa_activation: recommendations.filter(r => r.recommendation_type === 'vpa_activation').length,
ratio_adjustment: recommendations.filter(r => r.recommendation_type === 'ratio_adjustment').length
},
namespaces_affected: new Set(recommendations.map(r => r.namespace)).size
};
let html = `
<!-- Summary Cards -->
<div class="summary-cards" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem; margin-bottom: 2rem;">
<div class="summary-card" style="background: #e8f5e8; border: 1px solid #4caf50; border-radius: 8px; padding: 1rem; text-align: center;">
<h3 style="color: #2e7d32; margin: 0 0 0.5rem 0;">${summary.total_recommendations}</h3>
<p style="color: #388e3c; margin: 0; font-size: 0.9rem;">Total Recommendations</p>
</div>
<div class="summary-card" style="background: #ffebee; border: 1px solid #f44336; border-radius: 8px; padding: 1rem; text-align: center;">
<h3 style="color: #c62828; margin: 0 0 0.5rem 0;">${summary.by_priority.critical}</h3>
<p style="color: #d32f2f; margin: 0; font-size: 0.9rem;">Critical Issues</p>
</div>
<div class="summary-card" style="background: #fff3e0; border: 1px solid #ff9800; border-radius: 8px; padding: 1rem; text-align: center;">
<h3 style="color: #ef6c00; margin: 0 0 0.5rem 0;">${summary.by_priority.high}</h3>
<p style="color: #f57c00; margin: 0; font-size: 0.9rem;">High Priority</p>
</div>
<div class="summary-card" style="background: #e3f2fd; border: 1px solid #2196f3; border-radius: 8px; padding: 1rem; text-align: center;">
<h3 style="color: #1565c0; margin: 0 0 0.5rem 0;">${summary.namespaces_affected}</h3>
<p style="color: #1976d2; margin: 0; font-size: 0.9rem;">Namespaces Affected</p>
</div>
</div>
<!-- Filter Controls -->
<div class="filter-controls" style="margin-bottom: 2rem; display: flex; gap: 1rem; align-items: center; flex-wrap: wrap;">
<div>
<label for="priorityFilter" style="font-weight: 500; margin-right: 0.5rem;">Priority:</label>
<select id="priorityFilter" onchange="filterRecommendations()" style="padding: 0.5rem; border: 1px solid #ddd; border-radius: 4px;">
<option value="">All Priorities</option>
<option value="critical">Critical</option>
<option value="high">High</option>
<option value="medium">Medium</option>
<option value="low">Low</option>
</select>
</div>
<div>
<label for="typeFilter" style="font-weight: 500; margin-right: 0.5rem;">Type:</label>
<select id="typeFilter" onchange="filterRecommendations()" style="padding: 0.5rem; border: 1px solid #ddd; border-radius: 4px;">
<option value="">All Types</option>
<option value="resource_config">Resource Config</option>
<option value="vpa_activation">VPA Activation</option>
<option value="ratio_adjustment">Ratio Adjustment</option>
</select>
</div>
<button onclick="refreshRecommendations()" style="padding: 0.5rem 1rem; background: #3498db; color: white; border: none; border-radius: 4px; cursor: pointer;">
🔄 Refresh
</button>
</div>
<!-- Recommendations List -->
<div id="recommendationsList">
`;
if (recommendations.length === 0) {
html += `
<div style="text-align: center; padding: 3rem; color: #7f8c8d;">
<h3>🎉 No Recommendations Needed</h3>
<p>All workloads are properly configured with optimal resource settings.</p>
</div>
`;
} else {
recommendations.forEach((rec, index) => {
const priorityColor = {
'critical': '#e74c3c',
'high': '#f39c12',
'medium': '#3498db',
'low': '#95a5a6'
}[rec.priority] || '#95a5a6';
const priorityIcon = {
'critical': '🔴',
'high': '🟠',
'medium': '🔵',
'low': '⚪'
}[rec.priority] || '⚪';
html += `
<div class="recommendation-card" data-priority="${rec.priority}" data-type="${rec.recommendation_type}"
style="border: 1px solid #ddd; border-radius: 8px; margin-bottom: 1rem; overflow: hidden; background: white;">
<div class="recommendation-header" style="background: #f8f9fa; padding: 1rem; border-bottom: 1px solid #dee2e6; display: flex; justify-content: space-between; align-items: center;">
<div>
<h3 style="margin: 0 0 0.5rem 0; color: #2c3e50;">${priorityIcon} ${rec.title}</h3>
<p style="margin: 0; color: #7f8c8d; font-size: 0.9rem;">
<strong>Workload:</strong> ${rec.workload_name} |
<strong>Namespace:</strong> ${rec.namespace} |
<strong>Type:</strong> ${rec.recommendation_type.replace('_', ' ').toUpperCase()}
</p>
</div>
<div style="text-align: right;">
<span style="background: ${priorityColor}; color: white; padding: 0.25rem 0.5rem; border-radius: 4px; font-size: 0.8rem; font-weight: 500;">
${rec.priority.toUpperCase()}
</span>
${rec.confidence_level ? `<div style="margin-top: 0.5rem; font-size: 0.8rem; color: #7f8c8d;">Confidence: ${Math.round(rec.confidence_level * 100)}%</div>` : ''}
</div>
</div>
<div class="recommendation-body" style="padding: 1rem;">
<p style="margin: 0 0 1rem 0; color: #2c3e50;">${rec.description}</p>
${rec.implementation_steps ? `
<div style="margin-bottom: 1rem;">
<h4 style="color: #2c3e50; margin-bottom: 0.5rem; font-size: 1rem;">Implementation Steps:</h4>
<ol style="margin: 0; padding-left: 1.5rem; color: #555;">
${rec.implementation_steps.map(step => `<li style="margin-bottom: 0.25rem;">${step}</li>`).join('')}
</ol>
</div>
` : ''}
${rec.kubectl_commands && rec.kubectl_commands.length > 0 ? `
<div style="margin-bottom: 1rem;">
<h4 style="color: #2c3e50; margin-bottom: 0.5rem; font-size: 1rem;">Kubectl Commands:</h4>
<div style="background: #f8f9fa; border: 1px solid #dee2e6; border-radius: 4px; padding: 0.75rem; font-family: 'Courier New', monospace; font-size: 0.9rem;">
${rec.kubectl_commands.map(cmd => `<div style="margin-bottom: 0.5rem;">${cmd}</div>`).join('')}
</div>
</div>
` : ''}
<div style="display: flex; gap: 0.5rem; margin-top: 1rem;">
<button onclick="showRecommendationDetails(${index})"
style="padding: 0.5rem 1rem; background: #3498db; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 0.9rem;">
📋 View Details
</button>
${rec.vpa_yaml ? `
<button onclick="showVPAYaml(${index})"
style="padding: 0.5rem 1rem; background: #9b59b6; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 0.9rem;">
📄 VPA YAML
</button>
` : ''}
</div>
</div>
</div>
`;
});
}
html += `
</div>
`;
content.innerHTML = html;
// Store recommendations data for modal access
window.currentRecommendations = recommendations;
}
// Load VPA management
async function loadVPAManagement() {
try {
const response = await fetch('/api/vpa');
const data = await response.json();
updateVPAManagement(data);
} catch (error) {
console.error('Error loading VPA data:', error);
document.getElementById('vpaManagementContent').innerHTML =
'<p style="color: #e74c3c;">Failed to load VPA data</p>';
}
}
// Update VPA management
function updateVPAManagement(data) {
const content = document.getElementById('vpaManagementContent');
content.innerHTML = '<p>VPA management feature coming soon...</p>';
}
// Load historical analysis
async function loadHistoricalAnalysis() {
try {
const response = await fetch('/api/historical');
const data = await response.json();
updateHistoricalAnalysis(data);
} catch (error) {
console.error('Error loading historical data:', error);
document.getElementById('historicalAnalysisContent').innerHTML =
'<p style="color: #e74c3c;">Failed to load historical data</p>';
}
}
// Update historical analysis
function updateHistoricalAnalysis(data) {
const content = document.getElementById('historicalAnalysisContent');
content.innerHTML = '<p>Historical analysis feature coming soon...</p>';
}
// Action functions - these are defined above in the main functions
function showHistoricalAnalysis() {
// Create modal for historical analysis with real Prometheus data
let modal = document.getElementById('historicalModal');
if (!modal) {
modal = document.createElement('div');
modal.id = 'historicalModal';
modal.className = 'modal';
modal.innerHTML = `
<div class="modal-content" style="width: 90%; max-width: 1000px;">
<div class="modal-header">
<h2>📊 Resource Consumption Analysis - Real Numbers</h2>
<span class="close">&times;</span>
</div>
<div class="modal-body" id="historicalModalBody">
<div class="workload-selector">
<h3>Select Workload to Analyze:</h3>
<select id="workloadSelect" onchange="loadWorkloadMetrics()">
<option value="">Choose a workload...</option>
</select>
<select id="timeRangeSelect" onchange="loadWorkloadMetrics()">
<option value="1h">Last 1 hour</option>
<option value="6h">Last 6 hours</option>
<option value="24h" selected>Last 24 hours</option>
<option value="7d">Last 7 days</option>
</select>
</div>
<div id="metricsData" style="display: none;">
<div class="cluster-info">
<h3>🏢 Cluster Total Resources</h3>
<div id="clusterTotal"></div>
</div>
<div class="workload-metrics">
<h3>📈 Workload Resource Consumption</h3>
<div id="workloadData"></div>
</div>
</div>
</div>
</div>
`;
document.body.appendChild(modal);
// Add close functionality
modal.querySelector('.close').onclick = () => modal.style.display = 'none';
modal.onclick = (e) => {
if (e.target === modal) modal.style.display = 'none';
};
}
// Populate workload selector
populateWorkloadSelector();
modal.style.display = 'block';
}
function populateWorkloadSelector() {
if (!currentData || !currentData.namespaces) return;
const select = document.getElementById('workloadSelect');
select.innerHTML = '<option value="">Choose a workload...</option>';
currentData.namespaces.forEach(namespace => {
Object.values(namespace.pods || {}).forEach(pod => {
// Extract workload name from pod name (remove random suffix)
const workloadName = pod.pod_name.replace(/-\w{10}-\w{5}$/, '');
const option = document.createElement('option');
option.value = `${namespace.namespace}/${workloadName}`;
option.textContent = `${namespace.namespace}/${workloadName}`;
select.appendChild(option);
});
});
}
async function loadWorkloadMetrics() {
const workloadSelect = document.getElementById('workloadSelect');
const timeRangeSelect = document.getElementById('timeRangeSelect');
const metricsDiv = document.getElementById('metricsData');
if (!workloadSelect.value) {
metricsDiv.style.display = 'none';
return;
}
const [namespace, workload] = workloadSelect.value.split('/');
const timeRange = timeRangeSelect.value;
try {
metricsDiv.style.display = 'block';
// Show loading message in clusterTotal div
const clusterTotalDiv = document.getElementById('clusterTotal');
const workloadDataDiv = document.getElementById('workloadData');
if (clusterTotalDiv) clusterTotalDiv.innerHTML = '<p>Loading metrics from Prometheus...</p>';
if (workloadDataDiv) workloadDataDiv.innerHTML = '';
const response = await fetch(`/api/v1/workloads/${namespace}/${workload}/metrics?time_range=${timeRange}`);
const data = await response.json();
if (data.workload_metrics) {
renderMetricsData(data);
} else {
if (clusterTotalDiv) clusterTotalDiv.innerHTML = '<p>No metrics data available for this workload.</p>';
if (workloadDataDiv) workloadDataDiv.innerHTML = '';
}
} catch (error) {
console.error('Error loading metrics:', error);
const clusterTotalDiv = document.getElementById('clusterTotal');
const workloadDataDiv = document.getElementById('workloadData');
if (clusterTotalDiv) clusterTotalDiv.innerHTML = '<p>Error loading metrics. Please try again.</p>';
if (workloadDataDiv) workloadDataDiv.innerHTML = '';
}
}
function renderMetricsData(data) {
const clusterTotalDiv = document.getElementById('clusterTotal');
const workloadDataDiv = document.getElementById('workloadData');
// Add data source indicator
const dataSourceIndicator = data.data_source === 'prometheus' ?
'<div style="background: #d4edda; border: 1px solid #c3e6cb; border-radius: 4px; padding: 10px; margin-bottom: 15px; color: #155724;"><strong>✅ Live Data:</strong> Real metrics from Prometheus</div>' :
'<div style="background: #f8d7da; border: 1px solid #f5c6cb; border-radius: 4px; padding: 10px; margin-bottom: 15px; color: #721c24;"><strong>⚠️ No Data:</strong> No metrics available for this workload</div>';
// Render cluster total resources
clusterTotalDiv.innerHTML = dataSourceIndicator + `
<div class="cluster-stats">
<div class="stat-card">
<h4>CPU Total</h4>
<div class="stat-value">${data.cluster_total.cpu_cores.toFixed(2)} cores</div>
</div>
<div class="stat-card">
<h4>Memory Total</h4>
<div class="stat-value">${data.cluster_total.memory_gb.toFixed(2)} GB</div>
</div>
</div>
`;
// Render workload metrics
const cpu = data.workload_metrics.cpu;
const memory = data.workload_metrics.memory;
workloadDataDiv.innerHTML = `
<div class="metrics-grid">
<div class="metric-section">
<h4>🖥️ CPU Resources</h4>
<div class="metric-row">
<span class="metric-label">Current Usage:</span>
<span class="metric-value">${cpu.usage_cores.toFixed(3)} cores</span>
<span class="metric-percent">(${cpu.usage_percent}% of cluster)</span>
</div>
<div class="metric-row">
<span class="metric-label">Requests:</span>
<span class="metric-value">${cpu.requests_cores.toFixed(3)} cores</span>
<span class="metric-percent">(${cpu.requests_percent}% of cluster)</span>
</div>
<div class="metric-row">
<span class="metric-label">Limits:</span>
<span class="metric-value">${cpu.limits_cores.toFixed(3)} cores</span>
<span class="metric-percent">(${cpu.limits_percent}% of cluster)</span>
</div>
<div class="metric-row">
<span class="metric-label">Efficiency:</span>
<span class="metric-value ${cpu.usage_cores > 0 && cpu.requests_cores > 0 ? (cpu.usage_cores / cpu.requests_cores * 100).toFixed(1) + '%' : 'N/A'}">${cpu.usage_cores > 0 && cpu.requests_cores > 0 ? (cpu.usage_cores / cpu.requests_cores * 100).toFixed(1) + '%' : 'N/A'}</span>
<span class="metric-percent">(usage vs requests)</span>
</div>
</div>
<div class="metric-section">
<h4>💾 Memory Resources</h4>
<div class="metric-row">
<span class="metric-label">Current Usage:</span>
<span class="metric-value">${memory.usage_mb.toFixed(2)} MB</span>
<span class="metric-percent">(${memory.usage_percent}% of cluster)</span>
</div>
<div class="metric-row">
<span class="metric-label">Requests:</span>
<span class="metric-value">${memory.requests_mb.toFixed(2)} MB</span>
<span class="metric-percent">(${memory.requests_percent}% of cluster)</span>
</div>
<div class="metric-row">
<span class="metric-label">Limits:</span>
<span class="metric-value">${memory.limits_mb.toFixed(2)} MB</span>
<span class="metric-percent">(${memory.limits_percent}% of cluster)</span>
</div>
<div class="metric-row">
<span class="metric-label">Efficiency:</span>
<span class="metric-value ${memory.usage_bytes > 0 && memory.requests_bytes > 0 ? (memory.usage_bytes / memory.requests_bytes * 100).toFixed(1) + '%' : 'N/A'}">${memory.usage_bytes > 0 && memory.requests_bytes > 0 ? (memory.usage_bytes / memory.requests_bytes * 100).toFixed(1) + '%' : 'N/A'}</span>
<span class="metric-percent">(usage vs requests)</span>
</div>
</div>
</div>
${data.promql_queries ? `
<div class="promql-queries" style="margin-top: 30px; padding: 20px; background: #f8f9fa; border-radius: 8px; border: 1px solid #dee2e6;">
<h4>🔍 PromQL Queries Used</h4>
<p style="color: #6c757d; margin-bottom: 15px;">Copy these queries to validate in OpenShift Console → Monitoring → Metrics:</p>
<div class="query-section">
<h5>Cluster Total Resources:</h5>
<div class="query-item">
<label>CPU Total:</label>
<div class="query-box">
<code>${data.promql_queries.cluster_cpu_total}</code>
<button onclick="copyToClipboard('${data.promql_queries.cluster_cpu_total}')" class="copy-btn">📋 Copy</button>
</div>
</div>
<div class="query-item">
<label>Memory Total:</label>
<div class="query-box">
<code>${data.promql_queries.cluster_memory_total}</code>
<button onclick="copyToClipboard('${data.promql_queries.cluster_memory_total}')" class="copy-btn">📋 Copy</button>
</div>
</div>
</div>
<div class="query-section">
<h5>Workload Resource Usage:</h5>
<div class="query-item">
<label>CPU Usage:</label>
<div class="query-box">
<code>${data.promql_queries.cpu_usage}</code>
<button onclick="copyToClipboard('${data.promql_queries.cpu_usage}')" class="copy-btn">📋 Copy</button>
</div>
</div>
<div class="query-item">
<label>Memory Usage:</label>
<div class="query-box">
<code>${data.promql_queries.memory_usage}</code>
<button onclick="copyToClipboard('${data.promql_queries.memory_usage}')" class="copy-btn">📋 Copy</button>
</div>
</div>
</div>
<div class="query-section">
<h5>Workload Resource Requests:</h5>
<div class="query-item">
<label>CPU Requests:</label>
<div class="query-box">
<code>${data.promql_queries.cpu_requests}</code>
<button onclick="copyToClipboard('${data.promql_queries.cpu_requests}')" class="copy-btn">📋 Copy</button>
</div>
</div>
<div class="query-item">
<label>Memory Requests:</label>
<div class="query-box">
<code>${data.promql_queries.memory_requests}</code>
<button onclick="copyToClipboard('${data.promql_queries.memory_requests}')" class="copy-btn">📋 Copy</button>
</div>
</div>
</div>
<div class="query-section">
<h5>Workload Resource Limits:</h5>
<div class="query-item">
<label>CPU Limits:</label>
<div class="query-box">
<code>${data.promql_queries.cpu_limits}</code>
<button onclick="copyToClipboard('${data.promql_queries.cpu_limits}')" class="copy-btn">📋 Copy</button>
</div>
</div>
<div class="query-item">
<label>Memory Limits:</label>
<div class="query-box">
<code>${data.promql_queries.memory_limits}</code>
<button onclick="copyToClipboard('${data.promql_queries.memory_limits}')" class="copy-btn">📋 Copy</button>
</div>
</div>
</div>
</div>
` : ''}
`;
}
function exportComplianceReport() {
alert('Exporting compliance report...');
}
function fixAllIssues() {
if (confirm('Are you sure you want to fix all issues? This action cannot be undone.')) {
alert('Fixing all issues...');
}
}
function exportData() {
if (!currentData) {
alert('No data to export');
return;
}
const dataStr = JSON.stringify(currentData, null, 2);
const dataBlob = new Blob([dataStr], {type: 'application/json'});
const url = URL.createObjectURL(dataBlob);
const link = document.createElement('a');
link.href = url;
link.download = 'resource-governance-report.json';
link.click();
URL.revokeObjectURL(url);
}
// Copy to clipboard function for PromQL queries
async function copyToClipboard(text) {
try {
await navigator.clipboard.writeText(text);
// Show a brief success message
const button = event.target;
const originalText = button.textContent;
button.textContent = '✅ Copied!';
button.style.background = '#28a745';
setTimeout(() => {
button.textContent = originalText;
button.style.background = '';
}, 2000);
} catch (err) {
console.error('Failed to copy text: ', err);
// Fallback for older browsers
const textArea = document.createElement('textarea');
textArea.value = text;
document.body.appendChild(textArea);
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
const button = event.target;
const originalText = button.textContent;
button.textContent = '✅ Copied!';
button.style.background = '#28a745';
setTimeout(() => {
button.textContent = originalText;
button.style.background = '';
}, 2000);
}
}
// Smart Recommendations Functions
function filterRecommendations() {
const priorityFilter = document.getElementById('priorityFilter').value;
const typeFilter = document.getElementById('typeFilter').value;
const cards = document.querySelectorAll('.recommendation-card');
cards.forEach(card => {
const priority = card.getAttribute('data-priority');
const type = card.getAttribute('data-type');
let show = true;
if (priorityFilter && priority !== priorityFilter) {
show = false;
}
if (typeFilter && type !== typeFilter) {
show = false;
}
card.style.display = show ? 'block' : 'none';
});
}
function refreshRecommendations() {
loadSmartRecommendations();
}
function showRecommendationDetails(index) {
const rec = window.currentRecommendations[index];
if (!rec) return;
let modalContent = `
<div class="modal-header">
<h2>📋 Recommendation Details</h2>
<span class="close" onclick="closeModal()">&times;</span>
</div>
<div class="modal-body" style="padding: 2rem;">
<div style="margin-bottom: 2rem;">
<h3 style="color: #2c3e50; margin-bottom: 1rem;">${rec.title}</h3>
<div style="background: #f8f9fa; padding: 1rem; border-radius: 8px; margin-bottom: 1rem;">
<p><strong>Workload:</strong> ${rec.workload_name}</p>
<p><strong>Namespace:</strong> ${rec.namespace}</p>
<p><strong>Type:</strong> ${rec.recommendation_type.replace('_', ' ').toUpperCase()}</p>
<p><strong>Priority:</strong> <span style="background: ${getPriorityColor(rec.priority)}; color: white; padding: 0.25rem 0.5rem; border-radius: 4px; font-size: 0.8rem;">${rec.priority.toUpperCase()}</span></p>
${rec.confidence_level ? `<p><strong>Confidence:</strong> ${Math.round(rec.confidence_level * 100)}%</p>` : ''}
</div>
<h4 style="color: #2c3e50; margin-bottom: 1rem;">Description</h4>
<p style="margin-bottom: 2rem; color: #555;">${rec.description}</p>
${rec.implementation_steps ? `
<h4 style="color: #2c3e50; margin-bottom: 1rem;">Implementation Steps</h4>
<ol style="margin-bottom: 2rem; padding-left: 1.5rem; color: #555;">
${rec.implementation_steps.map(step => `<li style="margin-bottom: 0.5rem;">${step}</li>`).join('')}
</ol>
` : ''}
${rec.kubectl_commands && rec.kubectl_commands.length > 0 ? `
<h4 style="color: #2c3e50; margin-bottom: 1rem;">Kubectl Commands</h4>
<div style="background: #f8f9fa; border: 1px solid #dee2e6; border-radius: 4px; padding: 1rem; margin-bottom: 2rem; font-family: 'Courier New', monospace; font-size: 0.9rem;">
${rec.kubectl_commands.map(cmd => `<div style="margin-bottom: 0.5rem;">${cmd}</div>`).join('')}
</div>
` : ''}
<div style="text-align: center; margin-top: 2rem;">
<button onclick="copyRecommendationCommands(${index})" style="padding: 0.75rem 1.5rem; background: #28a745; color: white; border: none; border-radius: 4px; cursor: pointer; margin-right: 1rem;">
📋 Copy Commands
</button>
<button onclick="closeModal()" style="padding: 0.75rem 1.5rem; background: #6c757d; color: white; border: none; border-radius: 4px; cursor: pointer;">
Close
</button>
</div>
</div>
</div>
`;
showModal(modalContent);
}
function showVPAYaml(index) {
const rec = window.currentRecommendations[index];
if (!rec || !rec.vpa_yaml) return;
let modalContent = `
<div class="modal-header">
<h2>📄 VPA YAML Configuration</h2>
<span class="close" onclick="closeModal()">&times;</span>
</div>
<div class="modal-body" style="padding: 2rem;">
<div style="margin-bottom: 1rem;">
<p><strong>Workload:</strong> ${rec.workload_name} | <strong>Namespace:</strong> ${rec.namespace}</p>
</div>
<div style="background: #f8f9fa; border: 1px solid #dee2e6; border-radius: 4px; padding: 1rem; font-family: 'Courier New', monospace; font-size: 0.9rem; white-space: pre-wrap; overflow-x: auto;">
${rec.vpa_yaml}
</div>
<div style="text-align: center; margin-top: 1rem;">
<button onclick="copyVPAYaml(${index})" style="padding: 0.75rem 1.5rem; background: #28a745; color: white; border: none; border-radius: 4px; cursor: pointer; margin-right: 1rem;">
📋 Copy YAML
</button>
<button onclick="closeModal()" style="padding: 0.75rem 1.5rem; background: #6c757d; color: white; border: none; border-radius: 4px; cursor: pointer;">
Close
</button>
</div>
</div>
`;
showModal(modalContent);
}
function copyRecommendationCommands(index) {
const rec = window.currentRecommendations[index];
if (!rec || !rec.kubectl_commands) return;
const commands = rec.kubectl_commands.join('\n');
copyToClipboard(commands);
}
function copyVPAYaml(index) {
const rec = window.currentRecommendations[index];
if (!rec || !rec.vpa_yaml) return;
copyToClipboard(rec.vpa_yaml);
}
function getPriorityColor(priority) {
const colors = {
'critical': '#e74c3c',
'high': '#f39c12',
'medium': '#3498db',
'low': '#95a5a6'
};
return colors[priority] || '#95a5a6';
}
// Utility functions
function showLoading() {
document.getElementById('problemTableBody').innerHTML =
'<tr><td colspan="5" class="loading">Loading data...</td></tr>';
}
function showError(message) {
document.getElementById('problemTableBody').innerHTML =
`<tr><td colspan="5" style="text-align: center; color: #e74c3c;">${message}</td></tr>`;
}
function showOvercommitDetails(type) {
if (!window.overcommitData) {
alert('Overcommit data not available');
return;
}
const data = window.overcommitData;
let title, content;
if (type === 'cpu') {
title = '🖥️ CPU Overcommit Details';
const cpuCapacity = data.cpu_capacity || 0;
const cpuRequests = data.cpu_requests || 0;
content = `
<div class="overcommit-details">
<h3>CPU Resource Analysis</h3>
<div class="metric-detail">
<strong>Capacity Total:</strong> ${cpuCapacity} cores
</div>
<div class="metric-detail">
<strong>Requests Total:</strong> ${cpuRequests} cores
</div>
<div class="metric-detail">
<strong>Overcommit:</strong> ${data.cpu_overcommit_percent}% (${cpuRequests} ÷ ${cpuCapacity} × 100)
</div>
<div class="metric-detail">
<strong>Available:</strong> ${(cpuCapacity - cpuRequests).toFixed(2)} cores
</div>
</div>
`;
} else if (type === 'memory') {
title = '💾 Memory Overcommit Details';
const memoryCapacity = data.memory_capacity || 0;
const memoryRequests = data.memory_requests || 0;
const memoryCapacityGB = (memoryCapacity / (1024**3)).toFixed(1);
const memoryRequestsGB = (memoryRequests / (1024**3)).toFixed(1);
content = `
<div class="overcommit-details">
<h3>Memory Resource Analysis</h3>
<div class="metric-detail">
<strong>Capacity Total:</strong> ${memoryCapacity.toLocaleString()} bytes (≈ ${memoryCapacityGB} GB)
</div>
<div class="metric-detail">
<strong>Requests Total:</strong> ${memoryRequests.toLocaleString()} bytes (≈ ${memoryRequestsGB} GB)
</div>
<div class="metric-detail">
<strong>Overcommit:</strong> ${data.memory_overcommit_percent}% (${memoryRequestsGB} ÷ ${memoryCapacityGB} × 100)
</div>
<div class="metric-detail">
<strong>Available:</strong> ${((memoryCapacity - memoryRequests) / (1024**3)).toFixed(1)} GB
</div>
</div>
`;
}
const modal = document.createElement('div');
modal.className = 'modal';
modal.innerHTML = `
<div class="modal-content">
<div class="modal-header">
<h2>${title}</h2>
<span class="close">&times;</span>
</div>
<div class="modal-body">
${content}
</div>
</div>
`;
document.body.appendChild(modal);
modal.style.display = 'block';
// Add close functionality
const closeBtn = modal.querySelector('.close');
closeBtn.onclick = () => modal.remove();
modal.onclick = (e) => {
if (e.target === modal) modal.remove();
};
}
function showResourceUtilizationDetails() {
const modal = document.createElement('div');
modal.className = 'modal';
modal.innerHTML = `
<div class="modal-content">
<div class="modal-header">
<h2>📊 Resource Utilization Details</h2>
<span class="close">&times;</span>
</div>
<div class="modal-body">
<div class="overcommit-details">
<h3>Resource Utilization Analysis</h3>
<div class="metric-detail">
<strong>Current Status:</strong> Placeholder Implementation
</div>
<div class="metric-detail">
<strong>Purpose:</strong> Shows actual resource usage vs. requested resources across the cluster
</div>
<div class="metric-detail">
<strong>Formula:</strong> (Total Usage ÷ Total Requests) × 100
</div>
<div class="metric-detail">
<strong>Current Value:</strong> 75% (simulated data)
</div>
<div class="metric-detail">
<strong>Implementation Status:</strong>
<span style="color: #f39c12;">⚠️ Phase 2 - Smart Recommendations Engine</span>
</div>
<div class="metric-detail">
<strong>Next Steps:</strong>
<ul style="margin: 0.5rem 0; padding-left: 1.5rem;">
<li>Integrate real Prometheus usage metrics</li>
<li>Calculate actual cluster-wide resource utilization</li>
<li>Provide insights on resource efficiency</li>
<li>Identify over/under-provisioned workloads</li>
</ul>
</div>
</div>
</div>
</div>
`;
document.body.appendChild(modal);
modal.style.display = 'block';
// Add close functionality
const closeBtn = modal.querySelector('.close');
closeBtn.onclick = () => modal.remove();
modal.onclick = (e) => {
if (e.target === modal) modal.remove();
};
}
function closeModal() {
const modals = document.querySelectorAll('.modal');
modals.forEach(modal => modal.remove());
}
</script>
</body>
</html>