commit c653222887940b8f3154597bfc8866d701fc8c1f Author: Mohamed Moustafa Date: Mon Jul 27 23:27:12 2026 +0300 first commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..f617752 --- /dev/null +++ b/README.md @@ -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. diff --git a/common-mortakaz/integration-1.md b/common-mortakaz/integration-1.md new file mode 100644 index 0000000..997597a --- /dev/null +++ b/common-mortakaz/integration-1.md @@ -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. \ No newline at end of file diff --git a/common-mortakaz/integration-2.md b/common-mortakaz/integration-2.md new file mode 100644 index 0000000..073983b --- /dev/null +++ b/common-mortakaz/integration-2.md @@ -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. \ No newline at end of file diff --git a/common-qabilah/qabilah-profile.txt b/common-qabilah/qabilah-profile.txt new file mode 100644 index 0000000..4f89220 --- /dev/null +++ b/common-qabilah/qabilah-profile.txt @@ -0,0 +1 @@ +https://qabilah.com/profile/mohamed-moustafa20/ \ No newline at end of file diff --git a/ghaymah ui.png b/ghaymah ui.png new file mode 100644 index 0000000..fec730e Binary files /dev/null and b/ghaymah ui.png differ diff --git a/mithal.png b/mithal.png new file mode 100644 index 0000000..9f47393 Binary files /dev/null and b/mithal.png differ diff --git a/q1-deploy-monitor/Dockerfile b/q1-deploy-monitor/Dockerfile new file mode 100644 index 0000000..12ba0c3 --- /dev/null +++ b/q1-deploy-monitor/Dockerfile @@ -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"] diff --git a/q1-deploy-monitor/architecture.svg b/q1-deploy-monitor/architecture.svg new file mode 100644 index 0000000..7505fdf --- /dev/null +++ b/q1-deploy-monitor/architecture.svg @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Q1: Deploy & Monitor Architecture + Node.js API on Ghaymah Cloud + + + + User + Browser + + + + + + + + Load Balancer + Ghaymah + + + + + + + Docker Container + + Node.js + Express + Port 3000 + /health endpoint + Request counter + Static dashboard + + + + + + + Block Storage + Persistent Data + + + + Monitoring + + health-check.sh + Every 30 seconds + Dashboard + Status / Latency / Requests + HTML / CSS / JS + + + + + + + + ghaymah.systems + + + + + Infrastructure + + Application + + Storage + + Monitoring + Deploy: gy resource app launch + diff --git a/q1-deploy-monitor/ghaymah.json b/q1-deploy-monitor/ghaymah.json new file mode 100644 index 0000000..4e1cb7a --- /dev/null +++ b/q1-deploy-monitor/ghaymah.json @@ -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" + } +} diff --git a/q1-deploy-monitor/health-check.sh b/q1-deploy-monitor/health-check.sh new file mode 100755 index 0000000..f39b57c --- /dev/null +++ b/q1-deploy-monitor/health-check.sh @@ -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 diff --git a/q1-deploy-monitor/package.json b/q1-deploy-monitor/package.json new file mode 100644 index 0000000..8ea2ef7 --- /dev/null +++ b/q1-deploy-monitor/package.json @@ -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" + } +} diff --git a/q1-deploy-monitor/public/.DS_Store b/q1-deploy-monitor/public/.DS_Store new file mode 100644 index 0000000..5008ddf Binary files /dev/null and b/q1-deploy-monitor/public/.DS_Store differ diff --git a/q1-deploy-monitor/public/dashboard.css b/q1-deploy-monitor/public/dashboard.css new file mode 100644 index 0000000..2007c08 --- /dev/null +++ b/q1-deploy-monitor/public/dashboard.css @@ -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; + } +} diff --git a/q1-deploy-monitor/public/dashboard.html b/q1-deploy-monitor/public/dashboard.html new file mode 100644 index 0000000..b22ce35 --- /dev/null +++ b/q1-deploy-monitor/public/dashboard.html @@ -0,0 +1,41 @@ + + + + + + Ghaymah Monitoring Dashboard + + + +
+
+

Ghaymah Monitoring Dashboard

