first commit
@@ -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
|
||||
|
||||

|
||||
|
||||
### 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
|
||||
|
||||

|
||||
|
||||
### 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
|
||||
|
||||

|
||||
|
||||
### 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
|
||||
|
||||

|
||||
|
||||
### 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
|
||||
|
||||

|
||||
|
||||
### Monitor Output
|
||||
|
||||

|
||||
|
||||
### 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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -0,0 +1 @@
|
||||
https://qabilah.com/profile/mohamed-moustafa20/
|
||||
|
After Width: | Height: | Size: 279 KiB |
|
After Width: | Height: | Size: 133 KiB |
@@ -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"]
|
||||
@@ -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 & 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 |
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
@@ -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>
|
||||
@@ -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.");
|
||||
}
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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}`);
|
||||
});
|
||||
@@ -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.
|
||||
@@ -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 |
@@ -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
|
||||
```
|
||||
@@ -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 & 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 & monitoring</text>
|
||||
<text x="480" y="309" fill="#94a3b8" font-size="9">• Careful rollout process</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.7 KiB |
@@ -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."
|
||||
|
||||
|
After Width: | Height: | Size: 32 KiB |
@@ -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: >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 |
@@ -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.
|
||||
@@ -0,0 +1,10 @@
|
||||
__pycache__
|
||||
*.pyc
|
||||
*.pyo
|
||||
.git
|
||||
.gitignore
|
||||
screenshots
|
||||
*.log
|
||||
venv
|
||||
.env
|
||||
.DS_Store
|
||||
@@ -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"]
|
||||
@@ -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
|
||||
@@ -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">☼</span>
|
||||
<span class="icon-moon">☾</span>
|
||||
</button>
|
||||
<div class="refresh-info">
|
||||
<span id="countdown" class="countdown">30s</span>
|
||||
<button id="refreshBtn" class="refresh-btn" title="Refresh now">↻</button>
|
||||
</div>
|
||||
<button id="exportCsv" class="export-btn" title="Export CSV">↧ 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>
|
||||
@@ -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;"
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
@@ -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()
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 |
@@ -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();
|
||||
}
|
||||
})();
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 133 KiB |