From 4946d19ea75ca0b1fa39524c4bf027da885930ef Mon Sep 17 00:00:00 2001 From: aya shosha Date: Wed, 29 Jul 2026 23:34:53 +0300 Subject: [PATCH] Add root Dockerfile for deployment --- Dockerfile | 2 +- app.js | 82 ++++++++++++++++++++++++++ dashboard.js | 49 ++++++++++++++++ index.html | 50 ++++++++++++++++ package.json | 18 ++++++ style.css | 158 +++++++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 358 insertions(+), 1 deletion(-) create mode 100644 app.js create mode 100644 dashboard.js create mode 100644 index.html create mode 100644 package.json create mode 100644 style.css diff --git a/Dockerfile b/Dockerfile index 6edafc1..25e757c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ FROM node:16.3.0-alpine -COPY q1-deploy-monitor /app +COPY app.js dashboard.js index.html package.json /app/ RUN mkdir -p /logs && touch /logs/logs.txt diff --git a/app.js b/app.js new file mode 100644 index 0000000..b1a44e7 --- /dev/null +++ b/app.js @@ -0,0 +1,82 @@ +const express = require('express') +const fs = require('fs') +const path = require('path') + +const app = express() +const port = 5000 +const logDir = '/logs' +const logFile = path.join(logDir, 'logs.txt') + +if (!fs.existsSync(logDir)) { + fs.mkdirSync(logDir, { recursive: true }) +} + +function getStatsFromLogFile() { + if (!fs.existsSync(logFile)) { + return { requestCount: 0, averageResponseTime: 0 } + } + + const content = fs.readFileSync(logFile, 'utf8') + const lines = content.split('\n').filter(Boolean) + const requestCount = lines.length + + if (requestCount === 0) { + return { requestCount: 0, averageResponseTime: 0 } + } + + const totalResponseTime = lines.reduce((total, line) => { + const parts = line.trim().split(' ') + const duration = parseFloat(parts[1]) + return total + (Number.isNaN(duration) ? 0 : duration) + }, 0) + + return { + requestCount, + averageResponseTime: totalResponseTime / requestCount + } +} + +app.use('/dashboard', express.static(path.join(__dirname, 'dashboard'), { index: false })) + +app.use((req, res, next) => { + const startTime = Date.now() + const requestTime = new Date(startTime).toISOString() + + res.on('finish', () => { + const durationSeconds = (Date.now() - startTime) / 1000 + const line = `${req.path} ${durationSeconds.toFixed(3)} ${requestTime} ${res.statusCode}\n` + + fs.appendFile(logFile, line, (err) => { + if (err) console.error('Failed to write request log:', err) + }) + }) + + next() +}) + +app.get('/dashboard', (req, res) => { + res.sendFile(path.join(__dirname, 'dashboard', 'index.html')) +}) + +app.get('/api/stats', (req, res) => { + const { requestCount, averageResponseTime } = getStatsFromLogFile() + + res.json({ + requestCount, + averageResponseTime, + status: 'up' + }) +}) + +app.get('/', (req, res) => { + res.send('Hello World from GHAYMAH') +}) + +app.get('/health', (req, res) => { + res.send('The app up and runing') +}) + +app.listen(port, () => { + console.log(`Example app listening at http://localhost:${port}`) + console.log(`Dashboard available at http://localhost:${port}/dashboard`) +}) diff --git a/dashboard.js b/dashboard.js new file mode 100644 index 0000000..f3478d5 --- /dev/null +++ b/dashboard.js @@ -0,0 +1,49 @@ +const requestCountEl = document.getElementById('requestCount') +const averageTimeEl = document.getElementById('averageTime') +const appStatusEl = document.getElementById('appStatus') +const statusBadgeEl = document.getElementById('statusBadge') +const statusTextEl = document.getElementById('statusText') +const lastUpdatedEl = document.getElementById('lastUpdated') + +function setStatus(isUp) { + statusBadgeEl.classList.remove('up', 'down') + appStatusEl.classList.remove('up', 'down') + + if (isUp) { + statusBadgeEl.classList.add('up') + appStatusEl.classList.add('up') + statusTextEl.textContent = 'Online' + appStatusEl.textContent = 'Up' + return + } + + statusBadgeEl.classList.add('down') + appStatusEl.classList.add('down') + statusTextEl.textContent = 'Offline' + appStatusEl.textContent = 'Down' +} + +async function fetchStats() { + try { + const response = await fetch('/api/stats') + + if (!response.ok) { + throw new Error('Stats request failed') + } + + const data = await response.json() + + requestCountEl.textContent = data.requestCount.toLocaleString() + averageTimeEl.textContent = `${data.averageResponseTime.toFixed(3)}s` + setStatus(data.status === 'up') + lastUpdatedEl.textContent = `Last updated: ${new Date().toLocaleTimeString()}` + } catch (error) { + requestCountEl.textContent = '—' + averageTimeEl.textContent = '—' + setStatus(false) + lastUpdatedEl.textContent = 'Last updated: failed to connect' + } +} + +fetchStats() +setInterval(fetchStats, 5000) diff --git a/index.html b/index.html new file mode 100644 index 0000000..0331e20 --- /dev/null +++ b/index.html @@ -0,0 +1,50 @@ + + + + + + Server Dashboard + + + +
+
+
+

