Updated lint errors
Some checks failed
CI/CD Pipeline - Build, Test, and Deploy / 🧪 Test & Lint (push) Failing after 4m51s
CI/CD Pipeline - Build, Test, and Deploy / 🔒 Security Scan (push) Successful in 9m31s
CI/CD Pipeline - Build, Test, and Deploy / 🏗️ Build & Push Image (push) Has been skipped
CI/CD Pipeline - Build, Test, and Deploy / 🛡️ Image Security Scan (push) Has been skipped
CI/CD Pipeline - Build, Test, and Deploy / 🚀 Deploy to Development (push) Has been skipped
CI/CD Pipeline - Build, Test, and Deploy / 🏭 Deploy to Production (push) Has been skipped
CI/CD Pipeline - Build, Test, and Deploy / 🧹 Cleanup (push) Successful in 1s
Some checks failed
CI/CD Pipeline - Build, Test, and Deploy / 🧪 Test & Lint (push) Failing after 4m51s
CI/CD Pipeline - Build, Test, and Deploy / 🔒 Security Scan (push) Successful in 9m31s
CI/CD Pipeline - Build, Test, and Deploy / 🏗️ Build & Push Image (push) Has been skipped
CI/CD Pipeline - Build, Test, and Deploy / 🛡️ Image Security Scan (push) Has been skipped
CI/CD Pipeline - Build, Test, and Deploy / 🚀 Deploy to Development (push) Has been skipped
CI/CD Pipeline - Build, Test, and Deploy / 🏭 Deploy to Production (push) Has been skipped
CI/CD Pipeline - Build, Test, and Deploy / 🧹 Cleanup (push) Successful in 1s
This commit is contained in:
@ -1,34 +1,68 @@
|
||||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"commonjs": true,
|
||||
"es2021": true,
|
||||
"node": true,
|
||||
"jest": true
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended"
|
||||
"env": {
|
||||
"browser": true,
|
||||
"commonjs": true,
|
||||
"es2021": true,
|
||||
"node": true,
|
||||
"jest": true
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended"
|
||||
],
|
||||
"parserOptions": {
|
||||
"ecmaVersion": "latest"
|
||||
},
|
||||
"rules": {
|
||||
"indent": [
|
||||
"error",
|
||||
2
|
||||
],
|
||||
"parserOptions": {
|
||||
"ecmaVersion": "latest"
|
||||
},
|
||||
"rules": {
|
||||
"indent": ["error", 2],
|
||||
"linebreak-style": ["error", "unix"],
|
||||
"quotes": ["error", "single"],
|
||||
"semi": ["error", "always"],
|
||||
"no-unused-vars": ["error", { "argsIgnorePattern": "^_" }],
|
||||
"no-console": "off",
|
||||
"no-trailing-spaces": "error",
|
||||
"eol-last": "error",
|
||||
"comma-dangle": ["error", "never"],
|
||||
"object-curly-spacing": ["error", "always"],
|
||||
"array-bracket-spacing": ["error", "never"],
|
||||
"space-before-function-paren": ["error", "never"],
|
||||
"keyword-spacing": "error",
|
||||
"space-infix-ops": "error",
|
||||
"no-multiple-empty-lines": ["error", { "max": 2 }],
|
||||
"prefer-const": "error",
|
||||
"no-var": "error"
|
||||
}
|
||||
}
|
||||
"linebreak-style": [
|
||||
"error",
|
||||
"unix"
|
||||
],
|
||||
"quotes": [
|
||||
"error",
|
||||
"single"
|
||||
],
|
||||
"semi": [
|
||||
"error",
|
||||
"always"
|
||||
],
|
||||
"no-unused-vars": [
|
||||
"error",
|
||||
{
|
||||
"argsIgnorePattern": "^_"
|
||||
}
|
||||
],
|
||||
"no-console": "off",
|
||||
"no-trailing-spaces": "error",
|
||||
"eol-last": "error",
|
||||
"comma-dangle": [
|
||||
"error",
|
||||
"never"
|
||||
],
|
||||
"object-curly-spacing": [
|
||||
"error",
|
||||
"always"
|
||||
],
|
||||
"array-bracket-spacing": [
|
||||
"error",
|
||||
"never"
|
||||
],
|
||||
"space-before-function-paren": [
|
||||
"error",
|
||||
"never"
|
||||
],
|
||||
"keyword-spacing": "error",
|
||||
"space-infix-ops": "error",
|
||||
"no-multiple-empty-lines": [
|
||||
"error",
|
||||
{
|
||||
"max": 2
|
||||
}
|
||||
],
|
||||
"prefer-const": "error",
|
||||
"no-var": "error"
|
||||
}
|
||||
}
|
||||
94
package.json
94
package.json
@ -1,47 +1,53 @@
|
||||
{
|
||||
"name": "harbor-ci-cd-demo",
|
||||
"version": "1.0.0",
|
||||
"description": "System Monitoring Dashboard - Harbor CI/CD Demo",
|
||||
"main": "src/app.js",
|
||||
"scripts": {
|
||||
"start": "node src/app.js",
|
||||
"dev": "nodemon src/app.js",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"test:coverage": "jest --coverage",
|
||||
"lint": "eslint src/ tests/",
|
||||
"lint:fix": "eslint src/ tests/ --fix"
|
||||
},
|
||||
"keywords": ["monitoring", "dashboard", "nodejs", "cicd", "harbor"],
|
||||
"author": "Your Name",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"express": "^4.18.2",
|
||||
"cors": "^2.8.5",
|
||||
"helmet": "^7.1.0",
|
||||
"morgan": "^1.10.0",
|
||||
"compression": "^1.7.4",
|
||||
"dotenv": "^16.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"jest": "^29.7.0",
|
||||
"supertest": "^6.3.3",
|
||||
"eslint": "^8.53.0",
|
||||
"nodemon": "^3.0.1"
|
||||
},
|
||||
"jest": {
|
||||
"testEnvironment": "node",
|
||||
"collectCoverageFrom": [
|
||||
"src/**/*.js",
|
||||
"!src/public/**"
|
||||
],
|
||||
"coverageThreshold": {
|
||||
"global": {
|
||||
"branches": 80,
|
||||
"functions": 80,
|
||||
"lines": 80,
|
||||
"statements": 80
|
||||
}
|
||||
"name": "harbor-ci-cd-demo",
|
||||
"version": "1.0.0",
|
||||
"description": "System Monitoring Dashboard - Harbor CI/CD Demo",
|
||||
"main": "src/app.js",
|
||||
"scripts": {
|
||||
"start": "node src/app.js",
|
||||
"dev": "nodemon src/app.js",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"test:coverage": "jest --coverage",
|
||||
"lint": "eslint src/ tests/",
|
||||
"lint:fix": "eslint src/ tests/ --fix"
|
||||
},
|
||||
"keywords": [
|
||||
"monitoring",
|
||||
"dashboard",
|
||||
"nodejs",
|
||||
"cicd",
|
||||
"harbor"
|
||||
],
|
||||
"author": "Your Name",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"express": "^4.18.2",
|
||||
"cors": "^2.8.5",
|
||||
"helmet": "^7.1.0",
|
||||
"morgan": "^1.10.0",
|
||||
"compression": "^1.7.4",
|
||||
"dotenv": "^16.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"jest": "^29.7.0",
|
||||
"supertest": "^6.3.3",
|
||||
"eslint": "^8.53.0",
|
||||
"nodemon": "^3.0.1"
|
||||
},
|
||||
"jest": {
|
||||
"testEnvironment": "node",
|
||||
"collectCoverageFrom": [
|
||||
"src/**/*.js",
|
||||
"!src/public/**"
|
||||
],
|
||||
"coverageThreshold": {
|
||||
"global": {
|
||||
"branches": 80,
|
||||
"functions": 80,
|
||||
"lines": 80,
|
||||
"statements": 80
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -47,7 +47,7 @@ app.get('/debug/files', (req, res) => {
|
||||
url: `http://localhost:${PORT}/${file}`
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
res.json({
|
||||
publicPath,
|
||||
files: fileDetails,
|
||||
@ -101,7 +101,7 @@ app.get('/test-static', (req, res) => {
|
||||
app.get('/', (req, res) => {
|
||||
const indexPath = path.join(publicPath, 'index.html');
|
||||
console.log('🏠 Serving index.html from:', indexPath);
|
||||
|
||||
|
||||
if (require('fs').existsSync(indexPath)) {
|
||||
res.sendFile(indexPath);
|
||||
} else {
|
||||
@ -119,7 +119,7 @@ app.use((err, req, res, next) => {
|
||||
// 404 handler
|
||||
app.use('*', (req, res) => {
|
||||
console.log('🔍 404 for:', req.originalUrl);
|
||||
res.status(404).json({
|
||||
res.status(404).json({
|
||||
error: 'Not Found',
|
||||
url: req.originalUrl,
|
||||
message: 'Static file or route not found'
|
||||
|
||||
@ -3,10 +3,10 @@ const { incrementCounter, recordHistogram } = require('../utils/metrics');
|
||||
// Request logging and metrics middleware
|
||||
function logger(req, res, next) {
|
||||
const startTime = Date.now();
|
||||
|
||||
|
||||
// Log request start
|
||||
console.log(`${new Date().toISOString()} - ${req.method} ${req.url} - ${req.ip}`);
|
||||
|
||||
|
||||
// Increment request counter
|
||||
incrementCounter('http_requests_total', {
|
||||
method: req.method,
|
||||
@ -15,24 +15,24 @@ function logger(req, res, next) {
|
||||
|
||||
// Override res.end to capture response time and status
|
||||
const originalEnd = res.end;
|
||||
res.end = function(chunk, encoding) {
|
||||
res.end = function (chunk, encoding) {
|
||||
const responseTime = Date.now() - startTime;
|
||||
|
||||
|
||||
// Record response time histogram
|
||||
recordHistogram('http_request_duration_ms', responseTime, {
|
||||
method: req.method,
|
||||
status_code: res.statusCode
|
||||
});
|
||||
|
||||
|
||||
// Increment response counter
|
||||
incrementCounter('http_responses_total', {
|
||||
method: req.method,
|
||||
status_code: res.statusCode
|
||||
});
|
||||
|
||||
|
||||
// Log request completion
|
||||
console.log(`${new Date().toISOString()} - ${req.method} ${req.url} - ${res.statusCode} - ${responseTime}ms`);
|
||||
|
||||
|
||||
// Call original end method
|
||||
originalEnd.call(this, chunk, encoding);
|
||||
};
|
||||
|
||||
@ -1,113 +1,113 @@
|
||||
class MonitoringDashboard {
|
||||
constructor() {
|
||||
this.refreshInterval = null;
|
||||
this.autoRefresh = true;
|
||||
this.refreshRate = 30000; // 30 seconds
|
||||
this.init();
|
||||
}
|
||||
constructor() {
|
||||
this.refreshInterval = null;
|
||||
this.autoRefresh = true;
|
||||
this.refreshRate = 30000; // 30 seconds
|
||||
this.init();
|
||||
}
|
||||
|
||||
async init() {
|
||||
await this.loadAllData();
|
||||
this.startAutoRefresh();
|
||||
this.setupEventListeners();
|
||||
}
|
||||
async init() {
|
||||
await this.loadAllData();
|
||||
this.startAutoRefresh();
|
||||
this.setupEventListeners();
|
||||
}
|
||||
|
||||
async loadAllData() {
|
||||
this.updateStatus('loading', 'Loading...');
|
||||
|
||||
try {
|
||||
await Promise.all([
|
||||
this.loadSystemInfo(),
|
||||
this.loadMemoryUsage(),
|
||||
this.loadHealthStatus(),
|
||||
this.loadApiMetrics(),
|
||||
this.loadProcessInfo()
|
||||
]);
|
||||
|
||||
this.updateStatus('healthy', 'System Healthy');
|
||||
this.updateLastUpdated();
|
||||
} catch (error) {
|
||||
console.error('Error loading data:', error);
|
||||
this.updateStatus('error', 'Error Loading Data');
|
||||
}
|
||||
}
|
||||
async loadAllData() {
|
||||
this.updateStatus('loading', 'Loading...');
|
||||
|
||||
async loadSystemInfo() {
|
||||
try {
|
||||
const response = await fetch('/api/system');
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
this.renderSystemInfo(result.data);
|
||||
} else {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
} catch (error) {
|
||||
this.renderError('system-info', 'Failed to load system information');
|
||||
}
|
||||
}
|
||||
try {
|
||||
await Promise.all([
|
||||
this.loadSystemInfo(),
|
||||
this.loadMemoryUsage(),
|
||||
this.loadHealthStatus(),
|
||||
this.loadApiMetrics(),
|
||||
this.loadProcessInfo()
|
||||
]);
|
||||
|
||||
async loadMemoryUsage() {
|
||||
try {
|
||||
const response = await fetch('/api/memory');
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
this.renderMemoryUsage(result.data);
|
||||
} else {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
} catch (error) {
|
||||
this.renderError('memory-usage', 'Failed to load memory data');
|
||||
}
|
||||
this.updateStatus('healthy', 'System Healthy');
|
||||
this.updateLastUpdated();
|
||||
} catch (error) {
|
||||
console.error('Error loading data:', error);
|
||||
this.updateStatus('error', 'Error Loading Data');
|
||||
}
|
||||
}
|
||||
|
||||
async loadHealthStatus() {
|
||||
try {
|
||||
const response = await fetch('/health/detailed');
|
||||
const result = await response.json();
|
||||
|
||||
this.renderHealthStatus(result);
|
||||
this.updateOverallStatus(result.status);
|
||||
} catch (error) {
|
||||
this.renderError('health-status', 'Failed to load health data');
|
||||
this.updateStatus('error', 'Health Check Failed');
|
||||
}
|
||||
async loadSystemInfo() {
|
||||
try {
|
||||
const response = await fetch('/api/system');
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
this.renderSystemInfo(result.data);
|
||||
} else {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
} catch (error) {
|
||||
this.renderError('system-info', 'Failed to load system information');
|
||||
}
|
||||
}
|
||||
|
||||
async loadApiMetrics() {
|
||||
try {
|
||||
const response = await fetch('/api/metrics');
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
this.renderApiMetrics(result.data);
|
||||
} else {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
} catch (error) {
|
||||
this.renderError('api-metrics', 'Failed to load metrics');
|
||||
}
|
||||
async loadMemoryUsage() {
|
||||
try {
|
||||
const response = await fetch('/api/memory');
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
this.renderMemoryUsage(result.data);
|
||||
} else {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
} catch (error) {
|
||||
this.renderError('memory-usage', 'Failed to load memory data');
|
||||
}
|
||||
}
|
||||
|
||||
async loadProcessInfo() {
|
||||
try {
|
||||
const response = await fetch('/api/process');
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
this.renderProcessInfo(result.data);
|
||||
} else {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
} catch (error) {
|
||||
this.renderError('process-info', 'Failed to load process information');
|
||||
}
|
||||
async loadHealthStatus() {
|
||||
try {
|
||||
const response = await fetch('/health/detailed');
|
||||
const result = await response.json();
|
||||
|
||||
this.renderHealthStatus(result);
|
||||
this.updateOverallStatus(result.status);
|
||||
} catch (error) {
|
||||
this.renderError('health-status', 'Failed to load health data');
|
||||
this.updateStatus('error', 'Health Check Failed');
|
||||
}
|
||||
}
|
||||
|
||||
renderSystemInfo(data) {
|
||||
const container = document.getElementById('system-info');
|
||||
container.innerHTML = `
|
||||
async loadApiMetrics() {
|
||||
try {
|
||||
const response = await fetch('/api/metrics');
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
this.renderApiMetrics(result.data);
|
||||
} else {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
} catch (error) {
|
||||
this.renderError('api-metrics', 'Failed to load metrics');
|
||||
}
|
||||
}
|
||||
|
||||
async loadProcessInfo() {
|
||||
try {
|
||||
const response = await fetch('/api/process');
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
this.renderProcessInfo(result.data);
|
||||
} else {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
} catch (error) {
|
||||
this.renderError('process-info', 'Failed to load process information');
|
||||
}
|
||||
}
|
||||
|
||||
renderSystemInfo(data) {
|
||||
const container = document.getElementById('system-info');
|
||||
container.innerHTML = `
|
||||
<div class="metric-item">
|
||||
<strong>Hostname:</strong>
|
||||
<span class="metric-value">${data.hostname}</span>
|
||||
@ -137,18 +137,18 @@ class MonitoringDashboard {
|
||||
<span class="metric-value">${data.nodeVersion}</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
renderMemoryUsage(data) {
|
||||
const container = document.getElementById('memory-usage');
|
||||
|
||||
const systemPercentage = data.system.percentage;
|
||||
const systemClass = systemPercentage > 80 ? 'danger' : systemPercentage > 60 ? 'warning' : '';
|
||||
|
||||
const processHeapPercentage = (data.process.heapUsed / data.process.heapTotal) * 100;
|
||||
const processClass = processHeapPercentage > 80 ? 'danger' : processHeapPercentage > 60 ? 'warning' : '';
|
||||
|
||||
container.innerHTML = `
|
||||
renderMemoryUsage(data) {
|
||||
const container = document.getElementById('memory-usage');
|
||||
|
||||
const systemPercentage = data.system.percentage;
|
||||
const systemClass = systemPercentage > 80 ? 'danger' : systemPercentage > 60 ? 'warning' : '';
|
||||
|
||||
const processHeapPercentage = (data.process.heapUsed / data.process.heapTotal) * 100;
|
||||
const processClass = processHeapPercentage > 80 ? 'danger' : processHeapPercentage > 60 ? 'warning' : '';
|
||||
|
||||
container.innerHTML = `
|
||||
<div>
|
||||
<div class="memory-label">
|
||||
<span>System Memory</span>
|
||||
@ -180,15 +180,15 @@ class MonitoringDashboard {
|
||||
<span class="metric-value">${data.process.external} MB</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
renderHealthStatus(data) {
|
||||
const container = document.getElementById('health-status');
|
||||
|
||||
const statusClass = data.status === 'healthy' ? 'metric-item' :
|
||||
data.status === 'warning' ? 'metric-item warning' : 'metric-item error';
|
||||
|
||||
container.innerHTML = `
|
||||
renderHealthStatus(data) {
|
||||
const container = document.getElementById('health-status');
|
||||
|
||||
const statusClass = data.status === 'healthy' ? 'metric-item' :
|
||||
data.status === 'warning' ? 'metric-item warning' : 'metric-item error';
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="${statusClass}">
|
||||
<strong>Overall Status:</strong>
|
||||
<span class="health-value">${data.status.toUpperCase()}</span>
|
||||
@ -214,61 +214,61 @@ class MonitoringDashboard {
|
||||
<span class="health-value">${data.loadAverage.join(', ')}</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
renderApiMetrics(data) {
|
||||
const container = document.getElementById('api-metrics');
|
||||
|
||||
const requestCounters = data.counters.filter(c => c.name === 'http_requests_total');
|
||||
const responseCounters = data.counters.filter(c => c.name === 'http_responses_total');
|
||||
const durationHistograms = data.histograms.filter(h => h.name === 'http_request_duration_ms');
|
||||
|
||||
let html = '<div class="metrics-grid">';
|
||||
|
||||
// Total requests
|
||||
const totalRequests = requestCounters.reduce((sum, counter) => sum + counter.value, 0);
|
||||
html += `
|
||||
renderApiMetrics(data) {
|
||||
const container = document.getElementById('api-metrics');
|
||||
|
||||
const requestCounters = data.counters.filter(c => c.name === 'http_requests_total');
|
||||
const responseCounters = data.counters.filter(c => c.name === 'http_responses_total');
|
||||
const durationHistograms = data.histograms.filter(h => h.name === 'http_request_duration_ms');
|
||||
|
||||
let html = '<div class="metrics-grid">';
|
||||
|
||||
// Total requests
|
||||
const totalRequests = requestCounters.reduce((sum, counter) => sum + counter.value, 0);
|
||||
html += `
|
||||
<div class="metric-item">
|
||||
<strong>Total Requests:</strong>
|
||||
<span class="metric-value">${totalRequests}</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Average response time
|
||||
const avgResponseTime = durationHistograms.length > 0 ?
|
||||
durationHistograms.reduce((sum, hist) => sum + hist.average, 0) / durationHistograms.length : 0;
|
||||
html += `
|
||||
|
||||
// Average response time
|
||||
const avgResponseTime = durationHistograms.length > 0 ?
|
||||
durationHistograms.reduce((sum, hist) => sum + hist.average, 0) / durationHistograms.length : 0;
|
||||
html += `
|
||||
<div class="metric-item">
|
||||
<strong>Avg Response Time:</strong>
|
||||
<span class="metric-value">${Math.round(avgResponseTime)}ms</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Uptime
|
||||
html += `
|
||||
|
||||
// Uptime
|
||||
html += `
|
||||
<div class="metric-item">
|
||||
<strong>Service Uptime:</strong>
|
||||
<span class="metric-value">${this.formatUptime(data.runtime.uptime_seconds)}</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// API endpoints
|
||||
const apiCounters = requestCounters.filter(c => c.labels.route && c.labels.route.startsWith('/api'));
|
||||
if (apiCounters.length > 0) {
|
||||
html += '<div class="metric-item"><strong>API Endpoints:</strong><div>';
|
||||
apiCounters.forEach(counter => {
|
||||
html += `<div>${counter.labels.route}: ${counter.value} calls</div>`;
|
||||
});
|
||||
html += '</div></div>';
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
container.innerHTML = html;
|
||||
|
||||
// API endpoints
|
||||
const apiCounters = requestCounters.filter(c => c.labels.route && c.labels.route.startsWith('/api'));
|
||||
if (apiCounters.length > 0) {
|
||||
html += '<div class="metric-item"><strong>API Endpoints:</strong><div>';
|
||||
apiCounters.forEach(counter => {
|
||||
html += `<div>${counter.labels.route}: ${counter.value} calls</div>`;
|
||||
});
|
||||
html += '</div></div>';
|
||||
}
|
||||
|
||||
renderProcessInfo(data) {
|
||||
const container = document.getElementById('process-info');
|
||||
container.innerHTML = `
|
||||
html += '</div>';
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
renderProcessInfo(data) {
|
||||
const container = document.getElementById('process-info');
|
||||
container.innerHTML = `
|
||||
<div class="metric-item">
|
||||
<strong>Process ID:</strong>
|
||||
<span class="process-value">${data.pid}</span>
|
||||
@ -294,129 +294,129 @@ class MonitoringDashboard {
|
||||
<span class="process-value">${data.env}</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
renderError(containerId, message) {
|
||||
const container = document.getElementById(containerId);
|
||||
container.innerHTML = `<div class="error">${message}</div>`;
|
||||
}
|
||||
|
||||
updateStatus(status, text) {
|
||||
const indicator = document.getElementById('status-indicator');
|
||||
const dot = document.getElementById('status-dot');
|
||||
const statusText = document.getElementById('status-text');
|
||||
|
||||
indicator.className = `status-indicator ${status}`;
|
||||
statusText.textContent = text;
|
||||
}
|
||||
|
||||
updateOverallStatus(healthStatus) {
|
||||
if (healthStatus === 'healthy') {
|
||||
this.updateStatus('healthy', 'System Healthy');
|
||||
} else if (healthStatus === 'warning') {
|
||||
this.updateStatus('warning', 'System Warning');
|
||||
} else {
|
||||
this.updateStatus('error', 'System Error');
|
||||
}
|
||||
}
|
||||
|
||||
updateLastUpdated() {
|
||||
const element = document.getElementById('last-updated');
|
||||
element.textContent = new Date().toLocaleString();
|
||||
}
|
||||
|
||||
formatUptime(seconds) {
|
||||
const days = Math.floor(seconds / 86400);
|
||||
const hours = Math.floor((seconds % 86400) / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
const secs = Math.floor(seconds % 60);
|
||||
|
||||
if (days > 0) {
|
||||
return `${days}d ${hours}h ${minutes}m`;
|
||||
} else if (hours > 0) {
|
||||
return `${hours}h ${minutes}m ${secs}s`;
|
||||
} else if (minutes > 0) {
|
||||
return `${minutes}m ${secs}s`;
|
||||
} else {
|
||||
return `${secs}s`;
|
||||
}
|
||||
}
|
||||
|
||||
startAutoRefresh() {
|
||||
if (this.refreshInterval) {
|
||||
clearInterval(this.refreshInterval);
|
||||
}
|
||||
|
||||
renderError(containerId, message) {
|
||||
const container = document.getElementById(containerId);
|
||||
container.innerHTML = `<div class="error">${message}</div>`;
|
||||
if (this.autoRefresh) {
|
||||
this.refreshInterval = setInterval(() => {
|
||||
this.loadAllData();
|
||||
}, this.refreshRate);
|
||||
}
|
||||
}
|
||||
|
||||
updateStatus(status, text) {
|
||||
const indicator = document.getElementById('status-indicator');
|
||||
const dot = document.getElementById('status-dot');
|
||||
const statusText = document.getElementById('status-text');
|
||||
|
||||
indicator.className = `status-indicator ${status}`;
|
||||
statusText.textContent = text;
|
||||
}
|
||||
|
||||
updateOverallStatus(healthStatus) {
|
||||
if (healthStatus === 'healthy') {
|
||||
this.updateStatus('healthy', 'System Healthy');
|
||||
} else if (healthStatus === 'warning') {
|
||||
this.updateStatus('warning', 'System Warning');
|
||||
} else {
|
||||
this.updateStatus('error', 'System Error');
|
||||
}
|
||||
}
|
||||
|
||||
updateLastUpdated() {
|
||||
const element = document.getElementById('last-updated');
|
||||
element.textContent = new Date().toLocaleString();
|
||||
}
|
||||
|
||||
formatUptime(seconds) {
|
||||
const days = Math.floor(seconds / 86400);
|
||||
const hours = Math.floor((seconds % 86400) / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
const secs = Math.floor(seconds % 60);
|
||||
|
||||
if (days > 0) {
|
||||
return `${days}d ${hours}h ${minutes}m`;
|
||||
} else if (hours > 0) {
|
||||
return `${hours}h ${minutes}m ${secs}s`;
|
||||
} else if (minutes > 0) {
|
||||
return `${minutes}m ${secs}s`;
|
||||
} else {
|
||||
return `${secs}s`;
|
||||
}
|
||||
}
|
||||
|
||||
startAutoRefresh() {
|
||||
if (this.refreshInterval) {
|
||||
clearInterval(this.refreshInterval);
|
||||
}
|
||||
|
||||
if (this.autoRefresh) {
|
||||
this.refreshInterval = setInterval(() => {
|
||||
this.loadAllData();
|
||||
}, this.refreshRate);
|
||||
}
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
// Auto-refresh toggle (could add a button for this)
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (document.hidden) {
|
||||
clearInterval(this.refreshInterval);
|
||||
} else {
|
||||
this.startAutoRefresh();
|
||||
}
|
||||
});
|
||||
}
|
||||
setupEventListeners() {
|
||||
// Auto-refresh toggle (could add a button for this)
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (document.hidden) {
|
||||
clearInterval(this.refreshInterval);
|
||||
} else {
|
||||
this.startAutoRefresh();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Global functions for buttons
|
||||
async function refreshData() {
|
||||
const dashboard = window.dashboard;
|
||||
if (dashboard) {
|
||||
await dashboard.loadAllData();
|
||||
}
|
||||
const dashboard = window.dashboard;
|
||||
if (dashboard) {
|
||||
await dashboard.loadAllData();
|
||||
}
|
||||
}
|
||||
|
||||
async function testAPI() {
|
||||
try {
|
||||
const response = await fetch('/api/test');
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
alert('✅ API Test Successful!\n\n' +
|
||||
`Message: ${result.message}\n` +
|
||||
`Version: ${result.version}\n` +
|
||||
`Environment: ${result.environment}`);
|
||||
} else {
|
||||
alert('❌ API Test Failed');
|
||||
}
|
||||
} catch (error) {
|
||||
alert('❌ API Test Failed: ' + error.message);
|
||||
try {
|
||||
const response = await fetch('/api/test');
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
alert('✅ API Test Successful!\n\n' +
|
||||
`Message: ${result.message}\n` +
|
||||
`Version: ${result.version}\n` +
|
||||
`Environment: ${result.environment}`);
|
||||
} else {
|
||||
alert('❌ API Test Failed');
|
||||
}
|
||||
} catch (error) {
|
||||
alert('❌ API Test Failed: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadMetrics() {
|
||||
try {
|
||||
const response = await fetch('/api/metrics');
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
const dataStr = JSON.stringify(result.data, null, 2);
|
||||
const dataBlob = new Blob([dataStr], {type: 'application/json'});
|
||||
const url = URL.createObjectURL(dataBlob);
|
||||
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = `metrics-${new Date().toISOString().split('T')[0]}.json`;
|
||||
link.click();
|
||||
|
||||
URL.revokeObjectURL(url);
|
||||
} else {
|
||||
alert('Failed to download metrics');
|
||||
}
|
||||
} catch (error) {
|
||||
alert('Error downloading metrics: ' + error.message);
|
||||
try {
|
||||
const response = await fetch('/api/metrics');
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
const dataStr = JSON.stringify(result.data, null, 2);
|
||||
const dataBlob = new Blob([dataStr], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(dataBlob);
|
||||
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = `metrics-${new Date().toISOString().split('T')[0]}.json`;
|
||||
link.click();
|
||||
|
||||
URL.revokeObjectURL(url);
|
||||
} else {
|
||||
alert('Failed to download metrics');
|
||||
}
|
||||
} catch (error) {
|
||||
alert('Error downloading metrics: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize dashboard when page loads
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
window.dashboard = new MonitoringDashboard();
|
||||
window.dashboard = new MonitoringDashboard();
|
||||
});
|
||||
@ -1,81 +1,84 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>System Monitoring Dashboard</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>System Monitoring Dashboard</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<header>
|
||||
<h1>🖥️ System Monitoring Dashboard</h1>
|
||||
<p>Real-time system metrics and health monitoring</p>
|
||||
<div class="status-indicator" id="status-indicator">
|
||||
<span class="status-dot" id="status-dot"></span>
|
||||
<span id="status-text">Checking...</span>
|
||||
<header>
|
||||
<h1>🖥️ System Monitoring Dashboard</h1>
|
||||
<p>Real-time system metrics and health monitoring</p>
|
||||
<div class="status-indicator" id="status-indicator">
|
||||
<span class="status-dot" id="status-dot"></span>
|
||||
<span id="status-text">Checking...</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<div class="dashboard-grid">
|
||||
<!-- System Information Card -->
|
||||
<div class="card">
|
||||
<h2>📊 System Information</h2>
|
||||
<div class="metric-grid" id="system-info">
|
||||
<div class="loading">Loading system information...</div>
|
||||
</div>
|
||||
</header>
|
||||
</div>
|
||||
|
||||
<main>
|
||||
<div class="dashboard-grid">
|
||||
<!-- System Information Card -->
|
||||
<div class="card">
|
||||
<h2>📊 System Information</h2>
|
||||
<div class="metric-grid" id="system-info">
|
||||
<div class="loading">Loading system information...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Memory Usage Card -->
|
||||
<div class="card">
|
||||
<h2>💾 Memory Usage</h2>
|
||||
<div class="memory-bars" id="memory-usage">
|
||||
<div class="loading">Loading memory data...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Health Status Card -->
|
||||
<div class="card">
|
||||
<h2>❤️ Health Status</h2>
|
||||
<div class="health-grid" id="health-status">
|
||||
<div class="loading">Loading health data...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- API Metrics Card -->
|
||||
<div class="card">
|
||||
<h2>📡 API Metrics</h2>
|
||||
<div class="metrics-grid" id="api-metrics">
|
||||
<div class="loading">Loading metrics...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Process Information Card -->
|
||||
<div class="card">
|
||||
<h2>⚙️ Process Information</h2>
|
||||
<div class="process-grid" id="process-info">
|
||||
<div class="loading">Loading process data...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Actions Card -->
|
||||
<div class="card">
|
||||
<h2>🚀 Quick Actions</h2>
|
||||
<div class="actions-grid">
|
||||
<button onclick="refreshData()" class="btn btn-primary">🔄 Refresh Data</button>
|
||||
<button onclick="testAPI()" class="btn btn-secondary">🧪 Test API</button>
|
||||
<button onclick="downloadMetrics()" class="btn btn-secondary">📥 Download Metrics</button>
|
||||
<a href="/health/detailed" target="_blank" class="btn btn-info">🔍 Detailed Health</a>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Memory Usage Card -->
|
||||
<div class="card">
|
||||
<h2>💾 Memory Usage</h2>
|
||||
<div class="memory-bars" id="memory-usage">
|
||||
<div class="loading">Loading memory data...</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<p>Last updated: <span id="last-updated">Never</span></p>
|
||||
<p>Dashboard built with ❤️ for Harbor CI/CD Demo</p>
|
||||
</footer>
|
||||
<!-- Health Status Card -->
|
||||
<div class="card">
|
||||
<h2>❤️ Health Status</h2>
|
||||
<div class="health-grid" id="health-status">
|
||||
<div class="loading">Loading health data...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="app.js"></script>
|
||||
<!-- API Metrics Card -->
|
||||
<div class="card">
|
||||
<h2>📡 API Metrics</h2>
|
||||
<div class="metrics-grid" id="api-metrics">
|
||||
<div class="loading">Loading metrics...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Process Information Card -->
|
||||
<div class="card">
|
||||
<h2>⚙️ Process Information</h2>
|
||||
<div class="process-grid" id="process-info">
|
||||
<div class="loading">Loading process data...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Actions Card -->
|
||||
<div class="card">
|
||||
<h2>🚀 Quick Actions</h2>
|
||||
<div class="actions-grid">
|
||||
<button onclick="refreshData()" class="btn btn-primary">🔄 Refresh Data</button>
|
||||
<button onclick="testAPI()" class="btn btn-secondary">🧪 Test API</button>
|
||||
<button onclick="downloadMetrics()" class="btn btn-secondary">📥 Download Metrics</button>
|
||||
<a href="/health/detailed" target="_blank" class="btn btn-info">🔍 Detailed Health</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<p>Last updated: <span id="last-updated">Never</span></p>
|
||||
<p>Dashboard built with ❤️ for Harbor CI/CD Demo</p>
|
||||
</footer>
|
||||
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@ -1,276 +1,292 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: #333;
|
||||
min-height: 100vh;
|
||||
line-height: 1.6;
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: #333;
|
||||
min-height: 100vh;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
header {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
box-shadow: 0 2px 20px rgba(0, 0, 0, 0.1);
|
||||
margin-bottom: 2rem;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
box-shadow: 0 2px 20px rgba(0, 0, 0, 0.1);
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
color: #4a5568;
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
color: #4a5568;
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
header p {
|
||||
color: #718096;
|
||||
font-size: 1.1rem;
|
||||
margin-bottom: 1rem;
|
||||
color: #718096;
|
||||
font-size: 1.1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 25px;
|
||||
border: 2px solid #10b981;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 25px;
|
||||
border: 2px solid #10b981;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
background: #10b981;
|
||||
animation: pulse 2s infinite;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
background: #10b981;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
.status-indicator.warning {
|
||||
background: rgba(245, 158, 11, 0.1);
|
||||
border-color: #f59e0b;
|
||||
background: rgba(245, 158, 11, 0.1);
|
||||
border-color: #f59e0b;
|
||||
}
|
||||
|
||||
.status-indicator.warning .status-dot {
|
||||
background: #f59e0b;
|
||||
background: #f59e0b;
|
||||
}
|
||||
|
||||
.status-indicator.error {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
border-color: #ef4444;
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
border-color: #ef4444;
|
||||
}
|
||||
|
||||
.status-indicator.error .status-dot {
|
||||
background: #ef4444;
|
||||
background: #ef4444;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
main {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 0 2rem 2rem;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 0 2rem 2rem;
|
||||
}
|
||||
|
||||
.dashboard-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
|
||||
gap: 2rem;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: 16px;
|
||||
padding: 2rem;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: 16px;
|
||||
padding: 2rem;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15);
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.card h2 {
|
||||
color: #4a5568;
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
border-bottom: 2px solid #e2e8f0;
|
||||
padding-bottom: 0.5rem;
|
||||
color: #4a5568;
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
border-bottom: 2px solid #e2e8f0;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.metric-grid, .health-grid, .process-grid, .metrics-grid {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
.metric-grid,
|
||||
.health-grid,
|
||||
.process-grid,
|
||||
.metrics-grid {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.metric-item, .health-item, .process-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.75rem;
|
||||
background: rgba(247, 250, 252, 0.8);
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid #3b82f6;
|
||||
.metric-item,
|
||||
.health-item,
|
||||
.process-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.75rem;
|
||||
background: rgba(247, 250, 252, 0.8);
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid #3b82f6;
|
||||
}
|
||||
|
||||
.metric-item strong, .health-item strong, .process-item strong {
|
||||
color: #374151;
|
||||
font-weight: 600;
|
||||
.metric-item strong,
|
||||
.health-item strong,
|
||||
.process-item strong {
|
||||
color: #374151;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.metric-value, .health-value, .process-value {
|
||||
color: #6b7280;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-weight: 500;
|
||||
.metric-value,
|
||||
.health-value,
|
||||
.process-value {
|
||||
color: #6b7280;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.memory-bars {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.memory-bar {
|
||||
background: #e5e7eb;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
height: 30px;
|
||||
position: relative;
|
||||
background: #e5e7eb;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
height: 30px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.memory-bar-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #10b981, #059669);
|
||||
border-radius: 10px;
|
||||
transition: width 0.5s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #10b981, #059669);
|
||||
border-radius: 10px;
|
||||
transition: width 0.5s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.memory-bar-fill.warning {
|
||||
background: linear-gradient(90deg, #f59e0b, #d97706);
|
||||
background: linear-gradient(90deg, #f59e0b, #d97706);
|
||||
}
|
||||
|
||||
.memory-bar-fill.danger {
|
||||
background: linear-gradient(90deg, #ef4444, #dc2626);
|
||||
background: linear-gradient(90deg, #ef4444, #dc2626);
|
||||
}
|
||||
|
||||
.memory-label {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.actions-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
||||
gap: 1rem;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 0.75rem 1rem;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
display: inline-block;
|
||||
padding: 0.75rem 1rem;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, #3b82f6, #2563eb);
|
||||
color: white;
|
||||
background: linear-gradient(135deg, #3b82f6, #2563eb);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: linear-gradient(135deg, #2563eb, #1d4ed8);
|
||||
transform: translateY(-2px);
|
||||
background: linear-gradient(135deg, #2563eb, #1d4ed8);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: linear-gradient(135deg, #6b7280, #4b5563);
|
||||
color: white;
|
||||
background: linear-gradient(135deg, #6b7280, #4b5563);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: linear-gradient(135deg, #4b5563, #374151);
|
||||
transform: translateY(-2px);
|
||||
background: linear-gradient(135deg, #4b5563, #374151);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.btn-info {
|
||||
background: linear-gradient(135deg, #10b981, #059669);
|
||||
color: white;
|
||||
background: linear-gradient(135deg, #10b981, #059669);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-info:hover {
|
||||
background: linear-gradient(135deg, #059669, #047857);
|
||||
transform: translateY(-2px);
|
||||
background: linear-gradient(135deg, #059669, #047857);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
color: #9ca3af;
|
||||
font-style: italic;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
color: #9ca3af;
|
||||
font-style: italic;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.error {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
color: #dc2626;
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid #ef4444;
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
color: #dc2626;
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid #ef4444;
|
||||
}
|
||||
|
||||
footer {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
margin-top: 2rem;
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.dashboard-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
header {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
main {
|
||||
padding: 0 1rem 1rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.actions-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.dashboard-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
header {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
main {
|
||||
padding: 0 1rem 1rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.actions-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
@ -8,7 +8,7 @@ const router = express.Router();
|
||||
router.get('/system', (req, res) => {
|
||||
try {
|
||||
incrementCounter('api_calls_total', { endpoint: '/system' });
|
||||
|
||||
|
||||
const systemInfo = {
|
||||
hostname: os.hostname(),
|
||||
platform: os.platform(),
|
||||
@ -40,7 +40,7 @@ router.get('/system', (req, res) => {
|
||||
router.get('/memory', (req, res) => {
|
||||
try {
|
||||
incrementCounter('api_calls_total', { endpoint: '/memory' });
|
||||
|
||||
|
||||
const memoryUsage = process.memoryUsage();
|
||||
const totalMem = os.totalmem();
|
||||
const freeMem = os.freemem();
|
||||
@ -79,7 +79,7 @@ router.get('/memory', (req, res) => {
|
||||
router.get('/process', (req, res) => {
|
||||
try {
|
||||
incrementCounter('api_calls_total', { endpoint: '/process' });
|
||||
|
||||
|
||||
const processInfo = {
|
||||
pid: process.pid,
|
||||
ppid: process.ppid,
|
||||
@ -126,7 +126,7 @@ router.get('/metrics', (req, res) => {
|
||||
// Test endpoint for CI/CD validation
|
||||
router.get('/test', (req, res) => {
|
||||
incrementCounter('api_calls_total', { endpoint: '/test' });
|
||||
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'API is working correctly',
|
||||
|
||||
@ -18,18 +18,18 @@ router.get('/detailed', (req, res) => {
|
||||
const memoryUsage = process.memoryUsage();
|
||||
const totalMem = os.totalmem();
|
||||
const freeMem = os.freemem();
|
||||
|
||||
|
||||
// Check memory usage (warn if over 80%)
|
||||
const memoryPercentage = ((totalMem - freeMem) / totalMem) * 100;
|
||||
const memoryStatus = memoryPercentage > 80 ? 'warning' : 'healthy';
|
||||
|
||||
|
||||
// Check process memory (warn if heap over 100MB)
|
||||
const heapUsedMB = memoryUsage.heapUsed / 1024 / 1024;
|
||||
const processMemoryStatus = heapUsedMB > 100 ? 'warning' : 'healthy';
|
||||
|
||||
|
||||
// Overall status
|
||||
const overallStatus = (memoryStatus === 'warning' || processMemoryStatus === 'warning')
|
||||
? 'warning'
|
||||
const overallStatus = (memoryStatus === 'warning' || processMemoryStatus === 'warning')
|
||||
? 'warning'
|
||||
: 'healthy';
|
||||
|
||||
res.json({
|
||||
@ -64,9 +64,9 @@ router.get('/detailed', (req, res) => {
|
||||
router.get('/ready', (req, res) => {
|
||||
// Check if the application is ready to serve traffic
|
||||
// In a real app, you might check database connections, external services, etc.
|
||||
|
||||
|
||||
const isReady = true; // Simplified check
|
||||
|
||||
|
||||
if (isReady) {
|
||||
res.json({
|
||||
status: 'ready',
|
||||
|
||||
@ -2,94 +2,94 @@
|
||||
// In production, you'd use Prometheus, StatsD, or similar
|
||||
|
||||
const metrics = {
|
||||
counters: {},
|
||||
gauges: {},
|
||||
histograms: {},
|
||||
startTime: Date.now()
|
||||
};
|
||||
|
||||
// Increment a counter
|
||||
function incrementCounter(name, labels = {}) {
|
||||
const key = `${name}_${JSON.stringify(labels)}`;
|
||||
if (!metrics.counters[key]) {
|
||||
metrics.counters[key] = {
|
||||
name,
|
||||
labels,
|
||||
value: 0
|
||||
};
|
||||
}
|
||||
metrics.counters[key].value++;
|
||||
}
|
||||
|
||||
// Set a gauge value
|
||||
function setGauge(name, value, labels = {}) {
|
||||
const key = `${name}_${JSON.stringify(labels)}`;
|
||||
metrics.gauges[key] = {
|
||||
counters: {},
|
||||
gauges: {},
|
||||
histograms: {},
|
||||
startTime: Date.now()
|
||||
};
|
||||
|
||||
// Increment a counter
|
||||
function incrementCounter(name, labels = {}) {
|
||||
const key = `${name}_${JSON.stringify(labels)}`;
|
||||
if (!metrics.counters[key]) {
|
||||
metrics.counters[key] = {
|
||||
name,
|
||||
labels,
|
||||
value,
|
||||
timestamp: Date.now()
|
||||
value: 0
|
||||
};
|
||||
}
|
||||
|
||||
// Record histogram value (simplified)
|
||||
function recordHistogram(name, value, labels = {}) {
|
||||
const key = `${name}_${JSON.stringify(labels)}`;
|
||||
if (!metrics.histograms[key]) {
|
||||
metrics.histograms[key] = {
|
||||
name,
|
||||
labels,
|
||||
values: [],
|
||||
count: 0,
|
||||
sum: 0
|
||||
};
|
||||
}
|
||||
|
||||
const histogram = metrics.histograms[key];
|
||||
histogram.values.push(value);
|
||||
histogram.count++;
|
||||
histogram.sum += value;
|
||||
|
||||
// Keep only last 1000 values to prevent memory issues
|
||||
if (histogram.values.length > 1000) {
|
||||
histogram.values = histogram.values.slice(-1000);
|
||||
}
|
||||
}
|
||||
|
||||
// Get all metrics
|
||||
function getMetrics() {
|
||||
const runtime = {
|
||||
uptime_seconds: Math.round((Date.now() - metrics.startTime) / 1000),
|
||||
memory_usage_bytes: process.memoryUsage(),
|
||||
cpu_usage_percent: process.cpuUsage()
|
||||
};
|
||||
|
||||
return {
|
||||
counters: Object.values(metrics.counters),
|
||||
gauges: Object.values(metrics.gauges),
|
||||
histograms: Object.values(metrics.histograms).map(h => ({
|
||||
...h,
|
||||
average: h.count > 0 ? h.sum / h.count : 0,
|
||||
min: h.values.length > 0 ? Math.min(...h.values) : 0,
|
||||
max: h.values.length > 0 ? Math.max(...h.values) : 0
|
||||
})),
|
||||
runtime,
|
||||
timestamp: new Date().toISOString()
|
||||
metrics.counters[key].value++;
|
||||
}
|
||||
|
||||
// Set a gauge value
|
||||
function setGauge(name, value, labels = {}) {
|
||||
const key = `${name}_${JSON.stringify(labels)}`;
|
||||
metrics.gauges[key] = {
|
||||
name,
|
||||
labels,
|
||||
value,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
}
|
||||
|
||||
// Record histogram value (simplified)
|
||||
function recordHistogram(name, value, labels = {}) {
|
||||
const key = `${name}_${JSON.stringify(labels)}`;
|
||||
if (!metrics.histograms[key]) {
|
||||
metrics.histograms[key] = {
|
||||
name,
|
||||
labels,
|
||||
values: [],
|
||||
count: 0,
|
||||
sum: 0
|
||||
};
|
||||
}
|
||||
|
||||
// Reset all metrics
|
||||
function resetMetrics() {
|
||||
metrics.counters = {};
|
||||
metrics.gauges = {};
|
||||
metrics.histograms = {};
|
||||
metrics.startTime = Date.now();
|
||||
|
||||
const histogram = metrics.histograms[key];
|
||||
histogram.values.push(value);
|
||||
histogram.count++;
|
||||
histogram.sum += value;
|
||||
|
||||
// Keep only last 1000 values to prevent memory issues
|
||||
if (histogram.values.length > 1000) {
|
||||
histogram.values = histogram.values.slice(-1000);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
incrementCounter,
|
||||
setGauge,
|
||||
recordHistogram,
|
||||
getMetrics,
|
||||
resetMetrics
|
||||
};
|
||||
}
|
||||
|
||||
// Get all metrics
|
||||
function getMetrics() {
|
||||
const runtime = {
|
||||
uptime_seconds: Math.round((Date.now() - metrics.startTime) / 1000),
|
||||
memory_usage_bytes: process.memoryUsage(),
|
||||
cpu_usage_percent: process.cpuUsage()
|
||||
};
|
||||
|
||||
return {
|
||||
counters: Object.values(metrics.counters),
|
||||
gauges: Object.values(metrics.gauges),
|
||||
histograms: Object.values(metrics.histograms).map(h => ({
|
||||
...h,
|
||||
average: h.count > 0 ? h.sum / h.count : 0,
|
||||
min: h.values.length > 0 ? Math.min(...h.values) : 0,
|
||||
max: h.values.length > 0 ? Math.max(...h.values) : 0
|
||||
})),
|
||||
runtime,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
}
|
||||
|
||||
// Reset all metrics
|
||||
function resetMetrics() {
|
||||
metrics.counters = {};
|
||||
metrics.gauges = {};
|
||||
metrics.histograms = {};
|
||||
metrics.startTime = Date.now();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
incrementCounter,
|
||||
setGauge,
|
||||
recordHistogram,
|
||||
getMetrics,
|
||||
resetMetrics
|
||||
};
|
||||
Reference in New Issue
Block a user