feat: Ghayma Assessment - complete submission

هذا الالتزام موجود في:
2026-07-26 22:52:49 +03:00
التزام 01b477b139
12 ملفات معدلة مع 3496 إضافات و0 حذوفات

عرض الملف

@@ -0,0 +1,12 @@
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 3000
CMD ["npm","start"]

عرض الملف

@@ -0,0 +1,148 @@
#!/usr/bin/env python3
"""
Monitoring Script for Ghayma REST API
Scans the application health endpoint every 30 seconds and logs results.
"""
import urllib.request
import urllib.error
import json
import time
import datetime
import os
import sys
# Configuration
APP_URL = os.environ.get("APP_URL", "http://localhost:3000")
HEALTH_ENDPOINT = f"{APP_URL}/health"
CHECK_INTERVAL = 30 # seconds
LOG_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "monitor.log")
TIMEOUT = 5 # seconds
# ANSI colors for terminal output
GREEN = "\033[92m"
RED = "\033[91m"
YELLOW = "\033[93m"
CYAN = "\033[96m"
RESET = "\033[0m"
BOLD = "\033[1m"
def log(message, level="INFO"):
"""Log a message to both console and log file."""
timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
log_entry = f"[{timestamp}] [{level}] {message}"
# Color-coded console output
if level == "OK":
print(f"{GREEN}{log_entry}{RESET}")
elif level == "ERROR":
print(f"{RED}{BOLD}{log_entry}{RESET}")
elif level == "WARN":
print(f"{YELLOW}{log_entry}{RESET}")
else:
print(f"{CYAN}{log_entry}{RESET}")
# Append to log file
with open(LOG_FILE, "a", encoding="utf-8") as f:
f.write(log_entry + "\n")
def check_health():
"""Send a GET request to the health endpoint and return the result."""
try:
req = urllib.request.Request(HEALTH_ENDPOINT, method="GET")
start_time = time.time()
response = urllib.request.urlopen(req, timeout=TIMEOUT)
response_time_ms = round((time.time() - start_time) * 1000)
status_code = response.status
body = json.loads(response.read().decode("utf-8"))
return {
"healthy": status_code == 200 and body.get("status") == "UP",
"status_code": status_code,
"response_time_ms": response_time_ms,
"uptime": body.get("uptime"),
"error": None,
}
except urllib.error.HTTPError as e:
return {
"healthy": False,
"status_code": e.code,
"response_time_ms": None,
"uptime": None,
"error": f"HTTP {e.code}: {e.reason}",
}
except urllib.error.URLError as e:
return {
"healthy": False,
"status_code": None,
"response_time_ms": None,
"uptime": None,
"error": f"Connection failed: {e.reason}",
}
except Exception as e:
return {
"healthy": False,
"status_code": None,
"response_time_ms": None,
"uptime": None,
"error": str(e),
}
def main():
consecutive_failures = 0
print(f"{BOLD}{CYAN}")
print("=" * 55)
print(" Ghayma REST API - Health Monitor")
print(f" Target: {HEALTH_ENDPOINT}")
print(f" Interval: {CHECK_INTERVAL}s")
print(f" Log file: {LOG_FILE}")
print("=" * 55)
print(f"{RESET}")
log(f"Monitor started | Target: {HEALTH_ENDPOINT} | Interval: {CHECK_INTERVAL}s")
try:
while True:
result = check_health()
if result["healthy"]:
consecutive_failures = 0
uptime_str = f"{result['uptime']:.1f}s" if result["uptime"] else "N/A"
log(
f"HEALTHY | Status: {result['status_code']} | "
f"Response: {result['response_time_ms']}ms | "
f"Uptime: {uptime_str}",
level="OK",
)
else:
consecutive_failures += 1
log(
f"UNHEALTHY | Error: {result['error']} | "
f"Consecutive failures: {consecutive_failures}",
level="ERROR",
)
if consecutive_failures >= 3:
log(
f"ALERT: {consecutive_failures} consecutive failures! "
f"Application may be DOWN.",
level="WARN",
)
time.sleep(CHECK_INTERVAL)
except KeyboardInterrupt:
log("Monitor stopped by user.", level="INFO")
print(f"\n{YELLOW}Monitor stopped.{RESET}")
sys.exit(0)
if __name__ == "__main__":
main()