+ Last update: -- +
+ +
+
+
Application Status
+
--
+
+ +
+
Response Time
+
-- ms
+
+ +
+
Total Requests
+
--
+
+
+ +
+
+ Live +
+
+ + + + diff --git a/q1-deploy-monitor/public/dashboard.js b/q1-deploy-monitor/public/dashboard.js new file mode 100644 index 0000000..ad07649 --- /dev/null +++ b/q1-deploy-monitor/public/dashboard.js @@ -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); diff --git a/q1-deploy-monitor/public/index.html b/q1-deploy-monitor/public/index.html new file mode 100644 index 0000000..8425a6d --- /dev/null +++ b/q1-deploy-monitor/public/index.html @@ -0,0 +1,37 @@ + + + + + + Team Availability + + + +
+
+

Team Availability

+
+ +
+
+
Click save after updating each week separately
+ + + + + + + + + + + + + + + +
NameWeekMonTueWedThuFriSatSun
+
+ + + diff --git a/q1-deploy-monitor/public/script.js b/q1-deploy-monitor/public/script.js new file mode 100644 index 0000000..dac030a --- /dev/null +++ b/q1-deploy-monitor/public/script.js @@ -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."); + } +}); diff --git a/q1-deploy-monitor/public/styles.css b/q1-deploy-monitor/public/styles.css new file mode 100644 index 0000000..b9f9979 --- /dev/null +++ b/q1-deploy-monitor/public/styles.css @@ -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; +} diff --git a/q1-deploy-monitor/server.js b/q1-deploy-monitor/server.js new file mode 100644 index 0000000..36af9c7 --- /dev/null +++ b/q1-deploy-monitor/server.js @@ -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}`); +}); \ No newline at end of file diff --git a/q2-postmortem/postmortem-report.md b/q2-postmortem/postmortem-report.md new file mode 100644 index 0000000..6dbea27 --- /dev/null +++ b/q2-postmortem/postmortem-report.md @@ -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. \ No newline at end of file diff --git a/q2-postmortem/timeline.svg b/q2-postmortem/timeline.svg new file mode 100644 index 0000000..57310c0 --- /dev/null +++ b/q2-postmortem/timeline.svg @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + Q2: OOMKilled Incident — Postmortem Timeline + 45-minute outage caused by repeated memory limit breaches + + + + + + + + 10:00 + New version deployed + Deploy triggered normally + + + + 10:05 + Memory usage spikes + Rapid memory consumption detected + + + + 10:10 + First OOMKilled event + Container killed by Linux OOM Killer + + + + 10:11 - 10:35 + Crash loop continues + Multiple restart attempts, each hitting memory limit + + + + 10:35 + Team identifies root cause + Memory leak found, fix deployed + + + + Root Cause + + • Application exceeded memory limit + • Memory leak in new version + • No memory monitoring/alerting + • Auto-restart created crash loop + + + + Impact + + 45 min downtime • 100% request failure • 12+ restarts + + + + Fixed: Memory leak patched + limits increased + diff --git a/q3-cicd/Readme.md b/q3-cicd/Readme.md new file mode 100644 index 0000000..833ef38 --- /dev/null +++ b/q3-cicd/Readme.md @@ -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": "", + "projectId": "", + "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 +``` \ No newline at end of file diff --git a/q3-cicd/pipeline.svg b/q3-cicd/pipeline.svg new file mode 100644 index 0000000..e8469c1 --- /dev/null +++ b/q3-cicd/pipeline.svg @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + Q3: CI/CD Pipeline — GitHub Actions + Ghaymah + Automated Build → Push → Deploy with Manual Approval Gate + + + + + TRIGGER + git push + main branch + + + + + + + + BUILD + Docker + docker build + + + + + + + TEST + npm test + Verify build + + + + + + + PUSH + Registry + ghaymah CR + + + + + + + APPROVAL + Manual Gate + Required before + production deploy + + + + + + + DEPLOY + Live + prod + + + + Staging Environment + + • Test data, not real users + • Auto-deploy on push to main + • Developers & QA validate here + • Mirrors production config + + + Production Environment + + • Live users, real data + • Requires manual approval + • High availability & monitoring + • Careful rollout process + diff --git a/q3-cicd/workflow.yml b/q3-cicd/workflow.yml new file mode 100644 index 0000000..3c7fc13 --- /dev/null +++ b/q3-cicd/workflow.yml @@ -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." + \ No newline at end of file diff --git a/q4-scalability/architecture.png b/q4-scalability/architecture.png new file mode 100644 index 0000000..b4c637b Binary files /dev/null and b/q4-scalability/architecture.png differ diff --git a/q4-scalability/architecture.svg b/q4-scalability/architecture.svg new file mode 100644 index 0000000..1fefe6d --- /dev/null +++ b/q4-scalability/architecture.svg @@ -0,0 +1,155 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Q4: Scalability Architecture — 15,000 req/s + 39 Containers × 500 req/s each (30% safety margin) + + + + Users + 15K req/s + + + + CDN + Cache Layer + Static Assets + + + + + + + + + + + Load Balancer + Ghaymah LB + Round Robin + Health Checks + + + + Container Pool — 39 Containers + + + + + + + Container 1 + + Container 2 + + Container 3 + + Container 4 + + Cont. 5 + + + + + + Container 6 + + Container 7 + + Container 8 + + Container 9 + + Cont. 10 + + + + . . . + 39 total (500 req/s each) + + + + Warm Pool: 2-3 Containers + Ready for cold start traffic + + + + Auto-Scaling: >70% CPU + Spin up new containers + + + + Each Container + Node.js + Express + 500 req/s capacity + Stateless + Health: /health + + + + + + + + Block Storage + PostgreSQL / MongoDB + + + + + + + + Monitoring + Prometheus + Grafana + + + + + + + + ghaymah.systems + + + + + CDN + + Load Balancer + + Containers (39×) + + Block Storage + + Auto-Scaling + 15,000 req/s ÷ 500 req/s = 30 + 30% = 39 containers + diff --git a/q4-scalability/calculations.md b/q4-scalability/calculations.md new file mode 100644 index 0000000..d38c88a --- /dev/null +++ b/q4-scalability/calculations.md @@ -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. \ No newline at end of file diff --git a/q5-mithal-monitor/.dockerignore b/q5-mithal-monitor/.dockerignore new file mode 100644 index 0000000..ea58fbc --- /dev/null +++ b/q5-mithal-monitor/.dockerignore @@ -0,0 +1,10 @@ +__pycache__ +*.pyc +*.pyo +.git +.gitignore +screenshots +*.log +venv +.env +.DS_Store diff --git a/q5-mithal-monitor/Dockerfile b/q5-mithal-monitor/Dockerfile new file mode 100644 index 0000000..6e6964c --- /dev/null +++ b/q5-mithal-monitor/Dockerfile @@ -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"] diff --git a/q5-mithal-monitor/README.md b/q5-mithal-monitor/README.md new file mode 100644 index 0000000..b281445 --- /dev/null +++ b/q5-mithal-monitor/README.md @@ -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 diff --git a/q5-mithal-monitor/__pycache__/monitor.cpython-312.pyc b/q5-mithal-monitor/__pycache__/monitor.cpython-312.pyc new file mode 100644 index 0000000..e4eb09e Binary files /dev/null and b/q5-mithal-monitor/__pycache__/monitor.cpython-312.pyc differ diff --git a/q5-mithal-monitor/dashboard.html b/q5-mithal-monitor/dashboard.html new file mode 100644 index 0000000..56f4757 --- /dev/null +++ b/q5-mithal-monitor/dashboard.html @@ -0,0 +1,113 @@ + + + + + + Mithal.space Monitor Dashboard + + + + +
+ +
+
+

