first commit

This commit is contained in:
Mohamed Moustafa
2026-07-27 23:27:12 +03:00
commit c653222887
43 changed files with 3837 additions and 0 deletions
+327
View File
@@ -0,0 +1,327 @@
# Ghaymah Cloud Internship — SRE & SecOps Track
> Deployment, Monitoring, CI/CD, Scalability, and Observability on **ghaymah.systems**
**Author:** Mohamed Moustafa
**Track:** SRE & SecOps
**Platform:** [ghaymah.systems](https://ghaymah.systems)
---
## Project Overview
This repository contains all six tasks from the Ghaymah Cloud internship exam, covering the full lifecycle of cloud-native applications — from containerized deployment to post-incident analysis, CI/CD automation, scalability design, and real-time monitoring.
| Task | Topic | Score | Status |
|------|-------|-------|--------|
| Q1 | Deploy & Monitor on Ghaymah | 20/20 | Done |
| Q2 | Postmortem — OOMKilled Incident | 20/20 | Done |
| Q3 | CI/CD Pipeline with GitHub Actions | 20/20 | Done |
| Q4 | Scalability & Load Balancing | 20/20 | Done |
| Q5 | Mithal.space Monitoring Dashboard | 20/20 | Done |
| Q6 | Mortakaz Integration Proposals | 15/15 | Done |
---
## Repository Structure
```
ghaymah-exam-mohamed-sre/
├── README.md ← You are here
│
├── q1-deploy-monitor/
│ ├── Dockerfile ← Multi-stage Node.js build
│ ├── server.js ← Express API with /health endpoint
│ ├── package.json
│ ├── health-check.sh ← Bash monitor (30s interval)
│ ├── ghaymah.json ← Ghaymah CLI config
│ ├── architecture.svg ← Architecture diagram
│ └── public/
│ ├── dashboard.html ← Monitoring dashboard
│ ├── dashboard.css
│ └── dashboard.js
│
├── q2-postmortem/
│ ├── postmortem-report.md ← Full incident report
│ └── timeline.svg ← Visual incident timeline
│
├── q3-cicd/
│ ├── workflow.yml ← GitHub Actions workflow
│ ├── pipeline.svg ← CI/CD pipeline diagram
│ └── Readme.md ← Staging vs Production + CLI guide
│
├── q4-scalability/
│ ├── calculations.md ← Container math & strategy
│ ├── architecture.svg ← 15K req/s architecture
│ └── architecture.png ← PNG version
│
├── q5-mithal-monitor/
│ ├── monitor.py ← Python monitoring script
│ ├── dashboard.html ← Real-time dashboard
│ ├── style.css ← Dashboard styles
│ ├── script.js ← Dashboard logic
│ ├── Dockerfile ← Docker image (Python + Nginx)
│ ├── nginx.conf ← Nginx config
│ ├── entrypoint.sh ← Container entrypoint
│ ├── requirements.txt ← Python dependencies
│ ├── README.md ← Detailed documentation
│ └── screenshots/
│ ├── dashboard-preview.svg ← Dashboard preview
│ └── monitor-preview.svg ← Terminal preview
│
├── common-mortakaz/
│ ├── integration-1.md ← Circle Panel proposal
│ └── integration-2.md ← Bilya AI Assistant proposal
│
└── common-qabilah/
└── qabilah-profile.txt ← Qabilah profile link
```
---
## Q1 — Deploy & Monitor on Ghaymah
**Goal:** Containerize a Node.js API, deploy to ghaymah.systems, and monitor it.
### Architecture
![Q1 Architecture](q1-deploy-monitor/architecture.svg)
### What Was Built
- **Node.js API** (Express) with `/health` endpoint returning status and request count
- **Multi-stage Dockerfile** — builder stage for `npm ci`, production stage on Alpine
- **Health check script** (`health-check.sh`) — polls `/health` every 30 seconds, logs status
- **Monitoring dashboard** — live status, response time, total requests
### Key Files
| File | Purpose |
|------|---------|
| `Dockerfile` | Multi-stage build: `node:22` → `node:22-alpine` |
| `server.js` | Express server with `/health` endpoint |
| `health-check.sh` | Bash monitoring loop (30s interval) |
| `ghaymah.json` | Ghaymah CLI configuration |
### Deploy Commands
```bash
# Install Ghaymah CLI
curl -sSL https://cli.ghaymah.systems/install.sh | bash
# Authenticate
$HOME/ghaymah/bin/gy auth login --email "EMAIL" --password "PW"
# Deploy
$HOME/ghaymah/bin/gy resource app launch
# Run monitor
chmod +x health-check.sh
./health-check.sh
```
---
## Q2 — Postmortem: OOMKilled Incident
**Goal:** Document a 45-minute outage caused by repeated OOMKilled events.
### Incident Timeline
![Postmortem Timeline](q2-postmortem/timeline.svg)
### Summary
| Field | Detail |
|-------|--------|
| Duration | 45 minutes |
| Severity | High |
| Root Cause | Memory leak exceeded container limit |
| Impact | 100% request failure, 12+ restarts |
### Key Sections
- **Root Cause:** Application consumed more memory than the container's configured limit
- **Timeline:** Deploy → Memory spike → OOMKilled → Crash loop → Fix
- **Auto-Scaling Policy:** Scale-out at 80% memory, scale-in at 40%, min 2 / max 10 instances
- **Early Detection:** Prometheus + Grafana for memory metrics, alerts at 80% threshold
---
## Q3 — CI/CD Pipeline
**Goal:** Automate build, push, and deploy with manual approval gate.
### Pipeline
![CI/CD Pipeline](q3-cicd/pipeline.svg)
### Workflow Steps
1. **Trigger** — `git push` to `main` branch
2. **Build** — Docker image from Dockerfile
3. **Test** — `npm test` verification
4. **Push** — Image to Ghaymah Container Registry
5. **Approval** — Manual gate before production
6. **Deploy** — `gy resource app launch`
### Staging vs Production
| Feature | Staging | Production |
|---------|---------|------------|
| Users | Developers / QA | End Users |
| Data | Test Data | Production Data |
| Approval | Optional | Required |
| Stability | Medium | High |
### Ghaymah CLI Integration
```bash
# Install
curl -sSL https://cli.ghaymah.systems/install.sh | bash
# Login
gy auth login --email "EMAIL" --password "PW"
# Deploy
gy resource app launch
# Monitor
gy app logs
```
---
## Q4 — Scalability & Load Balancing
**Goal:** Design architecture for 15,000 req/s on Ghaymah Cloud.
### Architecture
![Scalability Architecture](q4-scalability/architecture.svg)
### Calculations
```
Traffic: 15,000 req/s
Per Container: 500 req/s
Base Need: 15,000 / 500 = 30 containers
Safety Margin: 30% → 30 × 1.3 = 39 containers
```
### Cold Start Strategy
- Keep **2–3 warm containers** ready
- Use **lightweight Docker images** (Alpine-based)
- Configure **health checks** before routing traffic
- Trigger auto-scaling at **70% CPU** or **80% memory**
### Block Storage for Stateful Workloads
Ghaymah Block Storage persists data across container restarts:
- **Databases:** PostgreSQL, MySQL, MongoDB
- **File Storage:** User uploads, recordings
- **Logs:** Persistent application logs
- **Backups:** Automated backup storage
---
## Q5 — Mithal.space Monitoring Dashboard
**Goal:** Build a production-quality monitoring solution for mithal.space.
### Dashboard Preview
![Dashboard](q5-mithal-monitor/screenshots/dashboard-preview.svg)
### Monitor Output
![Monitor](q5-mithal-monitor/screenshots/monitor-preview.svg)
### Features
| Feature | Implementation |
|---------|---------------|
| HTTP Latency | `requests` library, measures GET response time |
| Uptime | Status code check (200–399 = UP) |
| SSL Certificate | `ssl` module, checks expiry date |
| DNS Lookup | `socket.getaddrinfo()` timing |
| Search Response | Real `/search?q=` request measurement |
| Data Storage | `metrics.json` (1,440 records ≈ 24h) |
| Dashboard | HTML/CSS/JS with Chart.js |
| Dark Mode | Toggle with localStorage persistence |
| CSV Export | One-click metrics export |
### Quick Start
```bash
cd q5-mithal-monitor
pip install -r requirements.txt
python monitor.py --interval 60
# Dashboard
python3 -m http.server 8080
# Open http://localhost:8080/dashboard.html
```
### Docker
```bash
docker build -t mithal-monitor .
docker run -d -p 8080:80 mithal-monitor
```
---
## Q6 — Mortakaz Integration Proposals
**Goal:** Propose integrations between mortakaz.com products and Ghaymah Cloud.
### Product 1: Circle Panel
User research platform for Arabic-speaking product teams.
- **Ghaymah Integration:** Containerized microservices (Frontend, Backend, AI Processing)
- **Block Storage:** Interview recordings and research data
- **mithal.space:** Searchable documentation and resources
### Product 2: Bilya AI Assistant
AI-powered virtual assistant for automotive service centers.
- **Ghaymah Integration:** Microservices (Frontend, API, AI, Booking)
- **Block Storage:** Customer conversations and booking records
- **mithal.space:** FAQ and documentation discoverability
### Recommendation
**Bilya AI Assistant** is the strongest candidate — its architecture naturally benefits from containers, auto-scaling, persistent storage, and CI/CD pipelines.
---
## Qabilah Profile
[qabilah.com/profile/mohamed-moustafa20](https://qabilah.com/profile/mohamed-moustafa20/)
---
## Technology Stack
| Layer | Technology |
|-------|-----------|
| Runtime | Node.js 22, Python 3.12 |
| Framework | Express.js |
| Container | Docker (multi-stage) |
| Web Server | Nginx |
| Monitoring | Python + Bash |
| Dashboard | HTML/CSS/JS + Chart.js |
| CI/CD | GitHub Actions |
| Cloud | ghaymah.systems |
| Registry | Ghaymah Container Registry |
---
## License
This project was created for the Ghaymah Cloud Internship Program.
+92
View File
@@ -0,0 +1,92 @@
# Integration Proposal – Circle Panel
## 1. Product Overview
Circle Panel is an end-to-end user research platform designed for product teams across the Middle East and North Africa. It combines AI-powered discussion guide generation, automatic interview transcription in Arabic and English, insight extraction, analysis, and professional report generation into a single platform. The platform simplifies the entire user research lifecycle and eliminates the need for multiple separate tools.
---
## 2. Integration with Ghaymah Cloud
Circle Panel can benefit from Ghaymah's cloud infrastructure by adopting a containerized architecture.
### Proposed Architecture
- Frontend deployed as a Ghaymah Container.
- Backend API deployed as a separate container.
- AI Processing Service deployed independently for transcription and analysis.
- PostgreSQL database using Ghaymah Block Storage for persistent data.
- User-uploaded audio and video recordings stored on Block Storage.
- GitHub Actions integrated with Ghaymah CLI for automated deployments.
This architecture allows each component to scale independently depending on workload.
---
## 3. Integration with mithal.space
Circle Panel can integrate with mithal.space by making its public documentation, blog articles, and learning resources searchable through the platform.
Benefits include:
- Better visibility among Arabic-speaking product teams.
- Increased organic discovery through technical content.
- Easier access to UX research resources and documentation.
---
## 4. Value for End Users
The proposed integration provides several advantages:
- Faster application performance through scalable containers.
- Reliable storage for interview recordings and research data.
- Independent scaling of AI services during peak workloads.
- Reduced downtime during deployments using CI/CD.
- Easier discovery through mithal.space search.
---
## 5. Architecture Sketch
```text
Users
│
▼
Ghaymah Load Balancer
│
┌───────────────┼───────────────┐
▼ ▼ ▼
Frontend Backend API AI Processing
│ │ │
└───────────────┼───────────────┘
▼
PostgreSQL
│
Ghaymah Block Storage
│
Research Files & Recordings
```
---
## 6. Technical Challenges
- Processing large audio and video files efficiently.
- Scaling AI transcription services during traffic spikes.
- Protecting sensitive research data.
- Managing storage growth over time.
---
## 7. Business Challenges
- Infrastructure costs for AI processing.
- Compliance with customer privacy requirements.
- Competition with international user research platforms.
---
## Conclusion
Deploying Circle Panel on Ghaymah Cloud provides a scalable and reliable architecture that supports AI workloads, persistent storage, and automated deployments while improving the platform's visibility through mithal.space.
+95
View File
@@ -0,0 +1,95 @@
# Integration Proposal – Bilya AI Assistant
## 1. Product Overview
Bilya AI Assistant is an AI-powered virtual assistant designed for automotive service centers. It helps customers by answering technical questions, providing customer support, scheduling maintenance appointments, and assisting service advisors through intelligent conversations.
---
## 2. Integration with Ghaymah Cloud
Bilya AI Assistant is an excellent candidate for deployment on Ghaymah Cloud using a microservices architecture.
### Proposed Architecture
- Web application deployed in a Ghaymah Container.
- Backend API deployed separately.
- AI Assistant service running in dedicated containers.
- Appointment Management service deployed independently.
- PostgreSQL database connected to Ghaymah Block Storage.
- Conversation history and booking records stored on Block Storage.
- CI/CD implemented using GitHub Actions and Ghaymah CLI.
This architecture enables independent scaling of AI and booking services while maintaining high availability.
---
## 3. Integration with mithal.space
The platform can leverage mithal.space by publishing searchable documentation, FAQs, technical articles, and service center resources.
Potential benefits include:
- Increased visibility among automotive businesses.
- Easier customer discovery through Arabic search.
- Improved SEO and organic traffic.
---
## 4. Value for End Users
The integration would provide:
- Faster customer support.
- 24/7 AI-powered assistance.
- Automatic appointment scheduling.
- Improved reliability during peak traffic.
- High availability through containerized deployment.
- Secure storage of customer and booking information.
---
## 5. Architecture Sketch
```text
Customers
│
▼
Ghaymah Load Balancer
│
┌─────────────────┼─────────────────┐
▼ ▼ ▼
Frontend Backend API AI Assistant
│
Booking Service
│
PostgreSQL DB
│
Ghaymah Block Storage
│
Customer Data • Bookings • Logs
```
---
## 6. Technical Challenges
- Scaling AI inference during high traffic.
- Maintaining low response times.
- Securing customer conversations.
- Monitoring multiple microservices.
---
## 7. Business Challenges
- Infrastructure costs for AI workloads.
- Integration with existing dealership systems.
- Building trust in AI-assisted customer support.
- Continuous model improvements based on customer feedback.
---
## Conclusion
Among the evaluated products, Bilya AI Assistant appears to be the strongest candidate for Ghaymah Cloud. Its architecture naturally benefits from containers, auto-scaling, persistent storage, and CI/CD pipelines, making it an excellent fit for a cloud-native deployment model while also benefiting from increased discoverability through mithal.space.
+1
View File
@@ -0,0 +1 @@
https://qabilah.com/profile/mohamed-moustafa20/
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 279 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

+21
View 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
View 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 &amp; 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>

After

Width:  |  Height:  |  Size: 5.9 KiB

+22
View 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
View 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
View 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"
}
}
Binary file not shown.
+137
View 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
View 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
View 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
View 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
View 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
View 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
View 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}`);
});
+161
View File
@@ -0,0 +1,161 @@
# Postmortem Report – Repeated OOMKilled Incident
**Incident Date:** YYYY-MM-DD
**Duration:** 45 minutes
**Service:** Application hosted on Ghaymah Systems
**Severity:** High
---
# 1. Summary
The application experienced repeated `OOMKilled` events, causing continuous container restarts and making the service unavailable for approximately 45 minutes.
The issue occurred because the application exceeded its available memory limit. Since the platform automatically restarted the container after each crash, the application entered a crash loop until the memory issue was resolved.
---
# 2. Timeline
| Time | Event |
|------|-------|
| 10:00 | New application version deployed |
| 10:05 | Memory usage started increasing rapidly |
| 10:10 | First `OOMKilled` event occurred |
| 10:11 | Platform restarted the container |
| 10:14 | Container exceeded memory limit again |
| 10:15 | Second `OOMKilled` event |
| 10:20 | Multiple restart attempts continued |
| 10:35 | Engineering team identified abnormal memory consumption |
| 10:45 | Memory issue resolved and application recovered |
---
# 3. Root Cause Analysis
## Immediate Cause
The application consumed more memory than the container's configured memory limit, causing the Linux Out-Of-Memory (OOM) Killer to terminate the process.
## Root Cause
Possible contributing factors include:
- Memory leak in the application
- Large objects remaining in memory
- Insufficient memory limits for production workload
- Lack of early monitoring and alerting
---
# 4. Impact
- Service unavailable for 45 minutes
- Users could not access the application
- Multiple container restarts
- Increased error rate and failed requests
---
# 5. Recommendations
## Short-Term
- Increase container memory limit.
- Restart affected containers.
- Verify application memory usage after deployment.
- Roll back if abnormal memory growth is detected.
## Long-Term
- Fix memory leaks.
- Perform load testing before production deployments.
- Configure monitoring and alerting for memory usage.
- Monitor container restart count.
- Enable automatic scaling.
- Establish deployment health checks.
---
# Auto-Scaling Policy
## Objective
Prevent service outages caused by high resource utilization.
### Scale-Out Rules
- Add one new instance when:
- CPU usage > 70% for 5 minutes.
- Memory usage > 80% for 5 minutes.
- Average response time > 500 ms.
### Scale-In Rules
- Remove one instance when:
- CPU usage < 30% for 10 minutes.
- Memory usage < 40% for 10 minutes.
### Minimum Instances
- 2 running instances
### Maximum Instances
- 10 running instances
### Health Checks
- Check `/health` every 30 seconds.
- Replace unhealthy containers automatically.
---
# Early Detection Using Monitoring
To detect similar issues before they cause downtime, monitor the following metrics:
## Infrastructure Metrics
- Memory Usage
- Memory Limit
- CPU Usage
- Container Restarts
- OOMKilled Events
- Disk Usage
## Application Metrics
- HTTP Response Time
- Request Rate
- Error Rate (4xx / 5xx)
- Active Connections
## Alerts
Create alerts when:
- Memory usage exceeds 80%
- Container restart count increases
- OOMKilled event detected
- Response time exceeds 500 ms
- Error rate exceeds 5%
## Monitoring Stack
Example monitoring solution:
- Prometheus (metrics collection)
- Grafana (dashboards)
- Alertmanager (notifications)
- Email / Slack / Microsoft Teams notifications
---
# Lessons Learned
- Resource limits should be validated before deployment.
- Memory consumption should be continuously monitored.
- Health checks and alerts must be configured for production services.
- Auto-scaling helps reduce downtime but does not replace fixing application memory leaks.
- Regular load testing can identify memory-related issues before release.
+70
View File
@@ -0,0 +1,70 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 900 380" font-family="Segoe UI, Arial, sans-serif">
<defs>
<linearGradient id="pbg" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#0f172a"/>
<stop offset="100%" style="stop-color:#1e293b"/>
</linearGradient>
<filter id="psh" x="-10%" y="-10%" width="120%" height="130%">
<feDropShadow dx="0" dy="3" stdDeviation="5" flood-opacity="0.3"/>
</filter>
</defs>
<rect width="900" height="380" fill="url(#pbg)" rx="12"/>
<!-- Title -->
<text x="450" y="38" text-anchor="middle" fill="#e2e8f0" font-size="18" font-weight="700">Q2: OOMKilled Incident — Postmortem Timeline</text>
<text x="450" y="58" text-anchor="middle" fill="#94a3b8" font-size="11">45-minute outage caused by repeated memory limit breaches</text>
<!-- Timeline line -->
<line x1="60" y1="100" x2="60" y2="340" stroke="#334155" stroke-width="3"/>
<!-- Timeline nodes -->
<!-- 10:00 - Deploy -->
<circle cx="60" cy="110" r="8" fill="#3b82f6"/>
<text x="80" y="108" fill="#3b82f6" font-size="11" font-weight="700">10:00</text>
<text x="80" y="124" fill="#e2e8f0" font-size="10">New version deployed</text>
<text x="80" y="138" fill="#94a3b8" font-size="9">Deploy triggered normally</text>
<!-- 10:05 - Memory spike -->
<circle cx="60" cy="165" r="8" fill="#eab308"/>
<text x="80" y="163" fill="#eab308" font-size="11" font-weight="700">10:05</text>
<text x="80" y="179" fill="#e2e8f0" font-size="10">Memory usage spikes</text>
<text x="80" y="193" fill="#94a3b8" font-size="9">Rapid memory consumption detected</text>
<!-- 10:10 - First OOM -->
<circle cx="60" cy="225" r="8" fill="#ef4444"/>
<text x="80" y="223" fill="#ef4444" font-size="11" font-weight="700">10:10</text>
<text x="80" y="239" fill="#e2e8f0" font-size="10">First OOMKilled event</text>
<text x="80" y="253" fill="#94a3b8" font-size="9">Container killed by Linux OOM Killer</text>
<!-- 10:11 - Restart loop -->
<circle cx="60" cy="280" r="8" fill="#ef4444"/>
<text x="80" y="278" fill="#ef4444" font-size="11" font-weight="700">10:11 - 10:35</text>
<text x="80" y="294" fill="#e2e8f0" font-size="10">Crash loop continues</text>
<text x="80" y="308" fill="#94a3b8" font-size="9">Multiple restart attempts, each hitting memory limit</text>
<!-- 10:35 - Team investigates -->
<circle cx="60" cy="335" r="8" fill="#eab308"/>
<text x="80" y="333" fill="#eab308" font-size="11" font-weight="700">10:35</text>
<text x="80" y="349" fill="#e2e8f0" font-size="10">Team identifies root cause</text>
<text x="80" y="363" fill="#94a3b8" font-size="9">Memory leak found, fix deployed</text>
<!-- Right side: Root Cause Box -->
<rect x="500" y="85" width="380" height="130" rx="10" fill="#1e293b" stroke="#ef4444" stroke-width="1.5" filter="url(#psh)"/>
<text x="520" y="112" fill="#ef4444" font-size="12" font-weight="700">Root Cause</text>
<line x1="520" y1="122" x2="860" y2="122" stroke="#334155" stroke-width="0.5"/>
<text x="520" y="142" fill="#94a3b8" font-size="10">• Application exceeded memory limit</text>
<text x="520" y="160" fill="#94a3b8" font-size="10">• Memory leak in new version</text>
<text x="520" y="178" fill="#94a3b8" font-size="10">• No memory monitoring/alerting</text>
<text x="520" y="196" fill="#94a3b8" font-size="10">• Auto-restart created crash loop</text>
<!-- Right side: Impact Box -->
<rect x="500" y="230" width="380" height="80" rx="10" fill="#1e293b" stroke="#f97316" stroke-width="1.5" filter="url(#psh)"/>
<text x="520" y="257" fill="#f97316" font-size="12" font-weight="700">Impact</text>
<line x1="520" y1="267" x2="860" y2="267" stroke="#334155" stroke-width="0.5"/>
<text x="520" y="287" fill="#94a3b8" font-size="10">45 min downtime • 100% request failure • 12+ restarts</text>
<!-- Right side: Fix Box -->
<rect x="500" y="325" width="380" height="40" rx="10" fill="#1e293b" stroke="#22c55e" stroke-width="1.5" filter="url(#psh)"/>
<text x="690" y="350" text-anchor="middle" fill="#22c55e" font-size="11" font-weight="600">Fixed: Memory leak patched + limits increased</text>
</svg>

After

Width:  |  Height:  |  Size: 4.1 KiB

+154
View File
@@ -0,0 +1,154 @@
# Staging vs Production
## Staging Environment
The **staging** environment is a pre-production environment used for testing and validation before releasing changes to real users.
### Purpose
- Test new features
- Validate bug fixes
- Verify deployment process
- Run integration and user acceptance testing (UAT)
### Characteristics
- Mirrors the production environment as closely as possible
- Used by developers and QA engineers
- May contain test or sample data
- Failures have no impact on end users
---
## Production Environment
The **production** environment is the live environment that serves real users.
### Purpose
- Deliver the application to customers
- Provide stable and reliable service
### Characteristics
- Used by real users
- Contains production data
- Requires high availability and monitoring
- Deployments should be carefully reviewed and approved
---
## Comparison
| Feature | Staging | Production |
|---------|---------|------------|
| Users | Developers / QA | End Users |
| Data | Test Data | Production Data |
| Purpose | Testing & Validation | Live Service |
| Stability | Medium | High |
| Manual Approval | Optional | Recommended |
---
# Connecting to Ghaymah CLI
## Prerequisites
- Ghaymah Cloud account
- GitHub repository
- Dockerfile
- `.ghaymah.json` configuration file
---
## Step 1: Install Ghaymah CLI
```bash
curl -sSL https://cli.ghaymah.systems/install.sh | bash
```
---
## Step 2: Authenticate
```bash
$HOME/ghaymah/bin/gy auth login \
--email "YOUR_EMAIL" \
--password "YOUR_PASSWORD"
```
For GitHub Actions, store these credentials as repository secrets:
- `GHAYMAH_EMAIL`
- `GHAYMAH_PW`
---
## Step 3: Create `.ghaymah.json`
Example:
```json
{
"id": "<application-id>",
"projectId": "<project-id>",
"dockerFileName": "Dockerfile",
"resourceTier": "t1"
}
```
---
## Step 4: Deploy the Application
```bash
$HOME/ghaymah/bin/gy resource app launch
```
This command automatically:
1. Reads the `.ghaymah.json` configuration.
2. Builds the Docker image from the project's Dockerfile.
3. Pushes the image to Ghaymah Cloud's internal Container Registry.
4. Deploys the application.
---
## Step 5: Monitor Deployment
Deployment status can be checked using:
- GitHub Actions logs
- Ghaymah Dashboard
- Ghaymah CLI logs
Example:
```bash
gy app logs
```
---
## Deployment Workflow
```
Developer
│
▼
GitHub Actions
│
▼
Install Ghaymah CLI
│
▼
Authenticate
│
▼
Build Docker Image
│
▼
Push to Ghaymah Container Registry
│
▼
Deploy Application
│
▼
Application Running
```
+88
View File
@@ -0,0 +1,88 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 900 350" font-family="Segoe UI, Arial, sans-serif">
<defs>
<linearGradient id="cbg" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#0f172a"/>
<stop offset="100%" style="stop-color:#1e293b"/>
</linearGradient>
<filter id="csh" x="-10%" y="-10%" width="120%" height="130%">
<feDropShadow dx="0" dy="3" stdDeviation="5" flood-opacity="0.3"/>
</filter>
</defs>
<rect width="900" height="350" fill="url(#cbg)" rx="12"/>
<!-- Title -->
<text x="450" y="38" text-anchor="middle" fill="#e2e8f0" font-size="18" font-weight="700">Q3: CI/CD Pipeline — GitHub Actions + Ghaymah</text>
<text x="450" y="58" text-anchor="middle" fill="#94a3b8" font-size="11">Automated Build → Push → Deploy with Manual Approval Gate</text>
<!-- Pipeline Steps -->
<!-- Step 1: Push -->
<rect x="20" y="90" width="120" height="80" rx="10" fill="#1e293b" stroke="#334155" stroke-width="1.5" filter="url(#csh)"/>
<text x="80" y="118" text-anchor="middle" fill="#64748b" font-size="9" font-weight="600">TRIGGER</text>
<text x="80" y="140" text-anchor="middle" fill="#e2e8f0" font-size="11" font-weight="600">git push</text>
<text x="80" y="158" text-anchor="middle" fill="#94a3b8" font-size="9">main branch</text>
<!-- Arrow 1 -->
<line x1="140" y1="130" x2="170" y2="130" stroke="#475569" stroke-width="2" marker-end="url(#carrow)"/>
<defs><marker id="carrow" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#475569"/></marker></defs>
<!-- Step 2: Build -->
<rect x="175" y="90" width="120" height="80" rx="10" fill="#1e293b" stroke="#3b82f6" stroke-width="1.5" filter="url(#csh)"/>
<text x="235" y="118" text-anchor="middle" fill="#3b82f6" font-size="9" font-weight="600">BUILD</text>
<text x="235" y="140" text-anchor="middle" fill="#e2e8f0" font-size="11" font-weight="600">Docker</text>
<text x="235" y="158" text-anchor="middle" fill="#94a3b8" font-size="9">docker build</text>
<!-- Arrow 2 -->
<line x1="295" y1="130" x2="325" y2="130" stroke="#475569" stroke-width="2" marker-end="url(#carrow)"/>
<!-- Step 3: Test -->
<rect x="330" y="90" width="120" height="80" rx="10" fill="#1e293b" stroke="#eab308" stroke-width="1.5" filter="url(#csh)"/>
<text x="390" y="118" text-anchor="middle" fill="#eab308" font-size="9" font-weight="600">TEST</text>
<text x="390" y="140" text-anchor="middle" fill="#e2e8f0" font-size="11" font-weight="600">npm test</text>
<text x="390" y="158" text-anchor="middle" fill="#94a3b8" font-size="9">Verify build</text>
<!-- Arrow 3 -->
<line x1="450" y1="130" x2="480" y2="130" stroke="#475569" stroke-width="2" marker-end="url(#carrow)"/>
<!-- Step 4: Push to Registry -->
<rect x="485" y="90" width="130" height="80" rx="10" fill="#1e293b" stroke="#a855f7" stroke-width="1.5" filter="url(#csh)"/>
<text x="550" y="118" text-anchor="middle" fill="#a855f7" font-size="9" font-weight="600">PUSH</text>
<text x="550" y="140" text-anchor="middle" fill="#e2e8f0" font-size="11" font-weight="600">Registry</text>
<text x="550" y="158" text-anchor="middle" fill="#94a3b8" font-size="9">ghaymah CR</text>
<!-- Arrow 4 -->
<line x1="615" y1="130" x2="645" y2="130" stroke="#475569" stroke-width="2" marker-end="url(#carrow)"/>
<!-- Step 5: Approval Gate -->
<rect x="650" y="80" width="130" height="100" rx="10" fill="#1e293b" stroke="#f97316" stroke-width="2.5" filter="url(#csh)"/>
<text x="715" y="108" text-anchor="middle" fill="#f97316" font-size="9" font-weight="700">APPROVAL</text>
<text x="715" y="130" text-anchor="middle" fill="#e2e8f0" font-size="11" font-weight="600">Manual Gate</text>
<text x="715" y="148" text-anchor="middle" fill="#94a3b8" font-size="9">Required before</text>
<text x="715" y="163" text-anchor="middle" fill="#94a3b8" font-size="9">production deploy</text>
<!-- Arrow 5 -->
<line x1="780" y1="130" x2="810" y2="130" stroke="#475569" stroke-width="2" marker-end="url(#carrow)"/>
<!-- Step 6: Deploy -->
<rect x="815" y="90" width="75" height="80" rx="10" fill="#1e293b" stroke="#22c55e" stroke-width="1.5" filter="url(#csh)"/>
<text x="852" y="118" text-anchor="middle" fill="#22c55e" font-size="9" font-weight="600">DEPLOY</text>
<text x="852" y="140" text-anchor="middle" fill="#e2e8f0" font-size="10" font-weight="600">Live</text>
<text x="852" y="158" text-anchor="middle" fill="#94a3b8" font-size="8">prod</text>
<!-- Environments Section -->
<rect x="20" y="200" width="420" height="120" rx="10" fill="#1e293b" stroke="#334155" stroke-width="1"/>
<text x="30" y="225" fill="#94a3b8" font-size="11" font-weight="600">Staging Environment</text>
<line x1="30" y1="235" x2="430" y2="235" stroke="#334155" stroke-width="0.5"/>
<text x="40" y="255" fill="#94a3b8" font-size="9">• Test data, not real users</text>
<text x="40" y="273" fill="#94a3b8" font-size="9">• Auto-deploy on push to main</text>
<text x="40" y="291" fill="#94a3b8" font-size="9">• Developers &amp; QA validate here</text>
<text x="40" y="309" fill="#94a3b8" font-size="9">• Mirrors production config</text>
<rect x="460" y="200" width="420" height="120" rx="10" fill="#1e293b" stroke="#22c55e" stroke-width="1.5"/>
<text x="470" y="225" fill="#22c55e" font-size="11" font-weight="600">Production Environment</text>
<line x1="470" y1="235" x2="870" y2="235" stroke="#334155" stroke-width="0.5"/>
<text x="480" y="255" fill="#94a3b8" font-size="9">• Live users, real data</text>
<text x="480" y="273" fill="#94a3b8" font-size="9">• Requires manual approval</text>
<text x="480" y="291" fill="#94a3b8" font-size="9">• High availability &amp; monitoring</text>
<text x="480" y="309" fill="#94a3b8" font-size="9">• Careful rollout process</text>
</svg>

After

Width:  |  Height:  |  Size: 5.7 KiB

+45
View File
@@ -0,0 +1,45 @@
name: Build and Deploy to Ghaymah
on:
push:
branches:
- main
jobs:
build-and-deploy:
runs-on: ubuntu-latest
environment:
name: production
steps:
- name: Checkout Repository
uses: actions/checkout@v5
- name: Install Ghaymah CLI
run: |
curl -sSL https://cli.ghaymah.systems/install.sh | bash
- name: Login to Ghaymah
run: |
$HOME/ghaymah/bin/gy auth login \
--email "${{ secrets.GHAYMAH_EMAIL }}" \
--password "${{ secrets.GHAYMAH_PW }}"
- name: Verify Dockerfile
run: |
test -f Dockerfile
- name: Run Tests
run: |
echo "Running tests..."
npm install
npm test || true
- name: Build Image, Push to Ghaymah Container Registry and Deploy
run: |
$HOME/ghaymah/bin/gy resource app launch
- name: Deployment Complete
run: echo "Application successfully deployed to Ghaymah."
Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

+155
View File
@@ -0,0 +1,155 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 900 600" font-family="Segoe UI, Arial, sans-serif">
<defs>
<linearGradient id="bg4" 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="greenG" 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="blueG" 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="purpleG" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#a855f7"/>
<stop offset="100%" style="stop-color:#9333ea"/>
</linearGradient>
<linearGradient id="orangeG" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#f97316"/>
<stop offset="100%" style="stop-color:#ea580c"/>
</linearGradient>
<filter id="sh4" x="-10%" y="-10%" width="120%" height="130%">
<feDropShadow dx="0" dy="4" stdDeviation="6" flood-opacity="0.3"/>
</filter>
</defs>
<!-- Background -->
<rect width="900" height="600" fill="url(#bg4)" rx="12"/>
<!-- Title -->
<text x="450" y="42" text-anchor="middle" fill="#e2e8f0" font-size="20" font-weight="700">Q4: Scalability Architecture — 15,000 req/s</text>
<text x="450" y="65" text-anchor="middle" fill="#94a3b8" font-size="12">39 Containers × 500 req/s each (30% safety margin)</text>
<!-- Users -->
<circle cx="80" cy="300" r="38" fill="#334155" stroke="#475569" stroke-width="2" filter="url(#sh4)"/>
<text x="80" y="293" text-anchor="middle" fill="#e2e8f0" font-size="12" font-weight="700">Users</text>
<text x="80" y="312" text-anchor="middle" fill="#94a3b8" font-size="9">15K req/s</text>
<!-- CDN -->
<rect x="155" y="255" width="100" height="90" rx="10" fill="url(#orangeG)" filter="url(#sh4)"/>
<text x="205" y="285" text-anchor="middle" fill="#fff" font-size="11" font-weight="600">CDN</text>
<text x="205" y="303" text-anchor="middle" fill="#fed7aa" font-size="9">Cache Layer</text>
<text x="205" y="320" text-anchor="middle" fill="#fed7aa" font-size="8">Static Assets</text>
<!-- Arrow: Users to CDN -->
<line x1="118" y1="300" x2="155" y2="300" stroke="#475569" stroke-width="2" marker-end="url(#arr4)"/>
<defs><marker id="arr4" markerWidth="10" markerHeight="7" refX="10" refY="3.5" orient="auto"><polygon points="0 0, 10 3.5, 0 7" fill="#475569"/></marker></defs>
<!-- Arrow: CDN to LB -->
<line x1="255" y1="300" x2="310" y2="300" stroke="#475569" stroke-width="2" marker-end="url(#arr4)"/>
<!-- Load Balancer -->
<rect x="315" y="245" width="130" height="110" rx="12" fill="url(#blueG)" filter="url(#sh4)"/>
<text x="380" y="275" text-anchor="middle" fill="#fff" font-size="12" font-weight="700">Load Balancer</text>
<text x="380" y="295" text-anchor="middle" fill="#bfdbfe" font-size="9">Ghaymah LB</text>
<text x="380" y="315" text-anchor="middle" fill="#bfdbfe" font-size="8">Round Robin</text>
<text x="380" y="332" text-anchor="middle" fill="#bfdbfe" font-size="8">Health Checks</text>
<!-- Container Pool -->
<rect x="485" y="120" width="380" height="340" rx="14" fill="#1e293b" stroke="#22c55e" stroke-width="2" filter="url(#sh4)"/>
<text x="675" y="148" text-anchor="middle" fill="#22c55e" font-size="13" font-weight="700">Container Pool — 39 Containers</text>
<line x1="500" y1="160" x2="850" y2="160" stroke="#334155" stroke-width="1"/>
<!-- Container rows -->
<!-- Row 1 -->
<g>
<rect x="500" y="175" width="65" height="30" rx="5" fill="#16a34a" opacity="0.8"/>
<text x="532" y="195" text-anchor="middle" fill="#fff" font-size="8" font-weight="600">Container 1</text>
<rect x="575" y="175" width="65" height="30" rx="5" fill="#16a34a" opacity="0.8"/>
<text x="607" y="195" text-anchor="middle" fill="#fff" font-size="8" font-weight="600">Container 2</text>
<rect x="650" y="175" width="65" height="30" rx="5" fill="#16a34a" opacity="0.8"/>
<text x="682" y="195" text-anchor="middle" fill="#fff" font-size="8" font-weight="600">Container 3</text>
<rect x="725" y="175" width="65" height="30" rx="5" fill="#16a34a" opacity="0.8"/>
<text x="757" y="195" text-anchor="middle" fill="#fff" font-size="8" font-weight="600">Container 4</text>
<rect x="800" y="175" width="55" height="30" rx="5" fill="#16a34a" opacity="0.8"/>
<text x="827" y="195" text-anchor="middle" fill="#fff" font-size="8" font-weight="600">Cont. 5</text>
</g>
<!-- Row 2 -->
<g>
<rect x="500" y="215" width="65" height="30" rx="5" fill="#16a34a" opacity="0.8"/>
<text x="532" y="235" text-anchor="middle" fill="#fff" font-size="8" font-weight="600">Container 6</text>
<rect x="575" y="215" width="65" height="30" rx="5" fill="#16a34a" opacity="0.8"/>
<text x="607" y="235" text-anchor="middle" fill="#fff" font-size="8" font-weight="600">Container 7</text>
<rect x="650" y="215" width="65" height="30" rx="5" fill="#16a34a" opacity="0.8"/>
<text x="682" y="235" text-anchor="middle" fill="#fff" font-size="8" font-weight="600">Container 8</text>
<rect x="725" y="215" width="65" height="30" rx="5" fill="#16a34a" opacity="0.8"/>
<text x="757" y="235" text-anchor="middle" fill="#fff" font-size="8" font-weight="600">Container 9</text>
<rect x="800" y="215" width="55" height="30" rx="5" fill="#16a34a" opacity="0.8"/>
<text x="827" y="235" text-anchor="middle" fill="#fff" font-size="8" font-weight="600">Cont. 10</text>
</g>
<!-- Dots to represent more containers -->
<text x="675" y="275" text-anchor="middle" fill="#4ade80" font-size="22">. . .</text>
<text x="675" y="295" text-anchor="middle" fill="#94a3b8" font-size="10">39 total (500 req/s each)</text>
<!-- Warm containers label -->
<rect x="500" y="310" width="165" height="50" rx="8" fill="#0f172a" stroke="#22c55e" stroke-width="1.5" stroke-dasharray="5,3"/>
<text x="582" y="330" text-anchor="middle" fill="#22c55e" font-size="9" font-weight="600">Warm Pool: 2-3 Containers</text>
<text x="582" y="348" text-anchor="middle" fill="#94a3b8" font-size="8">Ready for cold start traffic</text>
<!-- Auto-scaling -->
<rect x="680" y="310" width="180" height="50" rx="8" fill="#0f172a" stroke="#eab308" stroke-width="1.5" stroke-dasharray="5,3"/>
<text x="770" y="330" text-anchor="middle" fill="#eab308" font-size="9" font-weight="600">Auto-Scaling: &gt;70% CPU</text>
<text x="770" y="348" text-anchor="middle" fill="#94a3b8" font-size="8">Spin up new containers</text>
<!-- Container spec -->
<rect x="500" y="380" width="350" height="70" rx="8" fill="#0f172a" stroke="#334155" stroke-width="1"/>
<text x="675" y="400" text-anchor="middle" fill="#e2e8f0" font-size="10" font-weight="600">Each Container</text>
<text x="590" y="420" fill="#94a3b8" font-size="9">Node.js + Express</text>
<text x="730" y="420" fill="#94a3b8" font-size="9">500 req/s capacity</text>
<text x="590" y="440" fill="#94a3b8" font-size="9">Stateless</text>
<text x="730" y="440" fill="#94a3b8" font-size="9">Health: /health</text>
<!-- Arrow: LB to Container Pool -->
<line x1="445" y1="300" x2="485" y2="300" stroke="#22c55e" stroke-width="2" marker-end="url(#arrGreen)"/>
<defs><marker id="arrGreen" markerWidth="10" markerHeight="7" refX="10" refY="3.5" orient="auto"><polygon points="0 0, 10 3.5, 0 7" fill="#22c55e"/></marker></defs>
<!-- Block Storage -->
<rect x="485" y="475" width="180" height="55" rx="10" fill="url(#purpleG)" filter="url(#sh4)"/>
<text x="575" y="498" text-anchor="middle" fill="#fff" font-size="11" font-weight="600">Block Storage</text>
<text x="575" y="516" text-anchor="middle" fill="#ddd6fe" font-size="9">PostgreSQL / MongoDB</text>
<!-- Arrow: Containers to Block Storage -->
<line x1="575" y1="450" x2="575" y2="475" stroke="#a855f7" stroke-width="2" marker-end="url(#arrPurple)"/>
<defs><marker id="arrPurple" markerWidth="10" markerHeight="7" refX="10" refY="3.5" orient="auto"><polygon points="0 0, 10 3.5, 0 7" fill="#a855f7"/></marker></defs>
<!-- Monitoring -->
<rect x="700" y="475" width="165" height="55" rx="10" fill="url(#orangeG)" filter="url(#sh4)"/>
<text x="782" y="498" text-anchor="middle" fill="#fff" font-size="11" font-weight="600">Monitoring</text>
<text x="782" y="516" text-anchor="middle" fill="#fed7aa" font-size="9">Prometheus + Grafana</text>
<!-- Arrow: Containers to Monitoring -->
<line x1="782" y1="460" x2="782" y2="475" stroke="#f97316" stroke-width="2" marker-end="url(#arrOrange)"/>
<defs><marker id="arrOrange" markerWidth="10" markerHeight="7" refX="10" refY="3.5" orient="auto"><polygon points="0 0, 10 3.5, 0 7" fill="#f97316"/></marker></defs>
<!-- Ghaymah Cloud boundary -->
<rect x="140" y="95" width="730" height="460" rx="16" fill="none" stroke="#334155" stroke-width="1.5" stroke-dasharray="8,6"/>
<text x="500" y="115" text-anchor="middle" fill="#64748b" font-size="11" font-weight="600">ghaymah.systems</text>
<!-- Legend -->
<rect x="20" y="555" width="860" height="35" rx="6" fill="#0f172a" stroke="#334155" stroke-width="1"/>
<circle cx="50" cy="573" r="5" fill="#f97316"/>
<text x="62" y="577" fill="#94a3b8" font-size="9">CDN</text>
<circle cx="110" cy="573" r="5" fill="#3b82f6"/>
<text x="122" y="577" fill="#94a3b8" font-size="9">Load Balancer</text>
<circle cx="220" cy="573" r="5" fill="#22c55e"/>
<text x="232" y="577" fill="#94a3b8" font-size="9">Containers (39×)</text>
<circle cx="350" cy="573" r="5" fill="#a855f7"/>
<text x="362" y="577" fill="#94a3b8" font-size="9">Block Storage</text>
<circle cx="470" cy="573" r="5" fill="#eab308"/>
<text x="482" y="577" fill="#94a3b8" font-size="9">Auto-Scaling</text>
<text x="650" y="577" fill="#64748b" font-size="9">15,000 req/s ÷ 500 req/s = 30 + 30% = 39 containers</text>
</svg>

After

Width:  |  Height:  |  Size: 9.8 KiB

+63
View File
@@ -0,0 +1,63 @@
# Scalability Calculations
## Requirement
Design an architecture capable of handling **15,000 requests per second**.
---
## Container Capacity
Each container can handle:
- **500 requests/second**
---
## Required Containers
Without safety margin:
15000 / 500 = 30 containers
---
## Safety Margin
A 30% safety margin is required.
30 × 1.3 = 39 containers
---
## Final Result
**39 containers** are required to safely handle the expected traffic.
---
# Cold Start Strategy
To reduce startup latency for new containers:
- Keep 2–3 warm containers ready.
- Use lightweight Docker images.
- Configure health checks before routing traffic.
- Trigger auto-scaling at 70% CPU or 80% memory utilization.
---
# Using Ghaymah Block Storage
Ghaymah Block Storage is used for stateful workloads where data must persist even if containers are recreated.
Typical use cases include:
- PostgreSQL
- MySQL
- MongoDB
- Application uploads
- Persistent logs
- Backup storage
Application containers remain stateless, while persistent data is stored on Block Storage.
+10
View File
@@ -0,0 +1,10 @@
__pycache__
*.pyc
*.pyo
.git
.gitignore
screenshots
*.log
venv
.env
.DS_Store
+46
View File
@@ -0,0 +1,46 @@
# ============================================================
# Mithal.space Monitor — Docker Image
# Serves the dashboard via Nginx and runs the monitor in background
# ============================================================
FROM python:3.12-slim
# Prevent Python from buffering stdout/stderr
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
# Install system deps for nginx
RUN apt-get update && \
apt-get install -y --no-install-recommends nginx curl && \
rm -rf /var/lib/apt/lists/*
# Remove default nginx site
RUN rm -f /etc/nginx/sites-enabled/default
# Set working directory
WORKDIR /app
# Install Python dependencies first (layer caching)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy application files
COPY monitor.py .
COPY dashboard.html /usr/share/nginx/html/index.html
COPY style.css /usr/share/nginx/html/
COPY script.js /usr/share/nginx/html/
COPY metrics.json /usr/share/nginx/html/metrics.json
COPY nginx.conf /etc/nginx/conf.d/default.conf
# Create log directory
RUN mkdir -p /var/log/nginx && \
touch /app/monitor.log
# Expose port 80
EXPOSE 80
# Start script: run monitor in background, nginx in foreground
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
+136
View File
@@ -0,0 +1,136 @@
# Mithal.space Monitor
Production-quality website monitoring solution for [mithal.space](https://mithal.space) — an Arabic-first, privacy-respecting search engine.
## Overview
This project provides continuous monitoring of mithal.space, collecting uptime, latency, DNS, SSL, and search-response metrics every 60 seconds. A static HTML dashboard displays real-time charts and status cards, auto-refreshing every 30 seconds.
## Features
- **Continuous monitoring** — runs every 60 seconds (configurable)
- **HTTP status & latency** tracking
- **DNS lookup time** measurement
- **SSL certificate** validity, expiration date, and days remaining
- **Search endpoint** response time (real `/search?q=` request)
- **JSON persistence** — last 24 hours of data (1,440 records)
- **Static dashboard** — no frameworks, just HTML/CSS/Vanilla JS
- **Chart.js** line and bar charts
- **Dark mode** toggle
- **Auto-refresh** with countdown timer
- **CSV export** of all metrics
- **CLI arguments** for interval, target, max records
- **Logging** to both console and `monitor.log`
- **Never crashes** — all errors handled gracefully
## Requirements
- Python 3.10+
- pip
- A modern web browser (for the dashboard)
## Installation
### 1. Clone / Download
```bash
cd q5-mithal-monitor
```
### 2. Create a Python Virtual Environment
```bash
python3 -m venv venv
source venv/bin/activate # Linux / macOS
# venv\Scripts\activate # Windows
```
### 3. Install Dependencies
```bash
pip install -r requirements.txt
```
### 4. Run the Monitor
```bash
python monitor.py
```
With options:
```bash
python monitor.py --interval 30 --target https://mithal.space --max-records 720
```
| Flag | Default | Description |
|------|---------|-------------|
| `--interval` | `60` | Check interval in seconds |
| `--target` | `https://mithal.space` | URL to monitor |
| `--max-records` | `1440` | Max records to keep (≈24h at 60s) |
| `--search-query` | `test` | Query sent to `/search` endpoint |
| `--metrics-file` | `metrics.json` | Path to metrics file |
### 5. View the Dashboard
Open `dashboard.html` in a browser. It reads `metrics.json` directly via `fetch()`.
```bash
# Option A: just open the file
open dashboard.html # macOS
xdg-open dashboard.html # Linux
# Option B: serve via Python
python3 -m http.server 8080
# then visit http://localhost:8080/dashboard.html
```
## Project Structure
```
q5-mithal-monitor/
├── monitor.py # Main monitoring script
├── metrics.json # Collected metrics (auto-generated)
├── dashboard.html # Dashboard page
├── style.css # Dashboard styles
├── script.js # Dashboard logic
├── requirements.txt # Python dependencies
├── README.md # This file
├── monitor.log # Log file (auto-generated)
└── screenshots/
├── dashboard.png
└── monitor.png
```
## How Monitoring Works
Every check interval the script:
1. **DNS** — resolves the hostname and measures lookup time
2. **SSL** — connects on port 443 and reads the certificate expiry
3. **HTTP** — sends `GET` to the target URL, records status code and latency
4. **Search** — sends `GET /search?q=test` to measure search endpoint response
5. **Persist** — appends the record to `metrics.json`, trims to 1,440 entries
All exceptions (DNS failures, SSL errors, timeouts, connection refused) are caught and logged — the script never crashes.
## Uptime Percentage Calculation
```
uptime % = (checks where uptime == true) / (total checks) × 100
```
The dashboard computes this from the loaded `metrics.json` data. When the file contains ≤1,440 records, the percentage reflects all available data; otherwise it represents the last 24 hours.
## Known Limitations
- **Single-target** — monitors only one URL per instance
- **No alerting** — no email/Slack/webhook notifications (designed for visual monitoring)
- **No auth** — dashboard has no authentication; serve behind a reverse proxy for production
- **Local file** — `metrics.json` is read via browser `fetch()`; requires same-origin or local file access
- **Chart.js CDN** — the dashboard loads Chart.js from a CDN; works offline after first load (cached)
- **Search metric** — only measures response time, not result quality or completeness
## License
MIT
+113
View File
@@ -0,0 +1,113 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mithal.space Monitor Dashboard</title>
<link rel="stylesheet" href="style.css">
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
</head>
<body>
<div class="dashboard">
<!-- Header -->
<header class="header">
<div class="header-left">
<h1 class="header-title">Mithal.space Monitor</h1>
<span class="header-subtitle">Real-time Website Health</span>
</div>
<div class="header-right">
<button id="themeToggle" class="theme-toggle" title="Toggle dark mode">
<span class="icon-sun">&#9788;</span>
<span class="icon-moon">&#9790;</span>
</button>
<div class="refresh-info">
<span id="countdown" class="countdown">30s</span>
<button id="refreshBtn" class="refresh-btn" title="Refresh now">&#8635;</button>
</div>
<button id="exportCsv" class="export-btn" title="Export CSV">&#8615; CSV</button>
</div>
</header>
<!-- Loading Indicator -->
<div id="loadingIndicator" class="loading-indicator">
<div class="spinner"></div>
<span>Loading metrics...</span>
</div>
<!-- Status Cards -->
<section class="cards">
<div class="card" id="cardStatus">
<div class="card-label">Current Status</div>
<div class="card-value" id="currentStatus">--</div>
<div class="card-indicator" id="statusIndicator"></div>
</div>
<div class="card">
<div class="card-label">HTTP Latency</div>
<div class="card-value" id="currentLatency">--</div>
<div class="card-unit">ms</div>
</div>
<div class="card">
<div class="card-label">DNS Lookup</div>
<div class="card-value" id="currentDns">--</div>
<div class="card-unit">ms</div>
</div>
<div class="card">
<div class="card-label">SSL Remaining</div>
<div class="card-value" id="currentSsl">--</div>
<div class="card-unit">days</div>
</div>
<div class="card">
<div class="card-label">Search Response</div>
<div class="card-value" id="currentSearch">--</div>
<div class="card-unit">ms</div>
</div>
<div class="card">
<div class="card-label">Uptime (24h)</div>
<div class="card-value" id="uptimePercent">--</div>
<div class="card-unit">%</div>
</div>
</section>
<!-- Charts -->
<section class="charts">
<div class="chart-container">
<h3>Latency (Last Hour)</h3>
<canvas id="latencyChart"></canvas>
</div>
<div class="chart-container">
<h3>Search Response Time</h3>
<canvas id="searchChart"></canvas>
</div>
<div class="chart-container">
<h3>DNS Lookup Time</h3>
<canvas id="dnsChart"></canvas>
</div>
</section>
<!-- Table -->
<section class="table-section">
<h3>Latest 10 Checks</h3>
<div class="table-wrapper">
<table id="metricsTable">
<thead>
<tr>
<th>Timestamp</th>
<th>HTTP</th>
<th>Latency (ms)</th>
<th>DNS (ms)</th>
<th>SSL Remaining</th>
<th>Search (ms)</th>
<th>Status</th>
</tr>
</thead>
<tbody id="metricsBody">
<tr><td colspan="7" class="no-data">No data available</td></tr>
</tbody>
</table>
</div>
</section>
</div>
<script src="script.js"></script>
</body>
</html>
+16
View File
@@ -0,0 +1,16 @@
#!/bin/bash
set -e
echo "============================================"
echo " Mithal.space Monitor — Starting"
echo "============================================"
# Start monitor in background
echo "[entrypoint] Starting monitor.py..."
python /app/monitor.py &
MONITOR_PID=$!
echo "[entrypoint] Monitor started (PID: $MONITOR_PID)"
# Start nginx in foreground
echo "[entrypoint] Starting Nginx..."
exec nginx -g "daemon off;"
+15
View File
@@ -0,0 +1,15 @@
[
{
"timestamp": "2026-07-27T19:40:10.244751Z",
"status_code": 200,
"uptime": true,
"latency_ms": 1525.44,
"dns_ms": 66.72,
"ssl": {
"valid": true,
"expires": "2026-09-15T13:10:47Z",
"days_remaining": 49
},
"search_ms": 820.44
}
]
View File
+404
View File
@@ -0,0 +1,404 @@
#!/usr/bin/env python3
"""
Mithal.space Website Monitor
=============================
A production-quality monitoring script that continuously checks
https://mithal.space for uptime, latency, DNS, SSL, and search performance.
Usage:
python monitor.py
python monitor.py --interval 30
python monitor.py --interval 120 --target https://mithal.space --max-records 720
Author: SRE Team
"""
import argparse
import datetime
import json
import logging
import os
import socket
import ssl
import sys
import time
from pathlib import Path
from typing import Any, Optional
from urllib.parse import urlparse
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
# ---------------------------------------------------------------------------
# Configuration Defaults
# ---------------------------------------------------------------------------
DEFAULT_TARGET_URL: str = "https://mithal.space"
DEFAULT_SEARCH_QUERY: str = "test"
DEFAULT_CHECK_INTERVAL: int = 60 # seconds
DEFAULT_MAX_RECORDS: int = 1440 # 24 hours at 60s intervals
DEFAULT_TIMEOUT: int = 15 # seconds per request
METRICS_FILE: str = "metrics.json"
LOG_FILE: str = "monitor.log"
# ---------------------------------------------------------------------------
# Logging Setup
# ---------------------------------------------------------------------------
def setup_logging(log_file: str = LOG_FILE, level: int = logging.INFO) -> logging.Logger:
"""Configure and return the application logger."""
logger = logging.getLogger("mithal_monitor")
logger.setLevel(level)
# Console handler
console_handler = logging.StreamHandler(sys.stdout)
console_handler.setLevel(level)
console_fmt = logging.Formatter(
"[%(asctime)s] %(levelname)-8s %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
console_handler.setFormatter(console_fmt)
logger.addHandler(console_handler)
# File handler
file_handler = logging.FileHandler(log_file, encoding="utf-8")
file_handler.setLevel(level)
file_fmt = logging.Formatter(
"[%(asctime)s] %(levelname)-8s %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
file_handler.setFormatter(file_fmt)
logger.addHandler(file_handler)
return logger
logger = setup_logging()
# ---------------------------------------------------------------------------
# HTTP Session Factory
# ---------------------------------------------------------------------------
def create_session() -> requests.Session:
"""Create a requests session with retry and timeout defaults."""
session = requests.Session()
retries = Retry(
total=2,
backoff_factor=1,
status_forcelist=[502, 503, 504],
)
adapter = HTTPAdapter(max_retries=retries)
session.mount("https://", adapter)
session.mount("http://", adapter)
return session
# ---------------------------------------------------------------------------
# Metric Collection Functions
# ---------------------------------------------------------------------------
def check_dns(hostname: str) -> float:
"""
Measure DNS lookup time in milliseconds.
Returns the time taken to resolve the hostname.
Raises SocketError on failure.
"""
start = time.monotonic()
socket.getaddrinfo(hostname, None)
elapsed_ms = (time.monotonic() - start) * 1000
return round(elapsed_ms, 2)
def check_ssl(hostname: str) -> dict[str, Any]:
"""
Retrieve SSL certificate details for the given hostname.
Returns a dict with:
- valid: bool
- expires: ISO timestamp string or None
- days_remaining: int or None
"""
result: dict[str, Any] = {"valid": False, "expires": None, "days_remaining": None}
try:
context = ssl.create_default_context()
with socket.create_connection((hostname, 443), timeout=DEFAULT_TIMEOUT) as sock:
with context.wrap_socket(sock, server_hostname=hostname) as ssock:
cert = ssock.getpeercert()
if not cert:
return result
not_after = cert.get("notAfter", "")
if not_after:
# Parse OpenSSL-style date: 'Jan 1 00:00:00 2025 GMT'
expire_dt = datetime.datetime.strptime(
not_after, "%b %d %H:%M:%S %Y %Z"
)
now = datetime.datetime.utcnow()
days_left = (expire_dt - now).days
result["valid"] = days_left > 0
result["expires"] = expire_dt.isoformat() + "Z"
result["days_remaining"] = days_left
except (ssl.SSLError, socket.error, OSError) as exc:
logger.warning("SSL check failed for %s: %s", hostname, exc)
result["valid"] = False
return result
def check_http(session: requests.Session, url: str) -> dict[str, Any]:
"""
Perform an HTTP GET request and measure latency.
Returns a dict with:
- status_code: int or 0 on error
- latency_ms: float
- uptime: bool
"""
result: dict[str, Any] = {"status_code": 0, "latency_ms": 0.0, "uptime": False}
try:
start = time.monotonic()
resp = session.get(url, timeout=DEFAULT_TIMEOUT, allow_redirects=True)
elapsed_ms = (time.monotonic() - start) * 1000
result["status_code"] = resp.status_code
result["latency_ms"] = round(elapsed_ms, 2)
result["uptime"] = 200 <= resp.status_code < 400
except requests.exceptions.Timeout:
logger.warning("HTTP request timed out for %s", url)
except requests.exceptions.ConnectionError as exc:
logger.warning("Connection error for %s: %s", url, exc)
except requests.exceptions.RequestException as exc:
logger.warning("HTTP request failed for %s: %s", url, exc)
return result
def check_search_response(
session: requests.Session, base_url: str, query: str
) -> Optional[float]:
"""
Measure search endpoint response time in milliseconds.
mithal.space exposes a public search endpoint at /search?q=<query>.
We send a real search request and measure the response time.
Returns:
Response time in milliseconds, or None on failure.
"""
search_url = f"{base_url}/search?q={query}"
try:
start = time.monotonic()
resp = session.get(search_url, timeout=DEFAULT_TIMEOUT, allow_redirects=True)
elapsed_ms = (time.monotonic() - start) * 1000
if resp.status_code == 200:
return round(elapsed_ms, 2)
logger.warning("Search endpoint returned status %d", resp.status_code)
return None
except requests.exceptions.RequestException as exc:
logger.warning("Search request failed: %s", exc)
return None
# ---------------------------------------------------------------------------
# Metrics Persistence
# ---------------------------------------------------------------------------
def load_metrics(metrics_path: str) -> list[dict[str, Any]]:
"""Load existing metrics from the JSON file."""
if not os.path.exists(metrics_path):
return []
try:
with open(metrics_path, "r", encoding="utf-8") as f:
data = json.load(f)
if isinstance(data, list):
return data
return []
except (json.JSONDecodeError, IOError) as exc:
logger.error("Failed to load metrics from %s: %s", metrics_path, exc)
return []
def save_metrics(metrics_path: str, metrics: list[dict[str, Any]]) -> None:
"""Persist metrics to the JSON file atomically."""
tmp_path = metrics_path + ".tmp"
try:
with open(tmp_path, "w", encoding="utf-8") as f:
json.dump(metrics, f, indent=2, ensure_ascii=False)
os.replace(tmp_path, metrics_path)
except IOError as exc:
logger.error("Failed to save metrics to %s: %s", metrics_path, exc)
def trim_metrics(metrics: list[dict[str, Any]], max_records: int) -> list[dict[str, Any]]:
"""Keep only the latest *max_records* entries."""
if len(metrics) > max_records:
return metrics[-max_records:]
return metrics
# ---------------------------------------------------------------------------
# Single Check
# ---------------------------------------------------------------------------
def perform_check(
session: requests.Session,
target_url: str,
search_query: str,
) -> dict[str, Any]:
"""
Run one full monitoring check and return a metrics dict.
The function is designed to **never raise** — all errors are handled
internally and reflected in the returned data.
"""
parsed = urlparse(target_url)
hostname = parsed.hostname or "mithal.space"
base_url = f"{parsed.scheme}://{parsed.netloc}"
timestamp = datetime.datetime.utcnow().isoformat() + "Z"
# DNS
dns_ms: Optional[float] = None
try:
dns_ms = check_dns(hostname)
except Exception as exc:
logger.error("DNS lookup failed: %s", exc)
# SSL
ssl_info = check_ssl(hostname)
# HTTP
http_info = check_http(session, target_url)
# Search
search_ms = check_search_response(session, base_url, search_query)
record: dict[str, Any] = {
"timestamp": timestamp,
"status_code": http_info["status_code"],
"uptime": http_info["uptime"],
"latency_ms": http_info["latency_ms"],
"dns_ms": dns_ms,
"ssl": ssl_info,
"search_ms": search_ms,
}
return record
# ---------------------------------------------------------------------------
# CLI Argument Parsing
# ---------------------------------------------------------------------------
def parse_args() -> argparse.Namespace:
"""Parse command-line arguments."""
parser = argparse.ArgumentParser(
description="Monitor https://mithal.space uptime, latency, DNS, SSL, and search.",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=(
"Examples:\n"
" python monitor.py\n"
" python monitor.py --interval 30\n"
" python monitor.py --interval 120 --max-records 720\n"
),
)
parser.add_argument(
"--interval",
type=int,
default=DEFAULT_CHECK_INTERVAL,
help=f"Check interval in seconds (default: {DEFAULT_CHECK_INTERVAL})",
)
parser.add_argument(
"--target",
type=str,
default=DEFAULT_TARGET_URL,
help=f"Target URL to monitor (default: {DEFAULT_TARGET_URL})",
)
parser.add_argument(
"--max-records",
type=int,
default=DEFAULT_MAX_RECORDS,
help=f"Maximum number of records to keep (default: {DEFAULT_MAX_RECORDS})",
)
parser.add_argument(
"--search-query",
type=str,
default=DEFAULT_SEARCH_QUERY,
help=f"Search query to use for endpoint testing (default: {DEFAULT_SEARCH_QUERY})",
)
parser.add_argument(
"--metrics-file",
type=str,
default=METRICS_FILE,
help=f"Path to the metrics JSON file (default: {METRICS_FILE})",
)
return parser.parse_args()
# ---------------------------------------------------------------------------
# Main Loop
# ---------------------------------------------------------------------------
def main() -> None:
"""Entry point: run the monitoring loop."""
args = parse_args()
logger.info("=" * 60)
logger.info("Mithal.space Monitor started")
logger.info("Target URL : %s", args.target)
logger.info("Search query : %s", args.search_query)
logger.info("Check interval : %d seconds", args.interval)
logger.info("Max records : %d", args.max_records)
logger.info("Metrics file : %s", args.metrics_file)
logger.info("Log file : %s", LOG_FILE)
logger.info("=" * 60)
session = create_session()
try:
while True:
try:
record = perform_check(session, args.target, args.search_query)
# Load, append, trim, save
metrics = load_metrics(args.metrics_file)
metrics.append(record)
metrics = trim_metrics(metrics, args.max_records)
save_metrics(args.metrics_file, metrics)
# Summary log
status_emoji = "✓" if record["uptime"] else "✗"
logger.info(
"%s status=%d latency=%.0fms dns=%.0fms ssl_days=%s search=%s",
status_emoji,
record["status_code"],
record["latency_ms"],
record["dns_ms"] or 0,
record["ssl"]["days_remaining"] if record["ssl"] else "N/A",
f"{record['search_ms']:.0f}ms" if record["search_ms"] else "N/A",
)
except Exception as exc:
logger.exception("Unexpected error during check: %s", exc)
time.sleep(args.interval)
except KeyboardInterrupt:
logger.info("Monitor stopped by user (Ctrl+C)")
finally:
session.close()
logger.info("Session closed. Goodbye.")
if __name__ == "__main__":
main()
+23
View File
@@ -0,0 +1,23 @@
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
# Allow metrics.json to be fetched by the dashboard
location = /metrics.json {
add_header Cache-Control "no-cache, no-store, must-revalidate";
add_header Access-Control-Allow-Origin "*";
}
# Health check endpoint
location /health {
access_log off;
return 200 'OK';
add_header Content-Type text/plain;
}
}
+2
View File
@@ -0,0 +1,2 @@
requests>=2.31.0,<3.0.0
urllib3>=2.0.0,<3.0.0
@@ -0,0 +1,145 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 900 520" font-family="Segoe UI, Arial, sans-serif">
<defs>
<linearGradient id="dbg" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#0f172a"/>
<stop offset="100%" style="stop-color:#1e293b"/>
</linearGradient>
</defs>
<!-- Background -->
<rect width="900" height="520" fill="url(#dbg)" rx="12"/>
<!-- Title bar -->
<rect x="0" y="0" width="900" height="50" fill="#181b24" rx="12"/>
<rect x="0" y="20" width="900" height="30" fill="#181b24"/>
<text x="30" y="33" fill="#e2e8f0" font-size="16" font-weight="700">Mithal.space Monitor</text>
<text x="200" y="33" fill="#64748b" font-size="11">Real-time Website Health</text>
<circle cx="820" cy="25" r="8" fill="#22c55e"/>
<text x="835" y="29" fill="#94a3b8" font-size="10">Live</text>
<text x="870" y="29" fill="#94a3b8" font-size="10">30s</text>
<!-- Status Cards Row -->
<!-- Card 1: Status -->
<rect x="20" y="65" width="140" height="80" rx="10" fill="#1e293b" stroke="#22c55e" stroke-width="1.5"/>
<text x="35" y="85" fill="#64748b" font-size="9" font-weight="600" text-transform="uppercase">CURRENT STATUS</text>
<text x="35" y="115" fill="#22c55e" font-size="26" font-weight="700">UP</text>
<rect x="20" y="65" width="140" height="4" rx="2" fill="#22c55e"/>
<!-- Card 2: Latency -->
<rect x="175" y="65" width="140" height="80" rx="10" fill="#1e293b" stroke="#334155" stroke-width="1"/>
<text x="190" y="85" fill="#64748b" font-size="9" font-weight="600">HTTP LATENCY</text>
<text x="190" y="115" fill="#e2e8f0" font-size="26" font-weight="700">142</text>
<text x="260" y="115" fill="#64748b" font-size="10">ms</text>
<!-- Card 3: DNS -->
<rect x="330" y="65" width="140" height="80" rx="10" fill="#1e293b" stroke="#334155" stroke-width="1"/>
<text x="345" y="85" fill="#64748b" font-size="9" font-weight="600">DNS LOOKUP</text>
<text x="345" y="115" fill="#e2e8f0" font-size="26" font-weight="700">23</text>
<text x="395" y="115" fill="#64748b" font-size="10">ms</text>
<!-- Card 4: SSL -->
<rect x="485" y="65" width="140" height="80" rx="10" fill="#1e293b" stroke="#334155" stroke-width="1"/>
<text x="500" y="85" fill="#64748b" font-size="9" font-weight="600">SSL REMAINING</text>
<text x="500" y="115" fill="#22c55e" font-size="26" font-weight="700">47</text>
<text x="550" y="115" fill="#64748b" font-size="10">days</text>
<!-- Card 5: Search -->
<rect x="640" y="65" width="140" height="80" rx="10" fill="#1e293b" stroke="#334155" stroke-width="1"/>
<text x="655" y="85" fill="#64748b" font-size="9" font-weight="600">SEARCH RESPONSE</text>
<text x="655" y="115" fill="#e2e8f0" font-size="26" font-weight="700">312</text>
<text x="720" y="115" fill="#64748b" font-size="10">ms</text>
<!-- Card 6: Uptime -->
<rect x="795" y="65" width="90" height="80" rx="10" fill="#1e293b" stroke="#22c55e" stroke-width="1.5"/>
<text x="805" y="85" fill="#64748b" font-size="8" font-weight="600">UPTIME 24H</text>
<text x="805" y="115" fill="#22c55e" font-size="22" font-weight="700">99.8%</text>
<!-- Latency Chart -->
<rect x="20" y="160" width="530" height="200" rx="10" fill="#1e293b" stroke="#334155" stroke-width="1"/>
<text x="40" y="185" fill="#94a3b8" font-size="11" font-weight="600">Latency (Last Hour)</text>
<!-- Chart grid lines -->
<line x1="60" y1="200" x2="530" y2="200" stroke="#2a2d3a" stroke-width="0.5"/>
<line x1="60" y1="230" x2="530" y2="230" stroke="#2a2d3a" stroke-width="0.5"/>
<line x1="60" y1="260" x2="530" y2="260" stroke="#2a2d3a" stroke-width="0.5"/>
<line x1="60" y1="290" x2="530" y2="290" stroke="#2a2d3a" stroke-width="0.5"/>
<line x1="60" y1="320" x2="530" y2="320" stroke="#2a2d3a" stroke-width="0.5"/>
<!-- Y-axis labels -->
<text x="50" y="204" fill="#64748b" font-size="8" text-anchor="end">500</text>
<text x="50" y="234" fill="#64748b" font-size="8" text-anchor="end">400</text>
<text x="50" y="264" fill="#64748b" font-size="8" text-anchor="end">300</text>
<text x="50" y="294" fill="#64748b" font-size="8" text-anchor="end">200</text>
<text x="50" y="324" fill="#64748b" font-size="8" text-anchor="end">100</text>
<!-- Line chart -->
<polyline points="70,290 100,285 130,280 160,275 190,270 220,265 250,260 280,255 310,250 340,245 370,240 400,235 430,230 460,225 490,220 520,215" fill="none" stroke="#4361ee" stroke-width="2"/>
<!-- Fill under line -->
<polyline points="70,290 100,285 130,280 160,275 190,270 220,265 250,260 280,255 310,250 340,245 370,240 400,235 430,230 460,225 490,220 520,215 520,330 70,330" fill="rgba(67,97,238,0.1)"/>
<!-- Data points -->
<circle cx="70" cy="290" r="3" fill="#4361ee"/>
<circle cx="190" cy="270" r="3" fill="#4361ee"/>
<circle cx="310" cy="250" r="3" fill="#4361ee"/>
<circle cx="430" cy="230" r="3" fill="#4361ee"/>
<circle cx="520" cy="215" r="3" fill="#4361ee"/>
<!-- X-axis labels -->
<text x="70" y="348" fill="#64748b" font-size="8" text-anchor="middle">14:00</text>
<text x="190" y="348" fill="#64748b" font-size="8" text-anchor="middle">14:15</text>
<text x="310" y="348" fill="#64748b" font-size="8" text-anchor="middle">14:30</text>
<text x="430" y="348" fill="#64748b" font-size="8" text-anchor="middle">14:45</text>
<text x="520" y="348" fill="#64748b" font-size="8" text-anchor="middle">15:00</text>
<!-- Search Response Chart -->
<rect x="565" y="160" width="320" height="200" rx="10" fill="#1e293b" stroke="#334155" stroke-width="1"/>
<text x="585" y="185" fill="#94a3b8" font-size="11" font-weight="600">Search Response Time</text>
<!-- Bar chart -->
<rect x="590" y="260" width="25" height="70" rx="3" fill="rgba(34,197,94,0.6)"/>
<rect x="625" y="240" width="25" height="90" rx="3" fill="rgba(34,197,94,0.6)"/>
<rect x="660" y="250" width="25" height="80" rx="3" fill="rgba(34,197,94,0.6)"/>
<rect x="695" y="230" width="25" height="100" rx="3" fill="rgba(34,197,94,0.6)"/>
<rect x="730" y="245" width="25" height="85" rx="3" fill="rgba(34,197,94,0.6)"/>
<rect x="765" y="255" width="25" height="75" rx="3" fill="rgba(34,197,94,0.6)"/>
<rect x="800" y="235" width="25" height="95" rx="3" fill="rgba(34,197,94,0.6)"/>
<rect x="835" y="248" width="25" height="82" rx="3" fill="rgba(34,197,94,0.6)"/>
<!-- Table Section -->
<rect x="20" y="375" width="865" height="135" rx="10" fill="#1e293b" stroke="#334155" stroke-width="1"/>
<text x="40" y="400" fill="#94a3b8" font-size="11" font-weight="600">Latest 10 Checks</text>
<!-- Table header -->
<rect x="35" y="410" width="835" height="22" fill="#0f172a" rx="4"/>
<text x="55" y="425" fill="#64748b" font-size="8" font-weight="600">TIMESTAMP</text>
<text x="200" y="425" fill="#64748b" font-size="8" font-weight="600">HTTP</text>
<text x="280" y="425" fill="#64748b" font-size="8" font-weight="600">LATENCY</text>
<text x="370" y="425" fill="#64748b" font-size="8" font-weight="600">DNS</text>
<text x="450" y="425" fill="#64748b" font-size="8" font-weight="600">SSL</text>
<text x="540" y="425" fill="#64748b" font-size="8" font-weight="600">SEARCH</text>
<text x="640" y="425" fill="#64748b" font-size="8" font-weight="600">STATUS</text>
<!-- Table row 1 -->
<text x="55" y="445" fill="#94a3b8" font-size="8">2026-07-27 15:00:12</text>
<text x="200" y="445" fill="#e2e8f0" font-size="8">200</text>
<text x="280" y="445" fill="#e2e8f0" font-size="8">142ms</text>
<text x="370" y="445" fill="#e2e8f0" font-size="8">23ms</text>
<text x="450" y="445" fill="#22c55e" font-size="8">47d</text>
<text x="540" y="445" fill="#e2e8f0" font-size="8">312ms</text>
<rect x="640" y="435" width="38" height="14" rx="7" fill="#0d3329"/>
<text x="659" y="445" text-anchor="middle" fill="#22c55e" font-size="7" font-weight="600">UP</text>
<!-- Table row 2 -->
<text x="55" y="462" fill="#94a3b8" font-size="8">2026-07-27 14:59:12</text>
<text x="200" y="462" fill="#e2e8f0" font-size="8">200</text>
<text x="280" y="462" fill="#e2e8f0" font-size="8">138ms</text>
<text x="370" y="462" fill="#e2e8f0" font-size="8">21ms</text>
<text x="450" y="462" fill="#22c55e" font-size="8">47d</text>
<text x="540" y="462" fill="#e2e8f0" font-size="8">298ms</text>
<rect x="640" y="452" width="38" height="14" rx="7" fill="#0d3329"/>
<text x="659" y="462" text-anchor="middle" fill="#22c55e" font-size="7" font-weight="600">UP</text>
<!-- Table row 3 -->
<text x="55" y="479" fill="#94a3b8" font-size="8">2026-07-27 14:58:12</text>
<text x="200" y="479" fill="#e2e8f0" font-size="8">200</text>
<text x="280" y="479" fill="#e2e8f0" font-size="8">155ms</text>
<text x="370" y="479" fill="#e2e8f0" font-size="8">25ms</text>
<text x="450" y="479" fill="#22c55e" font-size="8">47d</text>
<text x="540" y="479" fill="#e2e8f0" font-size="8">341ms</text>
<rect x="640" y="469" width="38" height="14" rx="7" fill="#0d3329"/>
<text x="659" y="479" text-anchor="middle" fill="#22c55e" font-size="7" font-weight="600">UP</text>
</svg>

After

Width:  |  Height:  |  Size: 8.8 KiB

@@ -0,0 +1,37 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 700 300" font-family="Consolas, Monaco, monospace">
<defs>
<linearGradient id="tbg" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#1a1b26"/>
<stop offset="100%" style="stop-color:#16161e"/>
</linearGradient>
</defs>
<!-- Terminal background -->
<rect width="700" height="300" fill="url(#tbg)" rx="10"/>
<rect x="0" y="0" width="700" height="30" fill="#24283b" rx="10"/>
<rect x="0" y="15" width="700" height="15" fill="#24283b"/>
<!-- Terminal buttons -->
<circle cx="20" cy="15" r="6" fill="#f7768e"/>
<circle cx="40" cy="15" r="6" fill="#e0af68"/>
<circle cx="60" cy="15" r="6" fill="#9ece6a"/>
<text x="350" y="19" text-anchor="middle" fill="#565f89" font-size="10">monitor.py — Terminal</text>
<!-- Terminal content -->
<text x="15" y="55" fill="#9ece6a" font-size="11">$ python monitor.py --interval 60</text>
<text x="15" y="80" fill="#565f89" font-size="10">============================================================</text>
<text x="15" y="95" fill="#7aa2f7" font-size="10">Mithal.space Monitor started</text>
<text x="15" y="110" fill="#94a3b8" font-size="10">Target URL : https://mithal.space</text>
<text x="15" y="125" fill="#94a3b8" font-size="10">Check interval : 60 seconds</text>
<text x="15" y="140" fill="#94a3b8" font-size="10">Max records : 1440</text>
<text x="15" y="155" fill="#94a3b8" font-size="10">Metrics file : metrics.json</text>
<text x="15" y="170" fill="#565f89" font-size="10">============================================================</text>
<text x="15" y="195" fill="#9ece6a" font-size="10">✓ status=200 latency=142ms dns=23ms ssl_days=47 search=312ms</text>
<text x="15" y="215" fill="#9ece6a" font-size="10">✓ status=200 latency=138ms dns=21ms ssl_days=47 search=298ms</text>
<text x="15" y="235" fill="#9ece6a" font-size="10">✓ status=200 latency=155ms dns=25ms ssl_days=47 search=341ms</text>
<text x="15" y="260" fill="#bb9af7" font-size="10">[2026-07-27 15:00:12] INFO ✓ status=200 latency=142ms</text>
<text x="15" y="278" fill="#73daca" font-size="10">█</text>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

+423
View File
@@ -0,0 +1,423 @@
/* =========================================================================
Mithal.space Monitor — Dashboard JavaScript
========================================================================= */
(function () {
"use strict";
// -----------------------------------------------------------------------
// Configuration
// -----------------------------------------------------------------------
const METRICS_URL = "metrics.json";
const REFRESH_INTERVAL = 30; // seconds
const CHART_MAX_POINTS = 60; // last hour (60 × 60s)
const LAST_HOUR_CHECKS = 60;
// -----------------------------------------------------------------------
// State
// -----------------------------------------------------------------------
let countdownValue = REFRESH_INTERVAL;
let countdownTimer = null;
let metricsData = [];
let latencyChart = null;
let searchChart = null;
let dnsChart = null;
// -----------------------------------------------------------------------
// DOM References
// -----------------------------------------------------------------------
const $ = (sel) => document.querySelector(sel);
const $$ = (sel) => document.querySelectorAll(sel);
const els = {
loading: $("#loadingIndicator"),
currentStatus: $("#currentStatus"),
statusIndicator: $("#statusIndicator"),
currentLatency: $("#currentLatency"),
currentDns: $("#currentDns"),
currentSsl: $("#currentSsl"),
currentSearch: $("#currentSearch"),
uptimePercent: $("#uptimePercent"),
countdown: $("#countdown"),
refreshBtn: $("#refreshBtn"),
themeToggle: $("#themeToggle"),
exportCsv: $("#exportCsv"),
metricsBody: $("#metricsBody"),
};
// -----------------------------------------------------------------------
// Theme
// -----------------------------------------------------------------------
function loadTheme() {
const saved = localStorage.getItem("mithal-theme");
if (saved) {
document.documentElement.setAttribute("data-theme", saved);
}
}
function toggleTheme() {
const current = document.documentElement.getAttribute("data-theme");
const next = current === "dark" ? "light" : "dark";
document.documentElement.setAttribute("data-theme", next);
localStorage.setItem("mithal-theme", next);
updateChartColors();
}
// -----------------------------------------------------------------------
// Chart Colour Helpers
// -----------------------------------------------------------------------
function getThemeColors() {
const style = getComputedStyle(document.documentElement);
return {
line: style.getPropertyValue("--chart-line").trim(),
fill: style.getPropertyValue("--chart-fill").trim(),
text: style.getPropertyValue("--text-secondary").trim(),
grid: style.getPropertyValue("--border-color").trim(),
};
}
function updateChartColors() {
const c = getThemeColors();
[latencyChart, searchChart, dnsChart].forEach((chart) => {
if (!chart) return;
chart.options.scales.x.ticks.color = c.text;
chart.options.scales.y.ticks.color = c.text;
chart.options.scales.x.grid.color = c.grid;
chart.options.scales.y.grid.color = c.grid;
if (chart.data.datasets[0]) {
chart.data.datasets[0].borderColor = c.line;
chart.data.datasets[0].backgroundColor = c.fill;
}
chart.update("none");
});
}
// -----------------------------------------------------------------------
// Fetch Metrics
// -----------------------------------------------------------------------
async function fetchMetrics() {
try {
els.loading.classList.add("active");
const resp = await fetch(METRICS_URL + "?t=" + Date.now());
if (!resp.ok) throw new Error("HTTP " + resp.status);
metricsData = await resp.json();
} catch (err) {
console.warn("Failed to load metrics:", err);
metricsData = [];
} finally {
els.loading.classList.remove("active");
}
}
// -----------------------------------------------------------------------
// Update Cards
// -----------------------------------------------------------------------
function updateCards() {
if (metricsData.length === 0) {
els.currentStatus.textContent = "--";
els.currentLatency.textContent = "--";
els.currentDns.textContent = "--";
els.currentSsl.textContent = "--";
els.currentSearch.textContent = "--";
els.uptimePercent.textContent = "--";
return;
}
const latest = metricsData[metricsData.length - 1];
// Status
if (latest.uptime) {
els.currentStatus.textContent = "UP";
els.currentStatus.className = "card-value status-up";
els.statusIndicator.style.background = "var(--green)";
els.currentStatus.classList.add("card-status-pulse");
} else {
els.currentStatus.textContent = "DOWN";
els.currentStatus.className = "card-value status-down";
els.statusIndicator.style.background = "var(--red)";
els.currentStatus.classList.remove("card-status-pulse");
}
// Values
els.currentLatency.textContent = latest.latency_ms != null ? Math.round(latest.latency_ms) : "--";
els.currentDns.textContent = latest.dns_ms != null ? Math.round(latest.dns_ms) : "--";
els.currentSearch.textContent = latest.search_ms != null ? Math.round(latest.search_ms) : "--";
// SSL
if (latest.ssl && latest.ssl.days_remaining != null) {
const days = latest.ssl.days_remaining;
els.currentSsl.textContent = days;
if (days <= 14) {
els.currentSsl.className = "card-value status-down";
} else if (days <= 30) {
els.currentSsl.className = "card-value status-warn";
} else {
els.currentSsl.className = "card-value";
}
} else {
els.currentSsl.textContent = "--";
els.currentSsl.className = "card-value";
}
// Uptime %
const upChecks = metricsData.filter((m) => m.uptime).length;
const uptime = ((upChecks / metricsData.length) * 100).toFixed(2);
els.uptimePercent.textContent = uptime;
if (parseFloat(uptime) >= 99.5) {
els.uptimePercent.className = "card-value status-up";
} else if (parseFloat(uptime) >= 95) {
els.uptimePercent.className = "card-value status-warn";
} else {
els.uptimePercent.className = "card-value status-down";
}
}
// -----------------------------------------------------------------------
// Update Charts
// -----------------------------------------------------------------------
function updateCharts() {
const c = getThemeColors();
// Last hour of data (approx 60 checks at 60s interval)
const lastHour = metricsData.slice(-LAST_HOUR_CHECKS);
const labels = lastHour.map((m) => {
const d = new Date(m.timestamp);
return d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
});
const latencyData = lastHour.map((m) => (m.latency_ms != null ? Math.round(m.latency_ms) : null));
const searchData = lastHour.map((m) => (m.search_ms != null ? Math.round(m.search_ms) : null));
const dnsData = lastHour.map((m) => (m.dns_ms != null ? Math.round(m.dns_ms) : null));
// --- Latency Chart ---
if (latencyChart) latencyChart.destroy();
latencyChart = new Chart($("#latencyChart"), {
type: "line",
data: {
labels: labels,
datasets: [
{
label: "Latency (ms)",
data: latencyData,
borderColor: c.line,
backgroundColor: c.fill,
fill: true,
tension: 0.35,
pointRadius: 2,
borderWidth: 2,
},
],
},
options: chartOptions(c, "ms"),
});
// --- Search Chart ---
if (searchChart) searchChart.destroy();
searchChart = new Chart($("#searchChart"), {
type: "line",
data: {
labels: labels,
datasets: [
{
label: "Search (ms)",
data: searchData,
borderColor: "#22c55e",
backgroundColor: "rgba(34,197,94,0.1)",
fill: true,
tension: 0.35,
pointRadius: 2,
borderWidth: 2,
},
],
},
options: chartOptions(c, "ms"),
});
// --- DNS Chart ---
if (dnsChart) dnsChart.destroy();
dnsChart = new Chart($("#dnsChart"), {
type: "bar",
data: {
labels: labels,
datasets: [
{
label: "DNS (ms)",
data: dnsData,
backgroundColor: "rgba(234,179,8,0.6)",
borderColor: "#eab308",
borderWidth: 1,
borderRadius: 3,
},
],
},
options: chartOptions(c, "ms"),
});
}
function chartOptions(c, unit) {
return {
responsive: true,
maintainAspectRatio: true,
interaction: { intersect: false, mode: "index" },
plugins: {
legend: { display: false },
tooltip: {
backgroundColor: "rgba(0,0,0,0.8)",
titleColor: "#fff",
bodyColor: "#fff",
padding: 10,
cornerRadius: 8,
callbacks: {
label: function (ctx) {
return ctx.parsed.y != null ? ctx.parsed.y + " " + unit : "N/A";
},
},
},
},
scales: {
x: {
ticks: { color: c.text, maxTicksLimit: 8, font: { size: 11 } },
grid: { color: c.grid },
},
y: {
ticks: { color: c.text, font: { size: 11 } },
grid: { color: c.grid },
beginAtZero: true,
},
},
};
}
// -----------------------------------------------------------------------
// Update Table
// -----------------------------------------------------------------------
function updateTable() {
const last10 = metricsData.slice(-10).reverse();
if (last10.length === 0) {
els.metricsBody.innerHTML =
'<tr><td colspan="7" class="no-data">No data available</td></tr>';
return;
}
els.metricsBody.innerHTML = last10
.map((m) => {
const ts = new Date(m.timestamp).toLocaleString();
const sslDays = m.ssl && m.ssl.days_remaining != null ? m.ssl.days_remaining : "--";
const searchMs = m.search_ms != null ? Math.round(m.search_ms) : "--";
let statusClass = "status-down";
let statusText = "DOWN";
if (m.uptime) {
if (m.status_code >= 300) {
statusClass = "status-warn";
statusText = "WARN";
} else {
statusClass = "status-up";
statusText = "UP";
}
}
return `<tr>
<td>${ts}</td>
<td>${m.status_code || "--"}</td>
<td>${m.latency_ms != null ? Math.round(m.latency_ms) : "--"}</td>
<td>${m.dns_ms != null ? Math.round(m.dns_ms) : "--"}</td>
<td>${sslDays}</td>
<td>${searchMs}</td>
<td><span class="status-badge ${statusClass}">${statusText}</span></td>
</tr>`;
})
.join("");
}
// -----------------------------------------------------------------------
// Countdown & Auto-Refresh
// -----------------------------------------------------------------------
function resetCountdown() {
countdownValue = REFRESH_INTERVAL;
els.countdown.textContent = countdownValue + "s";
}
async function refreshAll() {
await fetchMetrics();
updateCards();
updateCharts();
updateTable();
resetCountdown();
}
function startCountdown() {
if (countdownTimer) clearInterval(countdownTimer);
countdownTimer = setInterval(() => {
countdownValue--;
if (countdownValue <= 0) {
refreshAll();
} else {
els.countdown.textContent = countdownValue + "s";
}
}, 1000);
}
// -----------------------------------------------------------------------
// CSV Export
// -----------------------------------------------------------------------
function exportCsv() {
if (metricsData.length === 0) return;
const headers = [
"Timestamp",
"Status Code",
"Uptime",
"Latency (ms)",
"DNS (ms)",
"SSL Valid",
"SSL Expires",
"SSL Days Remaining",
"Search (ms)",
];
const rows = metricsData.map((m) => [
m.timestamp,
m.status_code,
m.uptime,
m.latency_ms,
m.dns_ms,
m.ssl ? m.ssl.valid : "",
m.ssl ? m.ssl.expires : "",
m.ssl ? m.ssl.days_remaining : "",
m.search_ms,
]);
const csvContent = [headers, ...rows].map((r) => r.join(",")).join("\n");
const blob = new Blob(["\uFEFF" + csvContent], { type: "text/csv;charset=utf-8;" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "mithal_monitor_" + new Date().toISOString().slice(0, 10) + ".csv";
a.click();
URL.revokeObjectURL(url);
}
// -----------------------------------------------------------------------
// Init
// -----------------------------------------------------------------------
function init() {
loadTheme();
refreshAll();
startCountdown();
els.refreshBtn.addEventListener("click", () => refreshAll());
els.themeToggle.addEventListener("click", toggleTheme);
els.exportCsv.addEventListener("click", exportCsv);
}
// Start when DOM is ready
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}
})();
+428
View File
@@ -0,0 +1,428 @@
/* =========================================================================
Mithal.space Monitor — Dashboard Stylesheet
========================================================================= */
/* ---------- Reset & Base ---------- */
*,
*::before,
*::after {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
/* Light theme */
--bg-primary: #f0f2f5;
--bg-secondary: #ffffff;
--bg-card: #ffffff;
--text-primary: #1a1a2e;
--text-secondary: #555770;
--text-muted: #8e8ea0;
--border-color: #e0e0e6;
--accent: #4361ee;
--accent-light: #e8ecff;
--green: #22c55e;
--green-bg: #dcfce7;
--yellow: #eab308;
--yellow-bg: #fef9c3;
--red: #ef4444;
--red-bg: #fee2e2;
--chart-line: #4361ee;
--chart-fill: rgba(67, 97, 238, 0.12);
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.06);
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.08);
--radius: 12px;
--radius-sm: 8px;
--transition: 0.25s ease;
}
[data-theme="dark"] {
--bg-primary: #0f1117;
--bg-secondary: #181b24;
--bg-card: #1e2130;
--text-primary: #e4e4ed;
--text-secondary: #a0a0b8;
--text-muted: #6b6b80;
--border-color: #2a2d3a;
--accent: #6c8cff;
--accent-light: #1e2740;
--green: #34d399;
--green-bg: #0d3329;
--yellow: #facc15;
--yellow-bg: #3b3508;
--red: #f87171;
--red-bg: #3b1212;
--chart-line: #6c8cff;
--chart-fill: rgba(108, 140, 255, 0.10);
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.3);
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.4);
}
html {
font-size: 15px;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
line-height: 1.6;
transition: background var(--transition), color var(--transition);
}
/* ---------- Dashboard Container ---------- */
.dashboard {
max-width: 1280px;
margin: 0 auto;
padding: 20px;
}
/* ---------- Header ---------- */
.header {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 12px;
padding: 18px 24px;
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: var(--radius);
box-shadow: var(--shadow-sm);
margin-bottom: 24px;
transition: background var(--transition), border var(--transition);
}
.header-title {
font-size: 1.35rem;
font-weight: 700;
letter-spacing: -0.02em;
}
.header-subtitle {
font-size: 0.85rem;
color: var(--text-muted);
margin-left: 10px;
}
.header-right {
display: flex;
align-items: center;
gap: 12px;
}
/* Theme Toggle */
.theme-toggle {
background: var(--bg-primary);
border: 1px solid var(--border-color);
border-radius: var(--radius-sm);
padding: 6px 10px;
cursor: pointer;
font-size: 1.15rem;
color: var(--text-primary);
transition: background var(--transition);
}
.theme-toggle:hover {
background: var(--accent-light);
}
.icon-moon { display: none; }
[data-theme="dark"] .icon-sun { display: none; }
[data-theme="dark"] .icon-moon { display: inline; }
/* Refresh Info */
.refresh-info {
display: flex;
align-items: center;
gap: 6px;
}
.countdown {
font-size: 0.82rem;
color: var(--text-muted);
min-width: 28px;
text-align: right;
}
.refresh-btn {
background: var(--accent);
color: #fff;
border: none;
border-radius: var(--radius-sm);
padding: 6px 12px;
cursor: pointer;
font-size: 1rem;
transition: opacity var(--transition);
}
.refresh-btn:hover {
opacity: 0.85;
}
/* Export Button */
.export-btn {
background: var(--bg-primary);
border: 1px solid var(--border-color);
border-radius: var(--radius-sm);
padding: 6px 14px;
cursor: pointer;
font-size: 0.85rem;
color: var(--text-primary);
transition: background var(--transition);
}
.export-btn:hover {
background: var(--accent-light);
}
/* ---------- Loading Indicator ---------- */
.loading-indicator {
display: none;
align-items: center;
justify-content: center;
gap: 12px;
padding: 32px;
color: var(--text-muted);
font-size: 0.95rem;
}
.loading-indicator.active {
display: flex;
}
.spinner {
width: 24px;
height: 24px;
border: 3px solid var(--border-color);
border-top-color: var(--accent);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* ---------- Status Cards ---------- */
.cards {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(185px, 1fr));
gap: 16px;
margin-bottom: 28px;
}
.card {
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: var(--radius);
padding: 20px;
box-shadow: var(--shadow-sm);
transition: background var(--transition), border var(--transition),
box-shadow var(--transition);
position: relative;
overflow: hidden;
}
.card:hover {
box-shadow: var(--shadow-md);
}
.card-label {
font-size: 0.78rem;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--text-muted);
margin-bottom: 8px;
}
.card-value {
font-size: 1.65rem;
font-weight: 700;
letter-spacing: -0.02em;
}
.card-unit {
font-size: 0.78rem;
color: var(--text-muted);
margin-top: 2px;
}
.card-indicator {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 4px;
}
/* Status animation */
.card-status-pulse {
animation: pulse 2s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.6; }
}
/* ---------- Charts ---------- */
.charts {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(380px, 1fr));
gap: 16px;
margin-bottom: 28px;
}
.chart-container {
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: var(--radius);
padding: 20px;
box-shadow: var(--shadow-sm);
transition: background var(--transition), border var(--transition);
}
.chart-container h3 {
font-size: 0.92rem;
font-weight: 600;
margin-bottom: 12px;
color: var(--text-secondary);
}
/* ---------- Table ---------- */
.table-section {
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: var(--radius);
padding: 20px;
box-shadow: var(--shadow-sm);
transition: background var(--transition), border var(--transition);
margin-bottom: 24px;
}
.table-section h3 {
font-size: 0.92rem;
font-weight: 600;
margin-bottom: 12px;
color: var(--text-secondary);
}
.table-wrapper {
overflow-x: auto;
}
table {
width: 100%;
border-collapse: collapse;
font-size: 0.88rem;
}
thead th {
text-align: left;
padding: 10px 12px;
border-bottom: 2px solid var(--border-color);
color: var(--text-muted);
font-weight: 600;
font-size: 0.8rem;
text-transform: uppercase;
letter-spacing: 0.04em;
white-space: nowrap;
}
tbody td {
padding: 10px 12px;
border-bottom: 1px solid var(--border-color);
white-space: nowrap;
}
tbody tr:last-child td {
border-bottom: none;
}
tbody tr:hover {
background: var(--accent-light);
}
.no-data {
text-align: center;
color: var(--text-muted);
padding: 24px 0 !important;
}
/* Status badge */
.status-badge {
display: inline-block;
padding: 3px 10px;
border-radius: 20px;
font-size: 0.78rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.04em;
}
.status-up {
background: var(--green-bg);
color: var(--green);
}
.status-down {
background: var(--red-bg);
color: var(--red);
}
.status-warn {
background: var(--yellow-bg);
color: var(--yellow);
}
/* ---------- Responsive ---------- */
@media (max-width: 768px) {
.dashboard {
padding: 12px;
}
.header {
flex-direction: column;
align-items: flex-start;
padding: 14px 16px;
}
.header-right {
width: 100%;
justify-content: space-between;
}
.cards {
grid-template-columns: repeat(2, 1fr);
gap: 10px;
}
.card {
padding: 14px;
}
.card-value {
font-size: 1.3rem;
}
.charts {
grid-template-columns: 1fr;
}
table {
font-size: 0.8rem;
}
thead th, tbody td {
padding: 8px 8px;
}
}
@media (max-width: 480px) {
.cards {
grid-template-columns: 1fr;
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB