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

This commit is contained in:
2025-07-02 16:01:38 -06:00
parent daf3dbe0ef
commit 9a23c1c05b
10 changed files with 731 additions and 672 deletions

View File

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

View File

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

View File

@ -15,7 +15,7 @@ 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

View File

@ -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 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 init() {
await this.loadAllData();
this.startAutoRefresh();
this.setupEventListeners();
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 loadAllData() {
this.updateStatus('loading', 'Loading...');
async loadMemoryUsage() {
try {
const response = await fetch('/api/memory');
const result = await response.json();
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');
}
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 loadSystemInfo() {
try {
const response = await fetch('/api/system');
const result = await response.json();
async loadHealthStatus() {
try {
const response = await fetch('/health/detailed');
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');
}
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 loadMemoryUsage() {
try {
const response = await fetch('/api/memory');
const result = await response.json();
async loadApiMetrics() {
try {
const response = await fetch('/api/metrics');
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');
}
if (result.success) {
this.renderApiMetrics(result.data);
} else {
throw new Error(result.error);
}
} catch (error) {
this.renderError('api-metrics', 'Failed to load metrics');
}
}
async loadHealthStatus() {
try {
const response = await fetch('/health/detailed');
const result = await response.json();
async loadProcessInfo() {
try {
const response = await fetch('/api/process');
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');
}
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 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 = `
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');
renderMemoryUsage(data) {
const container = document.getElementById('memory-usage');
const systemPercentage = data.system.percentage;
const systemClass = systemPercentage > 80 ? 'danger' : systemPercentage > 60 ? 'warning' : '';
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' : '';
const processHeapPercentage = (data.process.heapUsed / data.process.heapTotal) * 100;
const processClass = processHeapPercentage > 80 ? 'danger' : processHeapPercentage > 60 ? 'warning' : '';
container.innerHTML = `
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');
renderHealthStatus(data) {
const container = document.getElementById('health-status');
const statusClass = data.status === 'healthy' ? 'metric-item' :
data.status === 'warning' ? 'metric-item warning' : 'metric-item error';
const statusClass = data.status === 'healthy' ? 'metric-item' :
data.status === 'warning' ? 'metric-item warning' : 'metric-item error';
container.innerHTML = `
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');
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');
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">';
let html = '<div class="metrics-grid">';
// Total requests
const totalRequests = requestCounters.reduce((sum, counter) => sum + counter.value, 0);
html += `
// 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();
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);
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();
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);
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();
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);
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();
});

View File

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

View File

@ -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;
}
.dashboard-grid {
grid-template-columns: 1fr;
}
header {
padding: 1rem;
}
header {
padding: 1rem;
}
header h1 {
font-size: 2rem;
}
header h1 {
font-size: 2rem;
}
main {
padding: 0 1rem 1rem;
}
main {
padding: 0 1rem 1rem;
}
.card {
padding: 1.5rem;
}
.card {
padding: 1.5rem;
}
.actions-grid {
grid-template-columns: 1fr;
}
.actions-grid {
grid-template-columns: 1fr;
}
}

View File

@ -2,94 +2,94 @@
// In production, you'd use Prometheus, StatsD, or similar
const metrics = {
counters: {},
gauges: {},
histograms: {},
startTime: Date.now()
};
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] = {
// 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
};
}
metrics.counters[key].value++;
}
// 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()
};
}
// Reset all metrics
function resetMetrics() {
metrics.counters = {};
metrics.gauges = {};
metrics.histograms = {};
metrics.startTime = Date.now();
}
module.exports = {
incrementCounter,
setGauge,
recordHistogram,
getMetrics,
resetMetrics
// 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
};
}
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()
};
}
// Reset all metrics
function resetMetrics() {
metrics.counters = {};
metrics.gauges = {};
metrics.histograms = {};
metrics.startTime = Date.now();
}
module.exports = {
incrementCounter,
setGauge,
recordHistogram,
getMetrics,
resetMetrics
};