Q1: deploy and monitor app
هذا الالتزام موجود في:
51
@
Normal file
51
@
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
"""
|
||||||
|
Simple API app for Ghaymah SRE exam - Q1
|
||||||
|
Provides:
|
||||||
|
GET / -> basic info
|
||||||
|
GET /health -> health check endpoint (used by monitoring)
|
||||||
|
GET /metrics -> simple JSON metrics (request count, uptime)
|
||||||
|
"""
|
||||||
|
import time
|
||||||
|
from flask import Flask, jsonify
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
START_TIME = time.time()
|
||||||
|
REQUEST_COUNT = 0
|
||||||
|
|
||||||
|
|
||||||
|
@app.before_request
|
||||||
|
def count_requests():
|
||||||
|
global REQUEST_COUNT
|
||||||
|
REQUEST_COUNT += 1
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/")
|
||||||
|
def index():
|
||||||
|
return jsonify({
|
||||||
|
"service": "ghaymah-exam-api",
|
||||||
|
"message": "API is running"
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/health")
|
||||||
|
def health():
|
||||||
|
"""Used by ghaymah.systems platform + our monitoring script."""
|
||||||
|
return jsonify({
|
||||||
|
"status": "healthy",
|
||||||
|
"uptime_seconds": round(time.time() - START_TIME, 2)
|
||||||
|
}), 200
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/metrics")
|
||||||
|
def metrics():
|
||||||
|
return jsonify({
|
||||||
|
"uptime_seconds": round(time.time() - START_TIME, 2),
|
||||||
|
"total_requests": REQUEST_COUNT
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# 0.0.0.0 required so the container's port is reachable externally
|
||||||
|
app.run(host="0.0.0.0", port=5000)
|
||||||
|
|
||||||
20
Dockerfile
Normal file
20
Dockerfile
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
FROM python:3.12-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install dependencies first (layer caching)
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
# Copy app code
|
||||||
|
COPY app.py .
|
||||||
|
|
||||||
|
EXPOSE 5000
|
||||||
|
|
||||||
|
# Basic container-level health check (Docker/most platforms respect this)
|
||||||
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
||||||
|
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:5000/health')" || exit 1
|
||||||
|
|
||||||
|
# gunicorn for production-grade serving instead of Flask dev server
|
||||||
|
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--workers", "2", "app:app"]
|
||||||
|
|
||||||
60
app.py
Normal file
60
app.py
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
"""
|
||||||
|
Simple API app for Ghaymah SRE exam - Q1
|
||||||
|
Provides:
|
||||||
|
GET / -> basic info
|
||||||
|
GET /health -> health check endpoint (used by monitoring)
|
||||||
|
GET /metrics -> simple JSON metrics (request count, uptime)
|
||||||
|
"""
|
||||||
|
import time
|
||||||
|
from flask import Flask, jsonify
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@app.after_request
|
||||||
|
def add_cors_headers(response):
|
||||||
|
# Allows the dashboard (served from a different origin, e.g. file://
|
||||||
|
# or another host) to call this API from the browser.
|
||||||
|
response.headers["Access-Control-Allow-Origin"] = "*"
|
||||||
|
response.headers["Access-Control-Allow-Methods"] = "GET, OPTIONS"
|
||||||
|
response.headers["Access-Control-Allow-Headers"] = "Content-Type"
|
||||||
|
return response
|
||||||
|
|
||||||
|
START_TIME = time.time()
|
||||||
|
REQUEST_COUNT = 0
|
||||||
|
|
||||||
|
|
||||||
|
@app.before_request
|
||||||
|
def count_requests():
|
||||||
|
global REQUEST_COUNT
|
||||||
|
REQUEST_COUNT += 1
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/")
|
||||||
|
def index():
|
||||||
|
return jsonify({
|
||||||
|
"service": "ghaymah-exam-api",
|
||||||
|
"message": "API is running"
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/health")
|
||||||
|
def health():
|
||||||
|
"""Used by ghaymah.systems platform + our monitoring script."""
|
||||||
|
return jsonify({
|
||||||
|
"status": "healthy",
|
||||||
|
"uptime_seconds": round(time.time() - START_TIME, 2)
|
||||||
|
}), 200
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/metrics")
|
||||||
|
def metrics():
|
||||||
|
return jsonify({
|
||||||
|
"uptime_seconds": round(time.time() - START_TIME, 2),
|
||||||
|
"total_requests": REQUEST_COUNT
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# 0.0.0.0 required so the container's port is reachable externally
|
||||||
|
app.run(host="0.0.0.0", port=5000)
|
||||||
211
dashboard.html
Normal file
211
dashboard.html
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ar" dir="rtl">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Monitoring Dashboard - Ghaymah Exam Q1</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--up: #22c55e;
|
||||||
|
--down: #ef4444;
|
||||||
|
--bg: #0f172a;
|
||||||
|
--card: #1e293b;
|
||||||
|
--text: #e2e8f0;
|
||||||
|
--muted: #94a3b8;
|
||||||
|
}
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: 'Segoe UI', Tahoma, sans-serif;
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--text);
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
h1 { font-size: 22px; margin-bottom: 4px; }
|
||||||
|
.subtitle { color: var(--muted); margin-bottom: 24px; font-size: 14px; }
|
||||||
|
|
||||||
|
.config {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
.config input {
|
||||||
|
flex: 1;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #334155;
|
||||||
|
background: var(--card);
|
||||||
|
color: var(--text);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.config button {
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: none;
|
||||||
|
background: #3b82f6;
|
||||||
|
color: white;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.config button:hover { background: #2563eb; }
|
||||||
|
|
||||||
|
.grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
background: var(--card);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
border: 1px solid #334155;
|
||||||
|
}
|
||||||
|
.card .label {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 13px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.card .value {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.status-up { color: var(--up); }
|
||||||
|
.status-down { color: var(--down); }
|
||||||
|
|
||||||
|
.dot {
|
||||||
|
display: inline-block;
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
.dot-up { background: var(--up); box-shadow: 0 0 8px var(--up); }
|
||||||
|
.dot-down { background: var(--down); box-shadow: 0 0 8px var(--down); }
|
||||||
|
|
||||||
|
.log {
|
||||||
|
background: var(--card);
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid #334155;
|
||||||
|
padding: 16px;
|
||||||
|
max-height: 260px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
.log table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
||||||
|
.log th, .log td { text-align: right; padding: 6px 8px; border-bottom: 1px solid #334155; }
|
||||||
|
.log th { color: var(--muted); font-weight: 500; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<h1>لوحة مراقبة التطبيق</h1>
|
||||||
|
<div class="subtitle">Ghaymah SRE Exam — Q1 Monitoring Dashboard</div>
|
||||||
|
|
||||||
|
<div class="config">
|
||||||
|
<input id="appUrl" type="text" placeholder="ضع رابط التطبيق المنشور على ghaymah.systems (مثال: https://myapp.ghaymah.systems)">
|
||||||
|
<button onclick="startMonitoring()">ابدأ المراقبة</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid">
|
||||||
|
<div class="card">
|
||||||
|
<div class="label">الحالة (Status)</div>
|
||||||
|
<div class="value" id="statusValue">—</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="label">زمن الاستجابة (Response Time)</div>
|
||||||
|
<div class="value" id="latencyValue">— ms</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="label">عدد الطلبات (Total Requests)</div>
|
||||||
|
<div class="value" id="requestsValue">—</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="label">وقت التشغيل (Uptime)</div>
|
||||||
|
<div class="value" id="uptimeValue">—</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="log">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr><th>الوقت</th><th>الحالة</th><th>زمن الاستجابة</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="logBody"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let intervalId = null;
|
||||||
|
const MAX_LOG_ROWS = 15;
|
||||||
|
|
||||||
|
function startMonitoring() {
|
||||||
|
const url = document.getElementById('appUrl').value.trim();
|
||||||
|
if (!url) { alert('من فضلك ضع رابط التطبيق'); return; }
|
||||||
|
if (intervalId) clearInterval(intervalId);
|
||||||
|
checkNow(url);
|
||||||
|
intervalId = setInterval(() => checkNow(url), 30000);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkNow(baseUrl) {
|
||||||
|
const cleanUrl = baseUrl.replace(/\/$/, '');
|
||||||
|
const start = performance.now();
|
||||||
|
let status = 'DOWN';
|
||||||
|
let statusCode = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(cleanUrl + '/health', { cache: 'no-store' });
|
||||||
|
statusCode = res.status;
|
||||||
|
status = res.ok ? 'UP' : 'DEGRADED';
|
||||||
|
} catch (e) {
|
||||||
|
status = 'DOWN';
|
||||||
|
}
|
||||||
|
const latency = Math.round(performance.now() - start);
|
||||||
|
|
||||||
|
updateStatusCard(status, latency);
|
||||||
|
addLogRow(status, latency);
|
||||||
|
|
||||||
|
// metrics endpoint (requests count + uptime) - best effort
|
||||||
|
try {
|
||||||
|
const mRes = await fetch(cleanUrl + '/metrics', { cache: 'no-store' });
|
||||||
|
if (mRes.ok) {
|
||||||
|
const data = await mRes.json();
|
||||||
|
document.getElementById('requestsValue').textContent = data.total_requests ?? '—';
|
||||||
|
document.getElementById('uptimeValue').textContent = formatUptime(data.uptime_seconds);
|
||||||
|
}
|
||||||
|
} catch (e) { /* metrics optional */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateStatusCard(status, latency) {
|
||||||
|
const statusEl = document.getElementById('statusValue');
|
||||||
|
statusEl.textContent = status;
|
||||||
|
statusEl.className = 'value ' + (status === 'UP' ? 'status-up' : 'status-down');
|
||||||
|
|
||||||
|
document.getElementById('latencyValue').textContent = latency + ' ms';
|
||||||
|
}
|
||||||
|
|
||||||
|
function addLogRow(status, latency) {
|
||||||
|
const tbody = document.getElementById('logBody');
|
||||||
|
const row = document.createElement('tr');
|
||||||
|
const dotClass = status === 'UP' ? 'dot-up' : 'dot-down';
|
||||||
|
row.innerHTML = `
|
||||||
|
<td>${new Date().toLocaleTimeString('ar-EG')}</td>
|
||||||
|
<td><span class="dot ${dotClass}"></span>${status}</td>
|
||||||
|
<td>${latency} ms</td>
|
||||||
|
`;
|
||||||
|
tbody.prepend(row);
|
||||||
|
while (tbody.rows.length > MAX_LOG_ROWS) {
|
||||||
|
tbody.deleteRow(tbody.rows.length - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatUptime(seconds) {
|
||||||
|
if (seconds == null) return '—';
|
||||||
|
const h = Math.floor(seconds / 3600);
|
||||||
|
const m = Math.floor((seconds % 3600) / 60);
|
||||||
|
return `${h}h ${m}m`;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
79
health-check.py
Normal file
79
health-check.py
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Monitoring script for Q1 - Ghaymah SRE exam.
|
||||||
|
Checks the deployed app's /health endpoint every 30 seconds,
|
||||||
|
logs status + response time to a CSV file, and prints live status
|
||||||
|
to the console.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python3 health-check.py https://your-app-url.ghaymah.systems
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import csv
|
||||||
|
import os
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
import urllib.request
|
||||||
|
import urllib.error
|
||||||
|
|
||||||
|
CHECK_INTERVAL_SECONDS = 30
|
||||||
|
LOG_FILE = "monitor-log.csv"
|
||||||
|
|
||||||
|
|
||||||
|
def check_health(url: str) -> dict:
|
||||||
|
endpoint = url.rstrip("/") + "/health"
|
||||||
|
start = time.time()
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(endpoint, timeout=10) as response:
|
||||||
|
elapsed_ms = round((time.time() - start) * 1000, 2)
|
||||||
|
status_code = response.getcode()
|
||||||
|
return {
|
||||||
|
"timestamp": datetime.now(timezone.utc).isoformat(),
|
||||||
|
"status": "UP" if status_code == 200 else "DEGRADED",
|
||||||
|
"status_code": status_code,
|
||||||
|
"response_time_ms": elapsed_ms,
|
||||||
|
}
|
||||||
|
except (urllib.error.URLError, urllib.error.HTTPError, TimeoutError) as e:
|
||||||
|
elapsed_ms = round((time.time() - start) * 1000, 2)
|
||||||
|
return {
|
||||||
|
"timestamp": datetime.now(timezone.utc).isoformat(),
|
||||||
|
"status": "DOWN",
|
||||||
|
"status_code": None,
|
||||||
|
"response_time_ms": elapsed_ms,
|
||||||
|
"error": str(e),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def log_result(result: dict):
|
||||||
|
file_exists = os.path.isfile(LOG_FILE)
|
||||||
|
with open(LOG_FILE, "a", newline="") as f:
|
||||||
|
fieldnames = ["timestamp", "status", "status_code", "response_time_ms", "error"]
|
||||||
|
writer = csv.DictWriter(f, fieldnames=fieldnames)
|
||||||
|
if not file_exists:
|
||||||
|
writer.writeheader()
|
||||||
|
writer.writerow({**{"error": ""}, **result})
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
print("Usage: python3 health-check.py <app_url>")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
url = sys.argv[1]
|
||||||
|
print(f"Monitoring {url}/health every {CHECK_INTERVAL_SECONDS}s. Logging to {LOG_FILE}. Ctrl+C to stop.")
|
||||||
|
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
result = check_health(url)
|
||||||
|
log_result(result)
|
||||||
|
print(f"[{result['timestamp']}] {result['status']} "
|
||||||
|
f"({result['response_time_ms']}ms)"
|
||||||
|
+ (f" - {result.get('error')}" if result.get("error") else ""))
|
||||||
|
time.sleep(CHECK_INTERVAL_SECONDS)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\nMonitoring stopped.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
||||||
36
install-docker.sh
Executable file
36
install-docker.sh
Executable file
@@ -0,0 +1,36 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo ">>> Updating packages..."
|
||||||
|
sudo apt update
|
||||||
|
|
||||||
|
echo ">>> Installing prerequisites..."
|
||||||
|
sudo apt install -y ca-certificates curl gnupg
|
||||||
|
|
||||||
|
echo ">>> Setting up Docker keyring..."
|
||||||
|
sudo install -m 0755 -d /etc/apt/keyrings
|
||||||
|
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
|
||||||
|
sudo chmod a+r /etc/apt/keyrings/docker.gpg
|
||||||
|
|
||||||
|
echo ">>> Adding Docker repository..."
|
||||||
|
echo \
|
||||||
|
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo $VERSION_CODENAME) stable" | \
|
||||||
|
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
|
||||||
|
|
||||||
|
echo ">>> Installing Docker..."
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
|
||||||
|
|
||||||
|
echo ">>> Starting Docker service..."
|
||||||
|
sudo systemctl start docker
|
||||||
|
sudo systemctl enable docker
|
||||||
|
|
||||||
|
echo ">>> Adding current user to docker group..."
|
||||||
|
sudo usermod -aG docker $USER
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=========================================="
|
||||||
|
echo "Docker installed successfully."
|
||||||
|
echo "IMPORTANT: run 'newgrp docker' OR log out/in"
|
||||||
|
echo "then run: docker --version"
|
||||||
|
echo "=========================================="
|
||||||
35
monitor-log.csv
Normal file
35
monitor-log.csv
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
timestamp,status,status_code,response_time_ms,error
|
||||||
|
2026-07-27T10:17:38.379006+00:00,UP,200,14.79,
|
||||||
|
2026-07-27T10:18:08.381609+00:00,UP,200,2.08,
|
||||||
|
2026-07-27T10:18:38.383866+00:00,UP,200,1.82,
|
||||||
|
2026-07-27T10:19:08.386140+00:00,UP,200,1.77,
|
||||||
|
2026-07-27T10:19:38.388582+00:00,UP,200,2.01,
|
||||||
|
2026-07-27T10:20:08.390836+00:00,UP,200,1.85,
|
||||||
|
2026-07-27T10:20:38.394181+00:00,UP,200,2.97,
|
||||||
|
2026-07-27T10:21:08.396674+00:00,UP,200,1.98,
|
||||||
|
2026-07-27T10:21:38.398784+00:00,UP,200,1.7,
|
||||||
|
2026-07-27T10:22:08.401664+00:00,UP,200,1.84,
|
||||||
|
2026-07-27T10:22:38.403790+00:00,UP,200,1.71,
|
||||||
|
2026-07-27T10:23:08.406096+00:00,UP,200,1.78,
|
||||||
|
2026-07-27T10:23:38.407503+00:00,DOWN,,0.43,<urlopen error [Errno 111] Connection refused>
|
||||||
|
2026-07-27T10:24:08.408384+00:00,DOWN,,0.54,<urlopen error [Errno 111] Connection refused>
|
||||||
|
2026-07-27T10:24:38.409097+00:00,DOWN,,0.38,<urlopen error [Errno 111] Connection refused>
|
||||||
|
2026-07-27T10:25:08.409873+00:00,DOWN,,0.4,<urlopen error [Errno 111] Connection refused>
|
||||||
|
2026-07-27T10:25:38.412335+00:00,UP,200,2.12,
|
||||||
|
2026-07-27T10:26:08.414479+00:00,UP,200,1.78,
|
||||||
|
2026-07-27T10:26:38.416810+00:00,UP,200,1.92,
|
||||||
|
2026-07-27T10:27:08.418967+00:00,UP,200,1.74,
|
||||||
|
2026-07-27T10:27:38.421130+00:00,UP,200,1.68,
|
||||||
|
2026-07-27T10:28:08.423401+00:00,UP,200,1.76,
|
||||||
|
2026-07-27T10:28:38.425814+00:00,UP,200,1.99,
|
||||||
|
2026-07-27T10:29:08.428129+00:00,UP,200,1.76,
|
||||||
|
2026-07-27T10:29:38.430424+00:00,UP,200,1.85,
|
||||||
|
2026-07-27T10:30:08.432756+00:00,UP,200,1.87,
|
||||||
|
2026-07-27T10:30:38.435351+00:00,UP,200,1.77,
|
||||||
|
2026-07-27T10:31:08.437531+00:00,UP,200,1.78,
|
||||||
|
2026-07-27T10:31:38.439587+00:00,UP,200,1.69,
|
||||||
|
2026-07-27T10:32:08.441908+00:00,UP,200,1.94,
|
||||||
|
2026-07-27T10:32:38.443987+00:00,UP,200,1.68,
|
||||||
|
2026-07-27T10:33:08.446193+00:00,UP,200,1.75,
|
||||||
|
2026-07-27T10:33:38.448817+00:00,UP,200,1.85,
|
||||||
|
2026-07-27T10:34:08.451690+00:00,UP,200,1.73,
|
||||||
|
3
requirements.txt
Normal file
3
requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
flask==3.0.3
|
||||||
|
gunicorn==22.0.0
|
||||||
|
|
||||||
المرجع في مشكلة جديدة
حظر مستخدم