Mithal.space Monitor

+ Real-time Website Health +
+
+ +
+ 30s + +
+ +
+
+ + +
+
+ Loading metrics... +
+ + +
+
+
Current Status
+
--
+
+
+
+
HTTP Latency
+
--
+
ms
+
+
+
DNS Lookup
+
--
+
ms
+
+
+
SSL Remaining
+
--
+
days
+
+
+
Search Response
+
--
+
ms
+
+
+
Uptime (24h)
+
--
+
%
+
+
+ + +
+
+

Latency (Last Hour)

+ +
+
+

Search Response Time

+ +
+
+

DNS Lookup Time

+ +
+
+ + +
+

Latest 10 Checks

+
+ + + + + + + + + + + + + + + +
TimestampHTTPLatency (ms)DNS (ms)SSL RemainingSearch (ms)Status
No data available
+
+
+
+ + + + diff --git a/q5-mithal-monitor/entrypoint.sh b/q5-mithal-monitor/entrypoint.sh new file mode 100644 index 0000000..10e0415 --- /dev/null +++ b/q5-mithal-monitor/entrypoint.sh @@ -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;" diff --git a/q5-mithal-monitor/metrics.json b/q5-mithal-monitor/metrics.json new file mode 100644 index 0000000..0b4c98c --- /dev/null +++ b/q5-mithal-monitor/metrics.json @@ -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 + } +] \ No newline at end of file diff --git a/q5-mithal-monitor/monitor.log b/q5-mithal-monitor/monitor.log new file mode 100644 index 0000000..e69de29 diff --git a/q5-mithal-monitor/monitor.py b/q5-mithal-monitor/monitor.py new file mode 100644 index 0000000..68bd473 --- /dev/null +++ b/q5-mithal-monitor/monitor.py @@ -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=. + 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() diff --git a/q5-mithal-monitor/nginx.conf b/q5-mithal-monitor/nginx.conf new file mode 100644 index 0000000..5f81437 --- /dev/null +++ b/q5-mithal-monitor/nginx.conf @@ -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; + } +} diff --git a/q5-mithal-monitor/requirements.txt b/q5-mithal-monitor/requirements.txt new file mode 100644 index 0000000..abbb0fb --- /dev/null +++ b/q5-mithal-monitor/requirements.txt @@ -0,0 +1,2 @@ +requests>=2.31.0,<3.0.0 +urllib3>=2.0.0,<3.0.0 diff --git a/q5-mithal-monitor/screenshots/dashboard-preview.svg b/q5-mithal-monitor/screenshots/dashboard-preview.svg new file mode 100644 index 0000000..e49bf5c --- /dev/null +++ b/q5-mithal-monitor/screenshots/dashboard-preview.svg @@ -0,0 +1,145 @@ + + + + + + + + + + + + + + + Mithal.space Monitor + Real-time Website Health + + Live + 30s + + + + + CURRENT STATUS + UP + + + + + HTTP LATENCY + 142 + ms + + + + DNS LOOKUP + 23 + ms + + + + SSL REMAINING + 47 + days + + + + SEARCH RESPONSE + 312 + ms + + + + UPTIME 24H + 99.8% + + + + Latency (Last Hour) + + + + + + + + 500 + 400 + 300 + 200 + 100 + + + + + + + + + + + + 14:00 + 14:15 + 14:30 + 14:45 + 15:00 + + + + Search Response Time + + + + + + + + + + + + + Latest 10 Checks + + + + TIMESTAMP + HTTP + LATENCY + DNS + SSL + SEARCH + STATUS + + + 2026-07-27 15:00:12 + 200 + 142ms + 23ms + 47d + 312ms + + UP + + + 2026-07-27 14:59:12 + 200 + 138ms + 21ms + 47d + 298ms + + UP + + + 2026-07-27 14:58:12 + 200 + 155ms + 25ms + 47d + 341ms + + UP + diff --git a/q5-mithal-monitor/screenshots/monitor-preview.svg b/q5-mithal-monitor/screenshots/monitor-preview.svg new file mode 100644 index 0000000..c44381c --- /dev/null +++ b/q5-mithal-monitor/screenshots/monitor-preview.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + monitor.py — Terminal + + + $ python monitor.py --interval 60 + + ============================================================ + Mithal.space Monitor started + Target URL : https://mithal.space + Check interval : 60 seconds + Max records : 1440 + Metrics file : metrics.json + ============================================================ + + ✓ status=200 latency=142ms dns=23ms ssl_days=47 search=312ms + ✓ status=200 latency=138ms dns=21ms ssl_days=47 search=298ms + ✓ status=200 latency=155ms dns=25ms ssl_days=47 search=341ms + + [2026-07-27 15:00:12] INFO ✓ status=200 latency=142ms + + diff --git a/q5-mithal-monitor/script.js b/q5-mithal-monitor/script.js new file mode 100644 index 0000000..beea2bd --- /dev/null +++ b/q5-mithal-monitor/script.js @@ -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 = + 'No data available'; + 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 ` + ${ts} + ${m.status_code || "--"} + ${m.latency_ms != null ? Math.round(m.latency_ms) : "--"} + ${m.dns_ms != null ? Math.round(m.dns_ms) : "--"} + ${sslDays} + ${searchMs} + ${statusText} + `; + }) + .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(); + } +})(); diff --git a/q5-mithal-monitor/style.css b/q5-mithal-monitor/style.css new file mode 100644 index 0000000..1c21e60 --- /dev/null +++ b/q5-mithal-monitor/style.css @@ -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; + } +} diff --git a/teamsavailability .png b/teamsavailability .png new file mode 100644 index 0000000..23f3851 Binary files /dev/null and b/teamsavailability .png differ