first commit
هذا الالتزام موجود في:
ثنائية
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;
|
||||
}
|
||||
المرجع في مشكلة جديدة
حظر مستخدم