عرض الملف

@@ -0,0 +1,764 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Ghayma API — Monitoring Dashboard</title>
<meta name="description" content="Real-time monitoring dashboard for Ghayma REST API">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
<style>
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
:root {
--bg-primary: #0a0e1a;
--bg-secondary: #111827;
--bg-card: rgba(17, 24, 39, 0.7);
--border-subtle: rgba(255, 255, 255, 0.06);
--text-primary: #f1f5f9;
--text-secondary: #94a3b8;
--text-muted: #64748b;
--accent-green: #22c55e;
--accent-green-glow: rgba(34, 197, 94, 0.15);
--accent-red: #ef4444;
--accent-red-glow: rgba(239, 68, 68, 0.15);
--accent-blue: #3b82f6;
--accent-blue-glow: rgba(59, 130, 246, 0.12);
--accent-amber: #f59e0b;
--accent-purple: #a855f7;
--accent-purple-glow: rgba(168, 85, 247, 0.12);
--radius: 16px;
--radius-sm: 10px;
}
body {
font-family: 'Inter', -apple-system, sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
min-height: 100vh;
overflow-x: hidden;
}
/* Ambient background effects */
body::before {
content: '';
position: fixed;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: radial-gradient(ellipse at 20% 50%, rgba(59, 130, 246, 0.04) 0%, transparent 50%),
radial-gradient(ellipse at 80% 20%, rgba(168, 85, 247, 0.03) 0%, transparent 50%),
radial-gradient(ellipse at 50% 80%, rgba(34, 197, 94, 0.03) 0%, transparent 50%);
z-index: 0;
pointer-events: none;
}
.app { position: relative; z-index: 1; }
/* Header */
.header {
padding: 28px 40px;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid var(--border-subtle);
backdrop-filter: blur(20px);
background: rgba(10, 14, 26, 0.6);
position: sticky;
top: 0;
z-index: 100;
}
.header-left { display: flex; align-items: center; gap: 14px; }
.logo {
width: 40px;
height: 40px;
border-radius: 12px;
background: linear-gradient(135deg, var(--accent-blue), var(--accent-purple));
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
font-weight: 800;
color: #fff;
box-shadow: 0 4px 20px rgba(59, 130, 246, 0.25);
}
.header-title {
font-size: 20px;
font-weight: 700;
letter-spacing: -0.3px;
}
.header-title span { color: var(--text-muted); font-weight: 400; }
.header-right { display: flex; align-items: center; gap: 16px; }
.status-badge {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
border-radius: 100px;
font-size: 13px;
font-weight: 600;
letter-spacing: 0.3px;
transition: all 0.4s ease;
}
.status-badge.online {
background: var(--accent-green-glow);
color: var(--accent-green);
border: 1px solid rgba(34, 197, 94, 0.2);
}
.status-badge.offline {
background: var(--accent-red-glow);
color: var(--accent-red);
border: 1px solid rgba(239, 68, 68, 0.2);
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
position: relative;
}
.status-badge.online .status-dot { background: var(--accent-green); }
.status-badge.offline .status-dot { background: var(--accent-red); }
.status-dot::after {
content: '';
position: absolute;
top: -3px;
left: -3px;
width: 14px;
height: 14px;
border-radius: 50%;
animation: pulse-ring 2s ease-out infinite;
}
.status-badge.online .status-dot::after { background: rgba(34, 197, 94, 0.3); }
.status-badge.offline .status-dot::after { background: rgba(239, 68, 68, 0.3); }
@keyframes pulse-ring {
0% { transform: scale(1); opacity: 1; }
100% { transform: scale(2.2); opacity: 0; }
}
.last-updated {
font-size: 12px;
color: var(--text-muted);
}
/* Main content */
.main { padding: 32px 40px; max-width: 1400px; margin: 0 auto; }
/* Cards grid */
.metrics-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20px;
margin-bottom: 32px;
}
.metric-card {
background: var(--bg-card);
border: 1px solid var(--border-subtle);
border-radius: var(--radius);
padding: 24px;
backdrop-filter: blur(12px);
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.metric-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 2px;
border-radius: var(--radius) var(--radius) 0 0;
opacity: 0;
transition: opacity 0.3s ease;
}
.metric-card:hover { border-color: rgba(255, 255, 255, 0.1); transform: translateY(-2px); }
.metric-card:hover::before { opacity: 1; }
.metric-card.blue::before { background: linear-gradient(90deg, var(--accent-blue), transparent); }
.metric-card.green::before { background: linear-gradient(90deg, var(--accent-green), transparent); }
.metric-card.amber::before { background: linear-gradient(90deg, var(--accent-amber), transparent); }
.metric-card.purple::before { background: linear-gradient(90deg, var(--accent-purple), transparent); }
.metric-icon {
width: 42px;
height: 42px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
margin-bottom: 18px;
}
.metric-card.blue .metric-icon { background: var(--accent-blue-glow); }
.metric-card.green .metric-icon { background: var(--accent-green-glow); }
.metric-card.amber .metric-icon { background: rgba(245, 158, 11, 0.12); }
.metric-card.purple .metric-icon { background: var(--accent-purple-glow); }
.metric-label {
font-size: 13px;
color: var(--text-muted);
font-weight: 500;
margin-bottom: 6px;
text-transform: uppercase;
letter-spacing: 0.6px;
}
.metric-value {
font-size: 32px;
font-weight: 800;
letter-spacing: -1px;
line-height: 1;
}
.metric-card.blue .metric-value { color: var(--accent-blue); }
.metric-card.green .metric-value { color: var(--accent-green); }
.metric-card.amber .metric-value { color: var(--accent-amber); }
.metric-card.purple .metric-value { color: var(--accent-purple); }
.metric-sub {
font-size: 12px;
color: var(--text-muted);
margin-top: 8px;
}
/* Panels grid */
.panels-grid {
display: grid;
grid-template-columns: 2fr 1fr;
gap: 20px;
margin-bottom: 32px;
}
.panel {
background: var(--bg-card);
border: 1px solid var(--border-subtle);
border-radius: var(--radius);
backdrop-filter: blur(12px);
overflow: hidden;
}
.panel-header {
padding: 20px 24px;
border-bottom: 1px solid var(--border-subtle);
display: flex;
align-items: center;
justify-content: space-between;
}
.panel-title {
font-size: 15px;
font-weight: 600;
display: flex;
align-items: center;
gap: 8px;
}
.panel-body { padding: 24px; }
/* Response time chart */
.chart-container {
height: 200px;
display: flex;
align-items: flex-end;
gap: 6px;
padding-top: 10px;
}
.chart-bar-wrapper {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
height: 100%;
justify-content: flex-end;
}
.chart-bar {
width: 100%;
min-height: 4px;
border-radius: 4px 4px 2px 2px;
background: linear-gradient(to top, var(--accent-blue), rgba(59, 130, 246, 0.6));
transition: height 0.5s cubic-bezier(0.22, 1, 0.36, 1);
position: relative;
}
.chart-bar:hover {
background: linear-gradient(to top, var(--accent-purple), rgba(168, 85, 247, 0.7));
}
.chart-bar-label {
font-size: 10px;
color: var(--text-muted);
writing-mode: horizontal-tb;
}
.chart-empty {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
color: var(--text-muted);
font-size: 13px;
}
/* Endpoint list */
.endpoint-list { list-style: none; }
.endpoint-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 0;
border-bottom: 1px solid var(--border-subtle);
transition: all 0.2s ease;
}
.endpoint-item:last-child { border-bottom: none; }
.endpoint-item:hover { padding-left: 6px; }
.endpoint-name {
font-size: 13px;
font-weight: 500;
font-family: 'Inter', monospace;
color: var(--text-primary);
}
.endpoint-count {
font-size: 13px;
font-weight: 700;
color: var(--accent-blue);
background: var(--accent-blue-glow);
padding: 3px 10px;
border-radius: 100px;
}
.endpoint-empty {
padding: 20px 0;
text-align: center;
color: var(--text-muted);
font-size: 13px;
}
/* Status codes panel */
.status-codes-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
}
.status-code-card {
background: rgba(255, 255, 255, 0.03);
border: 1px solid var(--border-subtle);
border-radius: var(--radius-sm);
padding: 16px;
text-align: center;
transition: all 0.3s ease;
}
.status-code-card:hover { background: rgba(255, 255, 255, 0.05); }
.status-code-value {
font-size: 24px;
font-weight: 800;
margin-bottom: 4px;
}
.status-code-label {
font-size: 11px;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.5px;
font-weight: 500;
}
.code-2xx .status-code-value { color: var(--accent-green); }
.code-3xx .status-code-value { color: var(--accent-blue); }
.code-4xx .status-code-value { color: var(--accent-amber); }
.code-5xx .status-code-value { color: var(--accent-red); }
/* Bottom panels */
.bottom-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
/* Uptime display */
.uptime-display {
display: flex;
gap: 16px;
flex-wrap: wrap;
}
.uptime-unit {
flex: 1;
min-width: 80px;
text-align: center;
background: rgba(255, 255, 255, 0.03);
border: 1px solid var(--border-subtle);
border-radius: var(--radius-sm);
padding: 16px 12px;
}
.uptime-value {
font-size: 28px;
font-weight: 800;
color: var(--accent-blue);
line-height: 1;
}
.uptime-label {
font-size: 11px;
color: var(--text-muted);
margin-top: 6px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
/* Request log */
.request-log {
max-height: 180px;
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: rgba(255, 255, 255, 0.1) transparent;
}
.log-entry {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 0;
font-size: 12px;
border-bottom: 1px solid rgba(255, 255, 255, 0.03);
animation: fadeSlideIn 0.3s ease;
}
@keyframes fadeSlideIn {
from { opacity: 0; transform: translateX(-8px); }
to { opacity: 1; transform: translateX(0); }
}
.log-time { color: var(--text-muted); font-family: monospace; white-space: nowrap; }
.log-status {
padding: 2px 8px;
border-radius: 4px;
font-weight: 600;
font-size: 11px;
}
.log-status.ok { background: var(--accent-green-glow); color: var(--accent-green); }
.log-status.err { background: var(--accent-red-glow); color: var(--accent-red); }
.log-message { color: var(--text-secondary); flex: 1; }
/* Responsive */
@media (max-width: 1024px) {
.metrics-grid { grid-template-columns: repeat(2, 1fr); }
.panels-grid, .bottom-grid { grid-template-columns: 1fr; }
.main { padding: 20px; }
.header { padding: 20px; }
}
@media (max-width: 640px) {
.metrics-grid { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<div class="app">
<!-- Header -->
<header class="header">
<div class="header-left">
<div class="logo">G</div>
<h1 class="header-title">Ghayma <span>Monitoring</span></h1>
</div>
<div class="header-right">
<div id="statusBadge" class="status-badge offline">
<div class="status-dot"></div>
<span id="statusText">CONNECTING...</span>
</div>
<div class="last-updated">Updated: <span id="lastUpdated"></span></div>
</div>
</header>
<!-- Main Content -->
<main class="main">
<!-- Metric Cards -->
<section class="metrics-grid">
<div class="metric-card blue">
<div class="metric-icon">📊</div>
<div class="metric-label">Total Requests</div>
<div class="metric-value" id="totalRequests">0</div>
<div class="metric-sub" id="requestRate">0 req/min</div>
</div>
<div class="metric-card green">
<div class="metric-icon"></div>
<div class="metric-label">Successful</div>
<div class="metric-value" id="successRequests">0</div>
<div class="metric-sub" id="successRate">0% success rate</div>
</div>
<div class="metric-card amber">
<div class="metric-icon"></div>
<div class="metric-label">Avg Response Time</div>
<div class="metric-value" id="avgResponseTime">0<span style="font-size:16px;font-weight:400;"> ms</span></div>
<div class="metric-sub">Last 100 requests</div>
</div>
<div class="metric-card purple">
<div class="metric-icon"></div>
<div class="metric-label">Errors</div>
<div class="metric-value" id="errorRequests">0</div>
<div class="metric-sub" id="errorRate">0% error rate</div>
</div>
</section>
<!-- Response Time Chart + Endpoints -->
<section class="panels-grid">
<div class="panel">
<div class="panel-header">
<div class="panel-title">⏱️ Response Time (last 20 requests)</div>
</div>
<div class="panel-body">
<div class="chart-container" id="responseChart">
<div class="chart-empty">Waiting for data...</div>
</div>
</div>
</div>
<div class="panel">
<div class="panel-header">
<div class="panel-title">🔗 Requests by Endpoint</div>
</div>
<div class="panel-body">
<ul class="endpoint-list" id="endpointList">
<li class="endpoint-empty">No requests yet</li>
</ul>
</div>
</div>
</section>
<!-- Bottom: Uptime + Status Codes -->
<section class="bottom-grid">
<div class="panel">
<div class="panel-header">
<div class="panel-title">🕐 Uptime</div>
</div>
<div class="panel-body">
<div class="uptime-display" id="uptimeDisplay">
<div class="uptime-unit"><div class="uptime-value" id="uptDays">0</div><div class="uptime-label">Days</div></div>
<div class="uptime-unit"><div class="uptime-value" id="uptHours">0</div><div class="uptime-label">Hours</div></div>
<div class="uptime-unit"><div class="uptime-value" id="uptMins">0</div><div class="uptime-label">Minutes</div></div>
<div class="uptime-unit"><div class="uptime-value" id="uptSecs">0</div><div class="uptime-label">Seconds</div></div>
</div>
</div>
</div>
<div class="panel">
<div class="panel-header">
<div class="panel-title">📋 Status Codes</div>
</div>
<div class="panel-body">
<div class="status-codes-grid" id="statusCodesGrid">
<div class="status-code-card code-2xx">
<div class="status-code-value" id="code2xx">0</div>
<div class="status-code-label">2xx Success</div>
</div>
<div class="status-code-card code-3xx">
<div class="status-code-value" id="code3xx">0</div>
<div class="status-code-label">3xx Redirect</div>
</div>
<div class="status-code-card code-4xx">
<div class="status-code-value" id="code4xx">0</div>
<div class="status-code-label">4xx Client Err</div>
</div>
<div class="status-code-card code-5xx">
<div class="status-code-value" id="code5xx">0</div>
<div class="status-code-label">5xx Server Err</div>
</div>
</div>
</div>
</div>
</section>
</main>
</div>
<script>
const API_BASE = window.location.origin;
const POLL_INTERVAL = 5000; // 5 seconds
let previousTotal = 0;
let previousTime = Date.now();
async function fetchMetrics() {
try {
const response = await fetch(`${API_BASE}/metrics`);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
updateDashboard(data);
} catch (err) {
setOffline(err.message);
}
}
function updateDashboard(data) {
// Status badge
const badge = document.getElementById('statusBadge');
const statusText = document.getElementById('statusText');
badge.className = 'status-badge online';
statusText.textContent = 'ONLINE';
// Last updated
document.getElementById('lastUpdated').textContent =
new Date().toLocaleTimeString();
// Metric cards
animateValue('totalRequests', data.totalRequests);
animateValue('successRequests', data.successRequests);
document.getElementById('avgResponseTime').innerHTML =
`${data.avgResponseTime}<span style="font-size:16px;font-weight:400;"> ms</span>`;
animateValue('errorRequests', data.errorRequests);
// Rates
const now = Date.now();
const elapsed = (now - previousTime) / 60000; // minutes
if (elapsed > 0 && previousTotal > 0) {
const rate = ((data.totalRequests - previousTotal) / elapsed).toFixed(1);
document.getElementById('requestRate').textContent = `${rate} req/min`;
}
previousTotal = data.totalRequests;
previousTime = now;
const successRate = data.totalRequests > 0
? ((data.successRequests / data.totalRequests) * 100).toFixed(1)
: 0;
document.getElementById('successRate').textContent = `${successRate}% success rate`;
const errorRate = data.totalRequests > 0
? ((data.errorRequests / data.totalRequests) * 100).toFixed(1)
: 0;
document.getElementById('errorRate').textContent = `${errorRate}% error rate`;
// Response time chart
renderChart(data.lastResponseTimes || []);
// Endpoints
renderEndpoints(data.requestsPerEndpoint || {});
// Uptime
renderUptime(data.uptime || 0);
// Status codes
renderStatusCodes(data.statusCodes || {});
}
function setOffline(error) {
const badge = document.getElementById('statusBadge');
const statusText = document.getElementById('statusText');
badge.className = 'status-badge offline';
statusText.textContent = 'OFFLINE';
document.getElementById('lastUpdated').textContent =
new Date().toLocaleTimeString() + ` (${error})`;
}
function animateValue(id, target) {
const el = document.getElementById(id);
const current = parseInt(el.textContent) || 0;
if (current === target) return;
const diff = target - current;
const steps = 20;
const increment = diff / steps;
let step = 0;
const timer = setInterval(() => {
step++;
el.textContent = Math.round(current + increment * step);
if (step >= steps) {
el.textContent = target;
clearInterval(timer);
}
}, 20);
}
function renderChart(times) {
const container = document.getElementById('responseChart');
if (times.length === 0) {
container.innerHTML = '<div class="chart-empty">Waiting for data...</div>';
return;
}
const maxTime = Math.max(...times, 1);
container.innerHTML = times.map((t, i) => {
const height = Math.max((t / maxTime) * 180, 4);
return `
<div class="chart-bar-wrapper" title="${t}ms">
<div class="chart-bar" style="height: ${height}px;"></div>
<span class="chart-bar-label">${t}</span>
</div>
`;
}).join('');
}
function renderEndpoints(endpoints) {
const list = document.getElementById('endpointList');
const entries = Object.entries(endpoints);
if (entries.length === 0) {
list.innerHTML = '<li class="endpoint-empty">No requests yet</li>';
return;
}
entries.sort((a, b) => b[1] - a[1]);
list.innerHTML = entries.map(([name, count]) => `
<li class="endpoint-item">
<span class="endpoint-name">${name}</span>
<span class="endpoint-count">${count}</span>
</li>
`).join('');
}
function renderUptime(seconds) {
const days = Math.floor(seconds / 86400);
const hours = Math.floor((seconds % 86400) / 3600);
const mins = Math.floor((seconds % 3600) / 60);
const secs = Math.floor(seconds % 60);
document.getElementById('uptDays').textContent = days;
document.getElementById('uptHours').textContent = hours;
document.getElementById('uptMins').textContent = mins;
document.getElementById('uptSecs').textContent = secs;
}
function renderStatusCodes(codes) {
let c2 = 0, c3 = 0, c4 = 0, c5 = 0;
for (const [code, count] of Object.entries(codes)) {
const num = parseInt(code);
if (num >= 200 && num < 300) c2 += count;
else if (num >= 300 && num < 400) c3 += count;
else if (num >= 400 && num < 500) c4 += count;
else if (num >= 500) c5 += count;
}
document.getElementById('code2xx').textContent = c2;
document.getElementById('code3xx').textContent = c3;
document.getElementById('code4xx').textContent = c4;
document.getElementById('code5xx').textContent = c5;
}
// Start polling
fetchMetrics();
setInterval(fetchMetrics, POLL_INTERVAL);
</script>
</body>
</html>