Server Dashboard

+

Live monitoring for your Node.js application

+
+
+ + Checking... +
+
+ +
+
+

Total Requests

+

+

All requests received by the server

+
+ +
+

Average Response Time

+

+

Mean duration in seconds

+
+ +
+

Application Status

+

+

Based on health check

+
+
+ +
+ Last updated: — + Auto-refresh every 5 seconds +
+
+ + + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..a7719cf --- /dev/null +++ b/package.json @@ -0,0 +1,18 @@ +{ + "name": "nodeapp", + "version": "1.0.0", + "description": "", + "main": "app.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "express": "^4.17.1" + }, + "devDependencies": { + "nodemon": "^3.1.14" + } +} diff --git a/style.css b/style.css new file mode 100644 index 0000000..496da72 --- /dev/null +++ b/style.css @@ -0,0 +1,158 @@ +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif; + background: linear-gradient(135deg, #0f172a 0%, #1e293b 50%, #0f172a 100%); + min-height: 100vh; + color: #e2e8f0; + padding: 2rem; +} + +.dashboard { + max-width: 1100px; + margin: 0 auto; +} + +.header { + display: flex; + justify-content: space-between; + align-items: center; + gap: 1.5rem; + margin-bottom: 2rem; + flex-wrap: wrap; +} + +.header h1 { + font-size: 2rem; + font-weight: 700; + color: #f8fafc; +} + +.subtitle { + margin-top: 0.35rem; + color: #94a3b8; + font-size: 0.95rem; +} + +.status-badge { + display: inline-flex; + align-items: center; + gap: 0.6rem; + padding: 0.65rem 1.1rem; + border-radius: 999px; + background: rgba(15, 23, 42, 0.7); + border: 1px solid rgba(148, 163, 184, 0.25); + font-weight: 600; +} + +.status-dot { + width: 10px; + height: 10px; + border-radius: 50%; + background: #64748b; + box-shadow: 0 0 0 0 rgba(100, 116, 139, 0.5); +} + +.status-badge.up .status-dot { + background: #22c55e; + animation: pulse 2s infinite; +} + +.status-badge.down .status-dot { + background: #ef4444; +} + +.status-badge.up { + border-color: rgba(34, 197, 94, 0.35); + color: #bbf7d0; +} + +.status-badge.down { + border-color: rgba(239, 68, 68, 0.35); + color: #fecaca; +} + +.cards { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); + gap: 1.25rem; +} + +.card { + background: rgba(30, 41, 59, 0.85); + border: 1px solid rgba(148, 163, 184, 0.15); + border-radius: 16px; + padding: 1.5rem; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.25); + transition: transform 0.2s ease, border-color 0.2s ease; +} + +.card:hover { + transform: translateY(-2px); + border-color: rgba(96, 165, 250, 0.35); +} + +.card-label { + color: #94a3b8; + font-size: 0.85rem; + text-transform: uppercase; + letter-spacing: 0.08em; + margin-bottom: 0.75rem; +} + +.card-value { + font-size: 2.4rem; + font-weight: 700; + color: #f8fafc; + line-height: 1.1; +} + +.status-value.up { + color: #4ade80; +} + +.status-value.down { + color: #f87171; +} + +.card-hint { + margin-top: 0.75rem; + color: #64748b; + font-size: 0.85rem; +} + +.footer { + margin-top: 2rem; + display: flex; + justify-content: space-between; + gap: 1rem; + flex-wrap: wrap; + color: #64748b; + font-size: 0.85rem; +} + +@keyframes pulse { + 0% { + box-shadow: 0 0 0 0 rgba(34, 197, 94, 0.5); + } + 70% { + box-shadow: 0 0 0 8px rgba(34, 197, 94, 0); + } + 100% { + box-shadow: 0 0 0 0 rgba(34, 197, 94, 0); + } +} + +@media (max-width: 600px) { + body { + padding: 1rem; + } + + .card-value { + font-size: 2rem; + } +}