Refactor code structure for improved readability and maintainability
13
Q1/Dashboard/Dockerfile
Normal file
@@ -0,0 +1,13 @@
|
||||
|
||||
# Minimal static-file server for just the dashboard, so it can be deployed
|
||||
# as its own service on Ghaymah, separate from the API container.
|
||||
FROM nginx:alpine
|
||||
|
||||
# nginx's default port is 80; most container platforms (Ghaymah included,
|
||||
# per its port-configuration step) let you pick which port the service
|
||||
# listens on, so we keep nginx's default here and just set that port
|
||||
# when creating the service.
|
||||
COPY index.html /usr/share/nginx/html/index.html
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
213
Q1/Dashboard/index.html
Normal file
@@ -0,0 +1,213 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ar" dir="rtl">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>لوحة مراقبة الـ API</title>
|
||||
<style>
|
||||
:root{
|
||||
--bg:#f4f7f6;
|
||||
--card:#ffffff;
|
||||
--border:#e3e8ec;
|
||||
--text:#25313c;
|
||||
--muted:#7a8894;
|
||||
--accent:#0056b3;
|
||||
--up:#28a745;
|
||||
--down:#dc3545;
|
||||
--warn:#f0ad4e;
|
||||
}
|
||||
*{ box-sizing:border-box; }
|
||||
body{
|
||||
font-family:'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background:var(--bg);
|
||||
display:flex; justify-content:center; align-items:center;
|
||||
min-height:100vh; margin:0; padding:24px;
|
||||
}
|
||||
.dashboard{
|
||||
background:var(--card);
|
||||
padding:30px;
|
||||
border-radius:14px;
|
||||
box-shadow:0 8px 24px rgba(15,35,55,0.08);
|
||||
text-align:center;
|
||||
width:420px;
|
||||
max-width:100%;
|
||||
border:1px solid var(--border);
|
||||
}
|
||||
h2{
|
||||
color:var(--text);
|
||||
margin:0 0 22px;
|
||||
display:flex;
|
||||
align-items:center;
|
||||
justify-content:center;
|
||||
gap:10px;
|
||||
font-size:1.25rem;
|
||||
}
|
||||
.status-dot{
|
||||
width:12px; height:12px; border-radius:50%;
|
||||
background:#9aa5ad;
|
||||
box-shadow:0 0 0 0 rgba(154,165,173,0.5);
|
||||
flex-shrink:0;
|
||||
}
|
||||
.status-dot.online{ background:var(--up); animation:pulse-up 2s infinite; }
|
||||
.status-dot.offline{ background:var(--down); animation:pulse-down 2s infinite; }
|
||||
@keyframes pulse-up{
|
||||
0%{ box-shadow:0 0 0 0 rgba(40,167,69,0.55); }
|
||||
70%{ box-shadow:0 0 0 9px rgba(40,167,69,0); }
|
||||
100%{ box-shadow:0 0 0 0 rgba(40,167,69,0); }
|
||||
}
|
||||
@keyframes pulse-down{
|
||||
0%{ box-shadow:0 0 0 0 rgba(220,53,69,0.55); }
|
||||
70%{ box-shadow:0 0 0 9px rgba(220,53,69,0); }
|
||||
100%{ box-shadow:0 0 0 0 rgba(220,53,69,0); }
|
||||
}
|
||||
.metric{
|
||||
background:#eef2f5;
|
||||
margin:10px 0;
|
||||
padding:14px 16px;
|
||||
border-radius:9px;
|
||||
font-size:1.05rem;
|
||||
display:flex;
|
||||
justify-content:space-between;
|
||||
align-items:center;
|
||||
}
|
||||
.metric span{ font-weight:700; color:var(--accent); font-variant-numeric:tabular-nums; }
|
||||
.metric span.status-online{ color:var(--up); }
|
||||
.metric span.status-offline{ color:var(--down); }
|
||||
.meta-row{
|
||||
margin-top:16px;
|
||||
font-size:0.75rem;
|
||||
color:var(--muted);
|
||||
display:flex;
|
||||
justify-content:space-between;
|
||||
}
|
||||
.api-row{
|
||||
margin-top:18px;
|
||||
padding-top:14px;
|
||||
border-top:1px dashed var(--border);
|
||||
text-align:right;
|
||||
}
|
||||
.api-row label{
|
||||
display:block;
|
||||
font-size:0.72rem;
|
||||
color:var(--muted);
|
||||
margin-bottom:6px;
|
||||
}
|
||||
.api-row .input-group{
|
||||
display:flex;
|
||||
gap:6px;
|
||||
}
|
||||
.api-row input{
|
||||
flex:1;
|
||||
padding:8px 10px;
|
||||
border:1px solid var(--border);
|
||||
border-radius:7px;
|
||||
font-size:0.8rem;
|
||||
direction:ltr;
|
||||
text-align:left;
|
||||
}
|
||||
.api-row button{
|
||||
padding:8px 14px;
|
||||
border:none;
|
||||
border-radius:7px;
|
||||
background:var(--accent);
|
||||
color:#fff;
|
||||
font-size:0.8rem;
|
||||
cursor:pointer;
|
||||
}
|
||||
.api-row button:hover{ opacity:0.9; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="dashboard">
|
||||
<h2>مراقبة النظام <span id="statusDot" class="status-dot"></span></h2>
|
||||
|
||||
<div class="metric">
|
||||
الحالة: <span id="statusText">جاري التحميل...</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
زمن الاستجابة: <span id="responseTime">— ms</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
عدد الطلبات الإجمالي: <span id="requestCount">—</span>
|
||||
</div>
|
||||
|
||||
<div class="meta-row">
|
||||
<span id="uptimeText">التشغيل: —</span>
|
||||
<span id="lastUpdated">آخر تحديث: —</span>
|
||||
</div>
|
||||
|
||||
<div class="api-row">
|
||||
<label for="apiInput">عنوان الـ API (health endpoint):</label>
|
||||
<div class="input-group">
|
||||
<input id="apiInput" type="text" placeholder="https://backend-06febdb2d9f4.hosted.ghaymah.systems/health">
|
||||
<button id="apiSave">حفظ</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// This copy of the dashboard is meant to be hosted on its own — separately
|
||||
// from the API — so it needs to be told where the API lives. Resolution
|
||||
// order:
|
||||
// 1. ?api=<url> query-string param, e.g. index.html?api=https://foo/health
|
||||
// 2. the value typed into the input box below (kept only in memory /
|
||||
// in the URL — nothing is written to disk or browser storage)
|
||||
// 3. falls back to the hosted backend URL provided for this project.
|
||||
const POLL_MS = 5000;
|
||||
const DEFAULT_HEALTH_URL = 'https://backend-06febdb2d9f4.hosted.ghaymah.systems/health';
|
||||
let healthUrl = new URLSearchParams(window.location.search).get('api') || DEFAULT_HEALTH_URL;
|
||||
|
||||
const apiInput = document.getElementById('apiInput');
|
||||
apiInput.value = healthUrl === '/health' ? '' : healthUrl;
|
||||
|
||||
document.getElementById('apiSave').addEventListener('click', () => {
|
||||
const val = apiInput.value.trim();
|
||||
if (val) {
|
||||
healthUrl = val;
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set('api', val);
|
||||
window.history.replaceState({}, '', url); // shareable link, no storage used
|
||||
fetchMetrics();
|
||||
}
|
||||
});
|
||||
|
||||
function fmtUptime(seconds){
|
||||
if (seconds === undefined || seconds === null) return '—';
|
||||
const h = Math.floor(seconds / 3600);
|
||||
const m = Math.floor((seconds % 3600) / 60);
|
||||
const s = Math.floor(seconds % 60);
|
||||
return `${h}h ${m}m ${s}s`;
|
||||
}
|
||||
|
||||
async function fetchMetrics(){
|
||||
try {
|
||||
const res = await fetch(healthUrl, { cache: 'no-store' });
|
||||
if (!res.ok) throw new Error('HTTP ' + res.status);
|
||||
const data = await res.json();
|
||||
|
||||
const statusEl = document.getElementById('statusText');
|
||||
statusEl.innerText = data.status || 'Online';
|
||||
statusEl.className = 'status-online';
|
||||
document.getElementById('statusDot').className = 'status-dot online';
|
||||
|
||||
document.getElementById('responseTime').innerText = (data.response_time_ms ?? 0) + ' ms';
|
||||
document.getElementById('requestCount').innerText = data.request_count ?? '—';
|
||||
document.getElementById('uptimeText').innerText = 'التشغيل: ' + fmtUptime(data.uptime_seconds);
|
||||
document.getElementById('lastUpdated').innerText =
|
||||
'آخر تحديث: ' + new Date().toLocaleTimeString('ar-EG');
|
||||
|
||||
} catch (error) {
|
||||
const statusEl = document.getElementById('statusText');
|
||||
statusEl.innerText = 'انقطاع الاتصال (Offline)';
|
||||
statusEl.className = 'status-offline';
|
||||
document.getElementById('statusDot').className = 'status-dot offline';
|
||||
document.getElementById('responseTime').innerText = '—';
|
||||
}
|
||||
}
|
||||
|
||||
fetchMetrics();
|
||||
setInterval(fetchMetrics, POLL_MS);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
84
Q1/README.md
Normal file
@@ -0,0 +1,84 @@
|
||||
# API + Health Monitor + Dashboard (for ghaymah.systems)
|
||||
|
||||
This is your uploaded project (`server.js`, `monitor.sh`, `index.html`,
|
||||
`Dockerfile`) with three functional bugs fixed and a couple of production
|
||||
hardening tweaks. Nothing about your architecture changed.
|
||||
|
||||
## What was wrong and what I changed
|
||||
|
||||
1. **`index.html` was fetching the wrong URL.**
|
||||
`const API_URL = "";` then `fetch(API_URL)` calls the *current page*
|
||||
(`/`), not `/health` — so it was trying to `JSON.parse()` an HTML page
|
||||
and always fell into the `catch` block ("Offline"), even when the API
|
||||
was healthy. Fixed to `fetch('/health')`.
|
||||
|
||||
2. **`server.js` had no route serving `index.html`.**
|
||||
The dashboard needs to be reachable somewhere. I added
|
||||
`express.static('public')` and moved `index.html` into `public/`, so the
|
||||
dashboard is served at `/` and the plain-text welcome message moved to
|
||||
`/api` so the two don't collide.
|
||||
|
||||
3. **`monitor.sh` referenced `$response_time`, which was never set** — the
|
||||
response-time column was always empty. Fixed by measuring it directly
|
||||
with `curl -w "%{time_total}"`. Also added a fallback JSON parser for
|
||||
when `jq` isn't installed, and an optional CSV log file.
|
||||
|
||||
4. **Dockerfile ran `npm init -y && npm install` on every build**, with no
|
||||
lockfile/manifest — slow, non-reproducible, and re-downloads
|
||||
dependencies on every rebuild even if nothing changed. Added a real
|
||||
`package.json` and split `COPY package.json` from `COPY server.js` so
|
||||
Docker caches the `npm install` layer. Also switched
|
||||
`node:20-alpine3.16` (an older, unsupported Alpine point release) to
|
||||
`node:20-alpine` (current supported tag).
|
||||
|
||||
## Files
|
||||
|
||||
```
|
||||
.
|
||||
├── Dockerfile
|
||||
├── package.json
|
||||
├── server.js
|
||||
├── public/
|
||||
│ └── index.html ← dashboard, served at /
|
||||
└── monitor.sh ← external checker, run it against the deployed URL
|
||||
```
|
||||
|
||||
## Run locally
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm start
|
||||
# open http://localhost:3000 -> dashboard
|
||||
# open http://localhost:3000/health -> raw JSON
|
||||
```
|
||||
|
||||
In another terminal, point the monitor at it:
|
||||
|
||||
```bash
|
||||
chmod +x monitor.sh
|
||||
./monitor.sh http://localhost:3000 30
|
||||
```
|
||||
|
||||
## Build & run with Docker
|
||||
|
||||
```bash
|
||||
docker build -t api-health-demo .
|
||||
docker run -d -p 3000:3000 --name api-health-demo api-health-demo
|
||||
```
|
||||
|
||||
## `/health` response shape
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "Online",
|
||||
"uptime_seconds": 42,
|
||||
"request_count": 17,
|
||||
"response_time_ms": 3,
|
||||
"timestamp": "2026-07-27T20:10:00.000Z"
|
||||
}
|
||||
```
|
||||
|
||||
## Deploy to ghaymah.systems
|
||||
|
||||
See `DEPLOY_GHAYMAH.md` for the step-by-step (build → push → create
|
||||
service → set port 3000 → point `monitor.sh` at the live URL).
|
||||
@@ -1,60 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ar" dir="rtl">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>لوحة مراقبة الـ API</title>
|
||||
<style>
|
||||
body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: #f4f7f6; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; }
|
||||
.dashboard { background: white; padding: 30px; border-radius: 10px; box-shadow: 0 4px 15px rgba(0,0,0,0.1); text-align: center; width: 400px; }
|
||||
h2 { color: #333; margin-bottom: 20px; }
|
||||
.metric { background: #eef2f5; margin: 10px 0; padding: 15px; border-radius: 8px; font-size: 1.2em; display: flex; justify-content: space-between; }
|
||||
.metric span { font-weight: bold; color: #0056b3; }
|
||||
.status-dot { display: inline-block; width: 12px; height: 12px; border-radius: 50%; background: gray; margin-left: 8px; }
|
||||
.online { background: #28a745; }
|
||||
.offline { background: #dc3545; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="dashboard">
|
||||
<h2>مراقبة النظام <span id="statusDot" class="status-dot"></span></h2>
|
||||
|
||||
<div class="metric">
|
||||
الحالة: <span id="statusText">جاري التحميل...</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
زمن الاستجابة: <span id="responseTime">0 ms</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
عدد الطلبات الإجمالي: <span id="requestCount">0</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const API_URL = "";
|
||||
|
||||
async function fetchMetrics() {
|
||||
try {
|
||||
const res = await fetch(API_URL);
|
||||
const data = await res.json();
|
||||
|
||||
document.getElementById('statusText').innerText = data.status;
|
||||
document.getElementById('statusText').style.color = '#28a745';
|
||||
document.getElementById('statusDot').className = 'status-dot online';
|
||||
|
||||
document.getElementById('responseTime').innerText = data.response_time_ms + ' ms';
|
||||
document.getElementById('requestCount').innerText = data.request_count;
|
||||
} catch (error) {
|
||||
document.getElementById('statusText').innerText = 'انقطاع الاتصال (Offline)';
|
||||
document.getElementById('statusText').style.color = '#dc3545';
|
||||
document.getElementById('statusDot').className = 'status-dot offline';
|
||||
document.getElementById('responseTime').innerText = '-';
|
||||
}
|
||||
}
|
||||
|
||||
setInterval(fetchMetrics, 5000);
|
||||
fetchMetrics();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
14
Q1/package.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "api-health-monitor-demo",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"description": "Simple Express API with /health endpoint, monitored and dashboarded, deployed on ghaymah.systems",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"start": "node server.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.19.2"
|
||||
}
|
||||
}
|
||||
19
Q1/server.js
@@ -1,9 +1,12 @@
|
||||
const express = require('express');
|
||||
const cors = require('cors');
|
||||
const path = require('path');
|
||||
|
||||
const app = express();
|
||||
|
||||
app.use(cors());
|
||||
|
||||
// --- Simple in-memory metrics (reset when the process restarts) ---------
|
||||
let requestCount = 0;
|
||||
let lastResponseTime = 0;
|
||||
|
||||
@@ -16,20 +19,26 @@ app.use((req, res, next) => {
|
||||
next();
|
||||
});
|
||||
|
||||
// Serve the dashboard (public/index.html) at the root, plus any static assets
|
||||
app.use(express.static(path.join(__dirname, 'public')));
|
||||
|
||||
// --- Health check used by the monitor script and by ghaymah's platform --
|
||||
app.get('/health', (req, res) => {
|
||||
res.json({
|
||||
status: 'Online',
|
||||
uptime_seconds: process.uptime(),
|
||||
uptime_seconds: Math.round(process.uptime()),
|
||||
request_count: requestCount,
|
||||
response_time_ms: lastResponseTime
|
||||
response_time_ms: lastResponseTime,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/', (req, res) => {
|
||||
res.send('Welcome to the Backend API!');
|
||||
// Plain-text info route, separate from the dashboard which now owns '/'
|
||||
app.get('/api', (req, res) => {
|
||||
res.send('Welcome to the Backend API! Health check available at /health');
|
||||
});
|
||||
|
||||
const PORT = process.env.PORT || 3000;
|
||||
app.listen(PORT, () => {
|
||||
app.listen(PORT, '0.0.0.0', () => {
|
||||
console.log(`API is running on port ${PORT}`);
|
||||
});
|
||||
113
README.md
@@ -1,2 +1,113 @@
|
||||
# Ghaymah-Exam-GamalMohamed-SRE
|
||||
# Ghaymah SRE Project
|
||||
|
||||
This repository contains a full set of SRE and DevOps deliverables for the Ghaymah platform. It includes a health-monitoring web app, a containerized dashboard, an incident postmortem, CI/CD guidance, and a high-traffic architecture design.
|
||||
|
||||
## Project Overview
|
||||
|
||||
The project is organized into five main parts:
|
||||
|
||||
- Q1: API health monitoring app and dashboard
|
||||
- Q2: Incident postmortem and resilience recommendations
|
||||
- Q3-CICD: CI/CD workflow and deployment strategy
|
||||
- Q4: High-traffic architecture design for 15,000 RPS
|
||||
- Q5: Containerized monitoring service for uptime and SSL checks
|
||||
|
||||
## Repository Structure
|
||||
|
||||
```text
|
||||
Ghaymah_Task/
|
||||
├── Q1/
|
||||
│ ├── Dashboard/
|
||||
│ ├── Dockerfile
|
||||
│ ├── monitor.sh
|
||||
│ ├── package.json
|
||||
│ ├── README.md
|
||||
│ └── server.js
|
||||
├── Q2/
|
||||
│ └── readme,md
|
||||
├── Q3-CICD/
|
||||
│ ├── README.md
|
||||
│ └── workflow.yml
|
||||
├── Q4/
|
||||
│ ├── GhaymahAPI.png
|
||||
│ └── README.md
|
||||
├── Q5/
|
||||
│ ├── Dockerfile
|
||||
│ ├── Entrypoint.sh
|
||||
│ ├── README.md
|
||||
│ ├── index.html
|
||||
│ └── monitor.py
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## What Each Part Includes
|
||||
|
||||
### Q1 — API Health Monitor and Dashboard
|
||||
This section provides a lightweight web service that exposes a health endpoint and serves a dashboard for monitoring service status, response time, and request count.
|
||||
|
||||
Key files:
|
||||
- [Q1/server.js](Q1/server.js)
|
||||
- [Q1/monitor.sh](Q1/monitor.sh)
|
||||
- [Q1/Dashboard/index.html](Q1/Dashboard/index.html)
|
||||
|
||||
### Q2 — Incident Postmortem
|
||||
This section documents a major outage caused by memory exhaustion and proposes mitigation strategies such as autoscaling, pagination, and observability improvements.
|
||||
|
||||
Key file:
|
||||
- [Q2/readme,md](Q2/readme,md)
|
||||
|
||||
### Q3-CICD — Continuous Integration and Deployment
|
||||
This section outlines the CI/CD workflow for automating build, test, and deployment processes for the platform.
|
||||
|
||||
Key files:
|
||||
- [Q3-CICD/workflow.yml](Q3-CICD/workflow.yml)
|
||||
- [Q3-CICD/README.md](Q3-CICD/README.md)
|
||||
|
||||
### Q4 — High-Traffic Architecture Design
|
||||
This section explains how to design a scalable architecture for handling 15,000 requests per second, covering load balancing, caching, database replication, and storage planning.
|
||||
|
||||
Key files:
|
||||
- [Q4/README.md](Q4/README.md)
|
||||
- [Q4/GhaymahAPI.png](Q4/GhaymahAPI.png)
|
||||
|
||||
### Q5 — Containerized Monitoring Service
|
||||
This section contains a Dockerized monitoring solution that checks uptime, latency, SSL expiry, and dashboard availability using a simple Python script.
|
||||
|
||||
Key files:
|
||||
- [Q5/Dockerfile](Q5/Dockerfile)
|
||||
- [Q5/monitor.py](Q5/monitor.py)
|
||||
- [Q5/Entrypoint.sh](Q5/Entrypoint.sh)
|
||||
|
||||
## How to Use This Repository
|
||||
|
||||
1. Review the documentation in each folder.
|
||||
2. Run the applications locally as described in their own README files.
|
||||
3. Use the CI/CD workflow from Q3-CICD for deployment automation.
|
||||
4. Reference Q4 for the large-scale architecture approach.
|
||||
5. Use Q5 as a container-based monitoring example.
|
||||
|
||||
## Technologies Used
|
||||
|
||||
- Node.js / Express
|
||||
- Python
|
||||
- Docker
|
||||
- HTML / CSS / JavaScript
|
||||
- GitHub Actions workflow-based CI/CD
|
||||
|
||||
## Summary
|
||||
|
||||
This project demonstrates a practical SRE approach covering monitoring, incident response, automation, scalability, and container-based operations for a production-style platform.
|
||||
|
||||
---
|
||||
|
||||
## Author
|
||||
|
||||
**Gamal Mohamed**
|
||||
|
||||
DevSecOps Engineer
|
||||
|
||||
Qabilah Profile
|
||||
|
||||
```
|
||||
https://qabilah.com/profile/gamalmohammed0909/
|
||||
```
|
||||
ثنائية
docs/Screenshot 2026-07-27 at 23-54-33 Ghaymah Cloud.png
Normal file
|
بعد العرض: | الارتفاع: | الحجم: 131 KiB |
ثنائية
docs/Screenshot 2026-07-27 at 23-54-52 Site Watch — Status.png
Normal file
|
بعد العرض: | الارتفاع: | الحجم: 67 KiB |
ثنائية
docs/Screenshot 2026-07-27 at 23-56-18 .png
Normal file
|
بعد العرض: | الارتفاع: | الحجم: 9.8 KiB |
ثنائية
docs/Screenshot 2026-07-27 at 23-56-33 .png
Normal file
|
بعد العرض: | الارتفاع: | الحجم: 19 KiB |
ثنائية
docs/Screenshot 2026-07-27 at 23-57-37 Ghaymah Cloud.png
Normal file
|
بعد العرض: | الارتفاع: | الحجم: 236 KiB |
ثنائية
docs/Screenshot 2026-07-27 at 23-57-45 لوحة مراقبة الـ API.png
Normal file
|
بعد العرض: | الارتفاع: | الحجم: 35 KiB |
ثنائية
docs/brave_screenshot_deploy.ghaymah.systems.png
Normal file
|
بعد العرض: | الارتفاع: | الحجم: 132 KiB |