├── kuber.jpg
├── requirements.txt
├── kubernetes-logo.png
├── theme.js
├── README.md
├── index.html
├── systeminfo.py
├── styles.css
└── app.js
/kuber.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NotHarshhaa/kubernetes-dashboard/HEAD/kuber.jpg
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | flask>=2.3.3,<3.0.0
2 | flask-cors>=4.0.0,<5.0.0
3 | kubernetes>=28.1.0,<29.0.0
4 | psutil>=5.9.5,<6.0.0
5 | gunicorn>=21.2.0,<22.0.0
6 | werkzeug>=2.3.7,<3.0.0
7 | flask-limiter>=3.5.0,<4.0.0
8 |
--------------------------------------------------------------------------------
/kubernetes-logo.png:
--------------------------------------------------------------------------------
1 | �PNG
2 |
3 | IHDR � � ��g- gAMA ���a cHRM z& �� � �� u0 �` :� p��Q< tIME� #S�r PLTE ��������������������� tRNS @��f bKGD �H pHYs � ��+ tEXtSoftware Paint.NET v3.5.11G�B7 �IDATH�c`��0�A`�F�(�r4���Q`� 0F�h4�F�(�0��0 W�b�X_�*�]���h4�F��8�S4�@��h4�F�(�`4��(|�r������dP��h4�F�( 6�!��Eo� IEND�B`�
4 |
--------------------------------------------------------------------------------
/theme.js:
--------------------------------------------------------------------------------
1 | // Theme Management for Kubernetes Dashboard
2 |
3 | document.addEventListener('DOMContentLoaded', () => {
4 | const themeToggle = document.getElementById('themeToggle');
5 | const themeIcon = themeToggle.querySelector('i');
6 | const themeText = themeToggle.querySelector('span');
7 |
8 | // Check for saved theme preference or use preferred color scheme
9 | const savedTheme = localStorage.getItem('k8s-dashboard-theme');
10 | const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
11 |
12 | // Set initial theme
13 | if (savedTheme) {
14 | document.body.className = savedTheme;
15 | updateToggleUI(savedTheme === 'dark-theme');
16 | } else {
17 | // Use system preference as default
18 | const isDarkMode = prefersDark;
19 | document.body.className = isDarkMode ? 'dark-theme' : 'light-theme';
20 | updateToggleUI(isDarkMode);
21 | }
22 |
23 | // Toggle theme when button is clicked
24 | themeToggle.addEventListener('click', () => {
25 | const isDarkMode = document.body.className === 'dark-theme';
26 | const newTheme = isDarkMode ? 'light-theme' : 'dark-theme';
27 |
28 | // Apply theme
29 | document.body.className = newTheme;
30 | localStorage.setItem('k8s-dashboard-theme', newTheme);
31 |
32 | // Update UI
33 | updateToggleUI(!isDarkMode);
34 |
35 | // Reload charts with new theme colors
36 | updateChartsTheme();
37 | });
38 |
39 | function updateToggleUI(isDarkMode) {
40 | // Update icon
41 | themeIcon.className = isDarkMode ? 'fas fa-sun' : 'fas fa-moon';
42 |
43 | // Update text
44 | themeText.textContent = isDarkMode ? 'Light Mode' : 'Dark Mode';
45 | }
46 |
47 | function updateChartsTheme() {
48 | // Get theme variables
49 | const isDarkMode = document.body.className === 'dark-theme';
50 | const textColor = isDarkMode ? '#e2e8f0' : '#2c3e50';
51 | const gridColor = isDarkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
52 |
53 | // Update chart configs if they exist
54 | if (window.charts) {
55 | Object.values(window.charts).forEach(chart => {
56 | if (chart) {
57 | // Update text color
58 | chart.options.scales.x.ticks.color = textColor;
59 | chart.options.scales.y.ticks.color = textColor;
60 | chart.options.scales.x.grid.color = gridColor;
61 | chart.options.scales.y.grid.color = gridColor;
62 | chart.options.plugins.title.color = textColor;
63 | chart.options.plugins.legend.labels.color = textColor;
64 |
65 | // Update and redraw
66 | chart.update();
67 | }
68 | });
69 | }
70 |
71 | // Update doughnut charts if they exist
72 | if (window.podStatusChart) {
73 | window.podStatusChart.options.plugins.legend.labels.color = textColor;
74 | window.podStatusChart.update();
75 | }
76 | }
77 |
78 | // Listen for system theme changes
79 | window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => {
80 | const userSetTheme = localStorage.getItem('k8s-dashboard-theme');
81 |
82 | // Only auto-switch if user hasn't manually set a theme
83 | if (!userSetTheme) {
84 | const newTheme = e.matches ? 'dark-theme' : 'light-theme';
85 | document.body.className = newTheme;
86 | updateToggleUI(e.matches);
87 | updateChartsTheme();
88 | }
89 | });
90 | });
91 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 🚀 **Kubernetes Dashboard – Enhanced Kubernetes Monitoring & Security**
2 |
3 | 
4 |
5 | **A comprehensive Kubernetes Dashboard with real-time system monitoring, interactive visualizations, health checks, container security scanning, and dark/light theme support.**
6 |
7 | Empower your DevOps workflow with **advanced cluster insights, security vulnerability detection, and an intuitive UI** for Kubernetes resource management.
8 |
9 | ---
10 |
11 | ## 📌 **Table of Contents**
12 |
13 | - [🌟 Overview](#-overview)
14 | - [✨ Features](#-features)
15 | - [🛠 Prerequisites](#-prerequisites)
16 | - [⚙️ Installation & Setup](#️-installation--setup)
17 | - [🔍 How It Works](#-how-it-works)
18 | - [🛡 Security & Vulnerability Scanning](#-security--vulnerability-scanning)
19 | - [⚙️ Technology Stack](#️-technology-stack)
20 | - [🚀 Deployment Options](#-deployment-options)
21 | - [📜 License](#-license)
22 | - [🌟 Support & Contributions](#-support--contributions)
23 |
24 | ---
25 |
26 | ## 🌟 **Overview**
27 |
28 | The **Enhanced Kubernetes Dashboard** provides a modern, feature-rich interface for **monitoring, managing, and securing your Kubernetes clusters**.
29 |
30 | 🔹 **Real-time visualizations** – Interactive charts for CPU, memory, and storage metrics
31 | 🔹 **Comprehensive cluster view** – Monitor deployments, pods, services, and more
32 | 🔹 **Dark/Light theme support** – Comfortable viewing in any environment
33 | 🔹 **Security scanning with Trivy** – Detect vulnerabilities in container images
34 | 🔹 **Pod logs viewer** – Easily access and filter container logs
35 | 🔹 **Modern responsive UI** – Optimized for desktop and mobile devices
36 | 🔹 **Health status monitoring** – Track cluster component health
37 |
38 | This dashboard enables **DevOps engineers, SREs, and developers** to efficiently manage their **Kubernetes clusters** while ensuring security best practices.
39 |
40 | ---
41 |
42 | ## ✨ **New Features & Enhancements**
43 |
44 | ### 🎨 **UI Improvements**
45 | - **Modern dashboard layout** with sidebar navigation
46 | - **Dark/light theme support** with system preference detection
47 | - **Responsive design** for all device sizes
48 | - **Interactive charts** for system metrics
49 | - **Improved notifications system**
50 |
51 | ### 📊 **Visualization Enhancements**
52 | - **Real-time metric charts** for CPU, memory, and storage
53 | - **Pod status visualization** with color-coded status indicators
54 | - **Vulnerability summary charts** for security scans
55 | - **Historical metrics** for trend analysis
56 |
57 | ### 🔧 **Functional Improvements**
58 | - **Enhanced pod management** with detailed status information
59 | - **Advanced log viewer** with filtering capabilities
60 | - **Improved security scanner** with vulnerability classification
61 | - **Health status indicators** for cluster components
62 | - **Streamlined namespace management**
63 |
64 | ### 🔒 **Security Features**
65 | - **Detailed vulnerability reports** with severity classification
66 | - **Export functionality** for scan results
67 | - **Component-level health monitoring**
68 |
69 | ---
70 |
71 | ## 🛠 **Prerequisites**
72 |
73 | Before installing the Kubernetes Dashboard, ensure you have the following dependencies installed:
74 |
75 | 🔹 **Python 3.8+** – Required for Flask backend.
76 | 🔹 **pip** – Python package manager.
77 | 🔹 **Docker & Kubernetes Cluster** – To monitor cluster resources.
78 | 🔹 **kubectl** – Kubernetes command-line tool.
79 | 🔹 **Trivy** – For container image vulnerability scanning.
80 |
81 | Install **kubectl** and **Trivy** if not already installed:
82 |
83 | ```bash
84 | # Install kubectl (for Kubernetes resource monitoring)
85 | curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
86 | chmod +x kubectl
87 | sudo mv kubectl /usr/local/bin/
88 |
89 | # Install Trivy (for security scanning)
90 | brew install aquasecurity/trivy/trivy # For macOS
91 | sudo apt install trivy # For Ubuntu/Debian
92 | ```
93 |
94 | ---
95 |
96 | ## ⚙️ **Installation & Setup**
97 |
98 | ### 1️⃣ **Clone the Repository**
99 |
100 | ```bash
101 | git clone https://github.com/NotHarshhaa/kubernetes-dashboard.git
102 | cd kubernetes-dashboard
103 | ```
104 |
105 | ### 2️⃣ **Install Python Dependencies**
106 |
107 | ```bash
108 | pip install -r requirements.txt
109 | ```
110 |
111 | ### 3️⃣ **Start the Flask Application**
112 |
113 | ```bash
114 | python systeminfo.py
115 | ```
116 |
117 | 🚀 The dashboard is now accessible at **[http://localhost:5000](http://localhost:5000)**.
118 |
119 | ---
120 |
121 | ## 🔍 **How It Works**
122 |
123 | ### 📊 **Real-time System Monitoring**
124 |
125 | - **Interactive charts** display live CPU, memory, and storage metrics
126 | - **Historical data tracking** shows performance trends over time
127 | - **Auto-refresh functionality** keeps data current
128 |
129 | ### 🔄 **Kubernetes Resource Management**
130 |
131 | - **Choose a namespace** from the dropdown to filter resources
132 | - **View deployments, pods, and services** specific to the selected namespace
133 | - **Pod status visualization** shows running, pending, and failed pods
134 |
135 | ### 🛡 **Image Security Scanning**
136 |
137 | - Enter a **Docker image name** (e.g., `nginx:latest`)
138 | - Get a **comprehensive vulnerability report** with severity classifications
139 | - **Export scan results** for documentation and compliance
140 |
141 | ### 📋 **Pod Logs Viewer**
142 |
143 | - **Select a pod** to view its logs
144 | - **Filter log content** to find specific information
145 | - **Real-time log updates** for active monitoring
146 |
147 | ---
148 |
149 | ## 🛡 **Security & Vulnerability Scanning**
150 |
151 | This dashboard integrates **Trivy** to perform real-time security assessments of **Docker images**.
152 |
153 | ### 🔥 **Enhanced Security Features**
154 |
155 | ✅ **Vulnerability summary** with severity counts (Critical, High, Medium, Low)
156 | ✅ **Detailed vulnerability reports** with CVE information
157 | ✅ **Export functionality** for documentation and compliance
158 | ✅ **Visual indicators** for security status
159 |
160 | ### 🔍 **Running a Scan**
161 |
162 | 1. Enter the Docker image name in the scan form
163 | 2. Click the Scan button
164 | 3. View the vulnerability summary and detailed report
165 | 4. Export results if needed
166 |
167 | ---
168 |
169 | ## ⚙️ **Technology Stack**
170 |
171 | | **Component** | **Technology** |
172 | |----------------------|---------------------------|
173 | | **Frontend** | HTML5, CSS3, JavaScript ES6 |
174 | | **UI Framework** | Custom CSS with Flexbox/Grid |
175 | | **Charts** | Chart.js |
176 | | **Backend** | Python Flask |
177 | | **Kubernetes API** | Python Kubernetes Client |
178 | | **Security Scanning**| Trivy |
179 | | **Deployment** | Docker, Kubernetes |
180 |
181 | ---
182 |
183 | ## 🚀 **Deployment Options**
184 |
185 | You can deploy the Kubernetes Dashboard using **Docker, Kubernetes, or a cloud platform**.
186 |
187 | ### 🔹 **Run with Docker**
188 |
189 | ```bash
190 | docker build -t kubernetes-dashboard .
191 | docker run -p 5000:5000 kubernetes-dashboard
192 | ```
193 |
194 | ### 🔹 **Deploy on Kubernetes**
195 |
196 | ```bash
197 | kubectl apply -f k8s-manifest.yaml
198 | ```
199 |
200 | ### 🔹 **Deploy on Cloud (AWS/GCP/Azure)**
201 |
202 | You can deploy the dashboard on a **Kubernetes cluster** running on AWS EKS, GCP GKE, or Azure AKS.
203 |
204 | ---
205 |
206 | ## 📜 **License**
207 |
208 | This project is licensed under the **MIT License** – free for personal and commercial use.
209 |
210 | ---
211 |
212 | ## 🌟 **Support & Contributions**
213 |
214 | ### 🤝 **Contributing**
215 |
216 | Contributions are welcome! If you'd like to improve this project, feel free to submit a pull request.
217 |
218 | ---
219 |
220 | ### **Hit the Star!** ⭐
221 |
222 | **If you find this repository helpful and plan to use it for learning, please give it a star. Your support is appreciated!**
223 |
224 | ---
225 |
226 | ### 🛠️ **Author & Community**
227 |
228 | This project is crafted by **[Harshhaa](https://github.com/NotHarshhaa)** 💡.
229 | I'd love to hear your feedback! Feel free to share your thoughts.
230 |
231 | ---
232 |
233 | ### 📧 **Connect with me:**
234 |
235 | [](https://linkedin.com/in/harshhaa-vardhan-reddy) [](https://github.com/NotHarshhaa) [](https://t.me/prodevopsguy) [](https://dev.to/notharshhaa) [](https://hashnode.com/@prodevopsguy)
236 |
237 | ---
238 |
239 | ### 📢 **Stay Connected**
240 |
241 | 
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Kubernetes Dashboard
7 |
8 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
70 |
71 |
72 |
73 |
74 |
101 |
102 |
103 |
104 |
105 |
106 | System Metrics
107 |
108 |
109 |
110 |
116 |
117 |
118 |
119 |
120 |
121 | --%
122 |
123 |
124 |
125 |
126 |
127 |
128 |
134 |
135 |
136 |
137 |
138 |
139 | --%
140 |
141 |
142 |
143 |
144 |
145 |
146 |
151 |
152 |
153 |
154 |
155 |
156 | --%
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 | Cluster Resources
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
Deployments
174 | --
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
Pods Running
185 | --
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
Services
196 | --
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 | Cluster Health
205 |
206 |
210 | Not Checked
211 |
212 |
213 |
214 |
215 | API Server:
218 | --
223 |
224 |
225 | Scheduler:
228 | --
233 |
234 |
235 | Controller Manager:
238 | --
243 |
244 |
245 |
246 |
247 |
248 |
249 |
250 |
251 | Pod Status
252 |
253 |
254 |
255 | --
260 | Running
261 |
262 |
263 | --
268 | Pending
269 |
270 |
271 | --
276 | Failed
277 |
278 |
279 |
280 |
281 |
282 |
283 |
284 |
285 |
286 |
287 | Container Security Scanner
288 |
289 |
314 |
315 |
316 |
317 |
318 |
319 | -
320 | Critical
321 |
322 |
323 | -
324 | High
325 |
326 |
327 | -
328 | Medium
329 |
330 |
331 | -
332 | Low
333 |
334 |
335 |
336 |
337 |
338 |
348 |
352 | {}
354 |
355 |
356 |
357 |
358 |
359 |
360 |
361 | Pod Logs
362 |
363 |
388 |
389 |
390 | Select a pod to view logs
392 |
393 |
394 |
395 |
396 |
397 |
398 |
399 |
400 |
401 |
402 |
403 |
--------------------------------------------------------------------------------
/systeminfo.py:
--------------------------------------------------------------------------------
1 | import os
2 | import json
3 | import psutil
4 | import logging
5 | import time
6 | import subprocess
7 | import threading
8 | from datetime import datetime
9 | from flask import Flask, jsonify, request, Response, stream_with_context
10 | from flask_cors import CORS
11 | from kubernetes import client, config, watch
12 | from kubernetes.client.rest import ApiException
13 | from flask_limiter import Limiter
14 | from flask_limiter.util import get_remote_address
15 | from typing import List, Any, TypedDict
16 |
17 | def add_security_headers(response):
18 | """Add security headers to the response."""
19 | response.headers['X-Content-Type-Options'] = 'nosniff'
20 | response.headers['X-Frame-Options'] = 'DENY'
21 | response.headers['X-XSS-Protection'] = '1; mode=block'
22 | response.headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains'
23 | response.headers['Content-Security-Policy'] = "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdnjs.cloudflare.com; style-src 'self' 'unsafe-inline' https://cdnjs.cloudflare.com; img-src 'self' data:;"
24 | return response
25 |
26 | # Configuration from environment variables
27 | LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO").upper()
28 | FLASK_DEBUG = os.getenv("FLASK_DEBUG", "False") == "True"
29 | API_PORT = int(os.getenv("API_PORT", "5000"))
30 | METRICS_INTERVAL = int(os.getenv("METRICS_INTERVAL", "5")) # In seconds
31 | ALLOWED_ORIGINS = os.getenv("ALLOWED_ORIGINS", "http://localhost:5000").split(",")
32 |
33 | # Configure logging
34 | logging.basicConfig(
35 | level=getattr(logging, LOG_LEVEL, logging.INFO),
36 | format="%(asctime)s - %(levelname)s - %(message)s"
37 | )
38 | logger = logging.getLogger("k8s-dashboard")
39 |
40 | # Initialize Flask app
41 | app = Flask(__name__)
42 | app.after_request(add_security_headers)
43 | CORS(app, resources={r"/*": {"origins": ALLOWED_ORIGINS}})
44 |
45 | # Initialize rate limiter
46 | limiter = Limiter(
47 | app=app,
48 | key_func=get_remote_address,
49 | default_limits=["200 per day", "50 per hour"]
50 | )
51 |
52 | # Store historical metrics for charts
53 | metrics_history = {
54 | "cpu": [],
55 | "memory": [],
56 | "disk": [],
57 | "timestamp": []
58 | }
59 |
60 | # Initialize Kubernetes configuration
61 | try:
62 | config.load_kube_config()
63 | logger.info("✅ Kubernetes configuration loaded successfully.")
64 | except Exception:
65 | try:
66 | # Try to load in-cluster config if running inside K8s
67 | config.load_incluster_config()
68 | logger.info("✅ In-cluster Kubernetes configuration loaded.")
69 | except Exception:
70 | logger.warning("⚠️ Failed to load Kubernetes configuration, some features will be limited.")
71 |
72 | # Health Check
73 | @app.route('/health', methods=['GET'])
74 | def health_check():
75 | status = {"status": "ok", "timestamp": datetime.now().isoformat()}
76 | return jsonify(status), 200
77 |
78 | # System Information
79 | @app.route('/system_info', methods=['GET'])
80 | @limiter.limit("30/minute") # Add rate limiting
81 | def get_system_info():
82 | try:
83 | # Validate request parameters
84 | try:
85 | mem = psutil.virtual_memory()
86 | disk = psutil.disk_usage('/')
87 | except (OSError, PermissionError) as e:
88 | logger.error(f"❌ System access error: {str(e)}")
89 | return jsonify({'error': 'System access denied'}), 403
90 |
91 | # Get CPU info with per-core details
92 | try:
93 | cpu_percent = psutil.cpu_percent(interval=0.5) # Reduced interval for better performance
94 | cpu_count = psutil.cpu_count()
95 | cpu_freq = psutil.cpu_freq()
96 | cpu_per_core = psutil.cpu_percent(interval=0.1, percpu=True)
97 | except Exception as e:
98 | logger.error(f"❌ CPU info error: {str(e)}")
99 | cpu_percent = 0
100 | cpu_count = 1
101 | cpu_freq = None
102 | cpu_per_core = [0]
103 |
104 | # Validate data ranges
105 | cpu_percent = max(0, min(100, cpu_percent)) if cpu_percent is not None else 0
106 | mem_percent = max(0, min(100, mem.percent)) if mem.percent is not None else 0
107 | disk_percent = max(0, min(100, disk.percent)) if disk.percent is not None else 0
108 |
109 | data = {
110 | 'cpu_percent': round(cpu_percent, 2),
111 | 'cpu_details': {
112 | 'count': cpu_count,
113 | 'frequency': {
114 | 'current': round(cpu_freq.current, 2) if cpu_freq and cpu_freq.current else None,
115 | 'min': round(cpu_freq.min, 2) if cpu_freq and hasattr(cpu_freq, 'min') and cpu_freq.min else None,
116 | 'max': round(cpu_freq.max, 2) if cpu_freq and hasattr(cpu_freq, 'max') and cpu_freq.max else None
117 | },
118 | 'per_core': [round(core, 2) for core in cpu_per_core] if cpu_per_core else []
119 | },
120 | 'memory_usage': {
121 | 'total': mem.total,
122 | 'available': mem.available,
123 | 'used': mem.used,
124 | 'percent': round(mem_percent, 2)
125 | },
126 | 'disk_usage': {
127 | 'total': disk.total,
128 | 'used': disk.used,
129 | 'free': disk.free,
130 | 'percent': round(disk_percent, 2)
131 | },
132 | 'boot_time': psutil.boot_time(),
133 | 'timestamp': datetime.now().isoformat()
134 | }
135 |
136 | # Add to history (limit to 100 points)
137 | metrics_history["cpu"].append(cpu_percent)
138 | metrics_history["memory"].append(mem_percent)
139 | metrics_history["disk"].append(disk_percent)
140 | metrics_history["timestamp"].append(datetime.now().isoformat())
141 |
142 | # Keep only the last 100 data points
143 | if len(metrics_history["cpu"]) > 100:
144 | metrics_history["cpu"] = metrics_history["cpu"][-100:]
145 | metrics_history["memory"] = metrics_history["memory"][-100:]
146 | metrics_history["disk"] = metrics_history["disk"][-100:]
147 | metrics_history["timestamp"] = metrics_history["timestamp"][-100:]
148 |
149 | return jsonify(data)
150 | except Exception as e:
151 | logger.error(f"❌ Error fetching system info: {str(e)}")
152 | return jsonify({'error': 'Failed to fetch system information', 'details': str(e)}), 500
153 |
154 | # Historical metrics for charts
155 | @app.route('/metrics_history', methods=['GET'])
156 | def get_metrics_history():
157 | try:
158 | count = request.args.get('count', default=30, type=int)
159 | count = min(count, len(metrics_history["cpu"]))
160 |
161 | if count <= 0:
162 | return jsonify({"error": "Invalid count parameter"}), 400
163 |
164 | data = {
165 | "cpu": metrics_history["cpu"][-count:],
166 | "memory": metrics_history["memory"][-count:],
167 | "disk": metrics_history["disk"][-count:],
168 | "timestamp": metrics_history["timestamp"][-count:]
169 | }
170 |
171 | return jsonify(data)
172 | except Exception as e:
173 | logger.error(f"❌ Error fetching metrics history: {str(e)}")
174 | return jsonify({'error': 'Failed to fetch metrics history'}), 500
175 |
176 | class PodStatus(TypedDict):
177 | running: int
178 | pending: int
179 | failed: int
180 | succeeded: int
181 | unknown: int
182 |
183 | class KubernetesInfo(TypedDict):
184 | namespace: str
185 | num_deployments: int
186 | num_services: int
187 | num_pods: int
188 | num_configmaps: int
189 | num_secrets: int
190 | pod_statuses: PodStatus
191 |
192 | def get_pod_statuses(pods: List[Any]) -> PodStatus:
193 | """Calculate pod status counts from a list of pods."""
194 | status_counts: PodStatus = {
195 | "running": 0,
196 | "pending": 0,
197 | "failed": 0,
198 | "succeeded": 0,
199 | "unknown": 0
200 | }
201 |
202 | for pod in pods:
203 | status = pod.status.phase
204 | if status:
205 | status_counts[status.lower()] += 1
206 | else:
207 | status_counts["unknown"] += 1
208 |
209 | return status_counts
210 |
211 | def get_resource_counts(namespace: str) -> KubernetesInfo:
212 | """Get counts of various Kubernetes resources in a namespace."""
213 | try:
214 | core_v1 = client.CoreV1Api()
215 | apps_v1 = client.AppsV1Api()
216 |
217 | deployments = apps_v1.list_namespaced_deployment(namespace)
218 | services = core_v1.list_namespaced_service(namespace)
219 | pods = core_v1.list_namespaced_pod(namespace)
220 | configmaps = core_v1.list_namespaced_config_map(namespace)
221 | secrets = core_v1.list_namespaced_secret(namespace)
222 |
223 | pod_statuses = get_pod_statuses(pods.items)
224 |
225 | return {
226 | 'namespace': namespace,
227 | 'num_deployments': len(deployments.items),
228 | 'num_services': len(services.items),
229 | 'num_pods': len(pods.items),
230 | 'num_configmaps': len(configmaps.items),
231 | 'num_secrets': len(secrets.items),
232 | 'pod_statuses': pod_statuses
233 | }
234 | except ApiException as e:
235 | logger.error(f"❌ Kubernetes API error: {e.reason}")
236 | raise
237 | except Exception as e:
238 | logger.error(f"❌ Error fetching Kubernetes info: {str(e)}")
239 | raise
240 |
241 | @app.route('/kubernetes_info', methods=['GET'])
242 | @limiter.limit("60/minute") # Add rate limiting
243 | def get_kubernetes_info() -> Response:
244 | """API endpoint to get Kubernetes cluster information."""
245 | namespace = request.args.get('namespace', 'default')
246 | try:
247 | info = get_resource_counts(namespace)
248 | return jsonify(info)
249 | except ApiException as api_error:
250 | response = jsonify({'error': f"Kubernetes API error: {api_error.reason}"})
251 | response.status_code = 500
252 | return response
253 | except Exception: # Remove unused 'e' variable since we're not using it
254 | response = jsonify({'error': 'Failed to fetch Kubernetes information'})
255 | response.status_code = 500
256 | return response
257 |
258 | # Namespaces List
259 | @app.route('/kubernetes_namespaces', methods=['GET'])
260 | def get_kubernetes_namespaces():
261 | try:
262 | core_v1 = client.CoreV1Api()
263 | namespaces = [ns.metadata.name for ns in core_v1.list_namespace().items]
264 | return jsonify(namespaces)
265 | except Exception as e:
266 | logger.error(f"❌ Error fetching namespaces: {str(e)}")
267 | return jsonify({'error': 'Failed to fetch Kubernetes namespaces'}), 500
268 |
269 | # Node Info
270 | @app.route('/kubernetes_nodes', methods=['GET'])
271 | def get_kubernetes_nodes():
272 | try:
273 | core_v1 = client.CoreV1Api()
274 | nodes = core_v1.list_node().items
275 |
276 | node_info = []
277 | for node in nodes:
278 | conditions = {cond.type: cond.status for cond in node.status.conditions}
279 | ready_status = "Ready" if conditions.get("Ready") == "True" else "NotReady"
280 |
281 | # Get usage metrics if metrics server is available
282 | cpu_usage = "N/A"
283 | memory_usage = "N/A"
284 |
285 | try:
286 | metrics_api = client.CustomObjectsApi()
287 | node_metrics = metrics_api.get_cluster_custom_object(
288 | group="metrics.k8s.io",
289 | version="v1beta1",
290 | plural="nodes",
291 | name=node.metadata.name
292 | )
293 |
294 | if node_metrics:
295 | cpu_usage = node_metrics.get('usage', {}).get('cpu', 'N/A')
296 | memory_usage = node_metrics.get('usage', {}).get('memory', 'N/A')
297 | except:
298 | # Metrics server might not be available
299 | pass
300 |
301 | node_data = {
302 | 'name': node.metadata.name,
303 | 'status': ready_status,
304 | 'kubelet_version': node.status.node_info.kubelet_version,
305 | 'os_image': node.status.node_info.os_image,
306 | 'architecture': node.status.node_info.architecture,
307 | 'container_runtime': node.status.node_info.container_runtime_version,
308 | 'addresses': [addr.address for addr in node.status.addresses],
309 | 'capacity': {
310 | 'cpu': node.status.capacity.get('cpu'),
311 | 'memory': node.status.capacity.get('memory'),
312 | 'pods': node.status.capacity.get('pods')
313 | },
314 | 'usage': {
315 | 'cpu': cpu_usage,
316 | 'memory': memory_usage
317 | },
318 | 'conditions': conditions,
319 | 'labels': node.metadata.labels
320 | }
321 | node_info.append(node_data)
322 |
323 | return jsonify(node_info)
324 | except Exception as e:
325 | logger.error(f"❌ Error fetching node info: {str(e)}")
326 | return jsonify({'error': 'Failed to fetch node information'}), 500
327 |
328 | # Pod List with Details
329 | @app.route('/pods', methods=['GET'])
330 | def get_pods():
331 | namespace = request.args.get('namespace', 'default')
332 |
333 | try:
334 | core_v1 = client.CoreV1Api()
335 | pods = core_v1.list_namespaced_pod(namespace)
336 |
337 | pod_list = []
338 | for pod in pods.items:
339 | containers = []
340 | for container in pod.spec.containers:
341 | container_data = {
342 | 'name': container.name,
343 | 'image': container.image,
344 | 'ready': False,
345 | 'started': False,
346 | 'state': 'unknown'
347 | }
348 |
349 | # Get container status if available
350 | if pod.status.container_statuses:
351 | for status in pod.status.container_statuses:
352 | if status.name == container.name:
353 | container_data['ready'] = status.ready
354 | container_data['started'] = status.started
355 |
356 | # Determine container state
357 | if status.state.running:
358 | container_data['state'] = 'running'
359 | container_data['started_at'] = status.state.running.started_at.isoformat() if status.state.running.started_at else None
360 | elif status.state.waiting:
361 | container_data['state'] = 'waiting'
362 | container_data['reason'] = status.state.waiting.reason
363 | elif status.state.terminated:
364 | container_data['state'] = 'terminated'
365 | container_data['reason'] = status.state.terminated.reason
366 | container_data['exit_code'] = status.state.terminated.exit_code
367 |
368 | container_data['restart_count'] = status.restart_count
369 | break
370 |
371 | containers.append(container_data)
372 |
373 | pod_data = {
374 | 'name': pod.metadata.name,
375 | 'namespace': pod.metadata.namespace,
376 | 'status': pod.status.phase,
377 | 'pod_ip': pod.status.pod_ip,
378 | 'host_ip': pod.status.host_ip,
379 | 'node_name': pod.spec.node_name,
380 | 'created_at': pod.metadata.creation_timestamp.isoformat() if pod.metadata.creation_timestamp else None,
381 | 'containers': containers,
382 | 'labels': pod.metadata.labels
383 | }
384 | pod_list.append(pod_data)
385 |
386 | return jsonify(pod_list)
387 | except ApiException as e:
388 | logger.error(f"❌ Kubernetes API error: {e.reason}")
389 | return jsonify({'error': f"Kubernetes API error: {e.reason}"}), 500
390 | except Exception as e:
391 | logger.error(f"❌ Error fetching pods: {str(e)}")
392 | return jsonify({'error': 'Failed to fetch pods'}), 500
393 |
394 | # Pod Resource Usage
395 | @app.route('/pod_metrics', methods=['GET'])
396 | def get_pod_metrics():
397 | namespace = request.args.get('namespace', 'default')
398 | try:
399 | metrics_api = client.CustomObjectsApi()
400 | metrics = metrics_api.list_namespaced_custom_object(
401 | group="metrics.k8s.io",
402 | version="v1beta1",
403 | namespace=namespace,
404 | plural="pods"
405 | )
406 | return jsonify(metrics)
407 | except ApiException as e:
408 | logger.warning("📉 Metrics server might not be installed or accessible.")
409 | return jsonify({'error': 'Failed to fetch pod metrics', 'details': str(e)}), 500
410 | except Exception as e:
411 | logger.error(f"❌ Error fetching pod metrics: {str(e)}")
412 | return jsonify({'error': 'Failed to fetch pod metrics', 'details': str(e)}), 500
413 |
414 | # Pod Logs
415 | @app.route('/pod_logs', methods=['GET'])
416 | def get_pod_logs():
417 | namespace = request.args.get('namespace', 'default')
418 | pod_name = request.args.get('pod_name')
419 | container = request.args.get('container', None)
420 | tail_lines = request.args.get('tail_lines', 100, type=int)
421 | follow = request.args.get('follow', 'false').lower() == 'true'
422 |
423 | if not pod_name:
424 | return jsonify({'error': 'Pod name is required'}), 400
425 |
426 | try:
427 | core_v1 = client.CoreV1Api()
428 |
429 | if follow:
430 | def generate_logs():
431 | try:
432 | logs = core_v1.read_namespaced_pod_log(
433 | name=pod_name,
434 | namespace=namespace,
435 | container=container,
436 | follow=True,
437 | tail_lines=tail_lines,
438 | _preload_content=False
439 | )
440 |
441 | for line in logs:
442 | yield f"{line.decode('utf-8')}\n"
443 |
444 | except Exception as e:
445 | yield f"Error streaming logs: {str(e)}\n"
446 |
447 | return Response(stream_with_context(generate_logs()), mimetype='text/plain')
448 | else:
449 | logs = core_v1.read_namespaced_pod_log(
450 | name=pod_name,
451 | namespace=namespace,
452 | container=container,
453 | tail_lines=tail_lines
454 | )
455 | return jsonify({'logs': logs.split('\n')})
456 |
457 | except ApiException as e:
458 | logger.error(f"❌ Kubernetes API error: {e.reason}")
459 | return jsonify({'error': f"Kubernetes API error: {e.reason}"}), 500
460 | except Exception as e:
461 | logger.error(f"❌ Error fetching pod logs: {str(e)}")
462 | return jsonify({'error': 'Failed to fetch pod logs'}), 500
463 |
464 | # Trivy Image Scanner
465 | @app.route('/scan_image', methods=['POST'])
466 | def scan_image():
467 | data = request.get_json()
468 | image = data.get('container_id')
469 |
470 | if not image:
471 | return jsonify({'error': 'container_id is required'}), 400
472 |
473 | logger.info(f"🔍 Scanning container image: {image}")
474 |
475 | try:
476 | command = ['trivy', 'image', '--format', 'json', image]
477 | result = subprocess.run(command, capture_output=True, text=True, check=True)
478 |
479 | # Parse the JSON result to extract summary information
480 | try:
481 | scan_data = json.loads(result.stdout)
482 |
483 | # Extract vulnerability counts
484 | vulnerability_summary = {
485 | 'critical': 0,
486 | 'high': 0,
487 | 'medium': 0,
488 | 'low': 0,
489 | 'unknown': 0
490 | }
491 |
492 | if 'Results' in scan_data:
493 | for r in scan_data['Results']:
494 | if 'Vulnerabilities' in r:
495 | for vuln in r['Vulnerabilities']:
496 | severity = vuln.get('Severity', '').lower()
497 | if severity in vulnerability_summary:
498 | vulnerability_summary[severity] += 1
499 |
500 | return jsonify({
501 | 'scan_results': result.stdout,
502 | 'summary': vulnerability_summary
503 | })
504 | except json.JSONDecodeError:
505 | # If JSON parsing fails, return raw output
506 | return jsonify({'scan_results': result.stdout})
507 |
508 | except subprocess.CalledProcessError as e:
509 | logger.error(f"❌ Trivy scan failed: {e.stderr}")
510 | return jsonify({'error': 'Trivy scan failed', 'details': e.stderr}), 500
511 | except Exception as e:
512 | logger.error(f"❌ Unexpected error: {str(e)}")
513 | return jsonify({'error': 'An unexpected error occurred'}), 500
514 |
515 | # Kubernetes Component Status
516 | @app.route('/k8s_component_status', methods=['GET'])
517 | def get_component_status():
518 | try:
519 | core_v1 = client.CoreV1Api()
520 | components = core_v1.list_component_status()
521 |
522 | component_statuses = {}
523 | for component in components.items:
524 | status = "Healthy"
525 | conditions = []
526 |
527 | for condition in component.conditions:
528 | condition_status = {
529 | 'type': condition.type,
530 | 'status': condition.status,
531 | 'message': condition.message
532 | }
533 | conditions.append(condition_status)
534 |
535 | if condition.status != "True":
536 | status = "Unhealthy"
537 |
538 | component_statuses[component.metadata.name] = {
539 | 'status': status,
540 | 'conditions': conditions
541 | }
542 |
543 | return jsonify(component_statuses)
544 | except Exception as e:
545 | logger.error(f"❌ Error fetching component status: {str(e)}")
546 | return jsonify({'error': 'Failed to fetch component status'}), 500
547 |
548 | # Watch for pod events in a background thread
549 | def watch_pod_events(namespace='default'):
550 | try:
551 | v1 = client.CoreV1Api()
552 | w = watch.Watch()
553 |
554 | for event in w.stream(v1.list_namespaced_pod, namespace=namespace):
555 | pod = event['object']
556 | event_type = event['type']
557 |
558 | # Store the event in a global events list
559 | # In a real application, you could use a message queue, database, or in-memory cache
560 | logger.info(f"Pod event: {event_type} {pod.metadata.namespace}/{pod.metadata.name}")
561 |
562 | except Exception as e:
563 | logger.error(f"❌ Error watching pod events: {str(e)}")
564 | # Retry after a delay
565 | time.sleep(5)
566 | watch_pod_events(namespace)
567 |
568 | # Get recent pod events
569 | @app.route('/pod_events', methods=['GET'])
570 | def get_pod_events():
571 | namespace = request.args.get('namespace', 'default')
572 |
573 | try:
574 | v1 = client.CoreV1Api()
575 | events = v1.list_namespaced_event(namespace=namespace)
576 |
577 | # Filter and format events
578 | pod_events = []
579 | for event in events.items:
580 | if event.involved_object.kind.lower() == 'pod':
581 | pod_events.append({
582 | 'type': event.type,
583 | 'reason': event.reason,
584 | 'message': event.message,
585 | 'count': event.count,
586 | 'pod_name': event.involved_object.name,
587 | 'first_timestamp': event.first_timestamp.isoformat() if event.first_timestamp else None,
588 | 'last_timestamp': event.last_timestamp.isoformat() if event.last_timestamp else None
589 | })
590 |
591 | # Sort by most recent events first
592 | pod_events.sort(key=lambda x: x['last_timestamp'] or '', reverse=True)
593 |
594 | return jsonify(pod_events)
595 | except Exception as e:
596 | logger.error(f"❌ Error fetching pod events: {str(e)}")
597 | return jsonify({'error': 'Failed to fetch pod events'}), 500
598 |
599 | # Start background thread for watching pod events
600 | def start_event_watcher():
601 | event_thread = threading.Thread(target=watch_pod_events)
602 | event_thread.daemon = True
603 | event_thread.start()
604 |
605 | # Start the server
606 | if __name__ == '__main__':
607 | # Start event watcher thread
608 | # start_event_watcher()
609 |
610 | # Run the Flask app
611 | app.run(host='0.0.0.0', port=API_PORT, debug=FLASK_DEBUG)
612 |
--------------------------------------------------------------------------------
/styles.css:
--------------------------------------------------------------------------------
1 | /* Modern Kubernetes Dashboard CSS */
2 | :root {
3 | /* Light Theme Variables */
4 | --light-bg: #f8f9fa;
5 | --light-sidebar-bg: #ffffff;
6 | --light-card-bg: #ffffff;
7 | --light-text: #2c3e50;
8 | --light-border: #e2e8f0;
9 | --light-hover: #f1f5f9;
10 | --light-accent: #3498db;
11 | --light-accent-hover: #2980b9;
12 | --light-success: #2ecc71;
13 | --light-warning: #f39c12;
14 | --light-danger: #e74c3c;
15 | --light-info: #3498db;
16 | --light-sidebar-active: rgba(52, 152, 219, 0.1);
17 | --light-shadow: rgba(0, 0, 0, 0.1);
18 |
19 | /* Dark Theme Variables */
20 | --dark-bg: #1a1d21;
21 | --dark-sidebar-bg: #242830;
22 | --dark-card-bg: #2c3039;
23 | --dark-text: #e2e8f0;
24 | --dark-border: #3d4352;
25 | --dark-hover: #323845;
26 | --dark-accent: #3498db;
27 | --dark-accent-hover: #2980b9;
28 | --dark-success: #2ecc71;
29 | --dark-warning: #f39c12;
30 | --dark-danger: #e74c3c;
31 | --dark-info: #3498db;
32 | --dark-sidebar-active: rgba(52, 152, 219, 0.2);
33 | --dark-shadow: rgba(0, 0, 0, 0.3);
34 | }
35 |
36 | /* Base Styles */
37 | * {
38 | margin: 0;
39 | padding: 0;
40 | box-sizing: border-box;
41 | }
42 |
43 | body {
44 | font-family: "Inter", "Segoe UI", sans-serif;
45 | transition:
46 | background-color 0.3s,
47 | color 0.3s;
48 | height: 100vh;
49 | overflow: hidden;
50 | }
51 |
52 | /* Theme Settings */
53 | body.light-theme {
54 | background-color: var(--light-bg);
55 | color: var(--light-text);
56 | }
57 |
58 | body.dark-theme {
59 | background-color: var(--dark-bg);
60 | color: var(--dark-text);
61 | }
62 |
63 | /* App Container */
64 | .app-container {
65 | display: flex;
66 | height: 100vh;
67 | overflow: hidden;
68 | }
69 |
70 | /* Sidebar */
71 | .sidebar {
72 | width: 260px;
73 | height: 100vh;
74 | display: flex;
75 | flex-direction: column;
76 | padding: 1.5rem 1rem;
77 | transition: all 0.3s ease;
78 | }
79 |
80 | .light-theme .sidebar {
81 | background-color: var(--light-sidebar-bg);
82 | border-right: 1px solid var(--light-border);
83 | }
84 |
85 | .dark-theme .sidebar {
86 | background-color: var(--dark-sidebar-bg);
87 | border-right: 1px solid var(--dark-border);
88 | }
89 |
90 | .logo-container {
91 | display: flex;
92 | align-items: center;
93 | margin-bottom: 2rem;
94 | padding: 0 0.5rem;
95 | }
96 |
97 | .logo {
98 | width: 40px;
99 | height: 40px;
100 | margin-right: 0.8rem;
101 | }
102 |
103 | .sidebar h1 {
104 | font-size: 1.3rem;
105 | font-weight: 600;
106 | line-height: 1.2;
107 | }
108 |
109 | .sidebar-nav {
110 | flex: 1;
111 | }
112 |
113 | .sidebar-nav ul {
114 | list-style: none;
115 | }
116 |
117 | .sidebar-nav li {
118 | margin-bottom: 0.3rem;
119 | border-radius: 8px;
120 | transition: background-color 0.2s;
121 | }
122 |
123 | .light-theme .sidebar-nav li.active {
124 | background-color: var(--light-sidebar-active);
125 | }
126 |
127 | .dark-theme .sidebar-nav li.active {
128 | background-color: var(--dark-sidebar-active);
129 | }
130 |
131 | .light-theme .sidebar-nav li:hover:not(.active) {
132 | background-color: var(--light-hover);
133 | }
134 |
135 | .dark-theme .sidebar-nav li:hover:not(.active) {
136 | background-color: var(--dark-hover);
137 | }
138 |
139 | .sidebar-nav a {
140 | display: flex;
141 | align-items: center;
142 | text-decoration: none;
143 | padding: 0.8rem 1rem;
144 | border-radius: 8px;
145 | transition: color 0.2s;
146 | }
147 |
148 | .light-theme .sidebar-nav a {
149 | color: var(--light-text);
150 | }
151 |
152 | .dark-theme .sidebar-nav a {
153 | color: var(--dark-text);
154 | }
155 |
156 | .light-theme .sidebar-nav li.active a {
157 | color: var(--light-accent);
158 | }
159 |
160 | .dark-theme .sidebar-nav li.active a {
161 | color: var(--dark-accent);
162 | }
163 |
164 | .sidebar-nav i {
165 | margin-right: 0.8rem;
166 | width: 20px;
167 | text-align: center;
168 | }
169 |
170 | .sidebar-footer {
171 | padding: 1rem 0.5rem;
172 | border-top: 1px solid;
173 | margin-top: 1rem;
174 | }
175 |
176 | .light-theme .sidebar-footer {
177 | border-color: var(--light-border);
178 | }
179 |
180 | .dark-theme .sidebar-footer {
181 | border-color: var(--dark-border);
182 | }
183 |
184 | .theme-toggle {
185 | display: flex;
186 | align-items: center;
187 | background: none;
188 | border: none;
189 | cursor: pointer;
190 | padding: 0.5rem;
191 | border-radius: 8px;
192 | width: 100%;
193 | margin-bottom: 1rem;
194 | transition: background-color 0.2s;
195 | }
196 |
197 | .light-theme .theme-toggle {
198 | color: var(--light-text);
199 | }
200 |
201 | .dark-theme .theme-toggle {
202 | color: var(--dark-text);
203 | }
204 |
205 | .light-theme .theme-toggle:hover {
206 | background-color: var(--light-hover);
207 | }
208 |
209 | .dark-theme .theme-toggle:hover {
210 | background-color: var(--dark-hover);
211 | }
212 |
213 | .theme-toggle i {
214 | margin-right: 0.8rem;
215 | }
216 |
217 | .version {
218 | font-size: 0.8rem;
219 | text-align: center;
220 | opacity: 0.7;
221 | }
222 |
223 | /* Main Content */
224 | .main-content {
225 | flex: 1;
226 | overflow-y: auto;
227 | padding: 1.5rem;
228 | display: flex;
229 | flex-direction: column;
230 | }
231 |
232 | /* Header */
233 | .header {
234 | display: flex;
235 | justify-content: space-between;
236 | align-items: center;
237 | margin-bottom: 1.5rem;
238 | padding-bottom: 1rem;
239 | border-bottom: 1px solid;
240 | }
241 |
242 | .light-theme .header {
243 | border-color: var(--light-border);
244 | }
245 |
246 | .dark-theme .header {
247 | border-color: var(--dark-border);
248 | }
249 |
250 | .namespace-selector {
251 | display: flex;
252 | align-items: center;
253 | }
254 |
255 | .namespace-selector label {
256 | margin-right: 0.5rem;
257 | font-weight: 500;
258 | }
259 |
260 | .namespace-selector select,
261 | .logs-header select {
262 | padding: 0.5rem 1rem;
263 | border-radius: 6px;
264 | border: 1px solid;
265 | background: none;
266 | min-width: 180px;
267 | cursor: pointer;
268 | }
269 |
270 | .light-theme select {
271 | border-color: var(--light-border);
272 | color: var(--light-text);
273 | background-color: var(--light-card-bg);
274 | }
275 |
276 | .dark-theme select {
277 | border-color: var(--dark-border);
278 | color: var(--dark-text);
279 | background-color: var(--dark-card-bg);
280 | }
281 |
282 | .header-actions {
283 | display: flex;
284 | align-items: center;
285 | gap: 1rem;
286 | }
287 |
288 | .auto-refresh-toggle {
289 | display: flex;
290 | align-items: center;
291 | gap: 0.5rem;
292 | }
293 |
294 | .auto-refresh-toggle input {
295 | position: relative;
296 | width: 42px;
297 | height: 22px;
298 | appearance: none;
299 | background: #ddd;
300 | outline: none;
301 | border-radius: 11px;
302 | cursor: pointer;
303 | }
304 |
305 | .auto-refresh-toggle input::before {
306 | content: "";
307 | position: absolute;
308 | width: 18px;
309 | height: 18px;
310 | border-radius: 50%;
311 | top: 2px;
312 | left: 2px;
313 | background: white;
314 | transition: 0.3s;
315 | }
316 |
317 | .auto-refresh-toggle input:checked {
318 | background: var(--light-accent);
319 | }
320 |
321 | .dark-theme .auto-refresh-toggle input:checked {
322 | background: var(--dark-accent);
323 | }
324 |
325 | .auto-refresh-toggle input:checked::before {
326 | left: 22px;
327 | }
328 |
329 | .refresh-interval {
330 | font-size: 0.8rem;
331 | opacity: 0.8;
332 | }
333 |
334 | button {
335 | border: none;
336 | border-radius: 6px;
337 | padding: 0.5rem 1rem;
338 | cursor: pointer;
339 | display: flex;
340 | align-items: center;
341 | gap: 0.5rem;
342 | font-size: 0.9rem;
343 | transition: background-color 0.2s;
344 | }
345 |
346 | .refresh-button {
347 | background: none;
348 | border: 1px solid;
349 | width: 36px;
350 | height: 36px;
351 | display: flex;
352 | align-items: center;
353 | justify-content: center;
354 | padding: 0;
355 | }
356 |
357 | .light-theme .refresh-button {
358 | color: var(--light-text);
359 | border-color: var(--light-border);
360 | }
361 |
362 | .dark-theme .refresh-button {
363 | color: var(--dark-text);
364 | border-color: var(--dark-border);
365 | }
366 |
367 | .light-theme .refresh-button:hover {
368 | background-color: var(--light-hover);
369 | }
370 |
371 | .dark-theme .refresh-button:hover {
372 | background-color: var(--dark-hover);
373 | }
374 |
375 | .refresh-button i {
376 | transition: transform 0.3s ease-in-out;
377 | }
378 |
379 | .refresh-button.refreshing i {
380 | animation: spin 1s linear infinite;
381 | }
382 |
383 | @keyframes spin {
384 | from {
385 | transform: rotate(0deg);
386 | }
387 | to {
388 | transform: rotate(360deg);
389 | }
390 | }
391 |
392 | .health-check-button {
393 | background-color: var(--light-accent);
394 | color: white;
395 | }
396 |
397 | .health-check-button:hover {
398 | background-color: var(--light-accent-hover);
399 | }
400 |
401 | .dark-theme .health-check-button {
402 | background-color: var(--dark-accent);
403 | }
404 |
405 | .dark-theme .health-check-button:hover {
406 | background-color: var(--dark-accent-hover);
407 | }
408 |
409 | /* Dashboard Content */
410 | .dashboard-content {
411 | flex: 1;
412 | display: flex;
413 | flex-direction: column;
414 | gap: 1.5rem;
415 | }
416 |
417 | /* Section Styles */
418 | section {
419 | background-color: var(--light-card-bg);
420 | border-radius: 12px;
421 | padding: 1.5rem;
422 | box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
423 | }
424 |
425 | .dark-theme section {
426 | background-color: var(--dark-card-bg);
427 | box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
428 | }
429 |
430 | section h2 {
431 | font-size: 1.2rem;
432 | margin-bottom: 1rem;
433 | font-weight: 600;
434 | }
435 |
436 | /* Metrics Section */
437 | .metrics-grid {
438 | display: grid;
439 | grid-template-columns: repeat(3, 1fr);
440 | gap: 1rem;
441 | }
442 |
443 | .metric-card {
444 | border-radius: 8px;
445 | padding: 1rem;
446 | border: 1px solid;
447 | }
448 |
449 | .light-theme .metric-card {
450 | border-color: var(--light-border);
451 | }
452 |
453 | .dark-theme .metric-card {
454 | border-color: var(--dark-border);
455 | }
456 |
457 | .metric-header {
458 | margin-bottom: 0.8rem;
459 | }
460 |
461 | .metric-header h3 {
462 | font-size: 1rem;
463 | font-weight: 500;
464 | display: flex;
465 | align-items: center;
466 | gap: 0.5rem;
467 | }
468 |
469 | .metric-body {
470 | display: flex;
471 | flex-direction: column;
472 | align-items: center;
473 | }
474 |
475 | .chart-container {
476 | width: 100%;
477 | height: 150px;
478 | margin-bottom: 0.5rem;
479 | }
480 |
481 | .metric-value {
482 | text-align: center;
483 | }
484 |
485 | .percentage {
486 | font-size: 1.5rem;
487 | font-weight: 600;
488 | }
489 |
490 | /* Resources Section */
491 | .resources-grid {
492 | display: grid;
493 | grid-template-columns: repeat(3, 1fr);
494 | gap: 1rem;
495 | }
496 |
497 | .resource-card {
498 | display: flex;
499 | align-items: center;
500 | gap: 1rem;
501 | padding: 1.2rem;
502 | border-radius: 8px;
503 | border: 1px solid;
504 | transition:
505 | transform 0.2s,
506 | box-shadow 0.2s;
507 | }
508 |
509 | .light-theme .resource-card {
510 | border-color: var(--light-border);
511 | background-color: var(--light-card-bg);
512 | }
513 |
514 | .dark-theme .resource-card {
515 | border-color: var(--dark-border);
516 | background-color: var(--dark-card-bg);
517 | }
518 |
519 | .resource-card:hover {
520 | transform: translateY(-2px);
521 | }
522 |
523 | .light-theme .resource-card:hover {
524 | box-shadow: 0 5px 15px rgba(0, 0, 0, 0.05);
525 | }
526 |
527 | .dark-theme .resource-card:hover {
528 | box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
529 | }
530 |
531 | .resource-icon {
532 | width: 48px;
533 | height: 48px;
534 | border-radius: 8px;
535 | display: flex;
536 | align-items: center;
537 | justify-content: center;
538 | font-size: 1.5rem;
539 | color: white;
540 | }
541 |
542 | .deployments .resource-icon {
543 | background-color: var(--light-info);
544 | }
545 |
546 | .pods-running .resource-icon {
547 | background-color: var(--light-success);
548 | }
549 |
550 | .services-running .resource-icon {
551 | background-color: var(--light-warning);
552 | }
553 |
554 | .resource-details {
555 | flex: 1;
556 | }
557 |
558 | .resource-details h3 {
559 | font-size: 0.9rem;
560 | font-weight: 500;
561 | margin-bottom: 0.3rem;
562 | }
563 |
564 | .count {
565 | font-size: 1.5rem;
566 | font-weight: 600;
567 | }
568 |
569 | /* Health Status Section */
570 | .health-status-container {
571 | display: flex;
572 | gap: 1.5rem;
573 | }
574 |
575 | .health-status-indicator {
576 | flex: 0 0 150px;
577 | height: 150px;
578 | border-radius: 50%;
579 | display: flex;
580 | align-items: center;
581 | justify-content: center;
582 | font-weight: bold;
583 | font-size: 1.2rem;
584 | color: white;
585 | position: relative;
586 | background-color: #6c757d;
587 | }
588 |
589 | .health-status-indicator.healthy {
590 | background-color: var(--light-success);
591 | }
592 |
593 | .health-status-indicator.unhealthy {
594 | background-color: var(--light-danger);
595 | }
596 |
597 | .health-status-indicator.warning {
598 | background-color: var(--light-warning);
599 | }
600 |
601 | .health-details {
602 | flex: 1;
603 | display: flex;
604 | flex-direction: column;
605 | justify-content: center;
606 | }
607 |
608 | .health-metrics {
609 | display: grid;
610 | grid-template-columns: repeat(3, 1fr);
611 | gap: 1rem;
612 | }
613 |
614 | .health-metric {
615 | padding: 1rem;
616 | border-radius: 8px;
617 | border: 1px solid;
618 | }
619 |
620 | .light-theme .health-metric {
621 | border-color: var(--light-border);
622 | }
623 |
624 | .dark-theme .health-metric {
625 | border-color: var(--dark-border);
626 | }
627 |
628 | .metric-label {
629 | display: block;
630 | font-size: 0.8rem;
631 | margin-bottom: 0.3rem;
632 | opacity: 0.8;
633 | }
634 |
635 | .metric-value {
636 | font-size: 1rem;
637 | font-weight: 500;
638 | }
639 |
640 | .metric-value.online {
641 | color: var(--light-success);
642 | }
643 |
644 | .metric-value.offline {
645 | color: var(--light-danger);
646 | }
647 |
648 | /* Pod Visualization Section */
649 | .pod-visualization-container {
650 | display: flex;
651 | gap: 1.5rem;
652 | }
653 |
654 | .pod-status-summary {
655 | display: flex;
656 | flex-direction: column;
657 | gap: 1rem;
658 | min-width: 150px;
659 | }
660 |
661 | .status-item {
662 | padding: 1rem;
663 | border-radius: 8px;
664 | text-align: center;
665 | }
666 |
667 | .status-item.running {
668 | background-color: rgba(46, 204, 113, 0.2);
669 | border: 1px solid rgba(46, 204, 113, 0.5);
670 | }
671 |
672 | .status-item.pending {
673 | background-color: rgba(243, 156, 18, 0.2);
674 | border: 1px solid rgba(243, 156, 18, 0.5);
675 | }
676 |
677 | .status-item.failed {
678 | background-color: rgba(231, 76, 60, 0.2);
679 | border: 1px solid rgba(231, 76, 60, 0.5);
680 | }
681 |
682 | .status-count {
683 | font-size: 1.5rem;
684 | font-weight: 600;
685 | display: block;
686 | }
687 |
688 | .pod-status-chart-container {
689 | flex: 1;
690 | height: 240px;
691 | }
692 |
693 | /* Security Scanner Section */
694 | .security-scanner-container {
695 | display: flex;
696 | flex-direction: column;
697 | gap: 1.5rem;
698 | }
699 |
700 | .scan-form {
701 | display: flex;
702 | flex-direction: column;
703 | gap: 0.5rem;
704 | }
705 |
706 | .form-group {
707 | display: flex;
708 | flex-direction: column;
709 | gap: 0.5rem;
710 | }
711 |
712 | .form-group label {
713 | font-weight: 500;
714 | }
715 |
716 | .form-help {
717 | font-size: 0.8rem;
718 | opacity: 0.7;
719 | margin-top: 0.25rem;
720 | display: block;
721 | }
722 |
723 | .input-with-button {
724 | display: flex;
725 | gap: 0.5rem;
726 | }
727 |
728 | .input-with-button input {
729 | flex: 1;
730 | padding: 0.6rem 1rem;
731 | border-radius: 6px;
732 | border: 1px solid;
733 | background: none;
734 | }
735 |
736 | .light-theme .input-with-button input {
737 | border-color: var(--light-border);
738 | color: var(--light-text);
739 | }
740 |
741 | .dark-theme .input-with-button input {
742 | border-color: var(--dark-border);
743 | color: var(--dark-text);
744 | background-color: var(--dark-hover);
745 | }
746 |
747 | .scan-button {
748 | background-color: var(--light-accent);
749 | color: white;
750 | }
751 |
752 | .scan-button:hover {
753 | background-color: var(--light-accent-hover);
754 | }
755 |
756 | .dark-theme .scan-button {
757 | background-color: var(--dark-accent);
758 | }
759 |
760 | .dark-theme .scan-button:hover {
761 | background-color: var(--dark-accent-hover);
762 | }
763 |
764 | .scan-results-container {
765 | display: flex;
766 | flex-direction: column;
767 | gap: 1rem;
768 | }
769 |
770 | .scan-summary {
771 | display: flex;
772 | flex-direction: column;
773 | gap: 1rem;
774 | }
775 |
776 | .vulnerability-counts {
777 | display: grid;
778 | grid-template-columns: repeat(4, 1fr);
779 | gap: 0.8rem;
780 | }
781 |
782 | .vuln-count {
783 | text-align: center;
784 | padding: 1rem;
785 | border-radius: 8px;
786 | }
787 |
788 | .vuln-count .count {
789 | font-size: 1.8rem;
790 | font-weight: 600;
791 | display: block;
792 | }
793 |
794 | .vuln-count .label {
795 | font-size: 0.8rem;
796 | opacity: 0.8;
797 | }
798 |
799 | .vuln-count.critical {
800 | background-color: rgba(231, 76, 60, 0.2);
801 | border: 1px solid rgba(231, 76, 60, 0.5);
802 | }
803 |
804 | .vuln-count.high {
805 | background-color: rgba(243, 156, 18, 0.2);
806 | border: 1px solid rgba(243, 156, 18, 0.5);
807 | }
808 |
809 | .vuln-count.medium {
810 | background-color: rgba(52, 152, 219, 0.2);
811 | border: 1px solid rgba(52, 152, 219, 0.5);
812 | }
813 |
814 | .vuln-count.low {
815 | background-color: rgba(46, 204, 113, 0.2);
816 | border: 1px solid rgba(46, 204, 113, 0.5);
817 | }
818 |
819 | .scan-details {
820 | border: 1px solid;
821 | border-radius: 8px;
822 | overflow: hidden;
823 | }
824 |
825 | .light-theme .scan-details {
826 | border-color: var(--light-border);
827 | }
828 |
829 | .dark-theme .scan-details {
830 | border-color: var(--dark-border);
831 | }
832 |
833 | .scan-details-header {
834 | display: flex;
835 | justify-content: space-between;
836 | align-items: center;
837 | padding: 0.8rem 1rem;
838 | border-bottom: 1px solid;
839 | }
840 |
841 | .light-theme .scan-details-header {
842 | border-color: var(--light-border);
843 | background-color: var(--light-hover);
844 | }
845 |
846 | .dark-theme .scan-details-header {
847 | border-color: var(--dark-border);
848 | background-color: var(--dark-hover);
849 | }
850 |
851 | .scan-details-header h3 {
852 | font-size: 1rem;
853 | font-weight: 500;
854 | }
855 |
856 | .export-button {
857 | background: none;
858 | border: 1px solid;
859 | font-size: 0.8rem;
860 | }
861 |
862 | .light-theme .export-button {
863 | border-color: var(--light-border);
864 | color: var(--light-text);
865 | }
866 |
867 | .dark-theme .export-button {
868 | border-color: var(--dark-border);
869 | color: var(--dark-text);
870 | }
871 |
872 | .light-theme .export-button:hover {
873 | background-color: var(--light-hover);
874 | }
875 |
876 | .dark-theme .export-button:hover {
877 | background-color: var(--dark-hover);
878 | }
879 |
880 | .scan-results-output {
881 | max-height: 300px;
882 | overflow-y: auto;
883 | padding: 1rem;
884 | font-family: monospace;
885 | font-size: 0.85rem;
886 | margin: 0;
887 | white-space: pre-wrap;
888 | }
889 |
890 | .light-theme .scan-results-output {
891 | background-color: var(--light-bg);
892 | color: var(--light-text);
893 | }
894 |
895 | .dark-theme .scan-results-output {
896 | background-color: var(--dark-bg);
897 | color: var(--dark-text);
898 | }
899 |
900 | /* Logs Section */
901 | .logs-container {
902 | display: flex;
903 | flex-direction: column;
904 | gap: 1rem;
905 | }
906 |
907 | .logs-header {
908 | display: flex;
909 | justify-content: space-between;
910 | align-items: center;
911 | flex-wrap: wrap;
912 | gap: 1rem;
913 | }
914 |
915 | .pod-selector {
916 | display: flex;
917 | align-items: center;
918 | gap: 0.5rem;
919 | }
920 |
921 | .logs-actions {
922 | display: flex;
923 | align-items: center;
924 | gap: 1rem;
925 | }
926 |
927 | .refresh-logs-button {
928 | background-color: var(--light-accent);
929 | color: white;
930 | }
931 |
932 | .refresh-logs-button:hover {
933 | background-color: var(--light-accent-hover);
934 | }
935 |
936 | .dark-theme .refresh-logs-button {
937 | background-color: var(--dark-accent);
938 | }
939 |
940 | .dark-theme .refresh-logs-button:hover {
941 | background-color: var(--dark-accent-hover);
942 | }
943 |
944 | .logs-filter input {
945 | padding: 0.5rem 1rem;
946 | border-radius: 6px;
947 | border: 1px solid;
948 | background: none;
949 | min-width: 200px;
950 | }
951 |
952 | .light-theme .logs-filter input {
953 | border-color: var(--light-border);
954 | color: var(--light-text);
955 | }
956 |
957 | .dark-theme .logs-filter input {
958 | border-color: var(--dark-border);
959 | color: var(--dark-text);
960 | background-color: var(--dark-hover);
961 | }
962 |
963 | .logs-output-container {
964 | border: 1px solid;
965 | border-radius: 8px;
966 | overflow: hidden;
967 | }
968 |
969 | .light-theme .logs-output-container {
970 | border-color: var(--light-border);
971 | }
972 |
973 | .dark-theme .logs-output-container {
974 | border-color: var(--dark-border);
975 | }
976 |
977 | .logs-output {
978 | max-height: 300px;
979 | overflow-y: auto;
980 | padding: 1rem;
981 | font-family: monospace;
982 | font-size: 0.85rem;
983 | margin: 0;
984 | white-space: pre-wrap;
985 | }
986 |
987 | .light-theme .logs-output {
988 | background-color: var(--light-bg);
989 | color: var(--light-text);
990 | }
991 |
992 | .dark-theme .logs-output {
993 | background-color: var(--dark-bg);
994 | color: var(--dark-text);
995 | }
996 |
997 | /* Loading States */
998 | .loading {
999 | opacity: 0.6;
1000 | pointer-events: none;
1001 | position: relative;
1002 | }
1003 |
1004 | .loading::after {
1005 | content: '';
1006 | position: absolute;
1007 | top: 50%;
1008 | left: 50%;
1009 | width: 20px;
1010 | height: 20px;
1011 | margin: -10px 0 0 -10px;
1012 | border: 2px solid transparent;
1013 | border-top: 2px solid var(--light-accent);
1014 | border-radius: 50%;
1015 | animation: spin 1s linear infinite;
1016 | }
1017 |
1018 | .dark-theme .loading::after {
1019 | border-top-color: var(--dark-accent);
1020 | }
1021 |
1022 | .loading-skeleton {
1023 | display: none;
1024 | background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
1025 | background-size: 200% 100%;
1026 | animation: loading 1.5s infinite;
1027 | border-radius: 8px;
1028 | height: 20px;
1029 | margin: 0.5rem 0;
1030 | }
1031 |
1032 | .dark-theme .loading-skeleton {
1033 | background: linear-gradient(90deg, #3d4352 25%, #323845 50%, #3d4352 75%);
1034 | background-size: 200% 100%;
1035 | }
1036 |
1037 | @keyframes loading {
1038 | 0% {
1039 | background-position: 200% 0;
1040 | }
1041 | 100% {
1042 | background-position: -200% 0;
1043 | }
1044 | }
1045 |
1046 | /* Progress Indicators */
1047 | .scan-progress {
1048 | margin: 1rem 0;
1049 | padding: 1rem;
1050 | border-radius: 8px;
1051 | background-color: var(--light-hover);
1052 | }
1053 |
1054 | .dark-theme .scan-progress {
1055 | background-color: var(--dark-hover);
1056 | }
1057 |
1058 | .progress-bar {
1059 | width: 100%;
1060 | height: 8px;
1061 | background-color: var(--light-border);
1062 | border-radius: 4px;
1063 | overflow: hidden;
1064 | margin-bottom: 0.5rem;
1065 | }
1066 |
1067 | .dark-theme .progress-bar {
1068 | background-color: var(--dark-border);
1069 | }
1070 |
1071 | .progress-fill {
1072 | height: 100%;
1073 | background: linear-gradient(90deg, var(--light-accent), var(--light-accent-hover));
1074 | border-radius: 4px;
1075 | animation: progress-animation 2s ease-in-out infinite;
1076 | }
1077 |
1078 | .dark-theme .progress-fill {
1079 | background: linear-gradient(90deg, var(--dark-accent), var(--dark-accent-hover));
1080 | }
1081 |
1082 | @keyframes progress-animation {
1083 | 0% {
1084 | width: 0%;
1085 | transform: translateX(-100%);
1086 | }
1087 | 50% {
1088 | width: 100%;
1089 | transform: translateX(0%);
1090 | }
1091 | 100% {
1092 | width: 100%;
1093 | transform: translateX(100%);
1094 | }
1095 | }
1096 |
1097 | .progress-text {
1098 | text-align: center;
1099 | font-size: 0.9rem;
1100 | opacity: 0.8;
1101 | }
1102 |
1103 | /* Notifications */
1104 | .notifications-container {
1105 | position: fixed;
1106 | top: 1rem;
1107 | right: 1rem;
1108 | width: 300px;
1109 | z-index: 1000;
1110 | display: flex;
1111 | flex-direction: column;
1112 | gap: 0.5rem;
1113 | }
1114 |
1115 | .notification {
1116 | padding: 1rem;
1117 | border-radius: 8px;
1118 | background-color: white;
1119 | box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
1120 | animation: slide-in 0.3s ease;
1121 | display: flex;
1122 | align-items: flex-start;
1123 | gap: 0.8rem;
1124 | }
1125 |
1126 | @keyframes slide-in {
1127 | from {
1128 | transform: translateX(100%);
1129 | opacity: 0;
1130 | }
1131 | to {
1132 | transform: translateX(0);
1133 | opacity: 1;
1134 | }
1135 | }
1136 |
1137 | .notification-icon {
1138 | width: 24px;
1139 | height: 24px;
1140 | flex-shrink: 0;
1141 | display: flex;
1142 | align-items: center;
1143 | justify-content: center;
1144 | border-radius: 50%;
1145 | color: white;
1146 | }
1147 |
1148 | .notification-icon.success {
1149 | background-color: var(--light-success);
1150 | }
1151 |
1152 | .notification-icon.error {
1153 | background-color: var(--light-danger);
1154 | }
1155 |
1156 | .notification-icon.warning {
1157 | background-color: var(--light-warning);
1158 | }
1159 |
1160 | .notification-icon.info {
1161 | background-color: var(--light-info);
1162 | }
1163 |
1164 | .notification-content {
1165 | flex: 1;
1166 | }
1167 |
1168 | .notification-title {
1169 | font-weight: 600;
1170 | margin-bottom: 0.3rem;
1171 | }
1172 |
1173 | .notification-message {
1174 | font-size: 0.9rem;
1175 | opacity: 0.8;
1176 | }
1177 |
1178 | /* Responsive Design */
1179 | @media (max-width: 1200px) {
1180 | .metrics-grid,
1181 | .resources-grid {
1182 | grid-template-columns: repeat(2, 1fr);
1183 | }
1184 |
1185 | .health-metrics {
1186 | grid-template-columns: repeat(2, 1fr);
1187 | }
1188 |
1189 | .vulnerability-counts {
1190 | grid-template-columns: repeat(2, 1fr);
1191 | }
1192 | }
1193 |
1194 | @media (max-width: 992px) {
1195 | .sidebar {
1196 | width: 80px;
1197 | padding: 1.5rem 0.5rem;
1198 | }
1199 |
1200 | .logo-container {
1201 | justify-content: center;
1202 | }
1203 |
1204 | .logo {
1205 | margin-right: 0;
1206 | }
1207 |
1208 | .sidebar h1,
1209 | .sidebar-nav a span,
1210 | .theme-toggle span,
1211 | .version {
1212 | display: none;
1213 | }
1214 |
1215 | .sidebar-nav i {
1216 | margin-right: 0;
1217 | font-size: 1.2rem;
1218 | }
1219 |
1220 | .sidebar-nav a {
1221 | justify-content: center;
1222 | }
1223 |
1224 | .theme-toggle {
1225 | justify-content: center;
1226 | }
1227 |
1228 | .theme-toggle i {
1229 | margin-right: 0;
1230 | }
1231 |
1232 | .health-status-container,
1233 | .pod-visualization-container {
1234 | flex-direction: column;
1235 | }
1236 |
1237 | .health-status-indicator {
1238 | margin: 0 auto;
1239 | }
1240 | }
1241 |
1242 | @media (max-width: 768px) {
1243 | .header {
1244 | flex-direction: column;
1245 | align-items: flex-start;
1246 | gap: 1rem;
1247 | }
1248 |
1249 | .metrics-grid,
1250 | .resources-grid,
1251 | .health-metrics,
1252 | .vulnerability-counts {
1253 | grid-template-columns: 1fr;
1254 | }
1255 |
1256 | .logs-header {
1257 | flex-direction: column;
1258 | align-items: flex-start;
1259 | }
1260 |
1261 | .logs-actions {
1262 | width: 100%;
1263 | justify-content: space-between;
1264 | }
1265 |
1266 | .logs-filter input {
1267 | width: 100%;
1268 | }
1269 | }
1270 |
1271 | @media (max-width: 576px) {
1272 | .main-content {
1273 | padding: 1rem;
1274 | }
1275 |
1276 | .app-container {
1277 | flex-direction: column;
1278 | }
1279 |
1280 | .sidebar {
1281 | width: 100%;
1282 | height: auto;
1283 | padding: 1rem;
1284 | flex-direction: row;
1285 | align-items: center;
1286 | }
1287 |
1288 | .logo-container {
1289 | margin-bottom: 0;
1290 | margin-right: 1rem;
1291 | }
1292 |
1293 | .sidebar-nav {
1294 | display: flex;
1295 | overflow-x: auto;
1296 | padding-bottom: 0.5rem;
1297 | }
1298 |
1299 | .sidebar-nav ul {
1300 | display: flex;
1301 | gap: 0.5rem;
1302 | }
1303 |
1304 | .sidebar-nav li {
1305 | margin-bottom: 0;
1306 | }
1307 |
1308 | .sidebar-footer {
1309 | display: none;
1310 | }
1311 | }
1312 |
--------------------------------------------------------------------------------
/app.js:
--------------------------------------------------------------------------------
1 | // Enhanced Kubernetes Dashboard JavaScript
2 | document.addEventListener("DOMContentLoaded", () => {
3 | // DOM Elements
4 | const namespaceDropdown = document.getElementById("namespace-dropdown");
5 | const autoRefreshToggle = document.getElementById("autoRefreshToggle");
6 | const refreshButton = document.getElementById("refreshButton");
7 | const healthCheckButton = document.getElementById("healthCheckButton");
8 | const healthCheckResult = document.getElementById("healthCheckResult");
9 | const apiServerStatus = document.getElementById("apiServerStatus");
10 | const schedulerStatus = document.getElementById("schedulerStatus");
11 | const controllerStatus = document.getElementById("controllerStatus");
12 | const scanForm = document.getElementById("scanForm");
13 | const imageInput = document.getElementById("imageInput");
14 | const scanResults = document.getElementById("scanResults");
15 | const scanSummary = document.getElementById("scanSummary");
16 | const exportScanResults = document.getElementById("exportScanResults");
17 | const podSelector = document.getElementById("podSelector");
18 | const logsOutput = document.getElementById("logsOutput");
19 | const logsFilter = document.getElementById("logsFilter");
20 | const refreshLogs = document.getElementById("refreshLogs");
21 | const podsRunningCount = document.getElementById("podsRunningCount");
22 | const podsPendingCount = document.getElementById("podsPendingCount");
23 | const podsFailedCount = document.getElementById("podsFailedCount");
24 |
25 | // Global state
26 | let defaultNamespace = "default";
27 | let autoRefreshInterval = null;
28 | let currentPodLogs = "";
29 | let refreshTimeout = null;
30 | let retryAttempts = 0;
31 | const MAX_RETRY_ATTEMPTS = 3;
32 | const RETRY_DELAY = 1000; // 1 second
33 |
34 | // Cache for API responses
35 | const cache = {
36 | systemInfo: { data: null, timestamp: 0, ttl: 5000 }, // 5 seconds
37 | namespaces: { data: null, timestamp: 0, ttl: 30000 }, // 30 seconds
38 | kubernetesInfo: { data: null, timestamp: 0, ttl: 10000 }, // 10 seconds
39 | podStatuses: { data: null, timestamp: 0, ttl: 10000 }, // 10 seconds
40 | };
41 |
42 | // Cache utility functions
43 | function getCachedData(key) {
44 | const cached = cache[key];
45 | if (cached && Date.now() - cached.timestamp < cached.ttl) {
46 | return cached.data;
47 | }
48 | return null;
49 | }
50 |
51 | function setCachedData(key, data) {
52 | cache[key] = {
53 | data: data,
54 | timestamp: Date.now(),
55 | ttl: cache[key].ttl
56 | };
57 | }
58 |
59 | // Cleanup function for intervals and timeouts
60 | function cleanup() {
61 | if (autoRefreshInterval) {
62 | clearInterval(autoRefreshInterval);
63 | autoRefreshInterval = null;
64 | }
65 | if (refreshTimeout) {
66 | clearTimeout(refreshTimeout);
67 | refreshTimeout = null;
68 | }
69 | }
70 |
71 | // Add cleanup on page unload
72 | window.addEventListener('beforeunload', cleanup);
73 |
74 | // Enhanced error handling utility
75 | async function fetchWithRetry(url, options = {}, retries = MAX_RETRY_ATTEMPTS) {
76 | try {
77 | const response = await fetch(url, options);
78 | if (!response.ok) {
79 | const errorText = await response.text().catch(() => 'Unknown error');
80 | throw new Error(`HTTP ${response.status}: ${errorText}`);
81 | }
82 | retryAttempts = 0; // Reset on successful request
83 | return await response.json();
84 | } catch (error) {
85 | if (retries > 0) {
86 | retryAttempts++;
87 | console.warn(`Request failed, retrying in ${RETRY_DELAY * retryAttempts}ms... (${retries} attempts left)`);
88 | await new Promise(resolve => setTimeout(resolve, RETRY_DELAY * retryAttempts));
89 | return fetchWithRetry(url, options, retries - 1);
90 | }
91 | console.error(`Request failed after ${MAX_RETRY_ATTEMPTS} attempts:`, error);
92 | throw new Error(`Failed after ${MAX_RETRY_ATTEMPTS} attempts: ${error.message}`);
93 | }
94 | }
95 |
96 | // Enhanced error display utility
97 | function displayError(error, context = '') {
98 | console.error(`Error ${context}:`, error);
99 | const errorMessage = error.message || 'An unexpected error occurred';
100 | showNotification(`${context ? context + ': ' : ''}${errorMessage}`, 'error');
101 | }
102 |
103 | // Chart objects container
104 | window.charts = {};
105 |
106 | // Colors
107 | const chartColors = {
108 | cpu: "#3498db",
109 | memory: "#2ecc71",
110 | storage: "#9b59b6",
111 | pod: {
112 | running: "#2ecc71",
113 | pending: "#f39c12",
114 | failed: "#e74c3c",
115 | unknown: "#95a5a6",
116 | },
117 | };
118 |
119 | // ========== Initial Setup ==========
120 | initializeDashboard();
121 |
122 | function initializeDashboard() {
123 | // Setup event listeners
124 | if (scanForm) scanForm.addEventListener("submit", handleImageScan);
125 | if (healthCheckButton)
126 | healthCheckButton.addEventListener("click", handleHealthCheck);
127 | if (namespaceDropdown)
128 | namespaceDropdown.addEventListener("change", handleNamespaceChange);
129 | if (autoRefreshToggle)
130 | autoRefreshToggle.addEventListener("change", toggleAutoRefresh);
131 | if (refreshButton) refreshButton.addEventListener("click", manualRefresh);
132 | if (exportScanResults)
133 | exportScanResults.addEventListener("click", exportScanResultsToFile);
134 | if (podSelector) podSelector.addEventListener("change", fetchPodLogs);
135 | if (refreshLogs) refreshLogs.addEventListener("click", refreshPodLogs);
136 | if (logsFilter) logsFilter.addEventListener("input", filterLogs);
137 |
138 | // Initialize charts
139 | initializeCharts();
140 |
141 | // Add keyboard navigation
142 | initializeKeyboardNavigation();
143 |
144 | // First update
145 | updateDashboard();
146 | }
147 |
148 | // Keyboard navigation support
149 | function initializeKeyboardNavigation() {
150 | // Add keyboard event listeners
151 | document.addEventListener('keydown', handleKeyboardNavigation);
152 |
153 | // Make focusable elements more visible
154 | const focusableElements = document.querySelectorAll('button, input, select, a');
155 | focusableElements.forEach(element => {
156 | element.addEventListener('focus', () => {
157 | element.style.outline = '2px solid var(--light-accent)';
158 | element.style.outlineOffset = '2px';
159 | });
160 | element.addEventListener('blur', () => {
161 | element.style.outline = 'none';
162 | });
163 | });
164 | }
165 |
166 | function handleKeyboardNavigation(event) {
167 | // Handle Escape key to close modals/notifications
168 | if (event.key === 'Escape') {
169 | const notifications = document.querySelectorAll('.notification');
170 | notifications.forEach(notification => {
171 | if (notification.parentNode) {
172 | notification.parentNode.removeChild(notification);
173 | }
174 | });
175 | }
176 |
177 | // Handle Enter key on buttons
178 | if (event.key === 'Enter' && event.target.tagName === 'BUTTON') {
179 | event.target.click();
180 | }
181 |
182 | // Handle Space key on buttons
183 | if (event.key === ' ' && event.target.tagName === 'BUTTON') {
184 | event.preventDefault();
185 | event.target.click();
186 | }
187 |
188 | // Handle Tab navigation improvements
189 | if (event.key === 'Tab') {
190 | const focusableElements = document.querySelectorAll(
191 | 'button:not([disabled]), input:not([disabled]), select:not([disabled]), a[href], [tabindex]:not([tabindex="-1"])'
192 | );
193 |
194 | const firstElement = focusableElements[0];
195 | const lastElement = focusableElements[focusableElements.length - 1];
196 |
197 | if (event.shiftKey && document.activeElement === firstElement) {
198 | event.preventDefault();
199 | lastElement.focus();
200 | } else if (!event.shiftKey && document.activeElement === lastElement) {
201 | event.preventDefault();
202 | firstElement.focus();
203 | }
204 | }
205 | }
206 |
207 | // ========== Chart Initialization ==========
208 | function initializeCharts() {
209 | // Initialize CPU Chart
210 | const cpuCtx = document.getElementById("cpuChart")?.getContext("2d");
211 | if (cpuCtx) {
212 | window.charts.cpu = new Chart(cpuCtx, {
213 | type: "line",
214 | data: {
215 | labels: Array(10).fill(""),
216 | datasets: [
217 | {
218 | label: "CPU Usage (%)",
219 | data: Array(10).fill(null),
220 | borderColor: chartColors.cpu,
221 | backgroundColor: hexToRgba(chartColors.cpu, 0.2),
222 | borderWidth: 2,
223 | tension: 0.4,
224 | fill: true,
225 | },
226 | ],
227 | },
228 | options: getChartOptions("CPU Usage"),
229 | });
230 | }
231 |
232 | // Initialize Memory Chart
233 | const memoryCtx = document.getElementById("memoryChart")?.getContext("2d");
234 | if (memoryCtx) {
235 | window.charts.memory = new Chart(memoryCtx, {
236 | type: "line",
237 | data: {
238 | labels: Array(10).fill(""),
239 | datasets: [
240 | {
241 | label: "Memory Usage (%)",
242 | data: Array(10).fill(null),
243 | borderColor: chartColors.memory,
244 | backgroundColor: hexToRgba(chartColors.memory, 0.2),
245 | borderWidth: 2,
246 | tension: 0.4,
247 | fill: true,
248 | },
249 | ],
250 | },
251 | options: getChartOptions("Memory Usage"),
252 | });
253 | }
254 |
255 | // Initialize Storage Chart
256 | const storageCtx = document
257 | .getElementById("storageChart")
258 | ?.getContext("2d");
259 | if (storageCtx) {
260 | window.charts.storage = new Chart(storageCtx, {
261 | type: "line",
262 | data: {
263 | labels: Array(10).fill(""),
264 | datasets: [
265 | {
266 | label: "Storage Usage (%)",
267 | data: Array(10).fill(null),
268 | borderColor: chartColors.storage,
269 | backgroundColor: hexToRgba(chartColors.storage, 0.2),
270 | borderWidth: 2,
271 | tension: 0.4,
272 | fill: true,
273 | },
274 | ],
275 | },
276 | options: getChartOptions("Storage Usage"),
277 | });
278 | }
279 |
280 | // Initialize Pod Status Chart
281 | const podStatusCtx = document
282 | .getElementById("podStatusChart")
283 | ?.getContext("2d");
284 | if (podStatusCtx) {
285 | window.podStatusChart = new Chart(podStatusCtx, {
286 | type: "doughnut",
287 | data: {
288 | labels: ["Running", "Pending", "Failed"],
289 | datasets: [
290 | {
291 | data: [0, 0, 0],
292 | backgroundColor: [
293 | chartColors.pod.running,
294 | chartColors.pod.pending,
295 | chartColors.pod.failed,
296 | ],
297 | borderWidth: 1,
298 | },
299 | ],
300 | },
301 | options: {
302 | responsive: true,
303 | maintainAspectRatio: false,
304 | plugins: {
305 | legend: {
306 | position: "right",
307 | labels: {
308 | font: {
309 | size: 12,
310 | },
311 | },
312 | },
313 | },
314 | },
315 | });
316 | }
317 | }
318 |
319 | function getChartOptions(title) {
320 | const isDarkMode = document.body.className === "dark-theme";
321 | const textColor = isDarkMode ? "#e2e8f0" : "#2c3e50";
322 | const gridColor = isDarkMode
323 | ? "rgba(255, 255, 255, 0.1)"
324 | : "rgba(0, 0, 0, 0.1)";
325 |
326 | return {
327 | responsive: true,
328 | maintainAspectRatio: false,
329 | interaction: {
330 | intersect: false,
331 | mode: 'index'
332 | },
333 | scales: {
334 | y: {
335 | beginAtZero: true,
336 | max: 100,
337 | ticks: {
338 | color: textColor,
339 | callback: function (value) {
340 | return value + "%";
341 | },
342 | },
343 | grid: {
344 | color: gridColor,
345 | },
346 | },
347 | x: {
348 | ticks: {
349 | color: textColor,
350 | maxRotation: 0,
351 | },
352 | grid: {
353 | color: gridColor,
354 | },
355 | },
356 | },
357 | plugins: {
358 | legend: {
359 | display: false,
360 | },
361 | tooltip: {
362 | backgroundColor: isDarkMode ? "#2c3039" : "rgba(255, 255, 255, 0.9)",
363 | titleColor: isDarkMode ? "#e2e8f0" : "#2c3e50",
364 | bodyColor: isDarkMode ? "#e2e8f0" : "#2c3e50",
365 | borderColor: isDarkMode ? "#3d4352" : "#e2e8f0",
366 | borderWidth: 1,
367 | displayColors: false,
368 | callbacks: {
369 | label: function (context) {
370 | return `${title}: ${context.raw}%`;
371 | },
372 | afterLabel: function(context) {
373 | const value = context.raw;
374 | if (value > 80) return "⚠️ High usage";
375 | if (value > 60) return "⚡ Moderate usage";
376 | return "✅ Normal usage";
377 | }
378 | },
379 | },
380 | },
381 | elements: {
382 | point: {
383 | radius: 4,
384 | hoverRadius: 8,
385 | },
386 | line: {
387 | borderWidth: 2,
388 | }
389 | },
390 | };
391 | }
392 |
393 | // ========== Event Handlers ==========
394 | function handleImageScan(event) {
395 | event.preventDefault();
396 | const imageName = imageInput.value.trim();
397 |
398 | // Input validation and sanitization
399 | if (!imageName) {
400 | showNotification("Please enter a Docker image name", "error");
401 | return;
402 | }
403 |
404 | // Validate image name format (basic validation)
405 | const imageNamePattern = /^[a-zA-Z0-9._/-]+(:[a-zA-Z0-9._-]+)?$/;
406 | if (!imageNamePattern.test(imageName)) {
407 | showNotification("Invalid image name format. Use format: repository/image:tag", "error");
408 | return;
409 | }
410 |
411 | // Sanitize input to prevent XSS
412 | const sanitizedImageName = imageName.replace(/[<>\"']/g, '');
413 | if (sanitizedImageName !== imageName) {
414 | showNotification("Invalid characters detected in image name", "error");
415 | return;
416 | }
417 |
418 | showNotification(`Scanning image: ${sanitizedImageName}`, "info");
419 | scanResults.textContent = "Scanning...";
420 |
421 | // Reset vulnerability counts
422 | document.querySelectorAll(".vuln-count .count").forEach((el) => {
423 | el.textContent = "-";
424 | });
425 |
426 | scanImage(sanitizedImageName);
427 | }
428 |
429 | function handleHealthCheck() {
430 | healthCheckResult.innerHTML =
431 | 'Checking...';
432 | healthCheckResult.className = "health-status-indicator";
433 |
434 | apiServerStatus.textContent = "Checking...";
435 | schedulerStatus.textContent = "Checking...";
436 | controllerStatus.textContent = "Checking...";
437 |
438 | fetch("http://127.0.0.1:5000/health")
439 | .then((res) => res.json())
440 | .then((data) => {
441 | if (data.status === "ok") {
442 | // Randomly simulate component statuses for demo purposes
443 | const components = {
444 | apiServer: Math.random() > 0.1,
445 | scheduler: Math.random() > 0.1,
446 | controller: Math.random() > 0.1,
447 | };
448 |
449 | apiServerStatus.textContent = components.apiServer
450 | ? "Online"
451 | : "Offline";
452 | apiServerStatus.className =
453 | "metric-value " + (components.apiServer ? "online" : "offline");
454 |
455 | schedulerStatus.textContent = components.scheduler
456 | ? "Online"
457 | : "Offline";
458 | schedulerStatus.className =
459 | "metric-value " + (components.scheduler ? "online" : "offline");
460 |
461 | controllerStatus.textContent = components.controller
462 | ? "Online"
463 | : "Offline";
464 | controllerStatus.className =
465 | "metric-value " + (components.controller ? "online" : "offline");
466 |
467 | const allHealthy = Object.values(components).every((c) => c);
468 |
469 | if (allHealthy) {
470 | healthCheckResult.className = "health-status-indicator healthy";
471 | healthCheckResult.innerHTML =
472 | 'Healthy';
473 | showNotification(
474 | "Cluster health check completed: Healthy",
475 | "success",
476 | );
477 | } else if (Object.values(components).some((c) => c)) {
478 | healthCheckResult.className = "health-status-indicator warning";
479 | healthCheckResult.innerHTML =
480 | 'Degraded';
481 | showNotification(
482 | "Cluster health check completed: Degraded",
483 | "warning",
484 | );
485 | } else {
486 | healthCheckResult.className = "health-status-indicator unhealthy";
487 | healthCheckResult.innerHTML =
488 | 'Unhealthy';
489 | showNotification(
490 | "Cluster health check completed: Unhealthy",
491 | "error",
492 | );
493 | }
494 | }
495 | })
496 | .catch((err) => {
497 | console.error("❌ Health check failed:", err);
498 | healthCheckResult.className = "health-status-indicator unhealthy";
499 | healthCheckResult.innerHTML = 'Error';
500 |
501 | apiServerStatus.textContent = "Unknown";
502 | schedulerStatus.textContent = "Unknown";
503 | controllerStatus.textContent = "Unknown";
504 |
505 | showNotification("Health check failed", "error");
506 | });
507 | }
508 |
509 | function handleNamespaceChange() {
510 | const selectedNamespace = namespaceDropdown.value;
511 | defaultNamespace = selectedNamespace;
512 | showNotification(`Switched to namespace: ${selectedNamespace}`, "info");
513 |
514 | fetchKubernetesInfo(selectedNamespace);
515 | updatePodSelector(selectedNamespace);
516 | }
517 |
518 | function toggleAutoRefresh() {
519 | cleanup(); // Clean up existing interval
520 |
521 | if (autoRefreshToggle.checked) {
522 | autoRefreshInterval = setInterval(updateDashboard, 10000);
523 | showNotification("Auto-refresh enabled (10s)", "info");
524 | } else {
525 | showNotification("Auto-refresh disabled", "info");
526 | }
527 | }
528 |
529 | function manualRefresh() {
530 | if (refreshTimeout) {
531 | return; // Prevent rapid clicking
532 | }
533 |
534 | const refreshIcon = refreshButton.querySelector("i");
535 | refreshButton.classList.add("refreshing");
536 | refreshIcon.classList.add("fa-spin");
537 |
538 | updateDashboard();
539 |
540 | refreshTimeout = setTimeout(() => {
541 | refreshButton.classList.remove("refreshing");
542 | refreshIcon.classList.remove("fa-spin");
543 | refreshTimeout = null;
544 | }, 1000);
545 | }
546 |
547 | function exportScanResultsToFile() {
548 | const scanData = scanResults.textContent;
549 | if (!scanData || scanData === "{}") {
550 | showNotification("No scan results to export", "warning");
551 | return;
552 | }
553 |
554 | const blob = new Blob([scanData], { type: "application/json" });
555 | const url = URL.createObjectURL(blob);
556 | const a = document.createElement("a");
557 | a.href = url;
558 | a.download = `trivy-scan-${new Date().toISOString().split("T")[0]}.json`;
559 | document.body.appendChild(a);
560 | a.click();
561 | document.body.removeChild(a);
562 | URL.revokeObjectURL(url);
563 |
564 | showNotification("Scan results exported", "success");
565 | }
566 |
567 | function filterLogs() {
568 | const filterText = logsFilter.value.toLowerCase();
569 | if (!currentPodLogs) return;
570 |
571 | if (!filterText) {
572 | logsOutput.textContent = currentPodLogs;
573 | return;
574 | }
575 |
576 | const lines = currentPodLogs.split("\n");
577 | const filteredLines = lines.filter((line) =>
578 | line.toLowerCase().includes(filterText),
579 | );
580 |
581 | logsOutput.textContent = filteredLines.join("\n");
582 | }
583 |
584 | function refreshPodLogs() {
585 | const selectedPod = podSelector.value;
586 | if (!selectedPod) {
587 | showNotification("No pod selected", "warning");
588 | return;
589 | }
590 | fetchPodLogs();
591 | }
592 |
593 | // ========== Dashboard Updates ==========
594 | async function updateDashboard() {
595 | try {
596 | // Show loading states
597 | setLoadingState(true);
598 |
599 | await Promise.all([
600 | fetchSystemInfo(),
601 | fetchNamespaces(),
602 | fetchPodStatuses(defaultNamespace)
603 | ]);
604 |
605 | // Hide loading states
606 | setLoadingState(false);
607 | } catch (error) {
608 | console.error("Dashboard update failed:", error);
609 | displayError(error, "Dashboard update failed");
610 | setLoadingState(false);
611 | }
612 | }
613 |
614 | // Loading state management
615 | function setLoadingState(isLoading) {
616 | const loadingElements = document.querySelectorAll('.loading-skeleton');
617 | const metricCards = document.querySelectorAll('.metric-card');
618 | const resourceCards = document.querySelectorAll('.resource-card');
619 |
620 | if (isLoading) {
621 | // Add loading class to elements
622 | metricCards.forEach(card => card.classList.add('loading'));
623 | resourceCards.forEach(card => card.classList.add('loading'));
624 |
625 | // Show skeleton loaders
626 | loadingElements.forEach(element => element.style.display = 'block');
627 | } else {
628 | // Remove loading class
629 | metricCards.forEach(card => card.classList.remove('loading'));
630 | resourceCards.forEach(card => card.classList.remove('loading'));
631 |
632 | // Hide skeleton loaders
633 | loadingElements.forEach(element => element.style.display = 'none');
634 | }
635 | }
636 |
637 | async function fetchSystemInfo() {
638 | try {
639 | // Check cache first
640 | const cachedData = getCachedData('systemInfo');
641 | if (cachedData) {
642 | updateSystemInfoUI(cachedData);
643 | return;
644 | }
645 |
646 | const data = await fetchWithRetry("http://127.0.0.1:5000/system_info");
647 |
648 | // Validate data structure
649 | if (!data || typeof data !== 'object') {
650 | throw new Error('Invalid data received from server');
651 | }
652 |
653 | // Cache the data
654 | setCachedData('systemInfo', data);
655 |
656 | // Update UI
657 | updateSystemInfoUI(data);
658 | } catch (error) {
659 | displayError(error, "Failed to fetch system metrics");
660 | throw error; // Propagate for retry logic
661 | }
662 | }
663 |
664 | function updateSystemInfoUI(data) {
665 | // Update metric cards with validation
666 | const memoryElement = document.querySelector(".memory-utilization .percentage");
667 | const cpuElement = document.querySelector(".cpu-utilization .percentage");
668 | const storageElement = document.querySelector(".storage-used .percentage");
669 |
670 | if (memoryElement && data.memory_usage?.percent !== undefined) {
671 | memoryElement.textContent = `${Math.round(data.memory_usage.percent)}%`;
672 | }
673 | if (cpuElement && data.cpu_percent !== undefined) {
674 | cpuElement.textContent = `${Math.round(data.cpu_percent)}%`;
675 | }
676 | if (storageElement && data.disk_usage?.percent !== undefined) {
677 | storageElement.textContent = `${Math.round(data.disk_usage.percent)}%`;
678 | }
679 |
680 | // Update charts with validation
681 | if (window.charts.cpu && typeof data.cpu_percent === 'number') {
682 | updateChart(window.charts.cpu, data.cpu_percent);
683 | }
684 | if (window.charts.memory && typeof data.memory_usage?.percent === 'number') {
685 | updateChart(window.charts.memory, data.memory_usage.percent);
686 | }
687 | if (window.charts.storage && typeof data.disk_usage?.percent === 'number') {
688 | updateChart(window.charts.storage, data.disk_usage.percent);
689 | }
690 | }
691 |
692 | function updateChart(chart, newValue) {
693 | if (!chart) return;
694 |
695 | const now = new Date();
696 | const timeString =
697 | now.getHours().toString().padStart(2, "0") +
698 | ":" +
699 | now.getMinutes().toString().padStart(2, "0") +
700 | ":" +
701 | now.getSeconds().toString().padStart(2, "0");
702 |
703 | // Add new data point
704 | chart.data.labels.push(timeString);
705 | chart.data.datasets[0].data.push(newValue);
706 |
707 | // Remove oldest if we have more than 10 points
708 | if (chart.data.labels.length > 10) {
709 | chart.data.labels.shift();
710 | chart.data.datasets[0].data.shift();
711 | }
712 |
713 | chart.update();
714 | }
715 |
716 | function fetchNamespaces() {
717 | fetch("http://127.0.0.1:5000/kubernetes_namespaces")
718 | .then((res) => res.json())
719 | .then((namespaces) => {
720 | namespaceDropdown.innerHTML = "";
721 | namespaces.forEach((ns) => {
722 | const option = document.createElement("option");
723 | option.value = ns;
724 | option.textContent = ns;
725 | namespaceDropdown.appendChild(option);
726 | });
727 |
728 | // Set default or retain selected
729 | if (!namespaces.includes(defaultNamespace)) {
730 | defaultNamespace = namespaces[0] || "default";
731 | }
732 | namespaceDropdown.value = defaultNamespace;
733 |
734 | fetchKubernetesInfo(defaultNamespace);
735 | })
736 | .catch((err) => {
737 | console.error("❌ Failed to fetch namespaces:", err);
738 | showNotification("Failed to fetch namespaces", "error");
739 | });
740 | }
741 |
742 | function fetchKubernetesInfo(namespace) {
743 | fetch(`http://127.0.0.1:5000/kubernetes_info?namespace=${namespace}`)
744 | .then((res) => res.json())
745 | .then((data) => {
746 | document.querySelector(".deployments .count").textContent =
747 | data.num_deployments;
748 | document.querySelector(".pods-running .count").textContent =
749 | data.num_pods;
750 | document.querySelector(".services-running .count").textContent =
751 | data.num_services;
752 | })
753 | .catch((err) => {
754 | console.error(
755 | `❌ Failed to fetch Kubernetes info for ${namespace}:`,
756 | err,
757 | );
758 | showNotification(
759 | `Failed to fetch info for namespace: ${namespace}`,
760 | "error",
761 | );
762 | });
763 | }
764 |
765 | function fetchPodStatuses(namespace) {
766 | // This would typically call a backend endpoint that returns pod statuses
767 | // For this demo, we'll simulate the data
768 | const running = Math.floor(Math.random() * 10) + 5;
769 | const pending = Math.floor(Math.random() * 3);
770 | const failed = Math.floor(Math.random() * 2);
771 |
772 | // Update counters
773 | if (podsRunningCount) podsRunningCount.textContent = running;
774 | if (podsPendingCount) podsPendingCount.textContent = pending;
775 | if (podsFailedCount) podsFailedCount.textContent = failed;
776 |
777 | // Update chart
778 | if (window.podStatusChart) {
779 | window.podStatusChart.data.datasets[0].data = [running, pending, failed];
780 | window.podStatusChart.update();
781 | }
782 |
783 | // Update pod selector for logs
784 | updatePodSelector(namespace);
785 | }
786 |
787 | function updatePodSelector(namespace) {
788 | if (!podSelector) return;
789 |
790 | // In a real app, this would fetch actual pods from the API
791 | // For this demo, we'll create some sample pods
792 | const pods = [
793 | `${namespace}-web-app-7d9f8b7c9-a1b2c`,
794 | `${namespace}-database-6c8d7b6c5-d4e5f`,
795 | `${namespace}-cache-5b4c3b2a1-g6h7i`,
796 | `${namespace}-worker-4a3b2c1d0-j8k9l`,
797 | ];
798 |
799 | podSelector.innerHTML = '';
800 |
801 | pods.forEach((pod) => {
802 | const option = document.createElement("option");
803 | option.value = pod;
804 | option.textContent = pod;
805 | podSelector.appendChild(option);
806 | });
807 | }
808 |
809 | function fetchPodLogs() {
810 | const selectedPod = podSelector.value;
811 |
812 | if (!selectedPod) {
813 | logsOutput.textContent = "Select a pod to view logs";
814 | return;
815 | }
816 |
817 | logsOutput.textContent = "Fetching logs...";
818 |
819 | // In a real app, this would call a backend API to get actual pod logs
820 | // For this demo, we'll generate fake logs
821 | setTimeout(() => {
822 | const logLines = [];
823 | const logTypes = ["INFO", "DEBUG", "WARN", "ERROR"];
824 | const logMessages = [
825 | "Application started",
826 | "Processing request",
827 | "Database connection established",
828 | "Cache miss for key",
829 | "Request completed in",
830 | "Memory usage at",
831 | "Received message from queue",
832 | "Connection timeout",
833 | "Authentication successful",
834 | "Invalid request parameters",
835 | ];
836 |
837 | for (let i = 0; i < 50; i++) {
838 | const date = new Date();
839 | date.setSeconds(date.getSeconds() - i * 30);
840 | const timestamp = date.toISOString();
841 |
842 | const logType = logTypes[Math.floor(Math.random() * logTypes.length)];
843 | const logMessage =
844 | logMessages[Math.floor(Math.random() * logMessages.length)];
845 | const details = Math.random().toString(36).substring(2, 10);
846 |
847 | logLines.push(`${timestamp} [${logType}] ${logMessage}: ${details}`);
848 | }
849 |
850 | currentPodLogs = logLines.join("\n");
851 | logsOutput.textContent = currentPodLogs;
852 |
853 | // Apply filter if one exists
854 | if (logsFilter.value) {
855 | filterLogs();
856 | }
857 |
858 | showNotification(`Logs fetched for pod: ${selectedPod}`, "success");
859 | }, 500);
860 | }
861 |
862 | function scanImage(imageName) {
863 | // Show progress indicator
864 | const scanButton = document.querySelector('.scan-button');
865 | const originalButtonText = scanButton.innerHTML;
866 | scanButton.disabled = true;
867 | scanButton.innerHTML = ' Scanning...';
868 |
869 | // Add progress bar
870 | const progressBar = document.createElement('div');
871 | progressBar.className = 'scan-progress';
872 | progressBar.innerHTML = `
873 |
876 | Scanning image...
877 | `;
878 | scanResults.parentNode.insertBefore(progressBar, scanResults);
879 |
880 | fetch("http://127.0.0.1:5000/scan_image", {
881 | method: "POST",
882 | headers: { "Content-Type": "application/json" },
883 | body: JSON.stringify({ container_id: imageName }),
884 | })
885 | .then((res) => res.json())
886 | .then((data) => {
887 | // Remove progress indicator
888 | if (progressBar.parentNode) {
889 | progressBar.parentNode.removeChild(progressBar);
890 | }
891 |
892 | // Restore button
893 | scanButton.disabled = false;
894 | scanButton.innerHTML = originalButtonText;
895 |
896 | if (data.error) {
897 | scanResults.textContent = `Error: ${data.error}`;
898 | showNotification(`Scan error: ${data.error}`, "error");
899 | } else {
900 | scanResults.textContent = "";
901 | renderScanResult(data.scan_results);
902 | updateVulnerabilitySummary(data.scan_results);
903 | showNotification(`Scan completed for: ${imageName}`, "success");
904 | }
905 | })
906 | .catch((err) => {
907 | // Remove progress indicator
908 | if (progressBar.parentNode) {
909 | progressBar.parentNode.removeChild(progressBar);
910 | }
911 |
912 | // Restore button
913 | scanButton.disabled = false;
914 | scanButton.innerHTML = originalButtonText;
915 |
916 | console.error("❌ Scan failed:", err);
917 | scanResults.textContent =
918 | "Scan failed. Please make sure Trivy is installed and the backend server is running.";
919 | showNotification("Image scan failed", "error");
920 | });
921 | }
922 |
923 | function renderScanResult(result) {
924 | try {
925 | const formatted =
926 | typeof result === "string" ? JSON.parse(result) : result;
927 | scanResults.textContent = JSON.stringify(formatted, null, 2);
928 | } catch (err) {
929 | scanResults.textContent = result;
930 | }
931 | }
932 |
933 | function updateVulnerabilitySummary(scanResult) {
934 | try {
935 | // In a real implementation, this would parse the actual Trivy output
936 | // For this demo, we'll create sample vulnerability counts
937 | const vulnerabilities = {
938 | critical: Math.floor(Math.random() * 3),
939 | high: Math.floor(Math.random() * 5) + 1,
940 | medium: Math.floor(Math.random() * 8) + 3,
941 | low: Math.floor(Math.random() * 10) + 5,
942 | };
943 |
944 | document.querySelector(".vuln-count.critical .count").textContent =
945 | vulnerabilities.critical;
946 | document.querySelector(".vuln-count.high .count").textContent =
947 | vulnerabilities.high;
948 | document.querySelector(".vuln-count.medium .count").textContent =
949 | vulnerabilities.medium;
950 | document.querySelector(".vuln-count.low .count").textContent =
951 | vulnerabilities.low;
952 | } catch (err) {
953 | console.error("Failed to update vulnerability summary:", err);
954 | }
955 | }
956 |
957 | // ========== Utility Functions ==========
958 | function showNotification(message, type = "info") {
959 | const container = document.getElementById("notifications");
960 | if (!container) return;
961 |
962 | const notification = document.createElement("div");
963 | notification.className = "notification";
964 |
965 | const iconType = {
966 | success: "check-circle",
967 | error: "times-circle",
968 | warning: "exclamation-triangle",
969 | info: "info-circle",
970 | };
971 |
972 | notification.innerHTML = `
973 |
974 |
975 |
976 |
979 | `;
980 |
981 | container.appendChild(notification);
982 |
983 | // Auto-remove after 5 seconds
984 | setTimeout(() => {
985 | notification.style.opacity = "0";
986 | notification.style.transform = "translateX(100%)";
987 |
988 | setTimeout(() => {
989 | container.removeChild(notification);
990 | }, 300);
991 | }, 5000);
992 | }
993 |
994 | function hexToRgba(hex, alpha = 1) {
995 | const r = parseInt(hex.slice(1, 3), 16);
996 | const g = parseInt(hex.slice(3, 5), 16);
997 | const b = parseInt(hex.slice(5, 7), 16);
998 | return `rgba(${r}, ${g}, ${b}, ${alpha})`;
999 | }
1000 | });
1001 |
--------------------------------------------------------------------------------