first commit
هذا الالتزام موجود في:
21
q1-deploy-monitor/Dockerfile
Normal file
21
q1-deploy-monitor/Dockerfile
Normal file
@@ -0,0 +1,21 @@
|
||||
From node:22 As builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package*.json .
|
||||
|
||||
RUN npm ci
|
||||
|
||||
COPY . .
|
||||
|
||||
|
||||
|
||||
FROM node:22-alpine as production
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=builder /app .
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["npm", "start"]
|
||||
95
q1-deploy-monitor/architecture.svg
Normal file
95
q1-deploy-monitor/architecture.svg
Normal file
@@ -0,0 +1,95 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 500" font-family="Segoe UI, Arial, sans-serif">
|
||||
<defs>
|
||||
<linearGradient id="bg" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#0f172a"/>
|
||||
<stop offset="100%" style="stop-color:#1e293b"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="greenGrad" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#22c55e"/>
|
||||
<stop offset="100%" style="stop-color:#16a34a"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="blueGrad" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#3b82f6"/>
|
||||
<stop offset="100%" style="stop-color:#2563eb"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="purpleGrad" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#a855f7"/>
|
||||
<stop offset="100%" style="stop-color:#9333ea"/>
|
||||
</linearGradient>
|
||||
<filter id="shadow" x="-10%" y="-10%" width="120%" height="130%">
|
||||
<feDropShadow dx="0" dy="4" stdDeviation="6" flood-opacity="0.3"/>
|
||||
</filter>
|
||||
</defs>
|
||||
|
||||
<!-- Background -->
|
||||
<rect width="800" height="500" fill="url(#bg)" rx="12"/>
|
||||
|
||||
<!-- Title -->
|
||||
<text x="400" y="45" text-anchor="middle" fill="#e2e8f0" font-size="20" font-weight="700">Q1: Deploy & Monitor Architecture</text>
|
||||
<text x="400" y="68" text-anchor="middle" fill="#94a3b8" font-size="12">Node.js API on Ghaymah Cloud</text>
|
||||
|
||||
<!-- User -->
|
||||
<circle cx="100" cy="250" r="35" fill="#334155" stroke="#475569" stroke-width="2" filter="url(#shadow)"/>
|
||||
<text x="100" y="245" text-anchor="middle" fill="#e2e8f0" font-size="11" font-weight="600">User</text>
|
||||
<text x="100" y="262" text-anchor="middle" fill="#94a3b8" font-size="9">Browser</text>
|
||||
|
||||
<!-- Arrow: User to Load Balancer -->
|
||||
<line x1="135" y1="250" x2="195" y2="250" stroke="#475569" stroke-width="2" marker-end="url(#arrowhead)"/>
|
||||
<defs><marker id="arrowhead" markerWidth="10" markerHeight="7" refX="10" refY="3.5" orient="auto"><polygon points="0 0, 10 3.5, 0 7" fill="#475569"/></marker></defs>
|
||||
|
||||
<!-- Load Balancer -->
|
||||
<rect x="200" y="215" width="120" height="70" rx="10" fill="url(#blueGrad)" filter="url(#shadow)"/>
|
||||
<text x="260" y="245" text-anchor="middle" fill="#fff" font-size="11" font-weight="600">Load Balancer</text>
|
||||
<text x="260" y="263" text-anchor="middle" fill="#bfdbfe" font-size="9">Ghaymah</text>
|
||||
|
||||
<!-- Arrow: LB to Container -->
|
||||
<line x1="320" y1="250" x2="380" y2="250" stroke="#475569" stroke-width="2" marker-end="url(#arrowhead)"/>
|
||||
|
||||
<!-- Container -->
|
||||
<rect x="385" y="170" width="160" height="160" rx="12" fill="#1e293b" stroke="#22c55e" stroke-width="2" filter="url(#shadow)"/>
|
||||
<text x="465" y="198" text-anchor="middle" fill="#22c55e" font-size="11" font-weight="700">Docker Container</text>
|
||||
<line x1="400" y1="210" x2="530" y2="210" stroke="#334155" stroke-width="1"/>
|
||||
<text x="465" y="232" text-anchor="middle" fill="#e2e8f0" font-size="10">Node.js + Express</text>
|
||||
<text x="465" y="252" text-anchor="middle" fill="#94a3b8" font-size="9">Port 3000</text>
|
||||
<text x="465" y="272" text-anchor="middle" fill="#94a3b8" font-size="9">/health endpoint</text>
|
||||
<text x="465" y="292" text-anchor="middle" fill="#94a3b8" font-size="9">Request counter</text>
|
||||
<text x="465" y="312" text-anchor="middle" fill="#94a3b8" font-size="9">Static dashboard</text>
|
||||
|
||||
<!-- Arrow: Container to Block Storage -->
|
||||
<line x1="465" y1="330" x2="465" y2="380" stroke="#475569" stroke-width="2" marker-end="url(#arrowhead)"/>
|
||||
|
||||
<!-- Block Storage -->
|
||||
<rect x="385" y="385" width="160" height="55" rx="10" fill="url(#purpleGrad)" filter="url(#shadow)"/>
|
||||
<text x="465" y="408" text-anchor="middle" fill="#fff" font-size="11" font-weight="600">Block Storage</text>
|
||||
<text x="465" y="425" text-anchor="middle" fill="#ddd6fe" font-size="9">Persistent Data</text>
|
||||
|
||||
<!-- Monitoring Section -->
|
||||
<rect x="590" y="170" width="180" height="160" rx="12" fill="#1e293b" stroke="#eab308" stroke-width="2" filter="url(#shadow)"/>
|
||||
<text x="680" y="198" text-anchor="middle" fill="#eab308" font-size="11" font-weight="700">Monitoring</text>
|
||||
<line x1="605" y1="210" x2="755" y2="210" stroke="#334155" stroke-width="1"/>
|
||||
<text x="680" y="232" text-anchor="middle" fill="#e2e8f0" font-size="10">health-check.sh</text>
|
||||
<text x="680" y="252" text-anchor="middle" fill="#94a3b8" font-size="9">Every 30 seconds</text>
|
||||
<text x="680" y="272" text-anchor="middle" fill="#e2e8f0" font-size="10">Dashboard</text>
|
||||
<text x="680" y="292" text-anchor="middle" fill="#94a3b8" font-size="9">Status / Latency / Requests</text>
|
||||
<text x="680" y="312" text-anchor="middle" fill="#94a3b8" font-size="9">HTML / CSS / JS</text>
|
||||
|
||||
<!-- Arrow: Monitoring to Container -->
|
||||
<line x1="590" y1="250" x2="545" y2="250" stroke="#eab308" stroke-width="2" stroke-dasharray="6,4" marker-end="url(#arrowYellow)"/>
|
||||
<defs><marker id="arrowYellow" markerWidth="10" markerHeight="7" refX="10" refY="3.5" orient="auto"><polygon points="0 0, 10 3.5, 0 7" fill="#eab308"/></marker></defs>
|
||||
|
||||
<!-- Ghaymah Cloud Label -->
|
||||
<rect x="170" y="130" width="590" height="340" rx="16" fill="none" stroke="#334155" stroke-width="1.5" stroke-dasharray="8,6"/>
|
||||
<text x="465" y="155" text-anchor="middle" fill="#64748b" font-size="11" font-weight="600">ghaymah.systems</text>
|
||||
|
||||
<!-- Legend -->
|
||||
<rect x="20" y="440" width="760" height="45" rx="8" fill="#0f172a" stroke="#334155" stroke-width="1"/>
|
||||
<circle cx="50" cy="463" r="6" fill="#3b82f6"/>
|
||||
<text x="65" y="467" fill="#94a3b8" font-size="10">Infrastructure</text>
|
||||
<circle cx="180" cy="463" r="6" fill="#22c55e"/>
|
||||
<text x="195" y="467" fill="#94a3b8" font-size="10">Application</text>
|
||||
<circle cx="310" cy="463" r="6" fill="#a855f7"/>
|
||||
<text x="325" y="467" fill="#94a3b8" font-size="10">Storage</text>
|
||||
<circle cx="420" cy="463" r="6" fill="#eab308"/>
|
||||
<text x="435" y="467" fill="#94a3b8" font-size="10">Monitoring</text>
|
||||
<text x="600" y="467" fill="#64748b" font-size="10">Deploy: gy resource app launch</text>
|
||||
</svg>
|
||||
|
بعد العرض: | الارتفاع: | الحجم: 5.9 KiB |
22
q1-deploy-monitor/ghaymah.json
Normal file
22
q1-deploy-monitor/ghaymah.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"id": "proj_123abc",
|
||||
"name": "web-app-backend",
|
||||
"projectId": "b887e332-3ecd-4187-bdfb-17d8a437833e",
|
||||
|
||||
"ports": [
|
||||
{
|
||||
"expose": true,
|
||||
"number": 3000
|
||||
}
|
||||
],
|
||||
"publicAccess": {
|
||||
"enabled": true,
|
||||
"domain": "auto"
|
||||
},
|
||||
"resourceTier": "t1",
|
||||
"dockerFileName": "Dockerfile",
|
||||
"registry": {
|
||||
"type": "ghaymah-internal",
|
||||
"imageName": "web-app-backend:latest"
|
||||
}
|
||||
}
|
||||
75
q1-deploy-monitor/health-check.sh
Executable file
75
q1-deploy-monitor/health-check.sh
Executable file
@@ -0,0 +1,75 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
HEALTH_URL="${HEALTH_URL:-https://teams-ghayama-dc56d4853672.hosted.ghaymah.systems/health}"
|
||||
INTERVAL="${INTERVAL:-30}"
|
||||
LOG_FILE="${LOG_FILE:-monitor.log}"
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[0;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
timestamp() {
|
||||
date '+%Y-%m-%d %H:%M:%S'
|
||||
}
|
||||
|
||||
check_health() {
|
||||
local ts
|
||||
ts="$(timestamp)"
|
||||
|
||||
local response
|
||||
local http_code
|
||||
local response_time
|
||||
|
||||
response=$(curl -s -o /dev/null -w '%{http_code}|%{time_total}' \
|
||||
--connect-timeout 5 \
|
||||
--max-time 10 \
|
||||
"$HEALTH_URL" 2>&1) || true
|
||||
|
||||
if [[ $? -ne 0 ]] || [[ -z "$response" ]]; then
|
||||
printf "[%s]\n" "$ts"
|
||||
printf "${RED}Status: DOWN${NC}\n"
|
||||
printf "Error: Connection refused or timeout\n\n"
|
||||
printf "[%s] DOWN - Connection refused or timeout\n" "$ts" >> "$LOG_FILE"
|
||||
return
|
||||
fi
|
||||
|
||||
http_code=$(echo "$response" | cut -d'|' -f1)
|
||||
response_time=$(echo "$response" | cut -d'|' -f2)
|
||||
|
||||
local response_ms
|
||||
response_ms=$(awk "BEGIN {printf \"%.0f\", $response_time * 1000}")
|
||||
|
||||
if [[ "$http_code" -eq 200 ]]; then
|
||||
printf "[%s]\n" "$ts"
|
||||
printf "${GREEN}Status: UP${NC}\n"
|
||||
printf "HTTP: %s\n" "$http_code"
|
||||
printf "Response Time: %s ms\n\n" "$response_ms"
|
||||
printf "[%s] UP - HTTP %s - %s ms\n" "$ts" "$http_code" "$response_ms" >> "$LOG_FILE"
|
||||
else
|
||||
printf "[%s]\n" "$ts"
|
||||
printf "${RED}Status: DOWN${NC}\n"
|
||||
printf "HTTP: %s\n" "$http_code"
|
||||
printf "Response Time: %s ms\n\n" "$response_ms"
|
||||
printf "[%s] DOWN - HTTP %s - %s ms\n" "$ts" "$http_code" "$response_ms" >> "$LOG_FILE"
|
||||
fi
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
printf "\n${YELLOW}Monitoring stopped.${NC}\n"
|
||||
exit 0
|
||||
}
|
||||
|
||||
trap cleanup SIGINT SIGTERM
|
||||
|
||||
printf "${GREEN}Ghaymah Health Monitor${NC}\n"
|
||||
printf "URL: %s\n" "$HEALTH_URL"
|
||||
printf "Interval: %ss\n" "$INTERVAL"
|
||||
printf "Log: %s\n\n" "$LOG_FILE"
|
||||
|
||||
while true; do
|
||||
check_health
|
||||
sleep "$INTERVAL"
|
||||
done
|
||||
16
q1-deploy-monitor/package.json
Normal file
16
q1-deploy-monitor/package.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "version-1",
|
||||
"version": "1.0.0",
|
||||
"main": "script.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"start": "node server.js"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
"dependencies": {
|
||||
"express": "^5.1.0"
|
||||
}
|
||||
}
|
||||
ثنائية
q1-deploy-monitor/public/.DS_Store
مباع
Normal file
ثنائية
q1-deploy-monitor/public/.DS_Store
مباع
Normal file
ملف ثنائي غير معروض.
137
q1-deploy-monitor/public/dashboard.css
Normal file
137
q1-deploy-monitor/public/dashboard.css
Normal file
@@ -0,0 +1,137 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: #0f172a;
|
||||
color: #e2e8f0;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.dashboard {
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
padding: 40px 20px;
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 48px;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 1.75rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.025em;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.update-time {
|
||||
font-size: 0.85rem;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #1e293b;
|
||||
border: 1px solid #334155;
|
||||
border-radius: 16px;
|
||||
padding: 28px 24px;
|
||||
text-align: center;
|
||||
transition: border-color 0.3s, box-shadow 0.3s;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
border-color: #475569;
|
||||
}
|
||||
|
||||
.card-label {
|
||||
font-size: 0.8rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
color: #94a3b8;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.card-value {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.025em;
|
||||
}
|
||||
|
||||
.card.status-up {
|
||||
border-color: #22c55e;
|
||||
box-shadow: 0 0 20px rgba(34, 197, 94, 0.1);
|
||||
}
|
||||
|
||||
.card.status-up .card-value {
|
||||
color: #4ade80;
|
||||
}
|
||||
|
||||
.card.status-down {
|
||||
border-color: #ef4444;
|
||||
box-shadow: 0 0 20px rgba(239, 68, 68, 0.1);
|
||||
}
|
||||
|
||||
.card.status-down .card-value {
|
||||
color: #f87171;
|
||||
}
|
||||
|
||||
.card.response .card-value {
|
||||
color: #38bdf8;
|
||||
}
|
||||
|
||||
.card.requests .card-value {
|
||||
color: #c084fc;
|
||||
}
|
||||
|
||||
.pulse-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
margin-top: 36px;
|
||||
}
|
||||
|
||||
.pulse {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
background: #22c55e;
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.pulse.offline {
|
||||
background: #ef4444;
|
||||
}
|
||||
|
||||
.pulse-label {
|
||||
font-size: 0.85rem;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; transform: scale(1); }
|
||||
50% { opacity: 0.4; transform: scale(0.8); }
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.cards {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.card-value {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
41
q1-deploy-monitor/public/dashboard.html
Normal file
41
q1-deploy-monitor/public/dashboard.html
Normal file
@@ -0,0 +1,41 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Ghaymah Monitoring Dashboard</title>
|
||||
<link rel="stylesheet" href="dashboard.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="dashboard">
|
||||
<header class="header">
|
||||
<h1>Ghaymah Monitoring Dashboard</h1>
|
||||
<span class="update-time" id="updateTime">Last update: --</span>
|
||||
</header>
|
||||
|
||||
<div class="cards">
|
||||
<div class="card" id="statusCard">
|
||||
<div class="card-label">Application Status</div>
|
||||
<div class="card-value" id="appStatus">--</div>
|
||||
</div>
|
||||
|
||||
<div class="card" id="responseCard">
|
||||
<div class="card-label">Response Time</div>
|
||||
<div class="card-value" id="responseTime">-- ms</div>
|
||||
</div>
|
||||
|
||||
<div class="card" id="requestsCard">
|
||||
<div class="card-label">Total Requests</div>
|
||||
<div class="card-value" id="requestCount">--</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pulse-container" id="pulseContainer">
|
||||
<div class="pulse"></div>
|
||||
<span class="pulse-label">Live</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="dashboard.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
54
q1-deploy-monitor/public/dashboard.js
Normal file
54
q1-deploy-monitor/public/dashboard.js
Normal file
@@ -0,0 +1,54 @@
|
||||
const STATUS_ENDPOINT = '/health';
|
||||
const POLL_INTERVAL = 2000;
|
||||
|
||||
const statusCard = document.getElementById('statusCard');
|
||||
const responseCard = document.getElementById('responseCard');
|
||||
const requestsCard = document.getElementById('requestsCard');
|
||||
const appStatus = document.getElementById('appStatus');
|
||||
const responseTime = document.getElementById('responseTime');
|
||||
const requestCount = document.getElementById('requestCount');
|
||||
const updateTime = document.getElementById('updateTime');
|
||||
const pulse = document.querySelector('.pulse');
|
||||
|
||||
function setStatusCard(status) {
|
||||
statusCard.classList.remove('status-up', 'status-down');
|
||||
statusCard.classList.add(status === 'UP' ? 'status-up' : 'status-down');
|
||||
appStatus.textContent = status;
|
||||
}
|
||||
|
||||
function formatTimestamp(date) {
|
||||
const y = date.getFullYear();
|
||||
const m = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const d = String(date.getDate()).padStart(2, '0');
|
||||
const h = String(date.getHours()).padStart(2, '0');
|
||||
const min = String(date.getMinutes()).padStart(2, '0');
|
||||
const s = String(date.getSeconds()).padStart(2, '0');
|
||||
return `${y}-${m}-${d} ${h}:${min}:${s}`;
|
||||
}
|
||||
|
||||
async function poll() {
|
||||
try {
|
||||
const start = performance.now();
|
||||
const res = await fetch(STATUS_ENDPOINT);
|
||||
const elapsed = Math.round(performance.now() - start);
|
||||
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
setStatusCard(data.status || 'UP');
|
||||
responseTime.textContent = `${elapsed} ms`;
|
||||
requestCount.textContent = data.requestCount ?? '--';
|
||||
pulse.classList.remove('offline');
|
||||
} catch {
|
||||
setStatusCard('DOWN');
|
||||
responseTime.textContent = '-- ms';
|
||||
requestCount.textContent = '--';
|
||||
pulse.classList.add('offline');
|
||||
}
|
||||
|
||||
updateTime.textContent = `Last update: ${formatTimestamp(new Date())}`;
|
||||
}
|
||||
|
||||
poll();
|
||||
setInterval(poll, POLL_INTERVAL);
|
||||
37
q1-deploy-monitor/public/index.html
Normal file
37
q1-deploy-monitor/public/index.html
Normal file
@@ -0,0 +1,37 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<title>Team Availability</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h2>Team Availability</h2>
|
||||
<div class="buttons">
|
||||
<button id="saveBtn">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
<div>Click save after updating each week separately</div>
|
||||
<table id="scheduleTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Week</th>
|
||||
<th>Mon</th>
|
||||
<th>Tue</th>
|
||||
<th>Wed</th>
|
||||
<th>Thu</th>
|
||||
<th>Fri</th>
|
||||
<th>Sat</th>
|
||||
<th>Sun</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="tableBody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<script src="script.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
143
q1-deploy-monitor/public/script.js
Normal file
143
q1-deploy-monitor/public/script.js
Normal file
@@ -0,0 +1,143 @@
|
||||
let namesData = [];
|
||||
let weeksData = [];
|
||||
let statusesData = [];
|
||||
let historyData = {};
|
||||
|
||||
function createDropdown(options, selectedValue = "") {
|
||||
const select = document.createElement("select");
|
||||
options.forEach(opt => {
|
||||
const option = document.createElement("option");
|
||||
option.value = opt;
|
||||
option.textContent = opt;
|
||||
if (opt === selectedValue) {
|
||||
option.selected = true;
|
||||
}
|
||||
select.appendChild(option);
|
||||
});
|
||||
return select;
|
||||
}
|
||||
|
||||
function applyStatusColor(select) {
|
||||
select.className = 'status-select'; // Reset classes
|
||||
const selectedStatus = select.value;
|
||||
select.classList.add(`status-${selectedStatus}`);
|
||||
}
|
||||
|
||||
function renderTable() {
|
||||
const tableBody = document.getElementById("tableBody");
|
||||
tableBody.innerHTML = "";
|
||||
|
||||
namesData.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
namesData.forEach((emp, index) => {
|
||||
const row = document.createElement("tr");
|
||||
row.dataset.empId = emp.id;
|
||||
row.classList.add(index % 2 === 0 ? "even-row" : "odd-row");
|
||||
|
||||
// Name cell
|
||||
const nameCell = document.createElement("td");
|
||||
nameCell.textContent = emp.name;
|
||||
row.appendChild(nameCell);
|
||||
|
||||
// Week cell
|
||||
const weekCell = document.createElement("td");
|
||||
const defaultWeek = Object.keys(historyData[emp.id] || {})[0] || weeksData[0];
|
||||
const weekSelect = createDropdown(weeksData, defaultWeek);
|
||||
weekSelect.classList.add("week-select");
|
||||
weekCell.appendChild(weekSelect);
|
||||
row.appendChild(weekCell);
|
||||
|
||||
// Render status dropdowns for a selected week
|
||||
const renderDays = (week) => {
|
||||
while (row.children.length > 2) {
|
||||
row.removeChild(row.lastChild);
|
||||
}
|
||||
|
||||
const daysData = historyData[emp.id]?.[week] || {};
|
||||
["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"].forEach(day => {
|
||||
const cell = document.createElement("td");
|
||||
const selectedStatus = daysData[day] || "Empty";
|
||||
const daySelect = createDropdown(statusesData, selectedStatus);
|
||||
daySelect.classList.add("status-select");
|
||||
daySelect.dataset.day = day;
|
||||
applyStatusColor(daySelect);
|
||||
daySelect.addEventListener("change", () => applyStatusColor(daySelect));
|
||||
cell.appendChild(daySelect);
|
||||
row.appendChild(cell);
|
||||
});
|
||||
};
|
||||
|
||||
renderDays(defaultWeek);
|
||||
|
||||
// Update days when week changes
|
||||
weekSelect.addEventListener("change", () => {
|
||||
renderDays(weekSelect.value);
|
||||
});
|
||||
|
||||
tableBody.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
const namesRes = await fetch("/input/names.json");
|
||||
const weeksRes = await fetch("/input/selection.json");
|
||||
const statusRes = await fetch("/input/status.json");
|
||||
const historyRes = await fetch("/output/history.json");
|
||||
|
||||
namesData = await namesRes.json();
|
||||
weeksData = await weeksRes.json();
|
||||
statusesData = await statusRes.json();
|
||||
|
||||
try {
|
||||
historyData = await historyRes.json();
|
||||
} catch {
|
||||
historyData = {};
|
||||
}
|
||||
|
||||
// Cleanup invalid entries
|
||||
for (const empId in historyData) {
|
||||
if (!namesData.some(n => n.id === empId)) {
|
||||
delete historyData[empId];
|
||||
continue;
|
||||
}
|
||||
for (const week in historyData[empId]) {
|
||||
if (!weeksData.includes(week)) {
|
||||
delete historyData[empId][week];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
renderTable();
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", loadData);
|
||||
|
||||
document.getElementById("saveBtn").addEventListener("click", async () => {
|
||||
const rows = document.querySelectorAll("#tableBody tr");
|
||||
|
||||
rows.forEach(row => {
|
||||
const empId = row.dataset.empId;
|
||||
const week = row.querySelector(".week-select").value;
|
||||
const days = {};
|
||||
row.querySelectorAll(".status-select").forEach(sel => {
|
||||
days[sel.dataset.day] = sel.value;
|
||||
});
|
||||
|
||||
if (!historyData[empId]) {
|
||||
historyData[empId] = {};
|
||||
}
|
||||
historyData[empId][week] = days;
|
||||
});
|
||||
|
||||
const response = await fetch("/save-history", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(historyData, null, 2)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
alert("History saved successfully.");
|
||||
} else {
|
||||
alert("Error saving history.");
|
||||
}
|
||||
});
|
||||
93
q1-deploy-monitor/public/styles.css
Normal file
93
q1-deploy-monitor/public/styles.css
Normal file
@@ -0,0 +1,93 @@
|
||||
/* Basic reset for styling */
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
th, td {
|
||||
border: 1px solid #ccc;
|
||||
padding: 4px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Right-aligning names in the first column */
|
||||
td:first-child, th:first-child {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
/* Styling the status dropdown */
|
||||
select {
|
||||
width: 100%;
|
||||
padding: 5px;
|
||||
border-radius: 8px; /* Adding rounded edges to dropdown */
|
||||
border: 1px solid #ccc;
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
}
|
||||
|
||||
/* Status colors for dropdown items */
|
||||
.status-Empty {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
.status-Office {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.status-Remote {
|
||||
background-color: #28a745;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.status-Casual {
|
||||
background-color: #ffc107;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.status-Annual {
|
||||
background-color: #dc3545;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.status-Sick {
|
||||
background-color: #17a2b8;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.status-Off {
|
||||
background-color: #6c757d;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Alternating row background colors */
|
||||
.even-row {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
.odd-row {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
/* Optional: Hover effect on rows */
|
||||
tr:hover {
|
||||
background-color: #e9e9e9;
|
||||
}
|
||||
54
q1-deploy-monitor/server.js
Normal file
54
q1-deploy-monitor/server.js
Normal file
@@ -0,0 +1,54 @@
|
||||
const express = require('express');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const bodyParser = require('body-parser');
|
||||
|
||||
const app = express();
|
||||
const PORT = 3000;
|
||||
|
||||
let requestCount = 0;
|
||||
|
||||
// Middleware
|
||||
app.use(bodyParser.json());
|
||||
app.use((req, res, next) => {
|
||||
requestCount++;
|
||||
next();
|
||||
});
|
||||
|
||||
// Serve static frontend
|
||||
app.use(express.static(path.join(__dirname, 'public')));
|
||||
|
||||
// Serve input JSON files
|
||||
app.use('/input', express.static(path.join(__dirname, 'input')));
|
||||
|
||||
// Serve output folder (for history.json)
|
||||
app.use('/output', express.static(path.join(__dirname, 'output')));
|
||||
|
||||
// API to save history data
|
||||
app.post('/save-history', (req, res) => {
|
||||
const historyPath = path.join(__dirname, 'output', 'history.json');
|
||||
const json = JSON.stringify(req.body, null, 2);
|
||||
|
||||
fs.writeFile(historyPath, json, 'utf8', (err) => {
|
||||
if (err) {
|
||||
console.error('Error saving history.json:', err);
|
||||
res.status(500).send('Failed to save history.json');
|
||||
} else {
|
||||
console.log('History successfully saved.');
|
||||
res.status(200).send('Saved');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Health Check Endpoint
|
||||
app.get('/health', (req, res) => {
|
||||
res.status(200).json({
|
||||
status: 'UP',
|
||||
requestCount: requestCount
|
||||
});
|
||||
});
|
||||
|
||||
// Start server
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Server running at http://localhost:${PORT}`);
|
||||
});
|
||||
المرجع في مشكلة جديدة
حظر مستخدم