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 |