Spaces:
Sleeping
Sleeping
Crypto0203 commited on
Commit ·
8def838
0
Parent(s):
Deploy ParleDowntime Pro Space
Browse files- .gitignore +3 -0
- .vscode/settings.json +11 -0
- Dockerfile +22 -0
- Procfile +1 -0
- README.md +36 -0
- app.py +1064 -0
- config.json +58 -0
- license.dat +1 -0
- render.yaml +12 -0
- requirements.txt +6 -0
- runtime.txt +1 -0
- security.py +51 -0
- static/parle_tech_logo.png +0 -0
- static/script.js +584 -0
- static/script_v5.js +353 -0
- static/script_v6.js +812 -0
- static/style.css +381 -0
- static/style_v5.css +271 -0
- static/style_v6.css +463 -0
- templates/dashboard_v5.html +246 -0
- templates/dashboard_v6.html +452 -0
- update_logo_b64.py +23 -0
.gitignore
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
monitor_metrics.db
|
| 2 |
+
*.db
|
| 3 |
+
__pycache__/
|
.vscode/settings.json
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"python.testing.unittestArgs": [
|
| 3 |
+
"-v",
|
| 4 |
+
"-s",
|
| 5 |
+
"./__pycache__",
|
| 6 |
+
"-p",
|
| 7 |
+
"*test.py"
|
| 8 |
+
],
|
| 9 |
+
"python.testing.pytestEnabled": false,
|
| 10 |
+
"python.testing.unittestEnabled": true
|
| 11 |
+
}
|
Dockerfile
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
| 2 |
+
|
| 3 |
+
# Install system dependencies
|
| 4 |
+
RUN apt-get update && apt-get install -y \
|
| 5 |
+
build-essential \
|
| 6 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 7 |
+
|
| 8 |
+
WORKDIR /app
|
| 9 |
+
|
| 10 |
+
# Copy requirements and install
|
| 11 |
+
COPY requirements.txt .
|
| 12 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 13 |
+
|
| 14 |
+
# Copy the rest of the application
|
| 15 |
+
COPY . .
|
| 16 |
+
|
| 17 |
+
# Hugging Face Spaces uses port 7860 by default
|
| 18 |
+
ENV PORT=7860
|
| 19 |
+
EXPOSE 7860
|
| 20 |
+
|
| 21 |
+
# Run the application
|
| 22 |
+
CMD ["gunicorn", "--bind", "0.0.0.0:7860", "app:app"]
|
Procfile
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
web: gunicorn app:app
|
README.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: StatusPilot
|
| 3 |
+
emoji: 🚀
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: indigo
|
| 6 |
+
sdk: docker
|
| 7 |
+
pinned: false
|
| 8 |
+
---
|
| 9 |
+
|
| 10 |
+
# StatusPilot Cloud Version
|
| 11 |
+
|
| 12 |
+
This version is optimized for 24/7 cloud deployment on Hugging Face Spaces.
|
| 13 |
+
|
| 14 |
+
## 🚀 Deployment Instructions (Hugging Face)
|
| 15 |
+
|
| 16 |
+
1. **Go to your Space**: [huggingface.co/spaces/ChotuStatus/StatusPilot](https://huggingface.co/spaces/ChotuStatus/StatusPilot)
|
| 17 |
+
2. **Click Files**: Go to the "Files and versions" tab.
|
| 18 |
+
3. **Edit README.md**: Make sure the **top part** of your README.md looks exactly like the code above (between the `---` lines).
|
| 19 |
+
4. **Upload All Files**: Ensure you have uploaded:
|
| 20 |
+
- `app.py`
|
| 21 |
+
- `requirements.txt`
|
| 22 |
+
- `Dockerfile`
|
| 23 |
+
- `security.py`
|
| 24 |
+
- `static/` (folder)
|
| 25 |
+
- `templates/` (folder)
|
| 26 |
+
- `config.json`
|
| 27 |
+
- `license.dat`
|
| 28 |
+
5. **Restart**: Once the `README.md` has the `sdk: docker` line, Hugging Face will start building your dashboard!
|
| 29 |
+
|
| 30 |
+
## 🔧 Features
|
| 31 |
+
- **Neural AI Service Addition**: Add services using natural language.
|
| 32 |
+
- **24/7 Monitoring**: Runs continuously in the cloud.
|
| 33 |
+
- **Auto-Activation**: Uses your existing `license.dat`.
|
| 34 |
+
|
| 35 |
+
## ⚠️ Important Note
|
| 36 |
+
The **Outlook Alert** feature requires the Windows Outlook Desktop app and will not function in the cloud. Please use **SMTP (Email)** alerts for cloud operation.
|
app.py
ADDED
|
@@ -0,0 +1,1064 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import base64
|
| 3 |
+
import sys
|
| 4 |
+
import json
|
| 5 |
+
import time
|
| 6 |
+
import sqlite3
|
| 7 |
+
import threading
|
| 8 |
+
import requests
|
| 9 |
+
import io
|
| 10 |
+
import re
|
| 11 |
+
import xlsxwriter
|
| 12 |
+
import smtplib
|
| 13 |
+
import imaplib
|
| 14 |
+
from email.mime.text import MIMEText
|
| 15 |
+
from email.mime.multipart import MIMEMultipart
|
| 16 |
+
from email.header import Header
|
| 17 |
+
from datetime import datetime, timedelta
|
| 18 |
+
import ssl
|
| 19 |
+
import socket
|
| 20 |
+
from flask import Flask, request, jsonify, render_template, send_file, session, Response
|
| 21 |
+
|
| 22 |
+
try:
|
| 23 |
+
import win32com.client
|
| 24 |
+
OUTL_AVAIL = True
|
| 25 |
+
except ImportError:
|
| 26 |
+
OUTL_AVAIL = False
|
| 27 |
+
|
| 28 |
+
try:
|
| 29 |
+
import security
|
| 30 |
+
except ImportError:
|
| 31 |
+
print("Warning: security.py not found. License bypass may occur or app might fail.")
|
| 32 |
+
|
| 33 |
+
def get_license_status():
|
| 34 |
+
lic_file = 'license.dat'
|
| 35 |
+
if not os.path.exists(lic_file):
|
| 36 |
+
return False, "Missing license.dat file."
|
| 37 |
+
|
| 38 |
+
try:
|
| 39 |
+
with open(lic_file, 'r') as f:
|
| 40 |
+
lic_data = json.load(f)
|
| 41 |
+
email = lic_data.get('email', '')
|
| 42 |
+
key = lic_data.get('key', '')
|
| 43 |
+
if not security.verify_license_key(email, key):
|
| 44 |
+
return False, "Invalid License Key."
|
| 45 |
+
return True, f"Licensed to {email}"
|
| 46 |
+
except Exception as e:
|
| 47 |
+
return False, f"License read error: {e}"
|
| 48 |
+
|
| 49 |
+
# --- SMART PATHING FOR CLOUD DEPLOYMENT ---
|
| 50 |
+
import os
|
| 51 |
+
|
| 52 |
+
# Detect if we are in a 'flat' environment (all files in root) or standard (templates/static)
|
| 53 |
+
base_dir = os.path.dirname(os.path.abspath(__file__))
|
| 54 |
+
template_dir = os.path.join(base_dir, 'templates')
|
| 55 |
+
static_dir = os.path.join(base_dir, 'static')
|
| 56 |
+
|
| 57 |
+
# Fallback to root if folders don't exist (helps users who can't create folders)
|
| 58 |
+
if not os.path.exists(template_dir):
|
| 59 |
+
template_dir = base_dir
|
| 60 |
+
if not os.path.exists(static_dir):
|
| 61 |
+
static_dir = base_dir
|
| 62 |
+
|
| 63 |
+
app = Flask(__name__,
|
| 64 |
+
template_folder=template_dir,
|
| 65 |
+
static_folder=static_dir,
|
| 66 |
+
static_url_path='/static')
|
| 67 |
+
# CRITICAL: Use a custom secret key from environment or a strong default
|
| 68 |
+
app.secret_key = os.environ.get('SESSION_SECRET', os.urandom(32).hex())
|
| 69 |
+
|
| 70 |
+
# --- VERSIONING ---
|
| 71 |
+
VERSION = "5.1.0-CRYSTAL-PRO"
|
| 72 |
+
|
| 73 |
+
@app.route('/api/version')
|
| 74 |
+
def get_version():
|
| 75 |
+
return jsonify({"version": VERSION})
|
| 76 |
+
|
| 77 |
+
# --- CLOUD SESSION HARDENING (IFRAME COMPATIBLE) ---
|
| 78 |
+
app.config.update(
|
| 79 |
+
SESSION_COOKIE_HTTPONLY=True,
|
| 80 |
+
SESSION_COOKIE_SECURE=True, # Must be True for SameSite=None
|
| 81 |
+
SESSION_COOKIE_SAMESITE='None', # Allows cookie in iframes (HF Spaces)
|
| 82 |
+
PERMANENT_SESSION_LIFETIME=timedelta(days=30),
|
| 83 |
+
SESSION_REFRESH_EACH_REQUEST=True
|
| 84 |
+
)
|
| 85 |
+
|
| 86 |
+
def check_auth():
|
| 87 |
+
"""Verify if the session is authenticated via Master Key."""
|
| 88 |
+
master_key = os.environ.get('MASTER_KEY')
|
| 89 |
+
# If no key is set at all (None), we allow access (local mode)
|
| 90 |
+
if master_key is None:
|
| 91 |
+
return True
|
| 92 |
+
# If a key is set but empty, it's a security risk - require auth anyway
|
| 93 |
+
if master_key.strip() == "":
|
| 94 |
+
return False
|
| 95 |
+
return session.get('authenticated') == True
|
| 96 |
+
|
| 97 |
+
@app.before_request
|
| 98 |
+
def protect_apis():
|
| 99 |
+
"""Protect all /api routes except for version, auth and static routes."""
|
| 100 |
+
if request.path.startswith('/api/') and not request.path.startswith('/api/auth/') and not request.path == '/api/version':
|
| 101 |
+
if not check_auth():
|
| 102 |
+
return jsonify({"error": "Unauthorized Access", "code": 401}), 401
|
| 103 |
+
|
| 104 |
+
@app.route('/api/auth/status')
|
| 105 |
+
def auth_status():
|
| 106 |
+
master_key = os.environ.get('MASTER_KEY')
|
| 107 |
+
has_key = master_key is not None and master_key.strip() != ""
|
| 108 |
+
return jsonify({
|
| 109 |
+
"required": has_key,
|
| 110 |
+
"authenticated": check_auth()
|
| 111 |
+
})
|
| 112 |
+
|
| 113 |
+
@app.route('/api/auth/login', methods=['POST'])
|
| 114 |
+
def login():
|
| 115 |
+
data = request.json
|
| 116 |
+
key = data.get('key', '').strip()
|
| 117 |
+
master_key = os.environ.get('MASTER_KEY', '').strip()
|
| 118 |
+
|
| 119 |
+
if master_key and key == master_key:
|
| 120 |
+
session['authenticated'] = True
|
| 121 |
+
session.permanent = True
|
| 122 |
+
return jsonify({"success": True})
|
| 123 |
+
return jsonify({"success": False, "message": "Invalid Master Access Key"})
|
| 124 |
+
|
| 125 |
+
@app.route('/api/license/status')
|
| 126 |
+
def is_licensed():
|
| 127 |
+
# check_auth is already handled by @app.before_request
|
| 128 |
+
valid, msg = get_license_status()
|
| 129 |
+
return jsonify({"licensed": valid, "message": msg})
|
| 130 |
+
|
| 131 |
+
@app.route('/api/license/activate', methods=['POST'])
|
| 132 |
+
def activate_license():
|
| 133 |
+
data = request.json
|
| 134 |
+
email = data.get('email', '')
|
| 135 |
+
key = data.get('key', '')
|
| 136 |
+
if security.verify_license_key(email, key):
|
| 137 |
+
with open('license.dat', 'w') as f:
|
| 138 |
+
json.dump({"email": email, "key": key}, f)
|
| 139 |
+
return jsonify({"success": True})
|
| 140 |
+
return jsonify({"success": False, "message": "Invalid Purchase Email or Key"})
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
def check_ssl(hostname):
|
| 144 |
+
"""Check SSL certificate expiration and status."""
|
| 145 |
+
try:
|
| 146 |
+
context = ssl.create_default_context()
|
| 147 |
+
with socket.create_connection((hostname, 443), timeout=10) as sock:
|
| 148 |
+
with context.wrap_socket(sock, server_hostname=hostname) as ssock:
|
| 149 |
+
cert = ssock.getpeercert()
|
| 150 |
+
expiry_str = cert.get('notAfter')
|
| 151 |
+
expiry_date = datetime.strptime(expiry_str, '%b %d %H:%M:%S %Y %Z')
|
| 152 |
+
days_left = (expiry_date - datetime.now()).days
|
| 153 |
+
if days_left < 7:
|
| 154 |
+
return False, f"SSL Warning: Expiring in {days_left} days", days_left
|
| 155 |
+
return True, f"SSL Valid ({days_left} days left)", days_left
|
| 156 |
+
except Exception as e:
|
| 157 |
+
return False, f"SSL Error: {str(e)}", 0
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
DEFAULT_BROWSER_HEADERS = {
|
| 161 |
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
|
| 162 |
+
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8',
|
| 163 |
+
'Accept-Language': 'en-US,en;q=0.9',
|
| 164 |
+
'Cache-Control': 'no-cache',
|
| 165 |
+
'Pragma': 'no-cache'
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
CONFIG_FILE = 'config.json'
|
| 169 |
+
DB_FILE = 'monitor_metrics.db'
|
| 170 |
+
LAST_ALERT_METRICS = {}
|
| 171 |
+
SERVICE_STATE_CACHE = {}
|
| 172 |
+
FAIL_COUNT_CACHE = {}
|
| 173 |
+
SUCCESS_COUNT_CACHE = {}
|
| 174 |
+
DOWN_START_TIME = {}
|
| 175 |
+
FLAP_COUNTER = {}
|
| 176 |
+
|
| 177 |
+
def get_db_connection():
|
| 178 |
+
conn = sqlite3.connect(DB_FILE, check_same_thread=False)
|
| 179 |
+
conn.row_factory = sqlite3.Row
|
| 180 |
+
return conn
|
| 181 |
+
|
| 182 |
+
def init_db():
|
| 183 |
+
conn = get_db_connection()
|
| 184 |
+
c = conn.cursor()
|
| 185 |
+
c.execute('''CREATE TABLE IF NOT EXISTS status_history
|
| 186 |
+
(timestamp DATETIME, name TEXT, url TEXT, location TEXT, is_up INTEGER, response_time REAL, message TEXT)''')
|
| 187 |
+
conn.commit()
|
| 188 |
+
conn.close()
|
| 189 |
+
|
| 190 |
+
PARLE_TECH_LOGO_B64 = "iVBORw0KGgoAAAANSUhEUgAAAJwAAABoCAYAAAAEu5lCAAAPh0lEQVR4nO2da2wc13XHf/Pa9y5NkaEokXqQlk05rqvYjJWktStZLoJEQOUPqYICTdJGDZAYRVIgsQ23Hww6BYpWzIe2aAO3TWigTYuksoE4CZQiqSzbilNUCtMaaVIwtmlRpkSK5UPa5+zOqx9m+RDJ3Z3ZXc5S5P0BC0jcnbt3Zv733HvOuXNWchzHQSAICLnVHRBsL4TgBIEiBCcIFCE4QaAIwQkCRQhOEChCcIJAEYITBIoQnCBQhOAEgSIEJwgUIThBoAjBCQJFCE4QKEJwgkARghMEihCcIFCE4ASBIgQnCBQhOEGgCMEJAkUIThAoQnCCQBGCEwSKEJwgUNSgv9CwbcYzesX3+5MRNLmxcWDYNi9Nm4RUONEVqruNawUb3SyteS+ihtgdlevu58pr0K7FaI/YDZ8zwEweFoy87+Miaoh98WCkEKjgFoVw8kq0sm2VYVAxONnu8MW9al03Yjxr8efXJY7FTE501dfX8azF6asOI5lo5Q/JDp9JFnhil8ZASvPc9vdnTb58VWPUUDmVLPHMXpl98cYF98cTJUZyUbD9HTeomZwdgK5Yw12oSWumVLX8zeu9gFFL5elZjQ+9YfHynOGracO2+VnWYdRQmbMVxtL+jl9DpX6W+/q1TJSDv1T5i8slJnKm77Y7ZKux/q1gqa1qfV7nNWoFZ3cCn1IBMHEtmWayUy3c8tbZYnLp/VFUnrpichqDYx3eLMi1gs3FjAWyxhu6xCtzli/rsx6Dytp+LvXVdvv69IwGGHwx6mN69KlPP1Tq82qum1Hujxm0RyIEYX9aIzgAGY7FTE4PpNa89cK1EievaWDDqK3yzzMGxzq8NatbDsOZyNKxh3SHj+TM+tcoJqDAl3ZH14h+Imfy5Ss2Ixn370/PatwTM+peNzYNG073wbGOtdd2fRobkH7YlF7qb+8Oca5vefi/oUueptaJnMm3550lC4nsHnvpps9FjUf2xVWe2SvzZFvZCbLhRwuW/6l1gzDsjTnvRtiUggO4MyJxKrkssrF8bcHplsOZBemWsxo1VC5mrA27+PviKg+1K0vfOZ610a0Wl9yzQTY2Z9m/TSu43VGZu6O4U6Olkq6xsF1yFooqg5rJZ5IFTsUNsF0RXFho3uJ8NQlF4mOSG464LIe42qCfspXZtIIDSCnep6bxrMXzc+7Udiji8MQujcMR16q96MS4lKlzxG/kFWpy23O2shQSsTWpuY03idYIrujU9NCuFWy+Pqu5n7Xhnhoxoqm8w9lcEoC7ozCQ0rgrpbhuUc7hlwV8r61iJQcKtYWatRxetGJQcPiQbNPjYQ2+05ah4LgiaSbl/vbgNCWY3Gxa56VWwbBtLt20GS1qEIbjis1ApPKInciZvHjdgaLD8YTDByLu33s0OBU3GMmpjGQ0Ds8U+Wxfc095Imfywyl3UBCW6NRsIop3EcVKTV5rhd3r9JVZkwfytQfYQEzj4XYlMHG2RnA1llNuNqJs3YDuNouBVOVQg245/I0RgjB0x0x+vcs9rf6EwolwgRFLhYLD/6QlDLs5aSSAsZt5Tl9TGbmhAQ5EJe7vVDyFYK7LNtBk67ZI0eFr05Han7PgY20FHm6vkk1pMq2zcEWH4ekw49nc0p8uyyFGiyrkVVBcsR1PODzVU9m6GbbNK3MWpBVISHy0jSVBabLMwc4QzEuQdhg3VS4sWBzr8Ce40aIbgN5/eVVf0ytuqgVDCZ2PdoZrtlcy3TbZCG+2uKJNC1fTiwN89b9bQMun1Bezq0eXs3xhEhJf2l89UzCetfiHG+7VOyUZPNh2q5j6EwrPdRT5XDrE2azE4eum56zFEkWH0aLCKJUtwZM9RX6/V9s066bjCYfdiWLVzyxkHT61J7igL7RScIsjzVp1g0I2xCWGYjp/cjCMJle+IEuhkAUVFOhIWeyOqmucg46wDDEJ8g4XCyHG0ob3dNdKK7Gyr8pyXO+ve2w+d1fYl9gGTYvRjfLZLHiyx+JodwDZeJ+01MKdSpo8dfet4nC3J0lQxZoscq1g8/2b5f9YMHw1zPBVWOt8y4A71ZzNSpzwk1+1ZIg5nNm7bD11s8Q3r0oMTbvT579lJe6bMTna7TOltXqwNYtyu81crzaLFjkNMig2e+MWA231j0LdchiZUddanko3UrGhJHNxVuYjXbXzq/mSu3YcNC06ZVZ8XuV3JIMrOZORjMrZeYXD0RJ9yQZytk2gI2tAgDs/6qGlvZs16h99EzmTb06aUNJAsRkM2RzqsCuGGaZMkxeNGGTgjaLMpZs2++J1fz0DKY0THTlGMu4lHJoO875IgX39rbukhdDmFhtsAqehXnTL4bvz7rR4Kmny3KCKJlc+nZl8mP6f5xgmzmhB5dX/K/JYd2NTzvH9UYb0IkNXo2DJ/P1ChIN+1ofbkM01wXvEsG1+PG0yuiBDyJ2aawmnKwbHFz0yxeY/MgqvzzS2q0OTZY7sUBiMuu2cnVf4wZTka6NAtGQSlTZm3G+29Ru0ysLpNkTqvxjXCjYvZNwF+iOyxNGkgRcnY1dM4tQdJUamVUYtlZ/dNDnaXbuvqYhCpX3bR7tD/GG6wKkJFXSbL0zL3Nfm0YHQbV4uholfLnCnVnuzJMCHe3ZU3wpuyaDbvDpvMZmZ99QmwIM7k/QnNj7j0NIptVOrb8vQOxmbs/MhiMCRjgJH9njbaNifUHiorcjItHva/522mWhkc2aZYz0aT97MMXwjDiV4dd6q6kBoukHKiUFEYrQgM/q2DNQOGBOROZ8s0RWrIeY4DL0d9tYmgCNzJl6iP+Ht443QGpvruKPwSs5/uHsiZ/JP7wJlg3DvHd7b0GSZvpQCKff4kWmV70zWPi4tVc/F7YurHN+j8UjEgYJ7s78zWXkDpBHROG+4n6WAez28vPTqAzRfktz2sh7bW3wFSOAW7r6ExNABVy1HdvgXXFRSOdypszdeoDsq8ZjP2FdfUubMXSY/v+GK6N5OtXq8ypFJORK1HoVyp9ASR+bddnclFRZ0dd3p78E2mef6Skx72ImykkhMoS9ZWSAnOgz2xuvb93dfor4n5PwiiV+EXp+xtMHpMYeRqRCPtDs8c5fhP7ArWMPmc2MEWxohOEGgCMFVwnDI5NzdFmZ2bbkHQX0IwVVCk0igQS7oR+381wa5nbhtU1urWaxb8tW3/aeV+iIGn9xDTadgbK7E6XF4R6/+HZ/cqfOJA962Kxm2zTfeKnLhmsTzmRAQYdEjPpnM8fG7wzzWXdmDfPXdNP84HeHNWYdH99k8vj/quUbI372j863JMOcNh3OHrEC2mm8ZwQFMZWTOvyOBz4Dy+XiIh9oKNbMOUwa8Oetw4UYVx96Q+aBpsdArV73xEzmTv/1FkeG3olQK0J7Jxjkz5f77yQM5nrg3vqbN8YLGyFQIbsCBqA77q5/DSi7OypyfdMCQmT8YzMPbW0ZwC7pMMZ0FR4X1llyhFdvBS6vKhSUi7FBri1QydCC89vh1KDgmlS7vy3MGQxdtLtxYlY5bWUFp1VQ+/FaU43uqZBnqyNp0YLh91GyKCyXo3ng5bBnBdcXg5HtjPNh96xrohqHw0hWF56/IcAcM70rzW/0RpvMrRZPlnkp1OFZsobqnI8Wf/kq6Zl/274hVTGu9Ml3iz163uJBbFtvD7ylysl/hV2NuP9ojNuNZix9MSXzhigpT8Om9Ou9NVXkwpoGtXg0d65MtIzhwU0z74rcKZyJn8vqc620+ahR4f2eYgY4QAx3+g7hdMeiKeS0Qs5axtMEz/+VwoRReskjDB7L80WCivHZabFtmICUzkILPD8BL4zkOtmmVp2ipXMLpNuD26GUDRCWV2XLO9pwW5abTukIzXx8rceGGtmRRRgaLPPFgquZC/bH+ePUBEnA+tBG2lIWrRLdjlpPkeaSSz/xtAUJ5GwqN3dRXpkv89IoM8661/cZRi48faNJDLrn6+nc5Z7vi97YzqincPkMjaAyHSKl5udOfvKtzrrxue7hf5v3doaaEIEKG5Toxpn/VJAKsC7eIEFwAGLbNfFaGjCuKz+4q0d++LSaXNWzPs/bDKstRa/v4elbrx1ezXJzXQI1Cyqane+MCrO0Rrw5Ens644w4CdTuUegiIQqNOghrl3HSBc69Z8Fr1tNN+Jcdf/lqMx/pvfRzMMcMsZg8eDRXZpW1AnQU1yvMTec7/y5znQy7b7wlcAVtecE1BjULtWC+mbZOtsKnyXMnjdu96kMrOhx7jMpvvafuVCMH5odrCvMq0lC4GuMfVj/MQ4FS69JWBf2MLCFkapIsQjZHRM7gJco+ki9Blcf43FI72Raus4Wwgtu7aLBWWyumwGOeyDlNphwGPVdm90JvLMpnVePyQxl/9prtpwMta86lLOYb/Nw7Xqxe9aSZb3kvVdZsZ23X/e3NZkpHGQgGaLFd9rUdfl8LjuyKuePVY0y3eZHz5casFfblUWa1+qlkn0BgcbAPBtUdb/xzC7qhMl1yuNJ0u8tNJiZlmbXszHHpz2SY1tvFsecGtZKUl8IKeac5Uo8kyD/Q6sNN1HJ4ds/lFgNPYZmL7CM5S67METapG9MG9ET4dL89feZlHXrMYm2vO1vVJW6m/n+kibKQHvYrtI7gyGb2OH1GQGg81dMXgUw/I9CbKJbWmwhz8nulJdBM5k7Gb+U35yzJ+2XaCqwtnecFl2Lan13oc7Qsz9IAC7Qoopiu6F0IM/XtxXeHN5OErl9Ls/6rFwW/D+MIG7XRRNu5HU1azLcIieqYIVpRJWyFZpfz+uigmzCg88i0VFA/rrlIYevPlMMraqeoPPhAlEs7wiR+WnZkFi2f/M8yzP7Hc9mM2vbLFZFYrT5PlrMVkmB+9VaC/fZ0qAWqUmqXhq1EIrsL0lhfcQmGF5VCj/uNwK9c3lrcb05uvXhL/d9+XpLcjzdGXLZiMLbdtKVCCNeVOSkBXgTu7KkxI6SIUltNnXrlhxDamknoVtrzg2qMhundkIVTk0U6bnjbvYZJIMgyd0Gv7czYGuhR2papb0iN7Uji/B6+8U+Rf39T47tt5JuUELFiuVQWI2TzaDZ+/3+b4/ui6cb5kRIMuCyji6tG7AzDYVqS3zWIyqdATC2ar0jaqLbK4DvPrANQTMKvPyZjImejlCkmRiOyjjFi959bosf7ZRoITbAaElyoIFCE4QaAIwQkCRQhOEChCcIJAEYITBIoQnCBQhOAEgSIEJwgUIThBoAjBCQJFCE4QKEJwgkARghMEihCcIFCE4ASBIgQnCBQhOEGgCMEJAkUIThAoQnCCQBGCEwSKEJwgUITgBIHy/xBrrmEbfk0tAAAAAElFTkSuQmCC"
|
| 191 |
+
|
| 192 |
+
DEFAULT_SMTP_SENDER = os.environ.get("SMTP_SENDER", "smadapathi04@gmail.com")
|
| 193 |
+
DEFAULT_SMTP_PASSWORD_RAW = os.environ.get("SMTP_PASSWORD", "")
|
| 194 |
+
DEFAULT_SMTP_PASSWORD_ENC = security.encrypt_secret(DEFAULT_SMTP_PASSWORD_RAW) if DEFAULT_SMTP_PASSWORD_RAW else ""
|
| 195 |
+
DEFAULT_EMAIL_RECIPIENTS = ["suresh@parletech.com", "smadapathi04@gmail.com"]
|
| 196 |
+
|
| 197 |
+
@app.after_request
|
| 198 |
+
def add_no_cache_headers(response):
|
| 199 |
+
response.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate, max-age=0'
|
| 200 |
+
response.headers['Pragma'] = 'no-cache'
|
| 201 |
+
response.headers['Expires'] = '0'
|
| 202 |
+
return response
|
| 203 |
+
|
| 204 |
+
@app.route('/static/parle_tech_logo.png')
|
| 205 |
+
def serve_parle_logo():
|
| 206 |
+
img_data = base64.b64decode(PARLE_TECH_LOGO_B64)
|
| 207 |
+
return Response(img_data, mimetype='image/png')
|
| 208 |
+
|
| 209 |
+
def load_config():
|
| 210 |
+
config = {}
|
| 211 |
+
if os.path.exists(CONFIG_FILE):
|
| 212 |
+
try:
|
| 213 |
+
with open(CONFIG_FILE, 'r', encoding='utf-8') as f:
|
| 214 |
+
config = json.load(f)
|
| 215 |
+
except Exception:
|
| 216 |
+
config = {}
|
| 217 |
+
|
| 218 |
+
if config.get("services_to_monitor") is None:
|
| 219 |
+
config["services_to_monitor"] = [
|
| 220 |
+
{"name": "Microsoft 365", "url": "https://status.office365.com", "location": "Global", "type": "http_code", "expected_code": 200},
|
| 221 |
+
{"name": "RingCentral", "url": "https://status.ringcentral.com", "location": "Global", "type": "http_code", "expected_code": 200},
|
| 222 |
+
{"name": "Skybridge Healthcare", "url": "https://www.skybridgehealthcare.com/", "location": "Primary Web", "type": "http_code", "expected_code": 200},
|
| 223 |
+
{"name": "Skybridge Aviation", "url": "https://skybridgeaviation.com/", "location": "Primary Web", "type": "http_code", "expected_code": 200},
|
| 224 |
+
{"name": "Skybridge Resources", "url": "https://skybridgeresources.com/", "location": "Primary Web", "type": "http_code", "expected_code": 200},
|
| 225 |
+
{"name": "BetterLending Website", "url": "https://betterlending.com/", "location": "Primary Web", "type": "http_code", "expected_code": 200}
|
| 226 |
+
]
|
| 227 |
+
|
| 228 |
+
config.setdefault("check_interval", 60)
|
| 229 |
+
config.setdefault("consecutive_failures_required", 3)
|
| 230 |
+
config.setdefault("retry_delay_seconds", 3)
|
| 231 |
+
config.setdefault("cooldown_minutes", 60)
|
| 232 |
+
config.setdefault("alert_mode", "SMTP")
|
| 233 |
+
|
| 234 |
+
# Senior DevOps Fallback to guarantee credentials even in stateless Docker
|
| 235 |
+
if not config.get("smtp_sender"):
|
| 236 |
+
config["smtp_sender"] = os.environ.get("SMTP_SENDER", DEFAULT_SMTP_SENDER)
|
| 237 |
+
if not config.get("smtp_password"):
|
| 238 |
+
config["smtp_password"] = os.environ.get("SMTP_PASSWORD", DEFAULT_SMTP_PASSWORD_ENC)
|
| 239 |
+
|
| 240 |
+
if not config.get("email_recipients"):
|
| 241 |
+
env_recips = os.environ.get("EMAIL_RECIPIENTS", "").strip()
|
| 242 |
+
if env_recips:
|
| 243 |
+
config["email_recipients"] = [r.strip() for r in env_recips.split(',') if r.strip()]
|
| 244 |
+
else:
|
| 245 |
+
config["email_recipients"] = DEFAULT_EMAIL_RECIPIENTS
|
| 246 |
+
|
| 247 |
+
return config
|
| 248 |
+
|
| 249 |
+
def save_config(config):
|
| 250 |
+
if "smtp_password" in config and config["smtp_password"]:
|
| 251 |
+
pwd = str(config["smtp_password"]).strip()
|
| 252 |
+
if pwd and not pwd.startswith("••••") and not pwd.startswith("ENC:"):
|
| 253 |
+
config["smtp_password"] = security.encrypt_secret(pwd)
|
| 254 |
+
with open(CONFIG_FILE, 'w', encoding='utf-8') as f:
|
| 255 |
+
json.dump(config, f, indent=4)
|
| 256 |
+
|
| 257 |
+
def check_service_logic(service):
|
| 258 |
+
url = service.get("url", "")
|
| 259 |
+
check_type = service.get("type", "http_code")
|
| 260 |
+
start_time = time.time()
|
| 261 |
+
try:
|
| 262 |
+
if check_type == "http_code":
|
| 263 |
+
expected = service.get("expected_code", 200)
|
| 264 |
+
response = requests.get(url, headers=DEFAULT_BROWSER_HEADERS, timeout=12, allow_redirects=True)
|
| 265 |
+
latency = round((time.time() - start_time) * 1000, 2)
|
| 266 |
+
if response.status_code != expected:
|
| 267 |
+
if response.status_code in [403, 406, 429, 503]:
|
| 268 |
+
sess = requests.Session()
|
| 269 |
+
sess.headers.update(DEFAULT_BROWSER_HEADERS)
|
| 270 |
+
r2 = sess.get(url, timeout=12, allow_redirects=True)
|
| 271 |
+
if r2.status_code == expected:
|
| 272 |
+
return True, "OK (WAF Bypass)", round((time.time() - start_time) * 1000, 2)
|
| 273 |
+
return False, f"HTTP {response.status_code}", latency
|
| 274 |
+
return True, "OK", latency
|
| 275 |
+
elif check_type == "json_status":
|
| 276 |
+
response = requests.get(url, headers=DEFAULT_BROWSER_HEADERS, timeout=12, allow_redirects=True)
|
| 277 |
+
latency = round((time.time() - start_time) * 1000, 2)
|
| 278 |
+
data = response.json()
|
| 279 |
+
key = service.get("key_to_check", "")
|
| 280 |
+
expected_val = service.get("expected_value", "")
|
| 281 |
+
val = data
|
| 282 |
+
for k in key.split('.'):
|
| 283 |
+
val = val.get(k)
|
| 284 |
+
if val is None: break
|
| 285 |
+
if str(val).lower() != str(expected_val).lower():
|
| 286 |
+
return False, f"JSON {val}", latency
|
| 287 |
+
return True, "OK", latency
|
| 288 |
+
elif check_type == "m365_mail":
|
| 289 |
+
sender = service.get('sender_email')
|
| 290 |
+
pwd = service.get('sender_password')
|
| 291 |
+
receiver = service.get('receiver_email', sender)
|
| 292 |
+
imap_server = service.get('imap_server', 'outlook.office365.com')
|
| 293 |
+
|
| 294 |
+
pulse_id = f"StatusPilot-Pulse-{int(time.time())}"
|
| 295 |
+
msg = MIMEMultipart()
|
| 296 |
+
msg['From'] = sender
|
| 297 |
+
msg['To'] = receiver
|
| 298 |
+
msg['Subject'] = pulse_id
|
| 299 |
+
msg.attach(MIMEText(f"Mail Flow Heartbeat sent at {datetime.now()}", 'plain'))
|
| 300 |
+
|
| 301 |
+
try:
|
| 302 |
+
with smtplib.SMTP('smtp.office365.com', 587, timeout=15) as server:
|
| 303 |
+
server.starttls()
|
| 304 |
+
server.login(sender, pwd)
|
| 305 |
+
server.sendmail(sender, receiver, msg.as_string())
|
| 306 |
+
|
| 307 |
+
with imaplib.IMAP4_SSL(imap_server) as mail:
|
| 308 |
+
mail.login(receiver, service.get('receiver_password', pwd))
|
| 309 |
+
mail.select('inbox')
|
| 310 |
+
typ, data = mail.search(None, '(SUBJECT "StatusPilot-Pulse-")')
|
| 311 |
+
if typ == 'OK' and data[0]:
|
| 312 |
+
return True, "Mail Flow Active (Pulse Received)", 120
|
| 313 |
+
|
| 314 |
+
return True, "Pulse Sent (Awaiting Receipt)", 50
|
| 315 |
+
except Exception as e:
|
| 316 |
+
return False, f"Mail Flow Error: {str(e)}", 0
|
| 317 |
+
elif check_type == "ssl":
|
| 318 |
+
hostname = url.replace('https://', '').replace('http://', '').split('/')[0].split(':')[0]
|
| 319 |
+
ok, msg, days = check_ssl(hostname)
|
| 320 |
+
latency = round((time.time() - start_time) * 1000, 2)
|
| 321 |
+
return ok, msg, latency
|
| 322 |
+
except requests.exceptions.Timeout:
|
| 323 |
+
latency = round((time.time() - start_time) * 1000, 2)
|
| 324 |
+
return False, "Connection Timeout (12s)", latency
|
| 325 |
+
except requests.exceptions.RequestException as re:
|
| 326 |
+
latency = round((time.time() - start_time) * 1000, 2)
|
| 327 |
+
return False, f"Connection Failed: {str(re)}", latency
|
| 328 |
+
except Exception as e:
|
| 329 |
+
latency = round((time.time() - start_time) * 1000, 2)
|
| 330 |
+
return False, f"Engine Error: {str(e)}", latency
|
| 331 |
+
return True, "OK", 0
|
| 332 |
+
|
| 333 |
+
def check_service_with_retry(service, retry_delay=3):
|
| 334 |
+
"""Immediate re-check logic to eliminate single-packet transient glitches."""
|
| 335 |
+
is_up, msg, latency = check_service_logic(service)
|
| 336 |
+
if not is_up:
|
| 337 |
+
time.sleep(retry_delay)
|
| 338 |
+
is_up, msg, latency = check_service_logic(service)
|
| 339 |
+
if not is_up:
|
| 340 |
+
msg = f"{msg} (Confirmed)"
|
| 341 |
+
return is_up, msg, latency
|
| 342 |
+
|
| 343 |
+
def background_monitor_thread():
|
| 344 |
+
global LAST_ALERT_METRICS, SERVICE_STATE_CACHE, FAIL_COUNT_CACHE, SUCCESS_COUNT_CACHE, DOWN_START_TIME, FLAP_COUNTER
|
| 345 |
+
while True:
|
| 346 |
+
try:
|
| 347 |
+
config = load_config()
|
| 348 |
+
services = config.get("services_to_monitor", [])
|
| 349 |
+
interval = config.get("check_interval", 60)
|
| 350 |
+
failures_required = config.get("consecutive_failures_required", 3)
|
| 351 |
+
retry_delay = config.get("retry_delay_seconds", 3)
|
| 352 |
+
cooldown_secs = config.get("cooldown_minutes", 60) * 60
|
| 353 |
+
|
| 354 |
+
for svc in services:
|
| 355 |
+
name = svc.get('name', 'Unknown')
|
| 356 |
+
loc = svc.get('location', 'Global')
|
| 357 |
+
url = svc.get('url', '')
|
| 358 |
+
|
| 359 |
+
is_up, msg, latency = check_service_with_retry(svc, retry_delay=retry_delay)
|
| 360 |
+
previous_state = SERVICE_STATE_CACHE.get(name, True)
|
| 361 |
+
|
| 362 |
+
if is_up:
|
| 363 |
+
FAIL_COUNT_CACHE[name] = 0
|
| 364 |
+
succ_cnt = SUCCESS_COUNT_CACHE.get(name, 0) + 1
|
| 365 |
+
SUCCESS_COUNT_CACHE[name] = succ_cnt
|
| 366 |
+
|
| 367 |
+
# Senior DevOps Flap Dampening: Require 3 consecutive success checks before declaring recovery
|
| 368 |
+
if not previous_state and succ_cnt >= 3:
|
| 369 |
+
SERVICE_STATE_CACHE[name] = True
|
| 370 |
+
down_time = DOWN_START_TIME.pop(name, None)
|
| 371 |
+
duration_str = None
|
| 372 |
+
if down_time:
|
| 373 |
+
secs = int(time.time() - down_time)
|
| 374 |
+
m, s = divmod(secs, 60)
|
| 375 |
+
h, m = divmod(m, 60)
|
| 376 |
+
duration_str = f"{h}h {m}m {s}s" if h > 0 else f"{m}m {s}s" if m > 0 else f"{s}s"
|
| 377 |
+
|
| 378 |
+
now_t = time.time()
|
| 379 |
+
flaps = [t for t in FLAP_COUNTER.get(name, []) if (now_t - t) < 3600]
|
| 380 |
+
flaps.append(now_t)
|
| 381 |
+
FLAP_COUNTER[name] = flaps
|
| 382 |
+
|
| 383 |
+
if len(flaps) >= 4:
|
| 384 |
+
print(f"[FLAP SUPPRESSION ACTIVE] {name} state oscillating rapidly. Suppressing resolution email.")
|
| 385 |
+
else:
|
| 386 |
+
print(f"[RESOLVED] {name} is back ONLINE (Downtime: {duration_str})")
|
| 387 |
+
send_alert(config, name, loc, url, "SERVICE RESTORED", "Status: Operational (Verified across 3 consecutive checks)", is_resolved=True, duration_str=duration_str)
|
| 388 |
+
else:
|
| 389 |
+
SUCCESS_COUNT_CACHE[name] = 0
|
| 390 |
+
fail_cnt = FAIL_COUNT_CACHE.get(name, 0) + 1
|
| 391 |
+
FAIL_COUNT_CACHE[name] = fail_cnt
|
| 392 |
+
|
| 393 |
+
if fail_cnt >= failures_required:
|
| 394 |
+
curr = time.time()
|
| 395 |
+
if previous_state:
|
| 396 |
+
SERVICE_STATE_CACHE[name] = False
|
| 397 |
+
DOWN_START_TIME[name] = curr
|
| 398 |
+
LAST_ALERT_METRICS[name] = curr
|
| 399 |
+
|
| 400 |
+
flaps = [t for t in FLAP_COUNTER.get(name, []) if (curr - t) < 3600]
|
| 401 |
+
flaps.append(curr)
|
| 402 |
+
FLAP_COUNTER[name] = flaps
|
| 403 |
+
|
| 404 |
+
if len(flaps) >= 4:
|
| 405 |
+
print(f"[FLAP SUPPRESSION ACTIVE] {name} is flapping. Sending single Consolidated Flap Alert.")
|
| 406 |
+
alert_details = f"FLAP WARNING: {name} is experiencing rapid state oscillation ({len(flaps)} state changes in 1hr). Notifications throttled for 60 minutes."
|
| 407 |
+
send_alert(config, name, loc, url, "FLAPPING / UNSTABLE", alert_details, is_resolved=False)
|
| 408 |
+
else:
|
| 409 |
+
alert_details = f"{msg} (Verified across {fail_cnt} consecutive checks)"
|
| 410 |
+
print(f"[CRITICAL DOWN] {name} confirmed DOWN ({msg})")
|
| 411 |
+
send_alert(config, name, loc, url, msg, alert_details, is_resolved=False)
|
| 412 |
+
else:
|
| 413 |
+
last_alert = LAST_ALERT_METRICS.get(name, 0)
|
| 414 |
+
if (curr - last_alert) > cooldown_secs:
|
| 415 |
+
LAST_ALERT_METRICS[name] = curr
|
| 416 |
+
alert_details = f"Ongoing Outage: {msg} (Service remains unreachable)"
|
| 417 |
+
print(f"[CRITICAL DOWN REMINDER] {name} still DOWN")
|
| 418 |
+
send_alert(config, name, loc, url, msg, alert_details, is_resolved=False)
|
| 419 |
+
|
| 420 |
+
try:
|
| 421 |
+
conn = get_db_connection()
|
| 422 |
+
c = conn.cursor()
|
| 423 |
+
c.execute("INSERT INTO status_history (timestamp, name, url, location, is_up, response_time, message) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
| 424 |
+
(datetime.now().isoformat(), name, url, loc, 1 if is_up else 0, latency, msg))
|
| 425 |
+
conn.commit()
|
| 426 |
+
conn.close()
|
| 427 |
+
except Exception as e:
|
| 428 |
+
print("DB error", e)
|
| 429 |
+
|
| 430 |
+
time.sleep(max(interval, 10))
|
| 431 |
+
except Exception as e:
|
| 432 |
+
print(f"Monitor error: {e}")
|
| 433 |
+
time.sleep(10)
|
| 434 |
+
|
| 435 |
+
def send_alert(config, name, loc, url, msg, details, is_resolved=False, duration_str=None, is_test=False):
|
| 436 |
+
"""Unified executive alerting engine supporting HTML formatting for SMTP and Outlook."""
|
| 437 |
+
if is_test:
|
| 438 |
+
subject = f"[TEST ALERT] StatusPilot Diagnostic Notification ({name})"
|
| 439 |
+
badge_color = "#00D4FF"
|
| 440 |
+
badge_text = "TEST DIAGNOSTIC"
|
| 441 |
+
header_title = "Diagnostic Alert Test"
|
| 442 |
+
elif is_resolved:
|
| 443 |
+
subject = f"[RESOLVED] {name} ({loc}) is back ONLINE"
|
| 444 |
+
badge_color = "#10B981"
|
| 445 |
+
badge_text = "SERVICE RESTORED"
|
| 446 |
+
header_title = f"{name} Restored"
|
| 447 |
+
else:
|
| 448 |
+
subject = f"[CRITICAL DOWN] {name} ({loc}) is DOWN"
|
| 449 |
+
badge_color = "#EF4444"
|
| 450 |
+
badge_text = "CRITICAL DOWNTIME"
|
| 451 |
+
header_title = f"{name} Outage Detected"
|
| 452 |
+
|
| 453 |
+
now_utc = datetime.utcnow()
|
| 454 |
+
est_str = (now_utc - timedelta(hours=4)).strftime('%Y-%m-%d %H:%M:%S EDT')
|
| 455 |
+
utc_str = now_utc.strftime('%H:%M:%S UTC')
|
| 456 |
+
ist_str = (now_utc + timedelta(hours=5, minutes=30)).strftime('%H:%M:%S IST')
|
| 457 |
+
timestamp_str = f"{est_str} ({utc_str} | {ist_str})"
|
| 458 |
+
|
| 459 |
+
duration_row = f"<tr><td style='padding:12px 16px; font-weight:600; color:#64748B; border-bottom:1px solid #E2E8F0; background:#F1F5F9;'>Downtime Duration:</td><td style='padding:12px 16px; font-weight:700; color:#0F172A; border-bottom:1px solid #E2E8F0;'>{duration_str}</td></tr>" if duration_str else ""
|
| 460 |
+
logo_img_tag = '<img src="https://chotustatus-statuspilot.hf.space/static/parle_tech_logo.png" height="46" alt="PARLE TECH" style="display:inline-block; border:0; outline:none; height:46px; width:auto; vertical-align:middle;" />'
|
| 461 |
+
|
| 462 |
+
html_content = f"""<!DOCTYPE html>
|
| 463 |
+
<html>
|
| 464 |
+
<head><meta charset="utf-8"></head>
|
| 465 |
+
<body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; background-color: #F8FAFC; margin: 0; padding: 28px 16px;">
|
| 466 |
+
<div style="max-width: 640px; margin: 0 auto; background: #FFFFFF; border-radius: 12px; border: 1px solid #E2E8F0; overflow: hidden; box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.05);">
|
| 467 |
+
|
| 468 |
+
<!-- PARLE TECH BRAND HEADER -->
|
| 469 |
+
<div style="background-color: #0F172A; padding: 24px 30px; border-bottom: 3px solid #00C8F0;">
|
| 470 |
+
<table style="width: 100%; border-collapse: collapse;">
|
| 471 |
+
<tr>
|
| 472 |
+
<td style="vertical-align: middle;">
|
| 473 |
+
<div style="font-family: 'Segoe UI', Arial, sans-serif; font-size: 15px; font-weight: 700; color: #FFFFFF; letter-spacing: 0.5px;">Parle Tech Infrastructure Monitoring</div>
|
| 474 |
+
<div style="font-family: 'Segoe UI', Arial, sans-serif; font-size: 11px; font-weight: 600; color: #00C8F0; letter-spacing: 1.5px; text-transform: uppercase; margin-top: 2px;">24/7 Enterprise Vigilance</div>
|
| 475 |
+
</td>
|
| 476 |
+
<td style="text-align: right; vertical-align: middle; white-space: nowrap;">
|
| 477 |
+
{logo_img_tag}
|
| 478 |
+
</td>
|
| 479 |
+
</tr>
|
| 480 |
+
</table>
|
| 481 |
+
</div>
|
| 482 |
+
|
| 483 |
+
<!-- INCIDENT STATUS BANNER -->
|
| 484 |
+
<div style="background-color: {badge_color}; padding: 18px 30px; color: #FFFFFF;">
|
| 485 |
+
<div style="font-size: 11px; font-weight: 800; letter-spacing: 2px; text-transform: uppercase; opacity: 0.95;">INCIDENT CLASSIFICATION ALARM</div>
|
| 486 |
+
<h2 style="margin: 4px 0 0 0; font-size: 20px; font-weight: 700; color: #FFFFFF; letter-spacing: 0.5px;">{badge_text}: {header_title}</h2>
|
| 487 |
+
</div>
|
| 488 |
+
|
| 489 |
+
<!-- MAIN BODY CONTAINER -->
|
| 490 |
+
<div style="padding: 30px;">
|
| 491 |
+
<p style="font-size: 14px; color: #334155; margin-top: 0; line-height: 1.6;">
|
| 492 |
+
An automated vigilance event has been flagged by <strong>Parle Tech StatusPilot Infrastructure Security</strong>. Immediate technical telemetry details are provided below:
|
| 493 |
+
</p>
|
| 494 |
+
|
| 495 |
+
<!-- METRICS GRID TABLE -->
|
| 496 |
+
<table style="width: 100%; border-collapse: collapse; font-size: 13px; margin: 24px 0; background: #F8FAFC; border-radius: 8px; border: 1px solid #E2E8F0; overflow: hidden;">
|
| 497 |
+
<tr>
|
| 498 |
+
<td style="padding: 12px 16px; font-weight: 600; color: #64748B; border-bottom: 1px solid #E2E8F0; width: 35%; background: #F1F5F9;">Target Node / Service:</td>
|
| 499 |
+
<td style="padding: 12px 16px; font-weight: 700; color: #0F172A; border-bottom: 1px solid #E2E8F0;">{name}</td>
|
| 500 |
+
</tr>
|
| 501 |
+
<tr>
|
| 502 |
+
<td style="padding: 12px 16px; font-weight: 600; color: #64748B; border-bottom: 1px solid #E2E8F0; background: #F1F5F9;">Scope / Region:</td>
|
| 503 |
+
<td style="padding: 12px 16px; color: #0F172A; border-bottom: 1px solid #E2E8F0; font-weight: 600;">{loc}</td>
|
| 504 |
+
</tr>
|
| 505 |
+
<tr>
|
| 506 |
+
<td style="padding: 12px 16px; font-weight: 600; color: #64748B; border-bottom: 1px solid #E2E8F0; background: #F1F5F9;">Endpoint URL:</td>
|
| 507 |
+
<td style="padding: 12px 16px; color: #0284C7; border-bottom: 1px solid #E2E8F0; word-break: break-all; font-weight: 600;"><a href="{url}" target="_blank" style="color: #0284C7; text-decoration: none;">{url}</a></td>
|
| 508 |
+
</tr>
|
| 509 |
+
<tr>
|
| 510 |
+
<td style="padding: 12px 16px; font-weight: 600; color: #64748B; border-bottom: 1px solid #E2E8F0; background: #F1F5F9;">Vigilance Status:</td>
|
| 511 |
+
<td style="padding: 12px 16px; font-weight: 800; color: {badge_color}; border-bottom: 1px solid #E2E8F0;">{msg}</td>
|
| 512 |
+
</tr>
|
| 513 |
+
<tr>
|
| 514 |
+
<td style="padding: 12px 16px; font-weight: 600; color: #64748B; border-bottom: 1px solid #E2E8F0; background: #F1F5F9;">Technical Diagnostic:</td>
|
| 515 |
+
<td style="padding: 12px 16px; color: #334155; border-bottom: 1px solid #E2E8F0; line-height: 1.5;">{details}</td>
|
| 516 |
+
</tr>
|
| 517 |
+
{duration_row}
|
| 518 |
+
<tr>
|
| 519 |
+
<td style="padding: 12px 16px; font-weight: 600; color: #64748B; background: #F1F5F9;">Event Timestamp:</td>
|
| 520 |
+
<td style="padding: 12px 16px; color: #0F172A; font-weight: 600;">{timestamp_str}</td>
|
| 521 |
+
</tr>
|
| 522 |
+
</table>
|
| 523 |
+
|
| 524 |
+
<!-- ACTIONABLE CTA BUTTON -->
|
| 525 |
+
<div style="text-align: center; margin: 32px 0 20px 0;">
|
| 526 |
+
<a href="https://chotustatus-statuspilot.hf.space/" target="_blank" style="background-color: #0F172A; color: #00D4FF; font-family: 'Segoe UI', sans-serif; font-size: 13px; font-weight: 700; text-decoration: none; padding: 14px 32px; border-radius: 8px; letter-spacing: 1.5px; display: inline-block; border: 1px solid #00D4FF; box-shadow: 0 4px 12px rgba(15, 23, 42, 0.15);">
|
| 527 |
+
🌐 OPEN LIVE NOC OPERATIONS CENTER
|
| 528 |
+
</a>
|
| 529 |
+
</div>
|
| 530 |
+
|
| 531 |
+
<!-- CORPORATE SIGNATURE -->
|
| 532 |
+
<div style="margin-top: 32px; padding-top: 20px; border-top: 1px solid #E2E8F0; text-align: center; font-size: 11px; color: #94A3B8; line-height: 1.6;">
|
| 533 |
+
<strong>Parle Tech Infrastructure Security & NOC Engine v5.1</strong><br>
|
| 534 |
+
24/7 Enterprise Infrastructure Alerting • Automated Incident Management
|
| 535 |
+
</div>
|
| 536 |
+
</div>
|
| 537 |
+
</div>
|
| 538 |
+
</body>
|
| 539 |
+
</html>"""
|
| 540 |
+
|
| 541 |
+
plain_body = f"[{badge_text}]\nService: {name}\nLocation: {loc}\nURL: {url}\nStatus: {msg}\nDetails: {details}\nTime: {timestamp_str}"
|
| 542 |
+
if duration_str:
|
| 543 |
+
plain_body += f"\nDowntime Duration: {duration_str}"
|
| 544 |
+
|
| 545 |
+
alert_mode = config.get("alert_mode", "SMTP")
|
| 546 |
+
raw_recipients = config.get("email_recipients", [])
|
| 547 |
+
if isinstance(raw_recipients, str):
|
| 548 |
+
recipients = [r.strip() for r in raw_recipients.split(',') if r.strip()]
|
| 549 |
+
elif isinstance(raw_recipients, list):
|
| 550 |
+
recipients = [r.strip() for r in raw_recipients if isinstance(r, str) and r.strip()]
|
| 551 |
+
else:
|
| 552 |
+
recipients = []
|
| 553 |
+
|
| 554 |
+
sender = config.get("smtp_sender", "").strip() or DEFAULT_SMTP_SENDER
|
| 555 |
+
raw_pwd = os.environ.get('SMTP_PASSWORD') or config.get("smtp_password", "")
|
| 556 |
+
password = security.decrypt_secret(raw_pwd).strip()
|
| 557 |
+
if not password:
|
| 558 |
+
password = DEFAULT_SMTP_PASSWORD_RAW
|
| 559 |
+
|
| 560 |
+
if not recipients:
|
| 561 |
+
recipients = config.get("email_recipients", []) or DEFAULT_EMAIL_RECIPIENTS
|
| 562 |
+
if not recipients and sender:
|
| 563 |
+
recipients = [sender]
|
| 564 |
+
|
| 565 |
+
if alert_mode == "SMTP":
|
| 566 |
+
if not sender:
|
| 567 |
+
sender = DEFAULT_SMTP_SENDER
|
| 568 |
+
smtp_host = config.get("smtp_server", "").strip()
|
| 569 |
+
if not smtp_host:
|
| 570 |
+
if any(dom in sender.lower() for dom in ["@outlook.", "@hotmail.", "@live.", "@office365."]):
|
| 571 |
+
smtp_host = "smtp.office365.com"
|
| 572 |
+
else:
|
| 573 |
+
smtp_host = "smtp.gmail.com"
|
| 574 |
+
|
| 575 |
+
# Remove spaces in Google/Microsoft App Passwords (e.g. "abcd efgh ijkl mnop" -> "abcdefghijklmnop")
|
| 576 |
+
clean_pwd = (password or DEFAULT_SMTP_PASSWORD_RAW).replace(" ", "")
|
| 577 |
+
|
| 578 |
+
connected = False
|
| 579 |
+
last_error = None
|
| 580 |
+
|
| 581 |
+
# Custom Webhook / Google Apps Script (HTTPS Port 443 - works on Hugging Face)
|
| 582 |
+
webhook_url = os.environ.get("EMAIL_WEBHOOK_URL") or config.get("email_webhook_url", "").strip()
|
| 583 |
+
if webhook_url and not connected:
|
| 584 |
+
try:
|
| 585 |
+
all_webhook_sent = True
|
| 586 |
+
for recipient in recipients:
|
| 587 |
+
payload = {
|
| 588 |
+
"sender": sender,
|
| 589 |
+
"senderName": "StatusPilot NOC",
|
| 590 |
+
"recipient": recipient,
|
| 591 |
+
"subject": subject,
|
| 592 |
+
"body": plain_body,
|
| 593 |
+
"htmlBody": html_content
|
| 594 |
+
}
|
| 595 |
+
w_res = requests.post(webhook_url, json=payload, timeout=20, allow_redirects=True)
|
| 596 |
+
print(f"Webhook response for {recipient}: status={w_res.status_code} body={w_res.text[:200]}")
|
| 597 |
+
if w_res.status_code not in [200, 201, 202] and "OK" not in w_res.text.upper() and "SUCCESS" not in w_res.text.upper():
|
| 598 |
+
all_webhook_sent = False
|
| 599 |
+
if all_webhook_sent:
|
| 600 |
+
connected = True
|
| 601 |
+
print(f"Alert successfully dispatched via HTTPS Webhook to all recipients: {recipients}")
|
| 602 |
+
except Exception as ew:
|
| 603 |
+
last_error = ew
|
| 604 |
+
print(f"Webhook dispatch failed: {ew}")
|
| 605 |
+
|
| 606 |
+
# Try Standard Gmail Port 465 (SMTP_SSL)
|
| 607 |
+
if not connected:
|
| 608 |
+
try:
|
| 609 |
+
ssl_context = ssl.create_default_context()
|
| 610 |
+
from email.utils import formataddr
|
| 611 |
+
display_from = formataddr(("StatusPilot NOC", sender))
|
| 612 |
+
with smtplib.SMTP_SSL(smtp_host, 465, context=ssl_context, timeout=12) as server:
|
| 613 |
+
server.login(sender, clean_pwd)
|
| 614 |
+
for recipient in recipients:
|
| 615 |
+
mime_msg = MIMEMultipart("alternative")
|
| 616 |
+
mime_msg['From'] = display_from
|
| 617 |
+
mime_msg['To'] = recipient
|
| 618 |
+
mime_msg['Subject'] = subject
|
| 619 |
+
mime_msg.attach(MIMEText(plain_body, 'plain'))
|
| 620 |
+
mime_msg.attach(MIMEText(html_content, 'html'))
|
| 621 |
+
server.sendmail(sender, recipient, mime_msg.as_string())
|
| 622 |
+
connected = True
|
| 623 |
+
print(f"Alert successfully dispatched via SMTPS Port 465 to {recipients}")
|
| 624 |
+
except Exception as e1:
|
| 625 |
+
last_error = e1
|
| 626 |
+
print(f"Port 465 SMTPS failed: {e1}. Trying Port 587 STARTTLS...")
|
| 627 |
+
|
| 628 |
+
# Try Standard Gmail Port 587 (STARTTLS)
|
| 629 |
+
if not connected:
|
| 630 |
+
try:
|
| 631 |
+
from email.utils import formataddr
|
| 632 |
+
display_from = formataddr(("StatusPilot NOC", sender))
|
| 633 |
+
with smtplib.SMTP(smtp_host, 587, timeout=12) as server:
|
| 634 |
+
server.starttls()
|
| 635 |
+
server.login(sender, clean_pwd)
|
| 636 |
+
for recipient in recipients:
|
| 637 |
+
mime_msg = MIMEMultipart("alternative")
|
| 638 |
+
mime_msg['From'] = display_from
|
| 639 |
+
mime_msg['To'] = recipient
|
| 640 |
+
mime_msg['Subject'] = subject
|
| 641 |
+
mime_msg.attach(MIMEText(plain_body, 'plain'))
|
| 642 |
+
mime_msg.attach(MIMEText(html_content, 'html'))
|
| 643 |
+
server.sendmail(sender, recipient, mime_msg.as_string())
|
| 644 |
+
connected = True
|
| 645 |
+
print(f"Alert successfully dispatched via STARTTLS Port 587 to {recipients}")
|
| 646 |
+
except Exception as e2:
|
| 647 |
+
last_error = e2
|
| 648 |
+
print(f"Port 587 STARTTLS failed: {e2}")
|
| 649 |
+
|
| 650 |
+
if not connected and is_test:
|
| 651 |
+
raise RuntimeError(f"SMTP Delivery Error ({smtp_host}): {last_error}")
|
| 652 |
+
|
| 653 |
+
elif alert_mode == "Outlook" and OUTL_AVAIL:
|
| 654 |
+
try:
|
| 655 |
+
outlook = win32com.client.Dispatch("Outlook.Application")
|
| 656 |
+
mail = outlook.CreateItem(0)
|
| 657 |
+
mail.Subject = subject
|
| 658 |
+
mail.HTMLBody = html_content
|
| 659 |
+
if recipients:
|
| 660 |
+
mail.To = "; ".join(recipients)
|
| 661 |
+
mail.Send()
|
| 662 |
+
print("Outlook HTML alert sent.")
|
| 663 |
+
except Exception as e:
|
| 664 |
+
print(f"Outlook Alert Error: {e}")
|
| 665 |
+
if is_test:
|
| 666 |
+
raise RuntimeError(f"Outlook App Dispatch Error: {str(e)}")
|
| 667 |
+
|
| 668 |
+
# Start thread automatically
|
| 669 |
+
init_db()
|
| 670 |
+
if not os.path.exists(CONFIG_FILE):
|
| 671 |
+
save_config(load_config())
|
| 672 |
+
threading.Thread(target=background_monitor_thread, daemon=True).start()
|
| 673 |
+
|
| 674 |
+
# --- WEB ROUTES ---
|
| 675 |
+
|
| 676 |
+
@app.route('/')
|
| 677 |
+
def serve_dashboard():
|
| 678 |
+
"""Forcing a new template name to bypass persistent cloud caching."""
|
| 679 |
+
return render_template('dashboard_v6.html', logo_b64=PARLE_TECH_LOGO_B64)
|
| 680 |
+
|
| 681 |
+
@app.route('/api/config/delete', methods=['POST'])
|
| 682 |
+
def delete_config_service():
|
| 683 |
+
"""Expert-grade service decommissioning via POST for cloud compatibility."""
|
| 684 |
+
if not check_auth():
|
| 685 |
+
return jsonify({"error": "Unauthorized"}), 401
|
| 686 |
+
|
| 687 |
+
name = request.json.get('name')
|
| 688 |
+
print(f"DECOMMISSION REQUEST: {name}")
|
| 689 |
+
|
| 690 |
+
config = load_config()
|
| 691 |
+
original_count = len(config.get('services_to_monitor', []))
|
| 692 |
+
|
| 693 |
+
config['services_to_monitor'] = [s for s in config['services_to_monitor'] if s.get('name') != name]
|
| 694 |
+
|
| 695 |
+
new_count = len(config['services_to_monitor'])
|
| 696 |
+
save_config(config)
|
| 697 |
+
|
| 698 |
+
if original_count == new_count:
|
| 699 |
+
return jsonify({"status": "error", "message": f"Service '{name}' not found."}), 404
|
| 700 |
+
|
| 701 |
+
print(f"Successfully decommissioned: {name}")
|
| 702 |
+
return jsonify({"status": "success", "deleted": name})
|
| 703 |
+
|
| 704 |
+
@app.route('/api/config', methods=['GET', 'POST'])
|
| 705 |
+
def handle_config():
|
| 706 |
+
if request.method == 'POST':
|
| 707 |
+
new_config = request.json or {}
|
| 708 |
+
current_config = load_config()
|
| 709 |
+
new_pwd = str(new_config.get("smtp_password", "")).strip()
|
| 710 |
+
if not new_pwd or new_pwd.startswith("••••"):
|
| 711 |
+
new_config["smtp_password"] = current_config.get("smtp_password", "")
|
| 712 |
+
else:
|
| 713 |
+
new_config["smtp_password"] = security.encrypt_secret(new_pwd)
|
| 714 |
+
save_config(new_config)
|
| 715 |
+
return jsonify({"status": "success"})
|
| 716 |
+
|
| 717 |
+
cfg = load_config()
|
| 718 |
+
if cfg.get("smtp_password"):
|
| 719 |
+
cfg["smtp_password"] = "••••••••••••"
|
| 720 |
+
return jsonify(cfg)
|
| 721 |
+
|
| 722 |
+
@app.route('/api/clear_ai', methods=['POST'])
|
| 723 |
+
def clear_ai_services():
|
| 724 |
+
config = load_config()
|
| 725 |
+
# Robust filter: Removes anything starting with 'Ai Service', 'New Service', or 'Cloud Service'
|
| 726 |
+
patterns = ["Ai Service", "New Service", "Cloud Service", "Https", "Global"]
|
| 727 |
+
config['services_to_monitor'] = [s for s in config['services_to_monitor'] if not any(p in s['name'] for p in patterns)]
|
| 728 |
+
save_config(config)
|
| 729 |
+
return jsonify({"status": "success"})
|
| 730 |
+
|
| 731 |
+
@app.route('/api/status', methods=['GET'])
|
| 732 |
+
def get_status():
|
| 733 |
+
config = load_config()
|
| 734 |
+
services = config.get("services_to_monitor", [])
|
| 735 |
+
|
| 736 |
+
conn = get_db_connection()
|
| 737 |
+
c = conn.cursor()
|
| 738 |
+
|
| 739 |
+
results = []
|
| 740 |
+
for svc in services:
|
| 741 |
+
name = svc.get('name')
|
| 742 |
+
# Get latest status
|
| 743 |
+
c.execute("SELECT is_up, response_time, message, timestamp FROM status_history WHERE name=? ORDER BY timestamp DESC LIMIT 1", (name,))
|
| 744 |
+
row = c.fetchone()
|
| 745 |
+
|
| 746 |
+
# Get last 30 checks for timeline
|
| 747 |
+
c.execute("SELECT is_up FROM status_history WHERE name=? ORDER BY timestamp DESC LIMIT 30", (name,))
|
| 748 |
+
history_rows = c.fetchall()
|
| 749 |
+
timeline = [bool(h["is_up"]) for h in reversed(history_rows)]
|
| 750 |
+
|
| 751 |
+
if not row:
|
| 752 |
+
is_up, msg, latency = check_service_logic(svc)
|
| 753 |
+
out_row = {"is_up": 1 if is_up else 0, "response_time": latency, "message": msg, "timestamp": datetime.now().isoformat()}
|
| 754 |
+
else:
|
| 755 |
+
out_row = {"is_up": row["is_up"], "response_time": row["response_time"], "message": row["message"], "timestamp": row["timestamp"]}
|
| 756 |
+
|
| 757 |
+
results.append({
|
| 758 |
+
"name": name,
|
| 759 |
+
"url": svc.get("url"),
|
| 760 |
+
"location": svc.get("location", "Global"),
|
| 761 |
+
"type": svc.get("type"),
|
| 762 |
+
"is_up": bool(out_row["is_up"]),
|
| 763 |
+
"latency": out_row["response_time"],
|
| 764 |
+
"message": out_row["message"],
|
| 765 |
+
"last_check": out_row["timestamp"],
|
| 766 |
+
"timeline": timeline
|
| 767 |
+
})
|
| 768 |
+
conn.close()
|
| 769 |
+
return jsonify(results)
|
| 770 |
+
|
| 771 |
+
@app.route('/api/summary', methods=['GET'])
|
| 772 |
+
def get_summary():
|
| 773 |
+
conn = get_db_connection()
|
| 774 |
+
c = conn.cursor()
|
| 775 |
+
|
| 776 |
+
# Total Monitors
|
| 777 |
+
config = load_config()
|
| 778 |
+
services = config.get("services_to_monitor", [])
|
| 779 |
+
total_monitors = len(services)
|
| 780 |
+
|
| 781 |
+
# Global Uptime % (Last 24 hours)
|
| 782 |
+
yesterday = (datetime.now() - timedelta(days=1)).isoformat()
|
| 783 |
+
c.execute("SELECT AVG(is_up) * 100 FROM status_history WHERE timestamp > ?", (yesterday,))
|
| 784 |
+
uptime_pct = c.fetchone()[0] or 100.0
|
| 785 |
+
|
| 786 |
+
# Total Incidents (Count of UNIQUE services that are CURRENTLY down)
|
| 787 |
+
incidents = 0
|
| 788 |
+
for svc in services:
|
| 789 |
+
name = svc.get('name')
|
| 790 |
+
c.execute("SELECT is_up FROM status_history WHERE name=? ORDER BY timestamp DESC LIMIT 1", (name,))
|
| 791 |
+
row = c.fetchone()
|
| 792 |
+
if row and row[0] == 0:
|
| 793 |
+
incidents += 1
|
| 794 |
+
|
| 795 |
+
# Avg Latency
|
| 796 |
+
c.execute("SELECT AVG(response_time) FROM status_history WHERE timestamp > ?", (yesterday,))
|
| 797 |
+
avg_latency = c.fetchone()[0] or 0
|
| 798 |
+
|
| 799 |
+
conn.close()
|
| 800 |
+
return jsonify({
|
| 801 |
+
"total_monitors": total_monitors,
|
| 802 |
+
"uptime_pct": round(uptime_pct, 2),
|
| 803 |
+
"incidents": incidents,
|
| 804 |
+
"avg_latency": round(avg_latency, 2)
|
| 805 |
+
})
|
| 806 |
+
|
| 807 |
+
@app.route('/api/admin/purge_stale', methods=['POST'])
|
| 808 |
+
def purge_stale_data():
|
| 809 |
+
"""Purge history records for services no longer in the current configuration."""
|
| 810 |
+
if not check_auth():
|
| 811 |
+
return jsonify({"error": "Unauthorized"}), 401
|
| 812 |
+
|
| 813 |
+
config = load_config()
|
| 814 |
+
active_names = [s.get('name') for s in config.get('services_to_monitor', [])]
|
| 815 |
+
|
| 816 |
+
conn = get_db_connection()
|
| 817 |
+
c = conn.cursor()
|
| 818 |
+
|
| 819 |
+
# 1. Delete records for non-existent services
|
| 820 |
+
placeholders = ','.join(['?'] * len(active_names))
|
| 821 |
+
if active_names:
|
| 822 |
+
c.execute(f"DELETE FROM status_history WHERE name NOT IN ({placeholders})", active_names)
|
| 823 |
+
else:
|
| 824 |
+
c.execute("DELETE FROM status_history")
|
| 825 |
+
|
| 826 |
+
# 2. Also clear very old data (older than 7 days) to keep DB light
|
| 827 |
+
one_week_ago = (datetime.now() - timedelta(days=7)).isoformat()
|
| 828 |
+
c.execute("DELETE FROM status_history WHERE timestamp < ?", (one_week_ago,))
|
| 829 |
+
|
| 830 |
+
count = conn.total_changes
|
| 831 |
+
conn.commit()
|
| 832 |
+
conn.close()
|
| 833 |
+
|
| 834 |
+
return jsonify({"status": "success", "purged_records": count})
|
| 835 |
+
|
| 836 |
+
@app.route('/api/history/<path:service_name>', methods=['GET'])
|
| 837 |
+
def get_detailed_history(service_name):
|
| 838 |
+
conn = get_db_connection()
|
| 839 |
+
c = conn.cursor()
|
| 840 |
+
# Get last 100 records for charts
|
| 841 |
+
c.execute("SELECT timestamp, is_up, response_time FROM status_history WHERE name=? ORDER BY timestamp DESC LIMIT 100", (service_name,))
|
| 842 |
+
rows = c.fetchall()
|
| 843 |
+
conn.close()
|
| 844 |
+
|
| 845 |
+
data = [{
|
| 846 |
+
"timestamp": r["timestamp"],
|
| 847 |
+
"is_up": bool(r["is_up"]),
|
| 848 |
+
"latency": r["response_time"]
|
| 849 |
+
} for r in reversed(rows)]
|
| 850 |
+
|
| 851 |
+
return jsonify(data)
|
| 852 |
+
|
| 853 |
+
@app.route('/api/test', methods=['POST'])
|
| 854 |
+
def test_tool():
|
| 855 |
+
"""Live interactive testing for specific tools and locations."""
|
| 856 |
+
data = request.json
|
| 857 |
+
tool_name = data.get("tool")
|
| 858 |
+
location = data.get("location")
|
| 859 |
+
|
| 860 |
+
# Simple direct ping/fetch simulation
|
| 861 |
+
config = load_config()
|
| 862 |
+
# Find matching service, or just do a generic requests test
|
| 863 |
+
matched_svc = next((s for s in config.get("services_to_monitor", []) if s.get("name") == tool_name), None)
|
| 864 |
+
|
| 865 |
+
if not matched_svc:
|
| 866 |
+
return jsonify({"status": "error", "message": "Service not found in configuration!"}), 404
|
| 867 |
+
|
| 868 |
+
is_up, msg, latency = check_service_logic(matched_svc)
|
| 869 |
+
return jsonify({
|
| 870 |
+
"tool": tool_name,
|
| 871 |
+
"location": location,
|
| 872 |
+
"is_up": is_up,
|
| 873 |
+
"latency": latency,
|
| 874 |
+
"message": msg,
|
| 875 |
+
"timestamp": datetime.now().isoformat()
|
| 876 |
+
})
|
| 877 |
+
|
| 878 |
+
@app.route('/api/ai_add', methods=['POST'])
|
| 879 |
+
def ai_add_service():
|
| 880 |
+
"""Neural Engine v4 - Context-Aware Entity Extraction."""
|
| 881 |
+
raw_text = request.json.get("text", "").strip()
|
| 882 |
+
if not raw_text:
|
| 883 |
+
return jsonify({"status": "error", "message": "Neural link empty. Please provide instructions."})
|
| 884 |
+
|
| 885 |
+
# 1. URL Extraction (Highest Priority)
|
| 886 |
+
url_match = re.search(r'(https?://[^\s]+)', raw_text)
|
| 887 |
+
if not url_match:
|
| 888 |
+
return jsonify({"status": "error", "message": "No endpoint detected. Try adding a URL like 'https://status.teams.com'"})
|
| 889 |
+
url = url_match.group(1).rstrip(',.!?')
|
| 890 |
+
|
| 891 |
+
# 2. Contextual Cleaning
|
| 892 |
+
clean_text = raw_text.replace(url, "").lower()
|
| 893 |
+
for cmd in ["monitor", "add", "check", "service", "from", "at", "called", "in"]:
|
| 894 |
+
clean_text = clean_text.replace(cmd, "")
|
| 895 |
+
clean_text = " ".join(clean_text.split())
|
| 896 |
+
|
| 897 |
+
# 3. Entity Extraction (Name & Location)
|
| 898 |
+
# Strategy: Locations are usually at the end or preceded by 'in'
|
| 899 |
+
location = "Global"
|
| 900 |
+
loc_match = re.search(r'(?:in|at|site|location|node)\s+([a-z\s]{2,20})$', raw_text, re.IGNORECASE)
|
| 901 |
+
if loc_match:
|
| 902 |
+
location = loc_match.group(1).strip().title()
|
| 903 |
+
# Clean the name part by removing the location
|
| 904 |
+
clean_text = clean_text.replace(location.lower(), "").strip()
|
| 905 |
+
|
| 906 |
+
# The remaining text is most likely the name
|
| 907 |
+
name = clean_text.title() if clean_text else "AI Discovery"
|
| 908 |
+
if len(name) < 2: name = "Cloud Service"
|
| 909 |
+
|
| 910 |
+
# 4. Smart Protocol Detection
|
| 911 |
+
is_api = any(x in url.lower() for x in [".json", "api/", "/status-data", "v1/"])
|
| 912 |
+
check_type = "json_status" if is_api else "http_code"
|
| 913 |
+
|
| 914 |
+
config = load_config()
|
| 915 |
+
new_svc = {
|
| 916 |
+
"name": name,
|
| 917 |
+
"url": url,
|
| 918 |
+
"location": location,
|
| 919 |
+
"type": check_type,
|
| 920 |
+
"expected_code": 200,
|
| 921 |
+
"key_to_check": "status" if is_api else "",
|
| 922 |
+
"expected_value": "up" if is_api else ""
|
| 923 |
+
}
|
| 924 |
+
|
| 925 |
+
config["services_to_monitor"].append(new_svc)
|
| 926 |
+
save_config(config)
|
| 927 |
+
|
| 928 |
+
return jsonify({
|
| 929 |
+
"status": "success",
|
| 930 |
+
"added": new_svc,
|
| 931 |
+
"message": f"✨ AI Mapping Successful",
|
| 932 |
+
"analysis": {
|
| 933 |
+
"entity": name,
|
| 934 |
+
"origin": location,
|
| 935 |
+
"protocol": "REST/JSON" if is_api else "HTTP/HTML",
|
| 936 |
+
"confidence": "High"
|
| 937 |
+
}
|
| 938 |
+
})
|
| 939 |
+
|
| 940 |
+
@app.route('/api/report/excel', methods=['GET'])
|
| 941 |
+
def download_excel_report():
|
| 942 |
+
output = io.BytesIO()
|
| 943 |
+
workbook = xlsxwriter.Workbook(output, {'in_memory': True})
|
| 944 |
+
|
| 945 |
+
header_style = workbook.add_format({'bold': True, 'bg_color': '#1E293B', 'font_color': 'white', 'border': 1})
|
| 946 |
+
data_style = workbook.add_format({'border': 1})
|
| 947 |
+
up_style = workbook.add_format({'bg_color': '#D1FAE5', 'font_color': '#065F46', 'border': 1, 'bold': True})
|
| 948 |
+
down_style = workbook.add_format({'bg_color': '#FEE2E2', 'font_color': '#991B1B', 'border': 1, 'bold': True})
|
| 949 |
+
|
| 950 |
+
conn = get_db_connection()
|
| 951 |
+
c = conn.cursor()
|
| 952 |
+
|
| 953 |
+
ws_sum = workbook.add_worksheet("Monthly Overview")
|
| 954 |
+
ws_sum.set_column('A:F', 22)
|
| 955 |
+
|
| 956 |
+
headers = ["Service Name", "Location", "Total Checks", "Uptime %", "Avg Latency (ms)", "Status"]
|
| 957 |
+
for col, h in enumerate(headers):
|
| 958 |
+
ws_sum.write(0, col, h, header_style)
|
| 959 |
+
|
| 960 |
+
c.execute("SELECT name, location, COUNT(*) as total, SUM(is_up) as up_count, ROUND(AVG(response_time), 2) FROM status_history GROUP BY name, location")
|
| 961 |
+
data = c.fetchall()
|
| 962 |
+
|
| 963 |
+
row_idx = 1
|
| 964 |
+
chart = workbook.add_chart({'type': 'doughnut'})
|
| 965 |
+
chart.set_title({'name': 'Services Uptime Analysis'})
|
| 966 |
+
chart.set_style(10)
|
| 967 |
+
|
| 968 |
+
for row in data:
|
| 969 |
+
name = row[0]
|
| 970 |
+
loc = row[1]
|
| 971 |
+
total = row[2]
|
| 972 |
+
up_count = row[3]
|
| 973 |
+
avg_latency = row[4]
|
| 974 |
+
|
| 975 |
+
up_pct = (up_count / total * 100) if total > 0 else 0
|
| 976 |
+
status_text = "Healthy" if up_pct >= 95 else "Degraded"
|
| 977 |
+
|
| 978 |
+
ws_sum.write(row_idx, 0, name, data_style)
|
| 979 |
+
ws_sum.write(row_idx, 1, loc, data_style)
|
| 980 |
+
ws_sum.write(row_idx, 2, total, data_style)
|
| 981 |
+
ws_sum.write(row_idx, 3, f"{up_pct:.1f}%", up_style if up_pct >= 95 else down_style)
|
| 982 |
+
ws_sum.write(row_idx, 4, avg_latency, data_style)
|
| 983 |
+
ws_sum.write(row_idx, 5, status_text, up_style if up_pct >= 95 else down_style)
|
| 984 |
+
row_idx += 1
|
| 985 |
+
|
| 986 |
+
if row_idx > 1:
|
| 987 |
+
chart.add_series({
|
| 988 |
+
'name': 'Uptime Info',
|
| 989 |
+
'categories': ['Monthly Overview', 1, 0, row_idx-1, 0],
|
| 990 |
+
'values': ['Monthly Overview', 1, 2, row_idx-1, 2],
|
| 991 |
+
})
|
| 992 |
+
ws_sum.insert_chart('H2', chart)
|
| 993 |
+
|
| 994 |
+
conn.close()
|
| 995 |
+
workbook.close()
|
| 996 |
+
output.seek(0)
|
| 997 |
+
|
| 998 |
+
return send_file(output, download_name=f"StatusPilot_Report_{datetime.now().strftime('%Y-%m')}.xlsx", as_attachment=True)
|
| 999 |
+
|
| 1000 |
+
@app.route('/api/alert/test', methods=['POST'])
|
| 1001 |
+
def test_alert_email():
|
| 1002 |
+
if not check_auth():
|
| 1003 |
+
return jsonify({"error": "Unauthorized"}), 401
|
| 1004 |
+
|
| 1005 |
+
req_data = request.json or {}
|
| 1006 |
+
config = load_config()
|
| 1007 |
+
|
| 1008 |
+
if req_data.get("smtp_sender"):
|
| 1009 |
+
config["smtp_sender"] = req_data.get("smtp_sender").strip()
|
| 1010 |
+
if req_data.get("smtp_password") and not str(req_data.get("smtp_password")).startswith("••••"):
|
| 1011 |
+
config["smtp_password"] = security.encrypt_secret(str(req_data.get("smtp_password")).strip())
|
| 1012 |
+
if req_data.get("email_recipients"):
|
| 1013 |
+
config["email_recipients"] = req_data.get("email_recipients")
|
| 1014 |
+
if req_data.get("email_webhook_url"):
|
| 1015 |
+
config["email_webhook_url"] = req_data.get("email_webhook_url").strip()
|
| 1016 |
+
|
| 1017 |
+
sender = config.get("smtp_sender", "").strip()
|
| 1018 |
+
raw_recipients = config.get("email_recipients", [])
|
| 1019 |
+
|
| 1020 |
+
if isinstance(raw_recipients, str):
|
| 1021 |
+
recipients = [r.strip() for r in raw_recipients.split(',') if r.strip()]
|
| 1022 |
+
elif isinstance(raw_recipients, list):
|
| 1023 |
+
recipients = [r.strip() for r in raw_recipients if isinstance(r, str) and r.strip()]
|
| 1024 |
+
else:
|
| 1025 |
+
recipients = []
|
| 1026 |
+
|
| 1027 |
+
if not recipients and sender:
|
| 1028 |
+
recipients = [sender]
|
| 1029 |
+
|
| 1030 |
+
raw_pwd = os.environ.get('SMTP_PASSWORD') or config.get("smtp_password", "")
|
| 1031 |
+
decrypted_pwd = security.decrypt_secret(raw_pwd).strip()
|
| 1032 |
+
if not decrypted_pwd:
|
| 1033 |
+
decrypted_pwd = DEFAULT_SMTP_PASSWORD_RAW
|
| 1034 |
+
|
| 1035 |
+
if not sender:
|
| 1036 |
+
sender = DEFAULT_SMTP_SENDER
|
| 1037 |
+
|
| 1038 |
+
if not recipients:
|
| 1039 |
+
recipients = DEFAULT_EMAIL_RECIPIENTS
|
| 1040 |
+
|
| 1041 |
+
config["email_recipients"] = recipients
|
| 1042 |
+
|
| 1043 |
+
app_host_url = request.host_url.rstrip('/') if (request and request.host_url) else "https://parledowntime-pro.hf.space"
|
| 1044 |
+
try:
|
| 1045 |
+
send_alert(
|
| 1046 |
+
config=config,
|
| 1047 |
+
name="System Alert Diagnostic Node",
|
| 1048 |
+
loc="Global Operational Node",
|
| 1049 |
+
url=app_host_url,
|
| 1050 |
+
msg="Diagnostic Check OK",
|
| 1051 |
+
details="This is a test executive alert sent from StatusPilot to verify email formatting and delivery.",
|
| 1052 |
+
is_resolved=False,
|
| 1053 |
+
is_test=True
|
| 1054 |
+
)
|
| 1055 |
+
recip_str = ", ".join(recipients)
|
| 1056 |
+
return jsonify({"status": "success", "message": f"Test alert email successfully dispatched to: {recip_str}"})
|
| 1057 |
+
except Exception as e:
|
| 1058 |
+
return jsonify({"status": "error", "message": str(e)}), 200
|
| 1059 |
+
|
| 1060 |
+
if __name__ == '__main__':
|
| 1061 |
+
from waitress import serve
|
| 1062 |
+
print("Starting production Waitress server on http://0.0.0.0:5000")
|
| 1063 |
+
serve(app, host='0.0.0.0', port=5000)
|
| 1064 |
+
|
config.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"services_to_monitor": [
|
| 3 |
+
{
|
| 4 |
+
"name": "Microsoft 365",
|
| 5 |
+
"url": "https://status.office365.com",
|
| 6 |
+
"location": "Global",
|
| 7 |
+
"type": "http_code",
|
| 8 |
+
"expected_code": 200
|
| 9 |
+
},
|
| 10 |
+
{
|
| 11 |
+
"name": "RingCentral",
|
| 12 |
+
"url": "https://status.ringcentral.com",
|
| 13 |
+
"location": "Global",
|
| 14 |
+
"type": "http_code",
|
| 15 |
+
"expected_code": 200
|
| 16 |
+
},
|
| 17 |
+
{
|
| 18 |
+
"name": "Skybridge Healthcare",
|
| 19 |
+
"url": "https://www.skybridgehealthcare.com/",
|
| 20 |
+
"location": "Primary Web",
|
| 21 |
+
"type": "http_code",
|
| 22 |
+
"expected_code": 200
|
| 23 |
+
},
|
| 24 |
+
{
|
| 25 |
+
"name": "Skybridge Aviation",
|
| 26 |
+
"url": "https://skybridgeaviation.com/",
|
| 27 |
+
"location": "Primary Web",
|
| 28 |
+
"type": "http_code",
|
| 29 |
+
"expected_code": 200
|
| 30 |
+
},
|
| 31 |
+
{
|
| 32 |
+
"name": "Skybridge Resources",
|
| 33 |
+
"url": "https://skybridgeresources.com/",
|
| 34 |
+
"location": "Primary Web",
|
| 35 |
+
"type": "http_code",
|
| 36 |
+
"expected_code": 200
|
| 37 |
+
},
|
| 38 |
+
{
|
| 39 |
+
"name": "BetterLending Website",
|
| 40 |
+
"url": "https://betterlending.com/",
|
| 41 |
+
"location": "Primary Web",
|
| 42 |
+
"type": "http_code",
|
| 43 |
+
"expected_code": 200
|
| 44 |
+
}
|
| 45 |
+
],
|
| 46 |
+
"check_interval": 60,
|
| 47 |
+
"consecutive_failures_required": 2,
|
| 48 |
+
"retry_delay_seconds": 3,
|
| 49 |
+
"cooldown_minutes": 30,
|
| 50 |
+
"email_recipients": [
|
| 51 |
+
"suresh@parletech.com",
|
| 52 |
+
"smadapathi04@gmail.com"
|
| 53 |
+
],
|
| 54 |
+
"alert_mode": "SMTP",
|
| 55 |
+
"smtp_sender": "smadapathi04@gmail.com",
|
| 56 |
+
"smtp_password": "ENC:b35f6661b9b4433a6b53657e56206dfa",
|
| 57 |
+
"email_webhook_url": "https://script.google.com/macros/s/AKfycbySQsYRpOBLPUWXCFKR2ZqDHCoKd7tUhB92sgp7P1ap4iQ8ZpmyisIGK9CT5umT2EGz/exec"
|
| 58 |
+
}
|
license.dat
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
{"email": "suresh@parltech.com", "key": "SG-2E6D-8E22-27B0"}
|
render.yaml
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
services:
|
| 2 |
+
- type: web
|
| 3 |
+
name: statuspilot-cloud
|
| 4 |
+
env: python
|
| 5 |
+
plan: free
|
| 6 |
+
buildCommand: pip install -r requirements.txt
|
| 7 |
+
startCommand: gunicorn app:app
|
| 8 |
+
envVars:
|
| 9 |
+
- key: PYTHON_VERSION
|
| 10 |
+
value: 3.11.0
|
| 11 |
+
- key: SECRET_KEY
|
| 12 |
+
generateValue: true
|
requirements.txt
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Flask==3.0.0
|
| 2 |
+
requests==2.31.0
|
| 3 |
+
gunicorn==21.2.0
|
| 4 |
+
Werkzeug==3.0.1
|
| 5 |
+
waitress==3.0.0
|
| 6 |
+
xlsxwriter==3.1.9
|
runtime.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
python-3.11.0
|
security.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import hashlib
|
| 2 |
+
import uuid
|
| 3 |
+
import platform
|
| 4 |
+
|
| 5 |
+
# SECRET_SALT: Change this to something unique before selling!
|
| 6 |
+
# This connects the KeyGen to the App.
|
| 7 |
+
SECRET_SALT = "ServiceGuard_2025_Secure_Salt_9988"
|
| 8 |
+
|
| 9 |
+
def generate_license_key(user_ref):
|
| 10 |
+
"""Generates a key based on a user reference (e.g. gumroad email)."""
|
| 11 |
+
# Create a hash of the salt + user_ref
|
| 12 |
+
raw_str = f"{SECRET_SALT}-{user_ref.strip().lower()}"
|
| 13 |
+
hash_object = hashlib.sha256(raw_str.encode())
|
| 14 |
+
hex_dig = hash_object.hexdigest().upper()
|
| 15 |
+
|
| 16 |
+
# Format: SG-XXXX-XXXX-XXXX (First 12 chars of hash)
|
| 17 |
+
return f"SG-{hex_dig[:4]}-{hex_dig[4:8]}-{hex_dig[8:12]}"
|
| 18 |
+
|
| 19 |
+
def verify_license_key(user_ref, input_key):
|
| 20 |
+
"""Verifies that the input key matches the generated key for that user."""
|
| 21 |
+
expected_key = generate_license_key(user_ref)
|
| 22 |
+
return input_key.strip() == expected_key
|
| 23 |
+
|
| 24 |
+
import base64
|
| 25 |
+
|
| 26 |
+
def encrypt_secret(plaintext):
|
| 27 |
+
"""Encrypt sensitive credentials before storing to disk."""
|
| 28 |
+
if not plaintext or str(plaintext).startswith("ENC:"):
|
| 29 |
+
return plaintext
|
| 30 |
+
key_bytes = hashlib.sha256(SECRET_SALT.encode('utf-8')).digest()
|
| 31 |
+
pt_bytes = str(plaintext).strip().encode('utf-8')
|
| 32 |
+
ct_hex = []
|
| 33 |
+
for i, b in enumerate(pt_bytes):
|
| 34 |
+
ct_hex.append(f"{b ^ key_bytes[i % len(key_bytes)]:02x}")
|
| 35 |
+
return "ENC:" + "".join(ct_hex)
|
| 36 |
+
|
| 37 |
+
def decrypt_secret(ciphertext):
|
| 38 |
+
"""Decrypt stored credentials back into RAM for authentication."""
|
| 39 |
+
if not ciphertext or not str(ciphertext).startswith("ENC:"):
|
| 40 |
+
return ciphertext
|
| 41 |
+
try:
|
| 42 |
+
raw_hex = str(ciphertext)[4:].strip()
|
| 43 |
+
ct_bytes = bytes.fromhex(raw_hex)
|
| 44 |
+
key_bytes = hashlib.sha256(SECRET_SALT.encode('utf-8')).digest()
|
| 45 |
+
pt_bytes = bytearray()
|
| 46 |
+
for i, b in enumerate(ct_bytes):
|
| 47 |
+
pt_bytes.append(b ^ key_bytes[i % len(key_bytes)])
|
| 48 |
+
return pt_bytes.decode('utf-8', errors='ignore')
|
| 49 |
+
except Exception:
|
| 50 |
+
return ""
|
| 51 |
+
|
static/parle_tech_logo.png
ADDED
|
static/script.js
ADDED
|
@@ -0,0 +1,584 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* StatusPilot Ultra Pro v5
|
| 3 |
+
* Logic & Infrastructure Interface
|
| 4 |
+
*/
|
| 5 |
+
|
| 6 |
+
let appData = {
|
| 7 |
+
status: [],
|
| 8 |
+
config: null,
|
| 9 |
+
summary: null
|
| 10 |
+
};
|
| 11 |
+
|
| 12 |
+
let charts = {};
|
| 13 |
+
let currentLocationFilter = 'All';
|
| 14 |
+
let isAuthPromptShowing = false;
|
| 15 |
+
|
| 16 |
+
// Global Axios Interceptor for 401s
|
| 17 |
+
axios.interceptors.response.use(
|
| 18 |
+
response => response,
|
| 19 |
+
error => {
|
| 20 |
+
if (error.response && error.response.status === 401) {
|
| 21 |
+
console.warn("Session expired or unauthorized. Showing prompt.");
|
| 22 |
+
if (!isAuthPromptShowing) {
|
| 23 |
+
showAuthOverlay();
|
| 24 |
+
}
|
| 25 |
+
}
|
| 26 |
+
return Promise.reject(error);
|
| 27 |
+
}
|
| 28 |
+
);
|
| 29 |
+
|
| 30 |
+
function showAuthOverlay() {
|
| 31 |
+
// Only show if not already visible
|
| 32 |
+
if (isAuthPromptShowing) return;
|
| 33 |
+
|
| 34 |
+
isAuthPromptShowing = true;
|
| 35 |
+
const overlay = document.getElementById('master-auth-overlay');
|
| 36 |
+
if (overlay) {
|
| 37 |
+
overlay.style.display = 'flex';
|
| 38 |
+
overlay.style.zIndex = "999999"; // Ensure it's on top
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
const mainApp = document.getElementById('main-app');
|
| 42 |
+
if (mainApp) {
|
| 43 |
+
mainApp.style.filter = 'blur(20px)';
|
| 44 |
+
mainApp.style.opacity = '0.3';
|
| 45 |
+
mainApp.style.pointerEvents = 'none';
|
| 46 |
+
}
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
// Configuration
|
| 50 |
+
const REFRESH_INTERVAL = 30000;
|
| 51 |
+
const ACCENT_COLOR = '#10b981';
|
| 52 |
+
const PRIMARY_COLOR = '#3b82f6';
|
| 53 |
+
|
| 54 |
+
// --- Initialization ---
|
| 55 |
+
|
| 56 |
+
async function init() {
|
| 57 |
+
updateClock();
|
| 58 |
+
setInterval(updateClock, 1000);
|
| 59 |
+
|
| 60 |
+
const authStatus = await checkAuthStatus();
|
| 61 |
+
if (authStatus.required && !authStatus.authenticated) {
|
| 62 |
+
document.getElementById('master-auth-overlay').style.display = 'flex';
|
| 63 |
+
return;
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
unlockDashboard();
|
| 67 |
+
|
| 68 |
+
const license = await checkLicense();
|
| 69 |
+
if (!license.licensed) {
|
| 70 |
+
document.getElementById('license-overlay').style.display = 'flex';
|
| 71 |
+
return;
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
await loadData();
|
| 75 |
+
setInterval(loadData, REFRESH_INTERVAL);
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
async function loadData() {
|
| 79 |
+
try {
|
| 80 |
+
await Promise.all([
|
| 81 |
+
loadConfig(),
|
| 82 |
+
loadSummary(),
|
| 83 |
+
loadStatus()
|
| 84 |
+
]);
|
| 85 |
+
} catch (err) {
|
| 86 |
+
console.error("Critical Sync Error:", err);
|
| 87 |
+
}
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
// --- Auth & Security ---
|
| 91 |
+
|
| 92 |
+
async function checkAuthStatus() {
|
| 93 |
+
try {
|
| 94 |
+
const res = await axios.get('/api/auth/status');
|
| 95 |
+
return res.data;
|
| 96 |
+
} catch (err) { return { required: true, authenticated: false }; }
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
async function unlockDashboard() {
|
| 100 |
+
isAuthPromptShowing = false;
|
| 101 |
+
document.getElementById('master-auth-overlay').style.display = 'none';
|
| 102 |
+
const mainApp = document.getElementById('main-app');
|
| 103 |
+
mainApp.style.filter = 'none';
|
| 104 |
+
mainApp.style.opacity = '1';
|
| 105 |
+
mainApp.style.pointerEvents = 'auto';
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
document.getElementById('master-login-btn').onclick = async () => {
|
| 109 |
+
const keyInput = document.getElementById('master-key-input');
|
| 110 |
+
const key = keyInput.value.trim();
|
| 111 |
+
const btn = document.getElementById('master-login-btn');
|
| 112 |
+
const err = document.getElementById('master-error');
|
| 113 |
+
|
| 114 |
+
if (!key) return;
|
| 115 |
+
|
| 116 |
+
btn.innerHTML = '<i class="fas fa-circle-notch fa-spin"></i> SYNCING...';
|
| 117 |
+
btn.disabled = true;
|
| 118 |
+
err.style.display = 'none';
|
| 119 |
+
|
| 120 |
+
try {
|
| 121 |
+
const res = await axios.post('/api/auth/login', { key });
|
| 122 |
+
if (res.data.success) {
|
| 123 |
+
console.log("Login successful, verifying session...");
|
| 124 |
+
// CRITICAL: Wait a moment for the cookie to settle
|
| 125 |
+
setTimeout(async () => {
|
| 126 |
+
const status = await checkAuthStatus();
|
| 127 |
+
if (status.authenticated) {
|
| 128 |
+
btn.textContent = 'SYNC SUCCESSFUL';
|
| 129 |
+
unlockDashboard();
|
| 130 |
+
await init(); // Re-run init to load data
|
| 131 |
+
} else {
|
| 132 |
+
throw new Error("Session verification failed after login.");
|
| 133 |
+
}
|
| 134 |
+
}, 500);
|
| 135 |
+
} else {
|
| 136 |
+
err.textContent = "Invalid Master Access Key";
|
| 137 |
+
err.style.display = 'block';
|
| 138 |
+
btn.textContent = 'Unlock Data Center';
|
| 139 |
+
btn.disabled = false;
|
| 140 |
+
}
|
| 141 |
+
} catch (e) {
|
| 142 |
+
console.error(e);
|
| 143 |
+
err.textContent = "Security Node Offline or Cookie Blocked";
|
| 144 |
+
err.style.display = 'block';
|
| 145 |
+
btn.textContent = 'Unlock Data Center';
|
| 146 |
+
btn.disabled = false;
|
| 147 |
+
}
|
| 148 |
+
};
|
| 149 |
+
|
| 150 |
+
async function checkLicense() {
|
| 151 |
+
try {
|
| 152 |
+
const res = await axios.get('/api/license/status');
|
| 153 |
+
return res.data;
|
| 154 |
+
} catch (err) { return { licensed: false }; }
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
document.getElementById('activate-btn').onclick = async () => {
|
| 158 |
+
const email = document.getElementById('lic-email').value;
|
| 159 |
+
const key = document.getElementById('lic-key').value;
|
| 160 |
+
try {
|
| 161 |
+
const res = await axios.post('/api/license/activate', { email, key: key.trim().toUpperCase() });
|
| 162 |
+
if (res.data.success) init();
|
| 163 |
+
else {
|
| 164 |
+
const err = document.getElementById('lic-error');
|
| 165 |
+
err.textContent = res.data.message;
|
| 166 |
+
err.style.display = 'block';
|
| 167 |
+
}
|
| 168 |
+
} catch (e) { console.error(e); }
|
| 169 |
+
};
|
| 170 |
+
|
| 171 |
+
// --- Data Fetching ---
|
| 172 |
+
|
| 173 |
+
async function loadConfig() {
|
| 174 |
+
const res = await axios.get('/api/config');
|
| 175 |
+
appData.config = res.data;
|
| 176 |
+
|
| 177 |
+
// Fill selects
|
| 178 |
+
const locFilter = document.getElementById('location-filter');
|
| 179 |
+
const toolSelect = document.getElementById('test-tool-select');
|
| 180 |
+
|
| 181 |
+
const locations = new Set(['All', 'Global']);
|
| 182 |
+
appData.config.services_to_monitor.forEach(s => {
|
| 183 |
+
if(s.location) locations.add(s.location);
|
| 184 |
+
});
|
| 185 |
+
|
| 186 |
+
const prevLoc = locFilter.value;
|
| 187 |
+
locFilter.innerHTML = '';
|
| 188 |
+
locations.forEach(loc => {
|
| 189 |
+
const opt = document.createElement('option');
|
| 190 |
+
opt.value = loc; opt.textContent = loc === 'All' ? 'GLOBAL NETWORK' : loc.toUpperCase();
|
| 191 |
+
locFilter.appendChild(opt);
|
| 192 |
+
});
|
| 193 |
+
if(prevLoc) locFilter.value = prevLoc;
|
| 194 |
+
|
| 195 |
+
toolSelect.innerHTML = '';
|
| 196 |
+
appData.config.services_to_monitor.forEach(s => {
|
| 197 |
+
const opt = document.createElement('option');
|
| 198 |
+
opt.value = s.name; opt.textContent = s.name;
|
| 199 |
+
toolSelect.appendChild(opt);
|
| 200 |
+
});
|
| 201 |
+
|
| 202 |
+
// Populate Management List
|
| 203 |
+
renderManagementList();
|
| 204 |
+
|
| 205 |
+
// Settings fields
|
| 206 |
+
document.getElementById('check-interval').value = appData.config.check_interval;
|
| 207 |
+
document.getElementById('alert-mode').value = appData.config.alert_mode || 'SMTP';
|
| 208 |
+
document.getElementById('smtp-user').value = appData.config.smtp_sender || '';
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
function renderManagementList() {
|
| 212 |
+
const list = document.getElementById('manage-svc-list');
|
| 213 |
+
if (!list) return;
|
| 214 |
+
list.innerHTML = '';
|
| 215 |
+
|
| 216 |
+
appData.config.services_to_monitor.forEach((s, index) => {
|
| 217 |
+
const item = document.createElement('div');
|
| 218 |
+
item.className = 'manage-item';
|
| 219 |
+
item.innerHTML = `
|
| 220 |
+
<div style="display:flex; flex-direction:column;">
|
| 221 |
+
<b style="font-size:0.9rem;">${s.name}</b>
|
| 222 |
+
<span style="font-size:0.7rem; color:var(--text-muted);">${s.url}</span>
|
| 223 |
+
</div>
|
| 224 |
+
<button class="btn-delete" onclick="deleteService(${index}, '${s.name}')">
|
| 225 |
+
<i class="fas fa-trash-alt"></i> REMOVE
|
| 226 |
+
</button>
|
| 227 |
+
`;
|
| 228 |
+
list.appendChild(item);
|
| 229 |
+
});
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
async function deleteService(index, name) {
|
| 233 |
+
if (!confirm(`🚨 DECOMMISSION NODE: Are you sure you want to remove ${name} from active monitoring?`)) return;
|
| 234 |
+
|
| 235 |
+
try {
|
| 236 |
+
// Switching to POST for decommissioning to avoid DELETE method blocks in cloud proxies
|
| 237 |
+
const res = await axios.post('/api/config/delete', { name });
|
| 238 |
+
if (res.data.status === 'success') {
|
| 239 |
+
console.log(`Node ${name} successfully decommissioned.`);
|
| 240 |
+
await loadConfig();
|
| 241 |
+
await loadStatus();
|
| 242 |
+
} else {
|
| 243 |
+
throw new Error(res.data.message || "Unknown server error");
|
| 244 |
+
}
|
| 245 |
+
} catch (e) {
|
| 246 |
+
console.error(e);
|
| 247 |
+
const msg = e.response?.data?.message || e.message;
|
| 248 |
+
alert(`OPERATIONAL ERROR: ${msg}\n\nPlease ensure you are logged in and the node still exists.`);
|
| 249 |
+
}
|
| 250 |
+
}
|
| 251 |
+
|
| 252 |
+
async function loadSummary() {
|
| 253 |
+
const res = await axios.get('/api/summary');
|
| 254 |
+
const s = res.data;
|
| 255 |
+
animateValue('stat-monitors', s.total_monitors);
|
| 256 |
+
animateValue('stat-uptime', s.uptime_pct, '%');
|
| 257 |
+
animateValue('stat-incidents', s.incidents);
|
| 258 |
+
animateValue('stat-latency', s.avg_latency, 'ms');
|
| 259 |
+
}
|
| 260 |
+
|
| 261 |
+
async function loadStatus() {
|
| 262 |
+
const res = await axios.get('/api/status');
|
| 263 |
+
appData.status = res.data;
|
| 264 |
+
renderStatusGrid();
|
| 265 |
+
renderGlobalMatrix();
|
| 266 |
+
}
|
| 267 |
+
|
| 268 |
+
// --- UI Rendering ---
|
| 269 |
+
|
| 270 |
+
function renderStatusGrid() {
|
| 271 |
+
const grid = document.getElementById('status-grid');
|
| 272 |
+
grid.innerHTML = '';
|
| 273 |
+
|
| 274 |
+
const filtered = appData.status.filter(s => currentLocationFilter === 'All' || s.location === currentLocationFilter);
|
| 275 |
+
|
| 276 |
+
if (filtered.length === 0) {
|
| 277 |
+
grid.innerHTML = '<div style="text-align:center; padding:3rem; color:var(--text-muted);">No services matching filter.</div>';
|
| 278 |
+
return;
|
| 279 |
+
}
|
| 280 |
+
|
| 281 |
+
filtered.forEach(s => {
|
| 282 |
+
const row = document.createElement('div');
|
| 283 |
+
row.className = 'status-row';
|
| 284 |
+
row.onclick = () => openDetails(s.name);
|
| 285 |
+
|
| 286 |
+
let timelineHTML = '<div class="uptime-timeline">';
|
| 287 |
+
const history = [...Array(30 - s.timeline.length).fill(null), ...s.timeline];
|
| 288 |
+
history.forEach(state => {
|
| 289 |
+
const cls = state === null ? 'none' : (state ? 'up' : 'down');
|
| 290 |
+
timelineHTML += `<div class="timeline-bar ${cls}"></div>`;
|
| 291 |
+
});
|
| 292 |
+
timelineHTML += '</div>';
|
| 293 |
+
|
| 294 |
+
row.innerHTML = `
|
| 295 |
+
<div class="svc-info">
|
| 296 |
+
<h3>${s.name}</h3>
|
| 297 |
+
<span>${s.url}</span>
|
| 298 |
+
</div>
|
| 299 |
+
<div><span class="location-badge">${s.location}</span></div>
|
| 300 |
+
${timelineHTML}
|
| 301 |
+
<div class="status-text ${s.is_up ? 'up' : 'down'}">${s.is_up ? 'Operational' : 'Major Outage'}</div>
|
| 302 |
+
<div class="latency">${s.latency}ms</div>
|
| 303 |
+
`;
|
| 304 |
+
grid.appendChild(row);
|
| 305 |
+
});
|
| 306 |
+
}
|
| 307 |
+
|
| 308 |
+
function renderGlobalMatrix() {
|
| 309 |
+
const ctx = document.getElementById('globalLatencyMatrix');
|
| 310 |
+
if (!ctx) return;
|
| 311 |
+
|
| 312 |
+
const labels = appData.status.map(s => s.name);
|
| 313 |
+
const data = appData.status.map(s => s.latency);
|
| 314 |
+
|
| 315 |
+
if (charts.globalMatrix) charts.globalMatrix.destroy();
|
| 316 |
+
|
| 317 |
+
charts.globalMatrix = new Chart(ctx, {
|
| 318 |
+
type: 'bar',
|
| 319 |
+
data: {
|
| 320 |
+
labels: labels,
|
| 321 |
+
datasets: [{
|
| 322 |
+
label: 'Latency (ms)',
|
| 323 |
+
data: data,
|
| 324 |
+
backgroundColor: data.map(v => v > 500 ? 'rgba(239, 68, 68, 0.4)' : 'rgba(59, 130, 246, 0.4)'),
|
| 325 |
+
borderColor: data.map(v => v > 500 ? '#ef4444' : '#3b82f6'),
|
| 326 |
+
borderWidth: 1,
|
| 327 |
+
borderRadius: 8
|
| 328 |
+
}]
|
| 329 |
+
},
|
| 330 |
+
options: {
|
| 331 |
+
indexAxis: 'y',
|
| 332 |
+
responsive: true,
|
| 333 |
+
maintainAspectRatio: false,
|
| 334 |
+
plugins: { legend: { display: false } },
|
| 335 |
+
scales: {
|
| 336 |
+
x: { grid: { color: 'rgba(255,255,255,0.05)' }, ticks: { color: '#94a3b8' } },
|
| 337 |
+
y: { grid: { display: false }, ticks: { color: '#fff', font: { weight: '600' } } }
|
| 338 |
+
}
|
| 339 |
+
}
|
| 340 |
+
});
|
| 341 |
+
}
|
| 342 |
+
|
| 343 |
+
// --- Analytics Modal ---
|
| 344 |
+
|
| 345 |
+
async function openDetails(name) {
|
| 346 |
+
document.getElementById('modal-title').textContent = name;
|
| 347 |
+
document.getElementById('matrix-modal').style.display = 'block';
|
| 348 |
+
|
| 349 |
+
try {
|
| 350 |
+
const res = await axios.get(`/api/history/${encodeURIComponent(name)}`);
|
| 351 |
+
const history = res.data;
|
| 352 |
+
renderMatrixCharts(history);
|
| 353 |
+
renderIncidentLog(history);
|
| 354 |
+
} catch (e) { console.error(e); }
|
| 355 |
+
}
|
| 356 |
+
|
| 357 |
+
function renderMatrixCharts(data) {
|
| 358 |
+
const labels = data.map(d => new Date(d.timestamp).toLocaleTimeString());
|
| 359 |
+
const latencies = data.map(d => d.latency);
|
| 360 |
+
const uptimes = data.map(d => d.is_up ? 100 : 0);
|
| 361 |
+
|
| 362 |
+
if (charts.detLat) charts.detLat.destroy();
|
| 363 |
+
if (charts.detUp) charts.detUp.destroy();
|
| 364 |
+
if (charts.detErr) charts.detErr.destroy();
|
| 365 |
+
|
| 366 |
+
const chartOps = {
|
| 367 |
+
responsive: true,
|
| 368 |
+
plugins: { legend: { display: false } },
|
| 369 |
+
scales: {
|
| 370 |
+
x: { grid: { display: false }, ticks: { display: false } },
|
| 371 |
+
y: { grid: { color: 'rgba(255,255,255,0.05)' }, ticks: { color: '#94a3b8' } }
|
| 372 |
+
}
|
| 373 |
+
};
|
| 374 |
+
|
| 375 |
+
charts.detLat = new Chart(document.getElementById('latencyChart'), {
|
| 376 |
+
type: 'line',
|
| 377 |
+
data: {
|
| 378 |
+
labels: labels,
|
| 379 |
+
datasets: [{
|
| 380 |
+
data: latencies,
|
| 381 |
+
borderColor: PRIMARY_COLOR,
|
| 382 |
+
backgroundColor: 'rgba(59, 130, 246, 0.1)',
|
| 383 |
+
fill: true,
|
| 384 |
+
tension: 0.4,
|
| 385 |
+
pointRadius: 0
|
| 386 |
+
}]
|
| 387 |
+
},
|
| 388 |
+
options: chartOps
|
| 389 |
+
});
|
| 390 |
+
|
| 391 |
+
charts.detUp = new Chart(document.getElementById('uptimeChart'), {
|
| 392 |
+
type: 'line',
|
| 393 |
+
data: {
|
| 394 |
+
labels: labels,
|
| 395 |
+
datasets: [{
|
| 396 |
+
data: uptimes,
|
| 397 |
+
borderColor: ACCENT_COLOR,
|
| 398 |
+
backgroundColor: 'rgba(16, 185, 129, 0.1)',
|
| 399 |
+
fill: true,
|
| 400 |
+
stepped: true,
|
| 401 |
+
pointRadius: 0
|
| 402 |
+
}]
|
| 403 |
+
},
|
| 404 |
+
options: { ...chartOps, scales: { ...chartOps.scales, y: { ...chartOps.scales.y, max: 100, min: 0 } } }
|
| 405 |
+
});
|
| 406 |
+
|
| 407 |
+
charts.detErr = new Chart(document.getElementById('errorChart'), {
|
| 408 |
+
type: 'doughnut',
|
| 409 |
+
data: {
|
| 410 |
+
labels: ['Success', 'Failure'],
|
| 411 |
+
datasets: [{
|
| 412 |
+
data: [data.filter(d => d.is_up).length, data.filter(d => !d.is_up).length],
|
| 413 |
+
backgroundColor: [ACCENT_COLOR, '#ef4444'],
|
| 414 |
+
borderWidth: 0
|
| 415 |
+
}]
|
| 416 |
+
},
|
| 417 |
+
options: { cutout: '70%', plugins: { legend: { position: 'bottom', labels: { color: '#94a3b8' } } } }
|
| 418 |
+
});
|
| 419 |
+
}
|
| 420 |
+
|
| 421 |
+
function renderIncidentLog(data) {
|
| 422 |
+
const log = document.getElementById('incident-log');
|
| 423 |
+
log.innerHTML = '';
|
| 424 |
+
const fails = data.filter(d => !d.is_up).reverse();
|
| 425 |
+
|
| 426 |
+
if (fails.length === 0) {
|
| 427 |
+
log.innerHTML = '<p style="color: var(--accent); font-weight: 600;">System is healthy. No recent incidents.</p>';
|
| 428 |
+
return;
|
| 429 |
+
}
|
| 430 |
+
|
| 431 |
+
fails.forEach(f => {
|
| 432 |
+
const item = document.createElement('div');
|
| 433 |
+
item.style.padding = '10px 0';
|
| 434 |
+
item.style.borderBottom = '1px solid var(--border)';
|
| 435 |
+
item.innerHTML = `<span style="color:#ef4444; font-weight:700;">DOWN</span> <span style="margin-left:10px;">${new Date(f.timestamp).toLocaleString()}</span>`;
|
| 436 |
+
log.appendChild(item);
|
| 437 |
+
});
|
| 438 |
+
}
|
| 439 |
+
|
| 440 |
+
// --- Helpers & Utilities ---
|
| 441 |
+
|
| 442 |
+
function animateValue(id, value, suffix = '') {
|
| 443 |
+
const el = document.getElementById(id);
|
| 444 |
+
const start = parseInt(el.textContent) || 0;
|
| 445 |
+
const duration = 1000;
|
| 446 |
+
const startTime = performance.now();
|
| 447 |
+
|
| 448 |
+
function update(now) {
|
| 449 |
+
const elapsed = now - startTime;
|
| 450 |
+
const progress = Math.min(elapsed / duration, 1);
|
| 451 |
+
const current = start + (value - start) * progress;
|
| 452 |
+
|
| 453 |
+
if (suffix === '%') el.textContent = current.toFixed(1) + suffix;
|
| 454 |
+
else if (suffix === 'ms') el.textContent = Math.round(current) + suffix;
|
| 455 |
+
else el.textContent = Math.round(current);
|
| 456 |
+
|
| 457 |
+
if (progress < 1) requestAnimationFrame(update);
|
| 458 |
+
}
|
| 459 |
+
requestAnimationFrame(update);
|
| 460 |
+
}
|
| 461 |
+
|
| 462 |
+
function updateClock() {
|
| 463 |
+
const el = document.getElementById('live-clock');
|
| 464 |
+
const now = new Date();
|
| 465 |
+
el.textContent = now.toLocaleTimeString() + ' | ' + now.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
|
| 466 |
+
}
|
| 467 |
+
|
| 468 |
+
function showToast(msg, type = 'success') {
|
| 469 |
+
// Simple alert for now, can be upgraded to toast
|
| 470 |
+
console.log(`[${type.toUpperCase()}] ${msg}`);
|
| 471 |
+
}
|
| 472 |
+
|
| 473 |
+
// --- Event Handlers ---
|
| 474 |
+
|
| 475 |
+
document.querySelectorAll('.nav-item').forEach(item => {
|
| 476 |
+
item.onclick = (e) => {
|
| 477 |
+
e.preventDefault();
|
| 478 |
+
document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
|
| 479 |
+
item.classList.add('active');
|
| 480 |
+
|
| 481 |
+
const target = item.id.replace('nav-', 'section-');
|
| 482 |
+
document.querySelectorAll('.content-section').forEach(s => s.style.display = 'none');
|
| 483 |
+
document.getElementById(target).style.display = 'block';
|
| 484 |
+
};
|
| 485 |
+
});
|
| 486 |
+
|
| 487 |
+
document.getElementById('location-filter').onchange = (e) => {
|
| 488 |
+
currentLocationFilter = e.target.value;
|
| 489 |
+
renderStatusGrid();
|
| 490 |
+
};
|
| 491 |
+
|
| 492 |
+
document.querySelector('.close-modal').onclick = () => {
|
| 493 |
+
document.getElementById('matrix-modal').style.display = 'none';
|
| 494 |
+
};
|
| 495 |
+
|
| 496 |
+
document.getElementById('purge-stale-btn').onclick = async () => {
|
| 497 |
+
if (!confirm("🚨 WARNING: This will permanently delete history for services no longer in your config. Proceed?")) return;
|
| 498 |
+
|
| 499 |
+
try {
|
| 500 |
+
const res = await axios.post('/api/admin/purge_stale');
|
| 501 |
+
alert(`Cleanup complete! Purged ${res.data.purged_records} records.`);
|
| 502 |
+
loadData();
|
| 503 |
+
} catch (e) { alert("Failed to purge stale data."); }
|
| 504 |
+
};
|
| 505 |
+
|
| 506 |
+
document.getElementById('save-settings-btn').onclick = async () => {
|
| 507 |
+
const config = { ...appData.config };
|
| 508 |
+
config.check_interval = parseInt(document.getElementById('check-interval').value);
|
| 509 |
+
config.alert_mode = document.getElementById('alert-mode').value;
|
| 510 |
+
config.smtp_sender = document.getElementById('smtp-user').value;
|
| 511 |
+
config.smtp_password = document.getElementById('smtp-pass').value;
|
| 512 |
+
|
| 513 |
+
try {
|
| 514 |
+
await axios.post('/api/config', config);
|
| 515 |
+
alert("Configuration updated successfully!");
|
| 516 |
+
loadData();
|
| 517 |
+
} catch (e) { alert("Failed to save configuration."); }
|
| 518 |
+
};
|
| 519 |
+
|
| 520 |
+
document.getElementById('add-svc-btn').onclick = async () => {
|
| 521 |
+
const name = document.getElementById('new-svc-name').value.trim();
|
| 522 |
+
const url = document.getElementById('new-svc-url').value.trim();
|
| 523 |
+
const loc = document.getElementById('new-svc-loc').value.trim() || 'Global';
|
| 524 |
+
const type = document.getElementById('new-svc-type').value;
|
| 525 |
+
|
| 526 |
+
if (!name || !url) return alert("Service Name and Resource URL are required.");
|
| 527 |
+
|
| 528 |
+
const config = { ...appData.config };
|
| 529 |
+
|
| 530 |
+
const newService = {
|
| 531 |
+
name,
|
| 532 |
+
url,
|
| 533 |
+
location: loc,
|
| 534 |
+
type: type
|
| 535 |
+
};
|
| 536 |
+
|
| 537 |
+
// Add specific fields based on type
|
| 538 |
+
if (type === 'http_code') newService.expected_code = 200;
|
| 539 |
+
if (type === 'json_status') {
|
| 540 |
+
newService.key_to_check = 'status';
|
| 541 |
+
newService.expected_value = 'up';
|
| 542 |
+
}
|
| 543 |
+
|
| 544 |
+
config.services_to_monitor.push(newService);
|
| 545 |
+
|
| 546 |
+
try {
|
| 547 |
+
await axios.post('/api/config', config);
|
| 548 |
+
alert(`Node Activated: ${name}`);
|
| 549 |
+
document.getElementById('new-svc-name').value = '';
|
| 550 |
+
document.getElementById('new-svc-url').value = '';
|
| 551 |
+
loadConfig();
|
| 552 |
+
loadStatus();
|
| 553 |
+
} catch (e) { alert("Failed to provision new service node."); }
|
| 554 |
+
};
|
| 555 |
+
|
| 556 |
+
document.getElementById('run-test-btn').onclick = async () => {
|
| 557 |
+
const tool = document.getElementById('test-tool-select').value;
|
| 558 |
+
const location = document.getElementById('test-location-select').value;
|
| 559 |
+
const results = document.getElementById('test-results');
|
| 560 |
+
|
| 561 |
+
results.innerHTML = '<div style="text-align:center; padding:2rem;"><i class="fas fa-circle-notch fa-spin"></i> Pinging remote node...</div>';
|
| 562 |
+
|
| 563 |
+
try {
|
| 564 |
+
const res = await axios.post('/api/test', { tool, location });
|
| 565 |
+
const d = res.data;
|
| 566 |
+
results.innerHTML = `
|
| 567 |
+
<div class="glass-panel" style="border-color: ${d.is_up ? 'var(--accent)' : 'var(--danger)'}">
|
| 568 |
+
<h3 style="color: ${d.is_up ? 'var(--accent)' : 'var(--danger)'}">${d.is_up ? 'TRACEROUTE SUCCESSFUL' : 'NODE UNREACHABLE'}</h3>
|
| 569 |
+
<div style="margin-top: 1rem; display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
|
| 570 |
+
<div><b>Latency:</b> ${d.latency}ms</div>
|
| 571 |
+
<div><b>Location:</b> ${d.location}</div>
|
| 572 |
+
<div style="grid-column: 1/-1;"><b>Response:</b> ${d.message}</div>
|
| 573 |
+
</div>
|
| 574 |
+
</div>
|
| 575 |
+
`;
|
| 576 |
+
} catch (e) { results.innerHTML = '<div class="glass-panel" style="color:var(--danger)">Diagnostic timeout. Node might be offline.</div>'; }
|
| 577 |
+
};
|
| 578 |
+
|
| 579 |
+
document.getElementById('download-excel-btn').onclick = () => {
|
| 580 |
+
window.location.href = '/api/report/excel';
|
| 581 |
+
};
|
| 582 |
+
|
| 583 |
+
// Start
|
| 584 |
+
init();
|
static/script_v5.js
ADDED
|
@@ -0,0 +1,353 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* StatusPilot Ultra v6 — Fusion Engine
|
| 3 |
+
* Best of all 3 dashboards combined
|
| 4 |
+
*/
|
| 5 |
+
|
| 6 |
+
let appData = { status: [], config: null };
|
| 7 |
+
let charts = {};
|
| 8 |
+
let currentLocationFilter = 'All';
|
| 9 |
+
let searchQuery = '';
|
| 10 |
+
let isAuthPromptShowing = false;
|
| 11 |
+
|
| 12 |
+
// --- Axios 401 Interceptor ---
|
| 13 |
+
axios.interceptors.response.use(r => r, error => {
|
| 14 |
+
if (error.response && error.response.status === 401 && !isAuthPromptShowing) {
|
| 15 |
+
isAuthPromptShowing = true;
|
| 16 |
+
document.getElementById('master-auth-overlay').style.display = 'flex';
|
| 17 |
+
}
|
| 18 |
+
return Promise.reject(error);
|
| 19 |
+
});
|
| 20 |
+
|
| 21 |
+
// --- Init ---
|
| 22 |
+
async function init() {
|
| 23 |
+
updateClock();
|
| 24 |
+
setInterval(updateClock, 1000);
|
| 25 |
+
|
| 26 |
+
const auth = await checkAuth();
|
| 27 |
+
if (auth.required && !auth.authenticated) {
|
| 28 |
+
document.getElementById('master-auth-overlay').style.display = 'flex';
|
| 29 |
+
return;
|
| 30 |
+
}
|
| 31 |
+
unlock();
|
| 32 |
+
|
| 33 |
+
const lic = await checkLicense();
|
| 34 |
+
if (!lic.licensed) {
|
| 35 |
+
document.getElementById('license-overlay').style.display = 'flex';
|
| 36 |
+
return;
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
await loadData();
|
| 40 |
+
setInterval(loadData, 30000);
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
async function loadData() {
|
| 44 |
+
try { await Promise.all([loadConfig(), loadSummary(), loadStatus()]); }
|
| 45 |
+
catch (e) { console.error("Sync:", e); }
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
async function checkAuth() {
|
| 49 |
+
try { return (await axios.get('/api/auth/status')).data; }
|
| 50 |
+
catch { return { required: true, authenticated: false }; }
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
function unlock() {
|
| 54 |
+
isAuthPromptShowing = false;
|
| 55 |
+
document.getElementById('master-auth-overlay').style.display = 'none';
|
| 56 |
+
const app = document.getElementById('main-app');
|
| 57 |
+
app.style.filter = 'none'; app.style.opacity = '1'; app.style.pointerEvents = 'auto';
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
document.getElementById('master-login-btn').onclick = async () => {
|
| 61 |
+
const key = document.getElementById('master-key-input').value.trim();
|
| 62 |
+
if (!key) return;
|
| 63 |
+
try {
|
| 64 |
+
const res = await axios.post('/api/auth/login', { key });
|
| 65 |
+
if (res.data.success) {
|
| 66 |
+
setTimeout(async () => {
|
| 67 |
+
const s = await checkAuth();
|
| 68 |
+
if (s.authenticated) { unlock(); await init(); }
|
| 69 |
+
}, 500);
|
| 70 |
+
} else { alert("Invalid Master Key"); }
|
| 71 |
+
} catch { alert("Auth Node Offline"); }
|
| 72 |
+
};
|
| 73 |
+
|
| 74 |
+
async function checkLicense() {
|
| 75 |
+
try { return (await axios.get('/api/license/status')).data; }
|
| 76 |
+
catch { return { licensed: false }; }
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
// --- Config ---
|
| 80 |
+
async function loadConfig() {
|
| 81 |
+
const res = await axios.get('/api/config');
|
| 82 |
+
appData.config = res.data;
|
| 83 |
+
|
| 84 |
+
const locFilter = document.getElementById('location-filter');
|
| 85 |
+
const toolSelect = document.getElementById('test-tool-select');
|
| 86 |
+
const locs = new Set(['All', 'Global']);
|
| 87 |
+
appData.config.services_to_monitor.forEach(s => { if(s.location) locs.add(s.location); });
|
| 88 |
+
|
| 89 |
+
const prev = locFilter.value;
|
| 90 |
+
locFilter.innerHTML = '';
|
| 91 |
+
locs.forEach(l => {
|
| 92 |
+
const o = document.createElement('option');
|
| 93 |
+
o.value = l; o.textContent = l === 'All' ? 'All Regions' : l;
|
| 94 |
+
locFilter.appendChild(o);
|
| 95 |
+
});
|
| 96 |
+
if (prev) locFilter.value = prev;
|
| 97 |
+
|
| 98 |
+
toolSelect.innerHTML = '';
|
| 99 |
+
appData.config.services_to_monitor.forEach(s => {
|
| 100 |
+
const o = document.createElement('option');
|
| 101 |
+
o.value = s.name; o.textContent = s.name;
|
| 102 |
+
toolSelect.appendChild(o);
|
| 103 |
+
});
|
| 104 |
+
|
| 105 |
+
renderManageList();
|
| 106 |
+
document.getElementById('check-interval').value = appData.config.check_interval;
|
| 107 |
+
document.getElementById('alert-mode').value = appData.config.alert_mode || 'SMTP';
|
| 108 |
+
document.getElementById('smtp-user').value = appData.config.smtp_sender || '';
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
function renderManageList() {
|
| 112 |
+
const list = document.getElementById('manage-svc-list');
|
| 113 |
+
if (!list) return;
|
| 114 |
+
list.innerHTML = '';
|
| 115 |
+
appData.config.services_to_monitor.forEach(s => {
|
| 116 |
+
const item = document.createElement('div');
|
| 117 |
+
item.className = 'manage-item';
|
| 118 |
+
item.innerHTML = `
|
| 119 |
+
<div><b>${s.name}</b><br><span>${s.url}</span></div>
|
| 120 |
+
<button class="btn-remove" data-name="${s.name}"><i class="fas fa-trash-alt"></i> Remove</button>
|
| 121 |
+
`;
|
| 122 |
+
list.appendChild(item);
|
| 123 |
+
});
|
| 124 |
+
list.querySelectorAll('.btn-remove').forEach(btn => {
|
| 125 |
+
btn.onclick = (e) => {
|
| 126 |
+
const t = e.target.closest('.btn-remove');
|
| 127 |
+
if (t) deleteService(t.getAttribute('data-name'));
|
| 128 |
+
};
|
| 129 |
+
});
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
async function deleteService(name) {
|
| 133 |
+
try {
|
| 134 |
+
const res = await axios.post('/api/config/delete', { name });
|
| 135 |
+
if (res.data.status === 'success') { await loadConfig(); await loadStatus(); }
|
| 136 |
+
else alert("Error: " + res.data.message);
|
| 137 |
+
} catch (e) { alert("Delete failed."); }
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
// --- Summary ---
|
| 141 |
+
async function loadSummary() {
|
| 142 |
+
const s = (await axios.get('/api/summary')).data;
|
| 143 |
+
document.getElementById('stat-monitors').textContent = s.total_monitors;
|
| 144 |
+
document.getElementById('stat-uptime').textContent = s.uptime_pct + '%';
|
| 145 |
+
document.getElementById('stat-incidents').textContent = s.incidents;
|
| 146 |
+
document.getElementById('stat-latency').textContent = s.avg_latency + 'ms';
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
// --- Status ---
|
| 150 |
+
async function loadStatus() {
|
| 151 |
+
appData.status = (await axios.get('/api/status')).data;
|
| 152 |
+
renderStatusGrid();
|
| 153 |
+
renderLatencyMatrix();
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
function renderLatencyMatrix() {
|
| 157 |
+
const container = document.getElementById('latency-matrix-bars');
|
| 158 |
+
if (!container) return;
|
| 159 |
+
container.innerHTML = '';
|
| 160 |
+
const sorted = [...appData.status].sort((a, b) => b.latency - a.latency).slice(0, 8);
|
| 161 |
+
const max = Math.max(...sorted.map(s => s.latency), 100);
|
| 162 |
+
|
| 163 |
+
sorted.forEach(s => {
|
| 164 |
+
const pct = Math.min((s.latency / max) * 100, 100);
|
| 165 |
+
const row = document.createElement('div');
|
| 166 |
+
row.className = 'latency-row';
|
| 167 |
+
row.innerHTML = `
|
| 168 |
+
<span class="name">${s.name}</span>
|
| 169 |
+
<div class="bar-track"><div class="bar-fill" style="width:${pct}%"></div></div>
|
| 170 |
+
<span class="ms">${s.latency}ms</span>
|
| 171 |
+
`;
|
| 172 |
+
container.appendChild(row);
|
| 173 |
+
});
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
function renderStatusGrid() {
|
| 177 |
+
const grid = document.getElementById('status-grid');
|
| 178 |
+
grid.innerHTML = '';
|
| 179 |
+
const filtered = appData.status.filter(s => {
|
| 180 |
+
const locOk = currentLocationFilter === 'All' || s.location === currentLocationFilter;
|
| 181 |
+
const searchOk = !searchQuery || s.name.toLowerCase().includes(searchQuery.toLowerCase());
|
| 182 |
+
return locOk && searchOk;
|
| 183 |
+
});
|
| 184 |
+
|
| 185 |
+
filtered.forEach(s => {
|
| 186 |
+
const row = document.createElement('div');
|
| 187 |
+
row.className = 'svc-row';
|
| 188 |
+
row.onclick = () => openDetails(s.name);
|
| 189 |
+
|
| 190 |
+
let barsHTML = '<div class="uptime-bars">';
|
| 191 |
+
const history = [...Array(Math.max(0, 30 - s.timeline.length)).fill(null), ...s.timeline];
|
| 192 |
+
history.forEach(state => {
|
| 193 |
+
const cls = state === null ? 'none' : (state ? 'up' : 'down');
|
| 194 |
+
barsHTML += `<div class="bar ${cls}"></div>`;
|
| 195 |
+
});
|
| 196 |
+
barsHTML += '</div>';
|
| 197 |
+
|
| 198 |
+
const scopeClass = s.location === 'Security Node' ? 'security' : (s.location === 'Primary Web' ? 'web' : 'global');
|
| 199 |
+
|
| 200 |
+
row.innerHTML = `
|
| 201 |
+
<div><div class="svc-name">${s.name}</div><div class="svc-url">${s.url}</div></div>
|
| 202 |
+
<div><span class="scope-badge ${scopeClass}">${s.location}</span></div>
|
| 203 |
+
${barsHTML}
|
| 204 |
+
<div class="status-label ${s.is_up ? 'up' : 'down'}">${s.is_up ? 'ONLINE' : 'DOWN'}</div>
|
| 205 |
+
<div class="latency-chip">${s.latency}ms</div>
|
| 206 |
+
`;
|
| 207 |
+
grid.appendChild(row);
|
| 208 |
+
});
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
// --- Modal ---
|
| 212 |
+
async function openDetails(name) {
|
| 213 |
+
document.getElementById('modal-title').textContent = name;
|
| 214 |
+
document.getElementById('modal-subtitle').textContent = `Historical performance data for ${name}`;
|
| 215 |
+
document.getElementById('matrix-modal').classList.add('open');
|
| 216 |
+
try {
|
| 217 |
+
const res = await axios.get(`/api/history/${encodeURIComponent(name)}`);
|
| 218 |
+
renderModalCharts(res.data);
|
| 219 |
+
} catch (e) { console.error(e); }
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
function renderModalCharts(data) {
|
| 223 |
+
const labels = data.map(d => new Date(d.timestamp).toLocaleTimeString());
|
| 224 |
+
const latencies = data.map(d => d.latency);
|
| 225 |
+
const uptimes = data.map(d => d.is_up ? 100 : 0);
|
| 226 |
+
|
| 227 |
+
if (charts.detLat) charts.detLat.destroy();
|
| 228 |
+
if (charts.detUp) charts.detUp.destroy();
|
| 229 |
+
if (charts.detErr) charts.detErr.destroy();
|
| 230 |
+
|
| 231 |
+
const base = {
|
| 232 |
+
responsive: true, maintainAspectRatio: false,
|
| 233 |
+
plugins: { legend: { display: false } },
|
| 234 |
+
scales: {
|
| 235 |
+
x: { display: false },
|
| 236 |
+
y: { grid: { color: 'rgba(255,255,255,0.04)' }, ticks: { color: '#64748b', font: { size: 9 } } }
|
| 237 |
+
}
|
| 238 |
+
};
|
| 239 |
+
|
| 240 |
+
charts.detUp = new Chart(document.getElementById('uptimeChart'), {
|
| 241 |
+
type: 'line',
|
| 242 |
+
data: { labels, datasets: [{ data: uptimes, borderColor: '#10b981', backgroundColor: 'rgba(16,185,129,0.08)', fill: true, stepped: true, pointRadius: 0, borderWidth: 1.5 }] },
|
| 243 |
+
options: { ...base, scales: { ...base.scales, y: { ...base.scales.y, max: 105, min: -5 } } }
|
| 244 |
+
});
|
| 245 |
+
|
| 246 |
+
charts.detLat = new Chart(document.getElementById('latencyChart'), {
|
| 247 |
+
type: 'line',
|
| 248 |
+
data: { labels, datasets: [{ data: latencies, borderColor: '#0ea5e9', backgroundColor: 'rgba(14,165,233,0.08)', fill: true, tension: 0.4, pointRadius: 0, borderWidth: 1.5 }] },
|
| 249 |
+
options: base
|
| 250 |
+
});
|
| 251 |
+
|
| 252 |
+
const upCount = data.filter(d => d.is_up).length;
|
| 253 |
+
const downCount = data.filter(d => !d.is_up).length;
|
| 254 |
+
charts.detErr = new Chart(document.getElementById('errorChart'), {
|
| 255 |
+
type: 'doughnut',
|
| 256 |
+
data: { labels: ['Operational', 'Issues'], datasets: [{ data: [upCount, downCount], backgroundColor: ['#10b981', '#1e293b'], borderWidth: 0 }] },
|
| 257 |
+
options: { cutout: '75%', plugins: { legend: { position: 'bottom', labels: { color: '#64748b', padding: 12, font: { size: 10 } } } } }
|
| 258 |
+
});
|
| 259 |
+
|
| 260 |
+
renderIncidents(data);
|
| 261 |
+
}
|
| 262 |
+
|
| 263 |
+
function renderIncidents(data) {
|
| 264 |
+
const log = document.getElementById('incident-log');
|
| 265 |
+
if (!log) return;
|
| 266 |
+
log.innerHTML = '';
|
| 267 |
+
const fails = data.filter(d => !d.is_up).reverse().slice(0, 10);
|
| 268 |
+
if (fails.length === 0) {
|
| 269 |
+
log.innerHTML = '<div style="text-align:center; padding:1.5rem;"><i class="fas fa-check-circle" style="color:var(--accent); font-size:1rem;"></i><p style="color:var(--accent); font-size:0.72rem; font-weight:700; margin-top:6px;">✓ SYSTEM IS NOMINAL</p></div>';
|
| 270 |
+
return;
|
| 271 |
+
}
|
| 272 |
+
fails.forEach(f => {
|
| 273 |
+
const el = document.createElement('div');
|
| 274 |
+
el.style.cssText = 'display:flex; justify-content:space-between; align-items:center; padding:6px 10px; margin-bottom:4px; background:rgba(244,63,94,0.06); border-left:3px solid var(--danger); border-radius:4px;';
|
| 275 |
+
const msg = f.message || "Connection Error";
|
| 276 |
+
el.innerHTML = `<div><span style="color:var(--danger); font-size:0.6rem; font-weight:800; text-transform:uppercase;">Outage</span><br><span style="font-size:0.7rem; color:#fff;">${msg}</span></div><span style="font-size:0.65rem; color:var(--text-dim); font-weight:600;">${new Date(f.timestamp).toLocaleTimeString()}</span>`;
|
| 277 |
+
log.appendChild(el);
|
| 278 |
+
});
|
| 279 |
+
}
|
| 280 |
+
|
| 281 |
+
function updateClock() {
|
| 282 |
+
document.getElementById('live-clock').textContent = new Date().toLocaleTimeString();
|
| 283 |
+
}
|
| 284 |
+
|
| 285 |
+
// --- Event Handlers ---
|
| 286 |
+
|
| 287 |
+
// Modal close
|
| 288 |
+
document.getElementById('modal-close-btn').onclick = () => document.getElementById('matrix-modal').classList.remove('open');
|
| 289 |
+
document.getElementById('matrix-modal').onclick = (e) => { if (e.target === document.getElementById('matrix-modal')) document.getElementById('matrix-modal').classList.remove('open'); };
|
| 290 |
+
|
| 291 |
+
// Navigation
|
| 292 |
+
document.querySelectorAll('.sidebar-nav a').forEach(item => {
|
| 293 |
+
item.onclick = (e) => {
|
| 294 |
+
e.preventDefault();
|
| 295 |
+
document.querySelectorAll('.sidebar-nav a').forEach(n => n.classList.remove('active'));
|
| 296 |
+
item.classList.add('active');
|
| 297 |
+
const target = item.id.replace('nav-', 'section-');
|
| 298 |
+
document.querySelectorAll('.content-section').forEach(s => s.style.display = 'none');
|
| 299 |
+
document.getElementById(target).style.display = 'block';
|
| 300 |
+
};
|
| 301 |
+
});
|
| 302 |
+
|
| 303 |
+
// Location filter
|
| 304 |
+
document.getElementById('location-filter').onchange = (e) => { currentLocationFilter = e.target.value; renderStatusGrid(); };
|
| 305 |
+
|
| 306 |
+
// Search
|
| 307 |
+
document.getElementById('search-services').oninput = (e) => { searchQuery = e.target.value; renderStatusGrid(); };
|
| 308 |
+
|
| 309 |
+
// M365
|
| 310 |
+
document.getElementById('activate-m365-btn').onclick = async () => {
|
| 311 |
+
const sender = document.getElementById('m365-sender').value.trim();
|
| 312 |
+
const pass = document.getElementById('m365-pass').value.trim();
|
| 313 |
+
const recipient = document.getElementById('m365-recipient').value.trim();
|
| 314 |
+
if (!sender || !pass || !recipient) return alert("All M365 fields required.");
|
| 315 |
+
const config = { ...appData.config };
|
| 316 |
+
config.services_to_monitor.push({ name: "M365 Mail Flow", url: "https://outlook.office365.com", location: "Global", type: "m365_mail", m365_sender: sender, m365_password: pass, m365_recipient: recipient });
|
| 317 |
+
try { await axios.post('/api/config', config); alert("M365 Monitor Activated!"); loadConfig(); }
|
| 318 |
+
catch { alert("Failed to activate."); }
|
| 319 |
+
};
|
| 320 |
+
|
| 321 |
+
// Add service
|
| 322 |
+
document.getElementById('add-svc-btn').onclick = async () => {
|
| 323 |
+
const name = document.getElementById('new-svc-name').value.trim();
|
| 324 |
+
const url = document.getElementById('new-svc-url').value.trim();
|
| 325 |
+
const loc = document.getElementById('new-svc-loc').value.trim() || 'Global';
|
| 326 |
+
const type = document.getElementById('new-svc-type').value;
|
| 327 |
+
if (!name || !url) return alert("Name and URL required.");
|
| 328 |
+
const config = { ...appData.config };
|
| 329 |
+
config.services_to_monitor.push({ name, url, location: loc, type });
|
| 330 |
+
try { await axios.post('/api/config', config); alert("Node Provisioned!"); document.getElementById('new-svc-name').value = ''; document.getElementById('new-svc-url').value = ''; loadConfig(); loadStatus(); }
|
| 331 |
+
catch { alert("Error."); }
|
| 332 |
+
};
|
| 333 |
+
|
| 334 |
+
// Save settings
|
| 335 |
+
document.getElementById('save-settings-btn').onclick = async () => {
|
| 336 |
+
const config = { ...appData.config };
|
| 337 |
+
config.check_interval = parseInt(document.getElementById('check-interval').value);
|
| 338 |
+
config.alert_mode = document.getElementById('alert-mode').value;
|
| 339 |
+
config.smtp_sender = document.getElementById('smtp-user').value;
|
| 340 |
+
config.smtp_password = document.getElementById('smtp-pass').value;
|
| 341 |
+
try { await axios.post('/api/config', config); alert("Config Saved!"); loadConfig(); }
|
| 342 |
+
catch { alert("Save failed."); }
|
| 343 |
+
};
|
| 344 |
+
|
| 345 |
+
// Purge
|
| 346 |
+
const purgeBtn = document.getElementById('purge-stale-btn');
|
| 347 |
+
if (purgeBtn) purgeBtn.onclick = async () => { try { const r = await axios.post('/api/admin/purge_stale'); alert(`Purged ${r.data.purged_records} records.`); loadData(); } catch { alert("Purge failed."); } };
|
| 348 |
+
|
| 349 |
+
// Export
|
| 350 |
+
const exportBtn = document.getElementById('download-excel-btn');
|
| 351 |
+
if (exportBtn) exportBtn.onclick = () => window.location.href = '/api/report/excel';
|
| 352 |
+
|
| 353 |
+
init();
|
static/script_v6.js
ADDED
|
@@ -0,0 +1,812 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* StatusPilot Ultra v6 Engine
|
| 3 |
+
*/
|
| 4 |
+
|
| 5 |
+
let appData = { status: [], config: null };
|
| 6 |
+
let currentFilter = 'all';
|
| 7 |
+
let sortKey = 'latency';
|
| 8 |
+
let sortDir = 'desc';
|
| 9 |
+
let isAuthPromptShowing = false;
|
| 10 |
+
let trendData = [];
|
| 11 |
+
let mapNodes = [];
|
| 12 |
+
|
| 13 |
+
// --- Axios Interceptor ---
|
| 14 |
+
axios.interceptors.response.use(r => r, error => {
|
| 15 |
+
if (error.response && error.response.status === 401 && !isAuthPromptShowing) {
|
| 16 |
+
isAuthPromptShowing = true;
|
| 17 |
+
document.getElementById('master-auth-overlay').style.display = 'flex';
|
| 18 |
+
}
|
| 19 |
+
return Promise.reject(error);
|
| 20 |
+
});
|
| 21 |
+
|
| 22 |
+
// --- THEME ENGINE ---
|
| 23 |
+
const THEMES = [
|
| 24 |
+
{ id: 'dark', label: 'Dark Pro', icon: '🌙' },
|
| 25 |
+
{ id: 'light', label: 'Light Mode', icon: '☀️' },
|
| 26 |
+
{ id: 'cyber', label: 'Cyber Pulse', icon: '⚡' }
|
| 27 |
+
];
|
| 28 |
+
|
| 29 |
+
function initTheme() {
|
| 30 |
+
const saved = localStorage.getItem('statuspilot-theme') || 'dark';
|
| 31 |
+
applyTheme(saved);
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
function applyTheme(themeId) {
|
| 35 |
+
const t = THEMES.find(x => x.id === themeId) || THEMES[0];
|
| 36 |
+
if (t.id === 'dark') {
|
| 37 |
+
document.documentElement.removeAttribute('data-theme');
|
| 38 |
+
} else {
|
| 39 |
+
document.documentElement.setAttribute('data-theme', t.id);
|
| 40 |
+
}
|
| 41 |
+
localStorage.setItem('statuspilot-theme', t.id);
|
| 42 |
+
const iconEl = document.getElementById('theme-icon');
|
| 43 |
+
const labelEl = document.getElementById('theme-label');
|
| 44 |
+
if (iconEl) iconEl.textContent = t.icon;
|
| 45 |
+
if (labelEl) labelEl.textContent = t.label;
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
function cycleTheme() {
|
| 49 |
+
const current = localStorage.getItem('statuspilot-theme') || 'dark';
|
| 50 |
+
const idx = THEMES.findIndex(x => x.id === current);
|
| 51 |
+
const nextTheme = THEMES[(idx + 1) % THEMES.length];
|
| 52 |
+
applyTheme(nextTheme.id);
|
| 53 |
+
toast('info', 'Theme Switch', `Activated ${nextTheme.label} dashboard.`);
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
// --- INIT ---
|
| 57 |
+
window.addEventListener('load', async () => {
|
| 58 |
+
initTheme();
|
| 59 |
+
setTimeout(() => { document.getElementById('loader').classList.add('hide'); }, 1500);
|
| 60 |
+
initClock();
|
| 61 |
+
|
| 62 |
+
const auth = await checkAuth();
|
| 63 |
+
if (auth.required && !auth.authenticated) {
|
| 64 |
+
document.getElementById('master-auth-overlay').style.display = 'flex';
|
| 65 |
+
return;
|
| 66 |
+
}
|
| 67 |
+
unlock();
|
| 68 |
+
|
| 69 |
+
const lic = await checkLicense();
|
| 70 |
+
if (!lic.licensed) {
|
| 71 |
+
document.getElementById('license-overlay').style.display = 'flex';
|
| 72 |
+
return;
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
await loadData();
|
| 76 |
+
setInterval(loadData, 30000);
|
| 77 |
+
initTrendChart();
|
| 78 |
+
});
|
| 79 |
+
|
| 80 |
+
// --- CLOCK ---
|
| 81 |
+
function initClock() {
|
| 82 |
+
function tick() {
|
| 83 |
+
const sel = document.getElementById('tzSel');
|
| 84 |
+
const tz = sel ? sel.value : 'UTC';
|
| 85 |
+
const label = sel ? sel.options[sel.selectedIndex].dataset.label : 'UTC';
|
| 86 |
+
const d = new Date();
|
| 87 |
+
const timeStr = d.toLocaleTimeString('en-US', { timeZone: tz, hour12: false, hour:'2-digit', minute:'2-digit', second:'2-digit' });
|
| 88 |
+
document.getElementById('clock').textContent = timeStr + ' ' + label;
|
| 89 |
+
}
|
| 90 |
+
tick(); setInterval(tick, 1000);
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
// --- AUTH / LICENSE ---
|
| 94 |
+
async function checkAuth() {
|
| 95 |
+
try { return (await axios.get('/api/auth/status')).data; }
|
| 96 |
+
catch { return { required: true, authenticated: false }; }
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
function unlock() {
|
| 100 |
+
isAuthPromptShowing = false;
|
| 101 |
+
document.getElementById('master-auth-overlay').style.display = 'none';
|
| 102 |
+
const app = document.getElementById('main-app');
|
| 103 |
+
app.style.filter = 'none'; app.style.opacity = '1'; app.style.pointerEvents = 'auto';
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
document.getElementById('master-login-btn').onclick = async () => {
|
| 107 |
+
const key = document.getElementById('master-key-input').value.trim();
|
| 108 |
+
if (!key) return;
|
| 109 |
+
try {
|
| 110 |
+
const res = await axios.post('/api/auth/login', { key });
|
| 111 |
+
if (res.data.success) {
|
| 112 |
+
setTimeout(async () => {
|
| 113 |
+
const s = await checkAuth();
|
| 114 |
+
if (s.authenticated) { unlock(); window.location.reload(); }
|
| 115 |
+
}, 500);
|
| 116 |
+
} else { toast('err', 'Access Denied', 'Invalid Master Key'); }
|
| 117 |
+
} catch { toast('err', 'Auth Error', 'Node Offline'); }
|
| 118 |
+
};
|
| 119 |
+
|
| 120 |
+
async function checkLicense() {
|
| 121 |
+
try { return (await axios.get('/api/license/status')).data; }
|
| 122 |
+
catch { return { licensed: false }; }
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
// --- LOAD DATA ---
|
| 126 |
+
async function loadData() {
|
| 127 |
+
try {
|
| 128 |
+
await Promise.all([loadConfig(), loadSummary(), loadStatus()]);
|
| 129 |
+
} catch (e) {
|
| 130 |
+
console.error("Sync Error:", e);
|
| 131 |
+
}
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
async function loadConfig() {
|
| 135 |
+
const res = await axios.get('/api/config');
|
| 136 |
+
appData.config = res.data;
|
| 137 |
+
|
| 138 |
+
// Filters and Selects
|
| 139 |
+
const locFilter = document.getElementById('regionSel');
|
| 140 |
+
const toolSelect = document.getElementById('test-tool-select');
|
| 141 |
+
const locs = new Set(['all', 'Global']);
|
| 142 |
+
appData.config.services_to_monitor.forEach(s => { if(s.location) locs.add(s.location); });
|
| 143 |
+
|
| 144 |
+
const prev = locFilter.value;
|
| 145 |
+
locFilter.innerHTML = '<option value="all">🌐 All Regions</option>';
|
| 146 |
+
locs.forEach(l => {
|
| 147 |
+
if(l !== 'all') {
|
| 148 |
+
const o = document.createElement('option');
|
| 149 |
+
o.value = l; o.textContent = l;
|
| 150 |
+
locFilter.appendChild(o);
|
| 151 |
+
}
|
| 152 |
+
});
|
| 153 |
+
if (prev) locFilter.value = prev;
|
| 154 |
+
|
| 155 |
+
toolSelect.innerHTML = '';
|
| 156 |
+
appData.config.services_to_monitor.forEach(s => {
|
| 157 |
+
const o = document.createElement('option');
|
| 158 |
+
o.value = s.name; o.textContent = s.name;
|
| 159 |
+
toolSelect.appendChild(o);
|
| 160 |
+
});
|
| 161 |
+
|
| 162 |
+
// Populate Operations Forms
|
| 163 |
+
renderManageList();
|
| 164 |
+
document.getElementById('check-interval').value = appData.config.check_interval || 60;
|
| 165 |
+
document.getElementById('alert-mode').value = appData.config.alert_mode || 'SMTP';
|
| 166 |
+
document.getElementById('smtp-user').value = appData.config.smtp_sender || '';
|
| 167 |
+
document.getElementById('smtp-pass').value = appData.config.smtp_password ? '••••••••••••' : '';
|
| 168 |
+
const recs = appData.config.email_recipients || [];
|
| 169 |
+
const recsEl = document.getElementById('email-recipients') || document.getElementById('alert-recipients');
|
| 170 |
+
if (recsEl) recsEl.value = Array.isArray(recs) ? recs.join(', ') : recs;
|
| 171 |
+
const whEl = document.getElementById('email-webhook-url');
|
| 172 |
+
if (whEl) {
|
| 173 |
+
// Mask the webhook URL - show only status, never expose the real URL to UI
|
| 174 |
+
whEl.value = appData.config.email_webhook_url ? '••• Configured (Secured) •••' : '';
|
| 175 |
+
whEl.disabled = true;
|
| 176 |
+
whEl.style.color = '#888';
|
| 177 |
+
}
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
async function loadSummary() {
|
| 181 |
+
const s = (await axios.get('/api/summary')).data;
|
| 182 |
+
document.getElementById('kpi-mon').textContent = s.total_monitors;
|
| 183 |
+
|
| 184 |
+
// Uptime
|
| 185 |
+
document.getElementById('kpi-up').textContent = s.uptime_pct + '%';
|
| 186 |
+
const upDelta = document.getElementById('kpi-up-delta');
|
| 187 |
+
if (s.uptime_pct >= 99) { upDelta.className = 'kpi-delta up'; upDelta.textContent = '↑ Optimal'; }
|
| 188 |
+
else { upDelta.className = 'kpi-delta down'; upDelta.textContent = '↓ Below Target'; }
|
| 189 |
+
document.getElementById('kpi-up-bar').style.width = s.uptime_pct + '%';
|
| 190 |
+
|
| 191 |
+
// Incidents
|
| 192 |
+
document.getElementById('kpi-inc').textContent = s.incidents;
|
| 193 |
+
const incCard = document.getElementById('kpi-inc-card');
|
| 194 |
+
const incDesc = document.getElementById('kpi-inc-desc');
|
| 195 |
+
const incBar = document.getElementById('kpi-inc-bar');
|
| 196 |
+
if (s.incidents > 0) {
|
| 197 |
+
incCard.style.setProperty('--ac', 'var(--red)');
|
| 198 |
+
incDesc.className = 'kpi-delta down'; incDesc.textContent = s.incidents + ' Systems Down';
|
| 199 |
+
incBar.style.background = 'var(--red)';
|
| 200 |
+
document.getElementById('kpi-inc').style.setProperty('--vc', 'var(--red)');
|
| 201 |
+
document.getElementById('inc-badge').textContent = s.incidents + ' OPEN';
|
| 202 |
+
} else {
|
| 203 |
+
incCard.style.setProperty('--ac', 'var(--green)');
|
| 204 |
+
incDesc.className = 'kpi-delta up'; incDesc.textContent = 'All operational';
|
| 205 |
+
incBar.style.background = 'var(--green)';
|
| 206 |
+
document.getElementById('kpi-inc').style.setProperty('--vc', 'var(--green)');
|
| 207 |
+
document.getElementById('inc-badge').textContent = '0 OPEN';
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
// Latency
|
| 211 |
+
document.getElementById('kpi-lat').textContent = s.avg_latency + 'ms';
|
| 212 |
+
}
|
| 213 |
+
|
| 214 |
+
async function loadStatus() {
|
| 215 |
+
appData.status = (await axios.get('/api/status')).data;
|
| 216 |
+
renderTable();
|
| 217 |
+
renderLatMatrix();
|
| 218 |
+
renderIncidents();
|
| 219 |
+
buildHeatmap();
|
| 220 |
+
|
| 221 |
+
// Setup World Map Nodes based on status
|
| 222 |
+
mapNodes = [
|
| 223 |
+
{label:'US East (N.Va)', x:75, y:55, status: getNodeStatus('US') },
|
| 224 |
+
{label:'US West (CA)', x:45, y:60, status: getNodeStatus('Global') },
|
| 225 |
+
{label:'EU Central (FRA)',x:160,y:48, status: getNodeStatus('EU') },
|
| 226 |
+
{label:'UK (London)', x:145,y:44, status: getNodeStatus('Primary') },
|
| 227 |
+
{label:'Asia-Pacific', x:260,y:65, status: 'ok' },
|
| 228 |
+
{label:'South America', x:105,y:115,status: 'ok' }
|
| 229 |
+
];
|
| 230 |
+
renderWorldMap();
|
| 231 |
+
}
|
| 232 |
+
|
| 233 |
+
function getNodeStatus(locMatch) {
|
| 234 |
+
const nodes = appData.status.filter(s => s.location && s.location.includes(locMatch));
|
| 235 |
+
if (!nodes.length) return 'ok';
|
| 236 |
+
if (nodes.some(s => !s.is_up)) return 'down';
|
| 237 |
+
if (nodes.some(s => s.latency > 400)) return 'warn';
|
| 238 |
+
return 'ok';
|
| 239 |
+
}
|
| 240 |
+
|
| 241 |
+
// --- RENDERERS ---
|
| 242 |
+
|
| 243 |
+
function slaColor(pct) {
|
| 244 |
+
if(pct >= 99.5) return 'var(--green)';
|
| 245 |
+
if(pct >= 95) return 'var(--amber)';
|
| 246 |
+
return 'var(--red)';
|
| 247 |
+
}
|
| 248 |
+
|
| 249 |
+
function renderTable() {
|
| 250 |
+
const q = (document.getElementById('searchBox')||{}).value||'';
|
| 251 |
+
const locFilt = document.getElementById('regionSel').value;
|
| 252 |
+
|
| 253 |
+
let list = [...appData.status];
|
| 254 |
+
|
| 255 |
+
// Region Filter
|
| 256 |
+
if (locFilt !== 'all') list = list.filter(s => s.location === locFilt);
|
| 257 |
+
|
| 258 |
+
// Status Filter
|
| 259 |
+
if(currentFilter !== 'all') {
|
| 260 |
+
if (currentFilter === 'down') list = list.filter(s => !s.is_up);
|
| 261 |
+
else if (currentFilter === 'online') list = list.filter(s => s.is_up);
|
| 262 |
+
}
|
| 263 |
+
|
| 264 |
+
// Text Search
|
| 265 |
+
if(q) list = list.filter(s => s.name.toLowerCase().includes(q.toLowerCase()) || s.url.toLowerCase().includes(q.toLowerCase()));
|
| 266 |
+
|
| 267 |
+
// Sorting
|
| 268 |
+
list.sort((a,b) => {
|
| 269 |
+
let av = a[sortKey] || 0, bv = b[sortKey] || 0;
|
| 270 |
+
if (sortKey === 'name' || sortKey === 'scope') {
|
| 271 |
+
av = a.name; bv = b.name;
|
| 272 |
+
if (sortKey === 'scope') { av = a.location; bv = b.location; }
|
| 273 |
+
return sortDir === 'asc' ? av.localeCompare(bv) : bv.localeCompare(av);
|
| 274 |
+
}
|
| 275 |
+
if (sortKey === 'status') {
|
| 276 |
+
av = a.is_up ? 1 : 0; bv = b.is_up ? 1 : 0;
|
| 277 |
+
}
|
| 278 |
+
return sortDir === 'asc' ? av - bv : bv - av;
|
| 279 |
+
});
|
| 280 |
+
|
| 281 |
+
document.getElementById('svcCount').textContent = list.length + ' MONITORED';
|
| 282 |
+
|
| 283 |
+
document.getElementById('svcBody').innerHTML = list.map(s => {
|
| 284 |
+
const uptimePct = s.history ? (s.history.filter(h=>h.is_up).length / Math.max(s.history.length, 1)) * 100 : 100;
|
| 285 |
+
const sc = slaColor(uptimePct);
|
| 286 |
+
const statStr = s.is_up ? 'online' : 'down';
|
| 287 |
+
|
| 288 |
+
let sparkBars = '';
|
| 289 |
+
const history = [...Array(Math.max(0, 30 - s.timeline.length)).fill(null), ...s.timeline];
|
| 290 |
+
history.forEach(state => {
|
| 291 |
+
if (state === null) sparkBars += `<div class="sp-bar" style="background:rgba(255,255,255,0.1);height:10px"></div>`;
|
| 292 |
+
else sparkBars += `<div class="sp-bar ${state ? 'sp-up' : 'sp-down'}" style="height:${state ? 12+Math.random()*8 : 3+Math.random()*6}px"></div>`;
|
| 293 |
+
});
|
| 294 |
+
|
| 295 |
+
return `<tr onclick="openModal('${s.name}')">
|
| 296 |
+
<td><div class="td-name">${s.name}</div><div class="td-url">${s.url}</div></td>
|
| 297 |
+
<td><span class="scope-tag">${s.location}</span></td>
|
| 298 |
+
<td><div class="spark30">${sparkBars}</div></td>
|
| 299 |
+
<td><span class="pill ${statStr}"><span class="pill-dot"></span>${statStr.toUpperCase()}</span></td>
|
| 300 |
+
<td class="lat-td" style="color:${s.latency>400?'var(--amber)':s.latency>200?'var(--t2)':'var(--cyan)'}">${s.latency}ms</td>
|
| 301 |
+
</tr>`;
|
| 302 |
+
}).join('');
|
| 303 |
+
}
|
| 304 |
+
|
| 305 |
+
function renderLatMatrix() {
|
| 306 |
+
const sorted = [...appData.status].sort((a,b) => b.latency - a.latency).slice(0, 8);
|
| 307 |
+
const max = Math.max(...sorted.map(s => s.latency), 100);
|
| 308 |
+
|
| 309 |
+
document.getElementById('latMatrix').innerHTML = sorted.map(s => {
|
| 310 |
+
const pct = Math.min((s.latency/max)*100, 100);
|
| 311 |
+
const hot = s.latency > 400;
|
| 312 |
+
return `<div class="lat-row">
|
| 313 |
+
<div class="lat-name">${s.name}</div>
|
| 314 |
+
<div class="lat-track"><div class="lat-fill ${hot?'hot':''}" style="width:${pct}%"></div></div>
|
| 315 |
+
<div class="lat-ms" style="color:${hot?'var(--amber)':'var(--cyan)'}">${s.latency}ms</div>
|
| 316 |
+
</div>`;
|
| 317 |
+
}).join('');
|
| 318 |
+
}
|
| 319 |
+
|
| 320 |
+
function renderIncidents() {
|
| 321 |
+
const downs = appData.status.filter(s => !s.is_up);
|
| 322 |
+
const el = document.getElementById('incList');
|
| 323 |
+
|
| 324 |
+
// Alert Banner
|
| 325 |
+
const banner = document.getElementById('alert-banner');
|
| 326 |
+
if (downs.length > 0) {
|
| 327 |
+
banner.style.display = 'flex';
|
| 328 |
+
document.getElementById('alert-banner-text').textContent = downs.map(d=>d.name + ' is DOWN').join(' · ');
|
| 329 |
+
} else {
|
| 330 |
+
banner.style.display = 'none';
|
| 331 |
+
}
|
| 332 |
+
|
| 333 |
+
if (downs.length === 0) {
|
| 334 |
+
el.innerHTML = '<div style="padding:20px;text-align:center;color:var(--t4);font-size:11px;font-family:var(--mono);">NO ACTIVE INCIDENTS</div>';
|
| 335 |
+
return;
|
| 336 |
+
}
|
| 337 |
+
|
| 338 |
+
el.innerHTML = downs.map(inc => `
|
| 339 |
+
<div class="inc-item crit">
|
| 340 |
+
<div class="inc-hd">
|
| 341 |
+
<span class="inc-name" style="color:var(--red)">${inc.name}</span>
|
| 342 |
+
<span class="inc-tag down">DOWN</span>
|
| 343 |
+
</div>
|
| 344 |
+
<div class="inc-msg">Service failed to respond within timeout parameters.</div>
|
| 345 |
+
<div class="inc-time"><span>Currently Ongoing</span></div>
|
| 346 |
+
</div>
|
| 347 |
+
`).join('');
|
| 348 |
+
}
|
| 349 |
+
|
| 350 |
+
function buildHeatmap() {
|
| 351 |
+
const el = document.getElementById('heatmap');
|
| 352 |
+
const sample = appData.status.slice(0, 5);
|
| 353 |
+
el.innerHTML = sample.map(svc => {
|
| 354 |
+
const history = [...Array(Math.max(0, 90 - svc.timeline.length)).fill(null), ...svc.timeline.slice(-90)];
|
| 355 |
+
const cells = history.map(d => {
|
| 356 |
+
const bg = d === null ? 'rgba(30,120,255,0.05)' : (d ? 'rgba(0,232,135,0.8)' : 'rgba(255,58,92,0.8)');
|
| 357 |
+
return `<div class="hm-cell" style="background:${bg}"></div>`;
|
| 358 |
+
}).join('');
|
| 359 |
+
return `<div><div class="hm-label" style="margin-bottom:3px">${svc.name}</div><div class="hm-grid">${cells}</div></div>`;
|
| 360 |
+
}).join('');
|
| 361 |
+
}
|
| 362 |
+
|
| 363 |
+
// --- WORLD MAP ---
|
| 364 |
+
function renderWorldMap() {
|
| 365 |
+
const w = 320, h = 160;
|
| 366 |
+
const svg = `<svg viewBox="0 0 ${w} ${h}" width="100%" xmlns="http://www.w3.org/2000/svg" style="display:block">
|
| 367 |
+
<defs>
|
| 368 |
+
<radialGradient id="mapglow" cx="50%" cy="50%" r="50%">
|
| 369 |
+
<stop offset="0%" stop-color="var(--blue-glow)"/>
|
| 370 |
+
<stop offset="100%" stop-color="transparent"/>
|
| 371 |
+
</radialGradient>
|
| 372 |
+
</defs>
|
| 373 |
+
<rect width="${w}" height="${h}" fill="url(#mapglow)" rx="8"/>
|
| 374 |
+
${Array.from({length:10},(_,row)=>Array.from({length:20},(_,col)=>{
|
| 375 |
+
const x=col*16+8, y=row*15+8;
|
| 376 |
+
const land = isLand(x/w, y/h);
|
| 377 |
+
return land ? `<circle cx="${x}" cy="${y}" r="1.2" fill="var(--b3)" opacity="0.45"/>` : ''
|
| 378 |
+
}).join('')).join('')}
|
| 379 |
+
${mapNodes.map((n,i)=>mapNodes.slice(i+1).map(m=>`
|
| 380 |
+
<path d="M ${n.x} ${n.y} Q ${(n.x+m.x)/2} ${(n.y+m.y)/2 - 12} ${m.x} ${m.y}" stroke="var(--b2)" stroke-width="1" stroke-dasharray="2,3" opacity="0.6"/>
|
| 381 |
+
`).join('')).join('')}
|
| 382 |
+
${mapNodes.map(n=>{
|
| 383 |
+
const c = n.status==='ok' ? '#00E887' : n.status==='warn' ? '#FFAA00' : '#FF3A5C';
|
| 384 |
+
const labelWidth = n.label.length * 5 + 8;
|
| 385 |
+
return `<g class="map-node">
|
| 386 |
+
<circle cx="${n.x}" cy="${n.y}" r="12" fill="${c}" opacity="0.1" class="node-ring"/>
|
| 387 |
+
<circle cx="${n.x}" cy="${n.y}" r="5" fill="${c}" opacity="0.9"/>
|
| 388 |
+
<circle cx="${n.x}" cy="${n.y}" r="2.5" fill="#FFFFFF"/>
|
| 389 |
+
<rect x="${n.x - labelWidth/2}" y="${n.y + 8}" width="${labelWidth}" height="13" rx="3" fill="var(--surface)" stroke="var(--b2)" stroke-width="0.8" opacity="0.95"/>
|
| 390 |
+
<text x="${n.x}" y="${n.y + 17}" text-anchor="middle" fill="var(--t1)" font-size="7.5" font-weight="600" font-family="IBM Plex Mono,monospace">${n.label}</text>
|
| 391 |
+
</g>`;
|
| 392 |
+
}).join('')}
|
| 393 |
+
</svg>`;
|
| 394 |
+
document.getElementById('worldMap').innerHTML = svg;
|
| 395 |
+
}
|
| 396 |
+
|
| 397 |
+
function isLand(nx, ny) {
|
| 398 |
+
if(ny<0.08||ny>0.92) return false;
|
| 399 |
+
if(nx>0.10&&nx<0.44&&ny>0.12&&ny<0.55) return true;
|
| 400 |
+
if(nx>0.42&&nx<0.72&&ny>0.08&&ny<0.58) return true;
|
| 401 |
+
if(nx>0.68&&nx<0.96&&ny>0.08&&ny<0.68) return true;
|
| 402 |
+
if(nx>0.76&&nx<0.98&&ny>0.68&&ny<0.90) return true;
|
| 403 |
+
return false;
|
| 404 |
+
}
|
| 405 |
+
|
| 406 |
+
// --- TREND CHART ---
|
| 407 |
+
function initTrendChart() {
|
| 408 |
+
for(let i=0;i<60;i++) trendData.push(100+Math.random()*150);
|
| 409 |
+
drawTrend();
|
| 410 |
+
setInterval(()=>{
|
| 411 |
+
const avgLat = appData.status.length ? appData.status.reduce((s,x)=>s+x.latency,0)/appData.status.length : 100;
|
| 412 |
+
trendData.shift();
|
| 413 |
+
trendData.push(avgLat + (Math.random()*20-10));
|
| 414 |
+
drawTrend();
|
| 415 |
+
}, 5000);
|
| 416 |
+
}
|
| 417 |
+
|
| 418 |
+
function drawTrend() {
|
| 419 |
+
const canvas = document.getElementById('trendChart');
|
| 420 |
+
if(!canvas) return;
|
| 421 |
+
const W = canvas.parentElement.offsetWidth - 32;
|
| 422 |
+
canvas.width = W; canvas.height = 110;
|
| 423 |
+
const ctx = canvas.getContext('2d');
|
| 424 |
+
ctx.clearRect(0,0,W,110);
|
| 425 |
+
const max = Math.max(...trendData)+20, min = 0;
|
| 426 |
+
const sx = i => (i/(trendData.length-1))*W;
|
| 427 |
+
const sy = v => 110 - ((v-min)/(max-min))*100;
|
| 428 |
+
|
| 429 |
+
// grid lines
|
| 430 |
+
ctx.strokeStyle = 'rgba(30,120,255,0.07)';
|
| 431 |
+
ctx.lineWidth = 1;
|
| 432 |
+
[0,.25,.5,.75,1].forEach(t=>{
|
| 433 |
+
const y = 110 - t*100;
|
| 434 |
+
ctx.beginPath(); ctx.moveTo(0,y); ctx.lineTo(W,y); ctx.stroke();
|
| 435 |
+
});
|
| 436 |
+
|
| 437 |
+
const grad = ctx.createLinearGradient(0,0,0,110);
|
| 438 |
+
grad.addColorStop(0,'rgba(0,212,255,0.25)');
|
| 439 |
+
grad.addColorStop(1,'rgba(0,212,255,0)');
|
| 440 |
+
ctx.beginPath();
|
| 441 |
+
ctx.moveTo(sx(0),110);
|
| 442 |
+
trendData.forEach((v,i)=> ctx.lineTo(sx(i),sy(v)));
|
| 443 |
+
ctx.lineTo(sx(trendData.length-1),110);
|
| 444 |
+
ctx.closePath(); ctx.fillStyle=grad; ctx.fill();
|
| 445 |
+
|
| 446 |
+
ctx.beginPath();
|
| 447 |
+
trendData.forEach((v,i)=>{ i===0?ctx.moveTo(sx(i),sy(v)):ctx.lineTo(sx(i),sy(v)); });
|
| 448 |
+
ctx.strokeStyle='rgba(0,212,255,0.9)'; ctx.lineWidth=1.5; ctx.stroke();
|
| 449 |
+
}
|
| 450 |
+
|
| 451 |
+
// --- MODAL ---
|
| 452 |
+
async function openModal(name) {
|
| 453 |
+
const s = appData.status.find(x=>x.name === name);
|
| 454 |
+
if(!s) return;
|
| 455 |
+
|
| 456 |
+
document.getElementById('mTitle').textContent = s.name;
|
| 457 |
+
document.getElementById('mUrl').textContent = s.url;
|
| 458 |
+
const chip = document.getElementById('mStatusChip');
|
| 459 |
+
const statStr = s.is_up ? 'online' : 'down';
|
| 460 |
+
chip.className = `pill ${statStr}`;
|
| 461 |
+
chip.innerHTML = `<span class="pill-dot"></span><span id="mStatusTxt">${statStr.toUpperCase()}</span>`;
|
| 462 |
+
|
| 463 |
+
switchModalTab('overview', document.querySelector('.modal-tabs .mtab'));
|
| 464 |
+
document.getElementById('modal').classList.add('open');
|
| 465 |
+
|
| 466 |
+
try {
|
| 467 |
+
const res = await axios.get(`/api/history/${encodeURIComponent(name)}`);
|
| 468 |
+
renderModalCharts(s, res.data);
|
| 469 |
+
renderModalHistory(s, res.data);
|
| 470 |
+
} catch(e) { console.error(e); }
|
| 471 |
+
}
|
| 472 |
+
|
| 473 |
+
function renderModalCharts(s, histData) {
|
| 474 |
+
const ac = document.getElementById('mAvailChart');
|
| 475 |
+
ac.width=280; ac.height=72;
|
| 476 |
+
const actx = ac.getContext('2d');
|
| 477 |
+
const avPts = histData.map(d => d.is_up ? 100 : 0);
|
| 478 |
+
drawMiniLine(actx,280,72,avPts,'#00E887',-5,105);
|
| 479 |
+
|
| 480 |
+
const lc = document.getElementById('mLatChart');
|
| 481 |
+
lc.width=280; lc.height=72;
|
| 482 |
+
const lctx = lc.getContext('2d');
|
| 483 |
+
const lPts = histData.map(d => d.latency);
|
| 484 |
+
drawMiniLine(lctx,280,72,lPts,'#00D4FF',0, Math.max(...lPts)*1.2);
|
| 485 |
+
|
| 486 |
+
const upCount = histData.filter(d=>d.is_up).length;
|
| 487 |
+
const totCount = Math.max(histData.length, 1);
|
| 488 |
+
const okFrac = upCount / totCount;
|
| 489 |
+
|
| 490 |
+
document.getElementById('mOkPct').textContent = (okFrac*100).toFixed(1) + '%';
|
| 491 |
+
document.getElementById('mErrPct').textContent = ((1-okFrac)*100).toFixed(1) + '%';
|
| 492 |
+
drawDonut(okFrac);
|
| 493 |
+
|
| 494 |
+
const inc = document.getElementById('mIncidentBox');
|
| 495 |
+
if(!s.is_up) {
|
| 496 |
+
inc.innerHTML=`<div class="nominal"><div style="font-size:22px;">🔴</div><div class="incident-txt">INCIDENT ACTIVE</div></div>`;
|
| 497 |
+
} else {
|
| 498 |
+
inc.innerHTML=`<div class="nominal"><div class="nominal-ico">✅</div><div class="nominal-txt">SYSTEM NOMINAL</div></div>`;
|
| 499 |
+
}
|
| 500 |
+
}
|
| 501 |
+
|
| 502 |
+
function drawMiniLine(ctx, W, H, pts, color, minV, maxV) {
|
| 503 |
+
ctx.clearRect(0,0,W,H);
|
| 504 |
+
if(pts.length===0) return;
|
| 505 |
+
const sx = i=>(i/(Math.max(pts.length-1, 1)))*W;
|
| 506 |
+
const sy = v=>H-((v-minV)/(maxV-minV))*(H-8)-4;
|
| 507 |
+
|
| 508 |
+
const grad = ctx.createLinearGradient(0,0,0,H);
|
| 509 |
+
const cRgb = hexToRgb(color);
|
| 510 |
+
grad.addColorStop(0, `rgba(${cRgb},0.3)`);
|
| 511 |
+
grad.addColorStop(1, 'rgba(0,0,0,0)');
|
| 512 |
+
|
| 513 |
+
ctx.beginPath(); ctx.moveTo(sx(0),H);
|
| 514 |
+
pts.forEach((v,i)=>ctx.lineTo(sx(i),sy(v)));
|
| 515 |
+
ctx.lineTo(sx(pts.length-1),H); ctx.closePath();
|
| 516 |
+
ctx.fillStyle=grad; ctx.fill();
|
| 517 |
+
|
| 518 |
+
ctx.beginPath();
|
| 519 |
+
pts.forEach((v,i)=>i===0?ctx.moveTo(sx(i),sy(v)):ctx.lineTo(sx(i),sy(v)));
|
| 520 |
+
ctx.strokeStyle=color; ctx.lineWidth=1.5; ctx.stroke();
|
| 521 |
+
}
|
| 522 |
+
|
| 523 |
+
function hexToRgb(hex){
|
| 524 |
+
if(hex.startsWith('#')) {
|
| 525 |
+
return `${parseInt(hex.slice(1,3),16)},${parseInt(hex.slice(3,5),16)},${parseInt(hex.slice(5,7),16)}`;
|
| 526 |
+
}
|
| 527 |
+
return '0,212,255';
|
| 528 |
+
}
|
| 529 |
+
|
| 530 |
+
function drawDonut(okFrac) {
|
| 531 |
+
const canvas = document.getElementById('mDonut');
|
| 532 |
+
const ctx = canvas.getContext('2d');
|
| 533 |
+
ctx.clearRect(0,0,72,72);
|
| 534 |
+
const cx=36,cy=36,r=28,lw=10;
|
| 535 |
+
ctx.lineWidth=lw; ctx.lineCap='round';
|
| 536 |
+
|
| 537 |
+
ctx.beginPath(); ctx.arc(cx,cy,r,0,Math.PI*2);
|
| 538 |
+
ctx.strokeStyle='rgba(30,120,255,0.1)'; ctx.stroke();
|
| 539 |
+
|
| 540 |
+
const ok = okFrac*Math.PI*2;
|
| 541 |
+
ctx.beginPath(); ctx.arc(cx,cy,r,-Math.PI/2,-Math.PI/2+ok);
|
| 542 |
+
ctx.strokeStyle='#00E887'; ctx.stroke();
|
| 543 |
+
|
| 544 |
+
if(okFrac<1){
|
| 545 |
+
ctx.beginPath(); ctx.arc(cx,cy,r,-Math.PI/2+ok, Math.PI*1.5);
|
| 546 |
+
ctx.strokeStyle='#FF3A5C'; ctx.stroke();
|
| 547 |
+
}
|
| 548 |
+
}
|
| 549 |
+
|
| 550 |
+
function renderModalHistory(s, histData) {
|
| 551 |
+
const tbody = document.getElementById('histBody');
|
| 552 |
+
const fails = histData.slice().reverse().slice(0, 10);
|
| 553 |
+
tbody.innerHTML = fails.map(h => {
|
| 554 |
+
const ok = h.is_up;
|
| 555 |
+
const msg = h.message || (ok ? 'OK' : 'TIMEOUT');
|
| 556 |
+
return `<tr>
|
| 557 |
+
<td style="font-family:var(--mono);font-size:10px;color:var(--t3)">${new Date(h.timestamp).toUTCString().slice(5,25)}</td>
|
| 558 |
+
<td><span style="color:${ok?'var(--green)':'var(--red)'};font-family:var(--mono);font-size:10px">${ok?'● UP':'✕ DOWN'}</span></td>
|
| 559 |
+
<td style="font-family:var(--mono);font-size:10px">${h.latency}ms</td>
|
| 560 |
+
<td style="font-family:var(--mono);font-size:10px;color:${ok?'var(--green)':'var(--red)'}">${msg}</td>
|
| 561 |
+
</tr>`;
|
| 562 |
+
}).join('');
|
| 563 |
+
}
|
| 564 |
+
|
| 565 |
+
// --- OPERATIONS / UI INTERACTION ---
|
| 566 |
+
|
| 567 |
+
function switchMainTab(tabId, btnEl) {
|
| 568 |
+
document.querySelectorAll('.s-btn').forEach(b => b.classList.remove('act'));
|
| 569 |
+
if(btnEl) btnEl.classList.add('act');
|
| 570 |
+
document.querySelectorAll('.main-tab').forEach(t => t.classList.remove('act'));
|
| 571 |
+
document.getElementById('tab-' + tabId).classList.add('act');
|
| 572 |
+
}
|
| 573 |
+
|
| 574 |
+
function switchModalTab(name, el) {
|
| 575 |
+
document.querySelectorAll('.modal-tabs .mtab').forEach(t=>t.classList.remove('act'));
|
| 576 |
+
document.querySelectorAll('.mtab-panel').forEach(p=>p.classList.remove('act'));
|
| 577 |
+
el.classList.add('act');
|
| 578 |
+
document.getElementById('mtab-'+name).classList.add('act');
|
| 579 |
+
}
|
| 580 |
+
|
| 581 |
+
function closeModal() { document.getElementById('modal').classList.remove('open'); }
|
| 582 |
+
function bgClose(e) { if(e.target.id==='modal') closeModal(); }
|
| 583 |
+
document.addEventListener('keydown', e => { if(e.key==='Escape') closeModal(); });
|
| 584 |
+
|
| 585 |
+
function filterTable() { renderTable(); }
|
| 586 |
+
function filterRegion() { renderTable(); }
|
| 587 |
+
function setFilter(f, el) {
|
| 588 |
+
currentFilter = f;
|
| 589 |
+
document.querySelectorAll('.filter-chip').forEach(c=>c.classList.remove('sel'));
|
| 590 |
+
el.classList.add('sel');
|
| 591 |
+
renderTable();
|
| 592 |
+
}
|
| 593 |
+
function sortTable(key) {
|
| 594 |
+
if(sortKey===key) sortDir=sortDir==='asc'?'desc':'asc';
|
| 595 |
+
else { sortKey=key; sortDir='desc'; }
|
| 596 |
+
renderTable();
|
| 597 |
+
}
|
| 598 |
+
|
| 599 |
+
// --- TOASTS ---
|
| 600 |
+
function toast(type, title, msg) {
|
| 601 |
+
const el = document.createElement('div');
|
| 602 |
+
el.className = `toast ${type==='err'?'err':type==='ok'?'ok':'info'}`;
|
| 603 |
+
const ico = type==='err'?'🔴':type==='ok'?'✅':'🔵';
|
| 604 |
+
el.innerHTML=`<div class="toast-ico">${ico}</div><div class="toast-body"><div class="toast-title">${title}</div><div class="toast-msg">${msg}</div></div>`;
|
| 605 |
+
el.onclick=()=>dismissToast(el);
|
| 606 |
+
document.getElementById('toasts').prepend(el);
|
| 607 |
+
setTimeout(()=>dismissToast(el), 6000);
|
| 608 |
+
}
|
| 609 |
+
function dismissToast(el) {
|
| 610 |
+
el.classList.add('exit');
|
| 611 |
+
setTimeout(()=>el.remove(), 320);
|
| 612 |
+
}
|
| 613 |
+
|
| 614 |
+
// --- FORM SUBMISSIONS ---
|
| 615 |
+
|
| 616 |
+
// Add Service
|
| 617 |
+
document.getElementById('add-svc-btn').onclick = async () => {
|
| 618 |
+
const name = document.getElementById('new-svc-name').value.trim();
|
| 619 |
+
const url = document.getElementById('new-svc-url').value.trim();
|
| 620 |
+
const loc = document.getElementById('new-svc-loc').value.trim() || 'Global';
|
| 621 |
+
const type = document.getElementById('new-svc-type').value;
|
| 622 |
+
if (!name || !url) return toast('err', 'Validation Failed', 'Name and URL required.');
|
| 623 |
+
|
| 624 |
+
const config = { ...appData.config };
|
| 625 |
+
config.services_to_monitor.push({ name, url, location: loc, type });
|
| 626 |
+
try {
|
| 627 |
+
await axios.post('/api/config', config);
|
| 628 |
+
toast('ok', 'Node Provisioned', `${name} successfully added.`);
|
| 629 |
+
document.getElementById('new-svc-name').value = '';
|
| 630 |
+
document.getElementById('new-svc-url').value = '';
|
| 631 |
+
loadData();
|
| 632 |
+
} catch { toast('err', 'Provision Error', 'Failed to add node.'); }
|
| 633 |
+
};
|
| 634 |
+
|
| 635 |
+
// Remove Service
|
| 636 |
+
function renderManageList() {
|
| 637 |
+
const list = document.getElementById('manage-svc-list');
|
| 638 |
+
list.innerHTML = '';
|
| 639 |
+
appData.config.services_to_monitor.forEach(s => {
|
| 640 |
+
const item = document.createElement('div');
|
| 641 |
+
item.className = 'manage-item';
|
| 642 |
+
item.innerHTML = `<div class="manage-item-info"><b>${s.name}</b><span>${s.url}</span></div><button class="btn-remove" data-name="${s.name}">Remove</button>`;
|
| 643 |
+
list.appendChild(item);
|
| 644 |
+
});
|
| 645 |
+
list.querySelectorAll('.btn-remove').forEach(btn => {
|
| 646 |
+
btn.onclick = async (e) => {
|
| 647 |
+
const name = e.target.getAttribute('data-name');
|
| 648 |
+
try {
|
| 649 |
+
await axios.post('/api/config/delete', { name });
|
| 650 |
+
toast('info', 'Node Removed', `${name} decommissioned.`);
|
| 651 |
+
loadData();
|
| 652 |
+
} catch { toast('err', 'Error', 'Failed to remove node.'); }
|
| 653 |
+
};
|
| 654 |
+
});
|
| 655 |
+
}
|
| 656 |
+
|
| 657 |
+
// Save Settings
|
| 658 |
+
document.getElementById('save-settings-btn').onclick = async () => {
|
| 659 |
+
const config = { ...appData.config };
|
| 660 |
+
config.check_interval = parseInt(document.getElementById('check-interval').value) || 60;
|
| 661 |
+
config.alert_mode = document.getElementById('alert-mode').value;
|
| 662 |
+
config.smtp_sender = document.getElementById('smtp-user').value.trim();
|
| 663 |
+
config.smtp_password = document.getElementById('smtp-pass').value.trim();
|
| 664 |
+
const recsEl = document.getElementById('email-recipients') || document.getElementById('alert-recipients');
|
| 665 |
+
const rawRecs = recsEl ? recsEl.value.trim() : '';
|
| 666 |
+
config.email_recipients = rawRecs ? rawRecs.split(',').map(r => r.trim()).filter(Boolean) : [];
|
| 667 |
+
const whEl = document.getElementById('email-webhook-url');
|
| 668 |
+
// Webhook URL is server-managed, never read it back from UI to avoid overwriting with masked value
|
| 669 |
+
// if (whEl) config.email_webhook_url = whEl.value.trim();
|
| 670 |
+
try {
|
| 671 |
+
await axios.post('/api/config', config);
|
| 672 |
+
toast('ok', 'Config Saved', 'System settings updated.');
|
| 673 |
+
loadData();
|
| 674 |
+
} catch { toast('err', 'Error', 'Failed to save settings.'); }
|
| 675 |
+
};
|
| 676 |
+
|
| 677 |
+
// Send Test Alert Email
|
| 678 |
+
document.getElementById('test-alert-btn').onclick = async () => {
|
| 679 |
+
const sender = document.getElementById('smtp-user').value.trim();
|
| 680 |
+
const pass = document.getElementById('smtp-pass').value.trim();
|
| 681 |
+
const recsEl = document.getElementById('email-recipients') || document.getElementById('alert-recipients');
|
| 682 |
+
const rawRecs = recsEl ? recsEl.value.trim() : '';
|
| 683 |
+
const recipients = rawRecs ? rawRecs.split(',').map(r => r.trim()).filter(Boolean) : [];
|
| 684 |
+
const whEl = document.getElementById('email-webhook-url');
|
| 685 |
+
const webhookUrl = whEl ? whEl.value.trim() : '';
|
| 686 |
+
|
| 687 |
+
toast('info', 'Dispatching Test Email', 'Connecting to alert server...');
|
| 688 |
+
try {
|
| 689 |
+
const payload = {};
|
| 690 |
+
if (sender) payload.smtp_sender = sender;
|
| 691 |
+
if (pass && !pass.startsWith('••••')) payload.smtp_password = pass;
|
| 692 |
+
if (recipients.length) payload.email_recipients = recipients;
|
| 693 |
+
// Webhook URL is stored server-side only, never sent from UI
|
| 694 |
+
// if (webhookUrl) payload.email_webhook_url = webhookUrl;
|
| 695 |
+
|
| 696 |
+
const res = await axios.post('/api/alert/test', payload);
|
| 697 |
+
if (res.data.status === 'success') {
|
| 698 |
+
toast('ok', 'Test Email Dispatched', res.data.message);
|
| 699 |
+
} else {
|
| 700 |
+
toast('err', 'Test Alert Failed', res.data.message || 'Error sending test email.');
|
| 701 |
+
}
|
| 702 |
+
} catch (e) {
|
| 703 |
+
const msg = (e.response && e.response.data && e.response.data.message) ? e.response.data.message : 'Server error sending test alert.';
|
| 704 |
+
toast('err', 'Test Alert Failed', msg);
|
| 705 |
+
}
|
| 706 |
+
};
|
| 707 |
+
|
| 708 |
+
// Purge Data
|
| 709 |
+
document.getElementById('purge-stale-btn').onclick = async () => {
|
| 710 |
+
try {
|
| 711 |
+
const r = await axios.post('/api/admin/purge_stale');
|
| 712 |
+
toast('info', 'Data Purged', `Cleared ${r.data.purged_records} stale records.`);
|
| 713 |
+
loadData();
|
| 714 |
+
} catch { toast('err', 'Purge Error', 'Operation failed.'); }
|
| 715 |
+
};
|
| 716 |
+
|
| 717 |
+
// M365 Monitor
|
| 718 |
+
document.getElementById('activate-m365-btn').onclick = async () => {
|
| 719 |
+
const sender = document.getElementById('m365-sender').value.trim();
|
| 720 |
+
const pass = document.getElementById('m365-pass').value.trim();
|
| 721 |
+
const recipient = document.getElementById('m365-recipient').value.trim();
|
| 722 |
+
if (!sender || !pass || !recipient) return toast('err', 'Validation', 'All M365 fields required.');
|
| 723 |
+
|
| 724 |
+
const config = { ...appData.config };
|
| 725 |
+
config.services_to_monitor.push({ name: "M365 Mail Flow", url: "https://outlook.office365.com", location: "Global", type: "m365_mail", m365_sender: sender, m365_password: pass, m365_recipient: recipient });
|
| 726 |
+
try {
|
| 727 |
+
await axios.post('/api/config', config);
|
| 728 |
+
toast('ok', 'M365 Activated', 'Mail flow monitoring engaged.');
|
| 729 |
+
loadData();
|
| 730 |
+
} catch { toast('err', 'Activation Error', 'Failed to save M365 config.'); }
|
| 731 |
+
};
|
| 732 |
+
|
| 733 |
+
// Export CSV
|
| 734 |
+
function exportCSV() {
|
| 735 |
+
window.location.href = '/api/report/excel';
|
| 736 |
+
toast('ok', 'Export Initiated', 'Downloading history report.');
|
| 737 |
+
}
|
| 738 |
+
|
| 739 |
+
// Live Diagnostic Console Probe
|
| 740 |
+
const runTestBtn = document.getElementById('run-test-btn');
|
| 741 |
+
if (runTestBtn) {
|
| 742 |
+
runTestBtn.onclick = async () => {
|
| 743 |
+
const toolSelect = document.getElementById('test-tool-select');
|
| 744 |
+
const locSelect = document.getElementById('test-location-select');
|
| 745 |
+
const protoSelect = document.getElementById('test-proto-select');
|
| 746 |
+
|
| 747 |
+
const toolName = toolSelect.value;
|
| 748 |
+
const location = locSelect.value;
|
| 749 |
+
const proto = protoSelect ? protoSelect.value : 'HTTP_TLS';
|
| 750 |
+
|
| 751 |
+
if (!toolName) return toast('err', 'Diagnostic Error', 'Please select a target service.');
|
| 752 |
+
|
| 753 |
+
const consoleBox = document.getElementById('console-output');
|
| 754 |
+
const consoleBadge = document.getElementById('console-badge');
|
| 755 |
+
if (consoleBadge) consoleBadge.textContent = 'PROBING...';
|
| 756 |
+
|
| 757 |
+
consoleBox.innerHTML = '';
|
| 758 |
+
|
| 759 |
+
function logLine(txt, cls = 'text-dim') {
|
| 760 |
+
const timeStr = new Date().toTimeString().slice(0, 8);
|
| 761 |
+
const div = document.createElement('div');
|
| 762 |
+
div.className = `console-line ${cls}`;
|
| 763 |
+
div.textContent = `[${timeStr}] ${txt}`;
|
| 764 |
+
consoleBox.appendChild(div);
|
| 765 |
+
consoleBox.scrollTop = consoleBox.scrollHeight;
|
| 766 |
+
}
|
| 767 |
+
|
| 768 |
+
logLine(`DISPATCHING PROBE PROTOCOL [${proto}] -> Origin: ${location}`, 'text-cyan');
|
| 769 |
+
logLine(`Target Infrastructure Node: ${toolName}`, 'text-dim');
|
| 770 |
+
|
| 771 |
+
document.getElementById('tk-dns').textContent = '-- ms';
|
| 772 |
+
document.getElementById('tk-tcp').textContent = '-- ms';
|
| 773 |
+
document.getElementById('tk-ttfb').textContent = '-- ms';
|
| 774 |
+
document.getElementById('tk-status').textContent = 'TESTING';
|
| 775 |
+
document.getElementById('tk-status').style.color = 'var(--cyan)';
|
| 776 |
+
|
| 777 |
+
await new Promise(r => setTimeout(r, 350));
|
| 778 |
+
logLine(`[1/3] Resolving DNS A/AAAA Records & IP Routing... OK`, 'text-dim');
|
| 779 |
+
document.getElementById('tk-dns').textContent = (10 + Math.floor(Math.random() * 8)) + ' ms';
|
| 780 |
+
|
| 781 |
+
await new Promise(r => setTimeout(r, 400));
|
| 782 |
+
logLine(`[2/3] Establishing TLS 1.3 Handshake & Cipher Suite... OK`, 'text-dim');
|
| 783 |
+
document.getElementById('tk-tcp').textContent = (16 + Math.floor(Math.random() * 10)) + ' ms';
|
| 784 |
+
|
| 785 |
+
try {
|
| 786 |
+
const res = await axios.post('/api/test', { tool: toolName, location: location });
|
| 787 |
+
const data = res.data;
|
| 788 |
+
|
| 789 |
+
await new Promise(r => setTimeout(r, 300));
|
| 790 |
+
logLine(`[3/3] Response Received: ${data.message} (Latency: ${data.latency}ms)`, data.is_up ? 'text-ok' : 'text-err');
|
| 791 |
+
|
| 792 |
+
document.getElementById('tk-ttfb').textContent = data.latency + ' ms';
|
| 793 |
+
if (data.is_up) {
|
| 794 |
+
document.getElementById('tk-status').textContent = 'NOMINAL';
|
| 795 |
+
document.getElementById('tk-status').style.color = 'var(--green)';
|
| 796 |
+
logLine(`✅ DIAGNOSTIC TRACE COMPLETE: ${toolName} is Healthy & Operational.`, 'text-ok');
|
| 797 |
+
toast('ok', 'Probe Complete', `${toolName} responded in ${data.latency}ms.`);
|
| 798 |
+
} else {
|
| 799 |
+
document.getElementById('tk-status').textContent = 'DEGRADED';
|
| 800 |
+
document.getElementById('tk-status').style.color = 'var(--red)';
|
| 801 |
+
logLine(`🚨 DIAGNOSTIC ALERT: ${toolName} failed probe check. Reason: ${data.message}`, 'text-err');
|
| 802 |
+
toast('err', 'Probe Alert', `${toolName} check failed: ${data.message}`);
|
| 803 |
+
}
|
| 804 |
+
if (consoleBadge) consoleBadge.textContent = data.is_up ? 'COMPLETE' : 'INCIDENT';
|
| 805 |
+
} catch (e) {
|
| 806 |
+
logLine(`❌ PROBE FAILED: Internal Telemetry Exception.`, 'text-err');
|
| 807 |
+
document.getElementById('tk-status').textContent = 'ERROR';
|
| 808 |
+
document.getElementById('tk-status').style.color = 'var(--red)';
|
| 809 |
+
if (consoleBadge) consoleBadge.textContent = 'ERROR';
|
| 810 |
+
}
|
| 811 |
+
};
|
| 812 |
+
}
|
static/style.css
ADDED
|
@@ -0,0 +1,381 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/*
|
| 2 |
+
StatusPilot Crystal Pro v5 - The Ultimate NOC Aesthetic
|
| 3 |
+
Theme: High-Transparency, Crystalline Glass, Prism Refractions
|
| 4 |
+
*/
|
| 5 |
+
|
| 6 |
+
:root {
|
| 7 |
+
--bg-main: #010409;
|
| 8 |
+
--bg-card: rgba(13, 17, 23, 0.4);
|
| 9 |
+
--bg-glass: rgba(255, 255, 255, 0.03);
|
| 10 |
+
--accent: #00ff88;
|
| 11 |
+
--accent-glow: rgba(0, 255, 136, 0.3);
|
| 12 |
+
--primary: #00d4ff;
|
| 13 |
+
--primary-glow: rgba(0, 212, 255, 0.4);
|
| 14 |
+
--danger: #ff4d4d;
|
| 15 |
+
--warning: #ffcc00;
|
| 16 |
+
--text-primary: #ffffff;
|
| 17 |
+
--text-muted: #8b949e;
|
| 18 |
+
--border: rgba(255, 255, 255, 0.12);
|
| 19 |
+
--glass-blur: blur(25px);
|
| 20 |
+
--font-main: 'Outfit', sans-serif;
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
* {
|
| 24 |
+
box-sizing: border-box;
|
| 25 |
+
margin: 0;
|
| 26 |
+
padding: 0;
|
| 27 |
+
font-family: var(--font-main);
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
body {
|
| 31 |
+
background: var(--bg-main);
|
| 32 |
+
color: var(--text-primary);
|
| 33 |
+
min-height: 100vh;
|
| 34 |
+
overflow-x: hidden;
|
| 35 |
+
background-image:
|
| 36 |
+
radial-gradient(circle at 0% 0%, rgba(0, 212, 255, 0.08) 0%, transparent 40%),
|
| 37 |
+
radial-gradient(circle at 100% 100%, rgba(0, 255, 136, 0.08) 0%, transparent 40%);
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
/* App Layout */
|
| 41 |
+
.app-container {
|
| 42 |
+
display: flex;
|
| 43 |
+
height: 100vh;
|
| 44 |
+
width: 100vw;
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
/* Sidebar - Crystalline Vertical */
|
| 48 |
+
.sidebar {
|
| 49 |
+
width: 280px;
|
| 50 |
+
background: rgba(1, 4, 9, 0.7);
|
| 51 |
+
backdrop-filter: blur(40px);
|
| 52 |
+
border-right: 1px solid var(--border);
|
| 53 |
+
display: flex;
|
| 54 |
+
flex-direction: column;
|
| 55 |
+
padding: 2.5rem 0;
|
| 56 |
+
z-index: 100;
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
.logo {
|
| 60 |
+
display: flex;
|
| 61 |
+
align-items: center;
|
| 62 |
+
gap: 15px;
|
| 63 |
+
padding: 0 2rem;
|
| 64 |
+
margin-bottom: 4rem;
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
.logo-icon {
|
| 68 |
+
font-size: 26px;
|
| 69 |
+
color: var(--primary);
|
| 70 |
+
filter: drop-shadow(0 0 12px var(--primary));
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
.logo h2 {
|
| 74 |
+
font-size: 1.5rem;
|
| 75 |
+
font-weight: 900;
|
| 76 |
+
letter-spacing: -1px;
|
| 77 |
+
text-transform: uppercase;
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
.logo h2 span {
|
| 81 |
+
background: linear-gradient(135deg, #fff, var(--primary));
|
| 82 |
+
-webkit-background-clip: text;
|
| 83 |
+
-webkit-text-fill-color: transparent;
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
.sidebar-nav {
|
| 87 |
+
display: flex;
|
| 88 |
+
flex-direction: column;
|
| 89 |
+
gap: 0.5rem;
|
| 90 |
+
padding: 0 1rem;
|
| 91 |
+
flex-grow: 1;
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
.nav-item {
|
| 95 |
+
color: var(--text-muted);
|
| 96 |
+
text-decoration: none;
|
| 97 |
+
padding: 1.2rem 1.5rem;
|
| 98 |
+
border-radius: 12px;
|
| 99 |
+
font-weight: 700;
|
| 100 |
+
font-size: 0.9rem;
|
| 101 |
+
transition: all 0.4s cubic-bezier(0.23, 1, 0.32, 1);
|
| 102 |
+
display: flex;
|
| 103 |
+
align-items: center;
|
| 104 |
+
gap: 15px;
|
| 105 |
+
border: 1px solid transparent;
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
.nav-item:hover {
|
| 109 |
+
background: rgba(255, 255, 255, 0.03);
|
| 110 |
+
color: #fff;
|
| 111 |
+
border-color: rgba(255, 255, 255, 0.05);
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
.nav-item.active {
|
| 115 |
+
background: linear-gradient(90deg, rgba(0, 212, 255, 0.1), transparent);
|
| 116 |
+
color: var(--primary);
|
| 117 |
+
border-left: 4px solid var(--primary);
|
| 118 |
+
text-shadow: 0 0 10px var(--primary-glow);
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
.sidebar-footer {
|
| 122 |
+
padding: 0 1.5rem;
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
.cloud-status {
|
| 126 |
+
display: flex;
|
| 127 |
+
align-items: center;
|
| 128 |
+
gap: 10px;
|
| 129 |
+
font-size: 0.75rem;
|
| 130 |
+
color: var(--accent);
|
| 131 |
+
font-weight: 900;
|
| 132 |
+
letter-spacing: 1px;
|
| 133 |
+
background: rgba(0, 255, 136, 0.05);
|
| 134 |
+
padding: 12px;
|
| 135 |
+
border-radius: 10px;
|
| 136 |
+
border: 1px solid rgba(0, 255, 136, 0.2);
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
/* Main Content Area */
|
| 140 |
+
.main-content {
|
| 141 |
+
flex-grow: 1;
|
| 142 |
+
overflow-y: auto;
|
| 143 |
+
padding: 3rem 5rem;
|
| 144 |
+
position: relative;
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
.section-header {
|
| 148 |
+
display: flex;
|
| 149 |
+
justify-content: space-between;
|
| 150 |
+
align-items: flex-end;
|
| 151 |
+
margin-bottom: 4rem;
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
.header-text h1 {
|
| 155 |
+
font-size: 3.5rem;
|
| 156 |
+
font-weight: 900;
|
| 157 |
+
letter-spacing: -2px;
|
| 158 |
+
background: linear-gradient(to bottom, #fff 40%, rgba(255,255,255,0.4));
|
| 159 |
+
-webkit-background-clip: text;
|
| 160 |
+
-webkit-text-fill-color: transparent;
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
.header-text p {
|
| 164 |
+
color: var(--text-muted);
|
| 165 |
+
font-size: 1.2rem;
|
| 166 |
+
font-weight: 500;
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
/* Crystal Panels */
|
| 170 |
+
.glass-panel {
|
| 171 |
+
background: var(--bg-card);
|
| 172 |
+
backdrop-filter: var(--glass-blur);
|
| 173 |
+
border: 1px solid var(--border);
|
| 174 |
+
border-radius: 30px;
|
| 175 |
+
padding: 2.5rem;
|
| 176 |
+
box-shadow:
|
| 177 |
+
0 40px 100px -20px rgba(0, 0, 0, 0.7),
|
| 178 |
+
inset 0 0 20px rgba(255, 255, 255, 0.02);
|
| 179 |
+
transition: all 0.5s cubic-bezier(0.19, 1, 0.22, 1);
|
| 180 |
+
position: relative;
|
| 181 |
+
overflow: hidden;
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
.glass-panel::after {
|
| 185 |
+
content: '';
|
| 186 |
+
position: absolute;
|
| 187 |
+
top: 0; left: -100%; width: 50%; height: 100%;
|
| 188 |
+
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.05), transparent);
|
| 189 |
+
transition: 0.5s;
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
.glass-panel:hover::after {
|
| 193 |
+
left: 200%;
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
.glass-panel:hover {
|
| 197 |
+
border-color: rgba(255, 255, 255, 0.2);
|
| 198 |
+
transform: translateY(-5px);
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
/* Summary Stats */
|
| 202 |
+
.summary-stats {
|
| 203 |
+
display: grid;
|
| 204 |
+
grid-template-columns: repeat(4, 1fr);
|
| 205 |
+
gap: 2rem;
|
| 206 |
+
margin-bottom: 4rem;
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
.stat-card {
|
| 210 |
+
text-align: left;
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
.stat-card small {
|
| 214 |
+
color: var(--text-muted);
|
| 215 |
+
text-transform: uppercase;
|
| 216 |
+
font-size: 0.7rem;
|
| 217 |
+
font-weight: 900;
|
| 218 |
+
letter-spacing: 2px;
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
.stat-card h2 {
|
| 222 |
+
font-size: 3rem;
|
| 223 |
+
font-weight: 900;
|
| 224 |
+
margin-top: 0.5rem;
|
| 225 |
+
letter-spacing: -1px;
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
+
/* Status Grid & Diamond Rows */
|
| 229 |
+
.status-grid {
|
| 230 |
+
display: flex;
|
| 231 |
+
flex-direction: column;
|
| 232 |
+
gap: 1.5rem;
|
| 233 |
+
}
|
| 234 |
+
|
| 235 |
+
.status-row {
|
| 236 |
+
display: grid;
|
| 237 |
+
grid-template-columns: 2.5fr 1fr 3fr 1.5fr 1fr;
|
| 238 |
+
align-items: center;
|
| 239 |
+
gap: 3rem;
|
| 240 |
+
background: rgba(255, 255, 255, 0.02);
|
| 241 |
+
border: 1px solid rgba(255, 255, 255, 0.05);
|
| 242 |
+
padding: 2rem 3rem;
|
| 243 |
+
border-radius: 20px;
|
| 244 |
+
cursor: pointer;
|
| 245 |
+
transition: all 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
| 246 |
+
}
|
| 247 |
+
|
| 248 |
+
.status-row:hover {
|
| 249 |
+
background: rgba(255, 255, 255, 0.05);
|
| 250 |
+
border-color: var(--primary);
|
| 251 |
+
box-shadow: 0 0 30px rgba(0, 212, 255, 0.15);
|
| 252 |
+
}
|
| 253 |
+
|
| 254 |
+
.svc-info h3 {
|
| 255 |
+
font-size: 1.4rem;
|
| 256 |
+
font-weight: 800;
|
| 257 |
+
letter-spacing: -0.5px;
|
| 258 |
+
}
|
| 259 |
+
|
| 260 |
+
.location-badge {
|
| 261 |
+
background: rgba(0, 212, 255, 0.1);
|
| 262 |
+
color: var(--primary);
|
| 263 |
+
padding: 6px 16px;
|
| 264 |
+
border-radius: 8px;
|
| 265 |
+
font-size: 0.7rem;
|
| 266 |
+
font-weight: 900;
|
| 267 |
+
border: 1px solid var(--primary-glow);
|
| 268 |
+
}
|
| 269 |
+
|
| 270 |
+
.latency {
|
| 271 |
+
font-family: 'JetBrains Mono', monospace;
|
| 272 |
+
font-weight: 900;
|
| 273 |
+
color: var(--primary);
|
| 274 |
+
background: rgba(0, 0, 0, 0.3);
|
| 275 |
+
padding: 8px 16px;
|
| 276 |
+
border-radius: 10px;
|
| 277 |
+
border: 1px solid var(--border);
|
| 278 |
+
}
|
| 279 |
+
|
| 280 |
+
/* Uptime Timeline - Crystal Style */
|
| 281 |
+
.uptime-timeline {
|
| 282 |
+
display: flex;
|
| 283 |
+
gap: 6px;
|
| 284 |
+
height: 30px;
|
| 285 |
+
align-items: center;
|
| 286 |
+
}
|
| 287 |
+
|
| 288 |
+
.timeline-bar {
|
| 289 |
+
width: 8px;
|
| 290 |
+
height: 25px;
|
| 291 |
+
border-radius: 3px;
|
| 292 |
+
transition: 0.3s;
|
| 293 |
+
}
|
| 294 |
+
|
| 295 |
+
.timeline-bar.up {
|
| 296 |
+
background: linear-gradient(to bottom, var(--accent), #00cc6a);
|
| 297 |
+
box-shadow: 0 0 10px var(--accent-glow);
|
| 298 |
+
}
|
| 299 |
+
|
| 300 |
+
.timeline-bar.down {
|
| 301 |
+
background: linear-gradient(to bottom, var(--danger), #cc0000);
|
| 302 |
+
box-shadow: 0 0 10px rgba(255, 77, 77, 0.4);
|
| 303 |
+
}
|
| 304 |
+
|
| 305 |
+
.timeline-bar.none { background: rgba(255, 255, 255, 0.05); }
|
| 306 |
+
|
| 307 |
+
/* Manage List */
|
| 308 |
+
.manage-item {
|
| 309 |
+
display: flex;
|
| 310 |
+
justify-content: space-between;
|
| 311 |
+
align-items: center;
|
| 312 |
+
background: rgba(0, 0, 0, 0.2);
|
| 313 |
+
padding: 12px 20px;
|
| 314 |
+
border-radius: 12px;
|
| 315 |
+
border: 1px solid var(--border);
|
| 316 |
+
}
|
| 317 |
+
|
| 318 |
+
.btn-delete {
|
| 319 |
+
background: rgba(255, 77, 77, 0.1);
|
| 320 |
+
color: var(--danger);
|
| 321 |
+
border: 1px solid rgba(255, 77, 77, 0.2);
|
| 322 |
+
padding: 6px 12px;
|
| 323 |
+
border-radius: 8px;
|
| 324 |
+
cursor: pointer;
|
| 325 |
+
font-weight: 800;
|
| 326 |
+
font-size: 0.75rem;
|
| 327 |
+
transition: 0.3s;
|
| 328 |
+
}
|
| 329 |
+
|
| 330 |
+
.btn-delete:hover {
|
| 331 |
+
background: var(--danger);
|
| 332 |
+
color: #fff;
|
| 333 |
+
}
|
| 334 |
+
|
| 335 |
+
/* Modals & Overlays */
|
| 336 |
+
.license-overlay {
|
| 337 |
+
background: #010409;
|
| 338 |
+
}
|
| 339 |
+
|
| 340 |
+
.pro-input {
|
| 341 |
+
background: rgba(0, 0, 0, 0.5);
|
| 342 |
+
border: 1px solid var(--border);
|
| 343 |
+
padding: 16px 20px;
|
| 344 |
+
border-radius: 12px;
|
| 345 |
+
color: #fff;
|
| 346 |
+
font-weight: 600;
|
| 347 |
+
}
|
| 348 |
+
|
| 349 |
+
.pro-input:focus {
|
| 350 |
+
border-color: var(--primary);
|
| 351 |
+
box-shadow: 0 0 20px var(--primary-glow);
|
| 352 |
+
outline: none;
|
| 353 |
+
}
|
| 354 |
+
|
| 355 |
+
/* Buttons */
|
| 356 |
+
.btn-primary {
|
| 357 |
+
background: linear-gradient(135deg, var(--primary), #0088ff);
|
| 358 |
+
color: #fff;
|
| 359 |
+
padding: 16px 32px;
|
| 360 |
+
border-radius: 14px;
|
| 361 |
+
font-weight: 900;
|
| 362 |
+
text-transform: uppercase;
|
| 363 |
+
letter-spacing: 1px;
|
| 364 |
+
border: none;
|
| 365 |
+
cursor: pointer;
|
| 366 |
+
}
|
| 367 |
+
|
| 368 |
+
/* Animations */
|
| 369 |
+
@keyframes crystalSlide {
|
| 370 |
+
from { opacity: 0; transform: scale(0.98) translateY(20px); }
|
| 371 |
+
to { opacity: 1; transform: scale(1) translateY(0); }
|
| 372 |
+
}
|
| 373 |
+
|
| 374 |
+
.content-section {
|
| 375 |
+
animation: crystalSlide 0.8s cubic-bezier(0.19, 1, 0.22, 1) forwards;
|
| 376 |
+
}
|
| 377 |
+
|
| 378 |
+
/* Scrollbar */
|
| 379 |
+
::-webkit-scrollbar { width: 10px; }
|
| 380 |
+
::-webkit-scrollbar-track { background: transparent; }
|
| 381 |
+
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 10px; border: 3px solid var(--bg-main); }
|
static/style_v5.css
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* StatusPilot Ultra Dashboard v6 — Fusion of 3 Designs */
|
| 2 |
+
|
| 3 |
+
:root {
|
| 4 |
+
--bg-main: #0b1120;
|
| 5 |
+
--bg-card: #111b2a;
|
| 6 |
+
--bg-sidebar: #0a0f1e;
|
| 7 |
+
--accent: #10b981;
|
| 8 |
+
--accent-glow: rgba(16,185,129,0.15);
|
| 9 |
+
--primary: #0ea5e9;
|
| 10 |
+
--primary-glow: rgba(14,165,233,0.15);
|
| 11 |
+
--danger: #f43f5e;
|
| 12 |
+
--danger-glow: rgba(244,63,94,0.1);
|
| 13 |
+
--warning: #f59e0b;
|
| 14 |
+
--text-primary: #e2e8f0;
|
| 15 |
+
--text-secondary: #64748b;
|
| 16 |
+
--text-dim: #475569;
|
| 17 |
+
--border: rgba(148,163,184,0.12);
|
| 18 |
+
--border-hover: rgba(148,163,184,0.25);
|
| 19 |
+
--radius: 12px;
|
| 20 |
+
--font: 'Inter', system-ui, -apple-system, sans-serif;
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
| 24 |
+
|
| 25 |
+
body { font-family: var(--font); background: var(--bg-main); color: var(--text-primary); min-height: 100vh; }
|
| 26 |
+
|
| 27 |
+
/* ===== LAYOUT ===== */
|
| 28 |
+
.app { display: flex; min-height: 100vh; }
|
| 29 |
+
|
| 30 |
+
/* ===== SIDEBAR ===== */
|
| 31 |
+
.sidebar {
|
| 32 |
+
width: 220px; min-width: 220px;
|
| 33 |
+
background: var(--bg-sidebar);
|
| 34 |
+
border-right: 1px solid var(--border);
|
| 35 |
+
display: flex; flex-direction: column;
|
| 36 |
+
padding: 1.25rem 0;
|
| 37 |
+
}
|
| 38 |
+
.sidebar-logo { display: flex; align-items: center; gap: 8px; padding: 0 1rem; margin-bottom: 1.5rem; }
|
| 39 |
+
.sidebar-logo .icon { color: var(--primary); font-size: 1.2rem; position: relative; }
|
| 40 |
+
.sidebar-logo .dot { position: absolute; top: -2px; right: -2px; width: 6px; height: 6px; background: var(--accent); border-radius: 50%; }
|
| 41 |
+
.sidebar-logo span { font-size: 0.8rem; font-weight: 800; letter-spacing: 1.5px; color: var(--primary); text-transform: uppercase; }
|
| 42 |
+
.sidebar-nav { flex: 1; padding: 0 0.6rem; display: flex; flex-direction: column; gap: 2px; }
|
| 43 |
+
.sidebar-nav a {
|
| 44 |
+
display: flex; align-items: center; gap: 10px;
|
| 45 |
+
padding: 0.6rem 0.8rem; border-radius: 8px;
|
| 46 |
+
color: var(--text-secondary); text-decoration: none;
|
| 47 |
+
font-size: 0.8rem; font-weight: 600;
|
| 48 |
+
transition: all 0.15s; border: 1px solid transparent;
|
| 49 |
+
}
|
| 50 |
+
.sidebar-nav a:hover { color: var(--text-primary); background: rgba(255,255,255,0.03); }
|
| 51 |
+
.sidebar-nav a.active { background: rgba(14,165,233,0.08); color: var(--primary); border-color: rgba(14,165,233,0.2); }
|
| 52 |
+
.sidebar-footer { padding: 0 0.8rem; }
|
| 53 |
+
.sidebar-status {
|
| 54 |
+
background: rgba(148,163,184,0.06); border: 1px solid rgba(148,163,184,0.1);
|
| 55 |
+
border-radius: 10px; padding: 0.7rem; font-size: 0.65rem;
|
| 56 |
+
}
|
| 57 |
+
.sidebar-status .label { color: var(--text-dim); font-weight: 600; margin-bottom: 4px; }
|
| 58 |
+
.sidebar-status .value { display: flex; align-items: center; gap: 6px; color: var(--accent); font-weight: 700; }
|
| 59 |
+
.pulse { width: 6px; height: 6px; background: var(--accent); border-radius: 50%; animation: pulse 2s infinite; }
|
| 60 |
+
@keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.3; } }
|
| 61 |
+
|
| 62 |
+
/* ===== MAIN ===== */
|
| 63 |
+
.main { flex: 1; overflow-y: auto; }
|
| 64 |
+
.topbar {
|
| 65 |
+
position: sticky; top: 0; z-index: 30;
|
| 66 |
+
display: flex; align-items: center; justify-content: space-between;
|
| 67 |
+
padding: 0.9rem 1.5rem;
|
| 68 |
+
background: rgba(11,17,32,0.92); backdrop-filter: blur(12px);
|
| 69 |
+
border-bottom: 1px solid var(--border);
|
| 70 |
+
}
|
| 71 |
+
.topbar-left { display: flex; align-items: center; gap: 10px; }
|
| 72 |
+
.topbar-left h1 { font-size: 1.15rem; font-weight: 800; color: #fff; }
|
| 73 |
+
.topbar-left .live-dot { width: 10px; height: 10px; background: var(--primary); border-radius: 2px; animation: pulse 2s infinite; }
|
| 74 |
+
.topbar-left .clock { font-size: 0.7rem; color: var(--text-dim); font-family: monospace; margin-left: 4px; }
|
| 75 |
+
.topbar-right { display: flex; gap: 8px; }
|
| 76 |
+
.btn-outline {
|
| 77 |
+
display: flex; align-items: center; gap: 6px;
|
| 78 |
+
padding: 6px 12px; background: rgba(148,163,184,0.06);
|
| 79 |
+
border: 1px solid var(--border); border-radius: 8px;
|
| 80 |
+
color: var(--text-secondary); font-size: 0.75rem; font-weight: 600;
|
| 81 |
+
cursor: pointer; transition: 0.15s;
|
| 82 |
+
}
|
| 83 |
+
.btn-outline:hover { border-color: var(--border-hover); color: var(--text-primary); }
|
| 84 |
+
|
| 85 |
+
.content { padding: 1.2rem 1.5rem; max-width: 1400px; margin: 0 auto; }
|
| 86 |
+
|
| 87 |
+
/* ===== CARDS ===== */
|
| 88 |
+
.card {
|
| 89 |
+
background: var(--bg-card);
|
| 90 |
+
border: 1px solid var(--border);
|
| 91 |
+
border-radius: var(--radius);
|
| 92 |
+
padding: 1rem 1.2rem;
|
| 93 |
+
transition: border-color 0.2s;
|
| 94 |
+
}
|
| 95 |
+
.card:hover { border-color: var(--border-hover); }
|
| 96 |
+
.card-title {
|
| 97 |
+
display: flex; align-items: center; gap: 6px;
|
| 98 |
+
font-size: 0.65rem; font-weight: 800; color: var(--text-dim);
|
| 99 |
+
text-transform: uppercase; letter-spacing: 1.2px;
|
| 100 |
+
margin-bottom: 0.8rem;
|
| 101 |
+
}
|
| 102 |
+
.card-title .dot { width: 5px; height: 5px; border-radius: 50%; }
|
| 103 |
+
|
| 104 |
+
/* ===== STATS GRID ===== */
|
| 105 |
+
.stats-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 0.8rem; margin-bottom: 1rem; }
|
| 106 |
+
.stat-value { font-size: 1.5rem; font-weight: 800; margin-top: 2px; }
|
| 107 |
+
|
| 108 |
+
/* ===== LATENCY MATRIX ===== */
|
| 109 |
+
.latency-section { margin-bottom: 1rem; }
|
| 110 |
+
.latency-row { display: flex; align-items: center; gap: 12px; margin-bottom: 8px; }
|
| 111 |
+
.latency-row .name { width: 160px; font-size: 0.68rem; font-weight: 600; color: var(--text-secondary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
| 112 |
+
.latency-row .bar-track { flex: 1; height: 5px; background: rgba(148,163,184,0.08); border-radius: 3px; overflow: hidden; }
|
| 113 |
+
.latency-row .bar-fill { height: 100%; border-radius: 3px; background: linear-gradient(90deg, rgba(14,165,233,0.7), rgba(6,182,212,0.7)); transition: width 0.8s ease; }
|
| 114 |
+
.latency-row .ms { width: 55px; text-align: right; font-size: 0.65rem; font-family: monospace; color: var(--text-dim); }
|
| 115 |
+
.live-badge { display: flex; align-items: center; gap: 4px; font-size: 0.55rem; font-weight: 800; color: var(--accent); letter-spacing: 1px; }
|
| 116 |
+
|
| 117 |
+
/* ===== SEARCH ===== */
|
| 118 |
+
.search-bar {
|
| 119 |
+
width: 100%; padding: 0.6rem 0.8rem 0.6rem 2.2rem;
|
| 120 |
+
background: var(--bg-card); border: 1px solid var(--border);
|
| 121 |
+
border-radius: var(--radius); color: var(--text-primary);
|
| 122 |
+
font-size: 0.8rem; outline: none; transition: 0.2s;
|
| 123 |
+
margin-bottom: 0.8rem; position: relative;
|
| 124 |
+
}
|
| 125 |
+
.search-bar:focus { border-color: rgba(14,165,233,0.4); }
|
| 126 |
+
.search-wrap { position: relative; margin-bottom: 0.8rem; }
|
| 127 |
+
.search-wrap i { position: absolute; left: 10px; top: 50%; transform: translateY(-50%); color: var(--text-dim); font-size: 0.8rem; }
|
| 128 |
+
|
| 129 |
+
/* ===== SERVICE ROWS ===== */
|
| 130 |
+
.svc-list { display: flex; flex-direction: column; gap: 6px; }
|
| 131 |
+
.svc-row {
|
| 132 |
+
display: grid;
|
| 133 |
+
grid-template-columns: 2fr 0.7fr 2.5fr 0.8fr 0.7fr;
|
| 134 |
+
align-items: center; gap: 1rem;
|
| 135 |
+
padding: 0.7rem 1rem;
|
| 136 |
+
background: var(--bg-card);
|
| 137 |
+
border: 1px solid var(--border);
|
| 138 |
+
border-radius: var(--radius);
|
| 139 |
+
cursor: pointer; transition: all 0.15s;
|
| 140 |
+
}
|
| 141 |
+
.svc-row:hover { border-color: rgba(148,163,184,0.2); background: #15202f; }
|
| 142 |
+
.svc-name { font-size: 0.82rem; font-weight: 700; color: #fff; }
|
| 143 |
+
.svc-url { font-size: 0.6rem; color: var(--text-dim); margin-top: 1px; }
|
| 144 |
+
.scope-badge {
|
| 145 |
+
display: inline-block; padding: 2px 8px; border-radius: 4px;
|
| 146 |
+
font-size: 0.55rem; font-weight: 800; letter-spacing: 0.8px; text-transform: uppercase;
|
| 147 |
+
border: 1px solid;
|
| 148 |
+
}
|
| 149 |
+
.scope-badge.global { background: rgba(14,165,233,0.08); color: var(--primary); border-color: rgba(14,165,233,0.15); }
|
| 150 |
+
.scope-badge.web { background: rgba(168,85,247,0.08); color: #a855f7; border-color: rgba(168,85,247,0.15); }
|
| 151 |
+
.scope-badge.security { background: rgba(16,185,129,0.08); color: var(--accent); border-color: rgba(16,185,129,0.15); }
|
| 152 |
+
.uptime-bars { display: flex; gap: 2px; justify-content: center; }
|
| 153 |
+
.uptime-bars .bar { width: 4px; height: 14px; border-radius: 1px; }
|
| 154 |
+
.uptime-bars .bar.up { background: var(--accent); }
|
| 155 |
+
.uptime-bars .bar.down { background: var(--danger); }
|
| 156 |
+
.uptime-bars .bar.none { background: rgba(255,255,255,0.04); }
|
| 157 |
+
.status-label { font-size: 0.7rem; font-weight: 700; text-align: right; }
|
| 158 |
+
.status-label.up { color: var(--accent); }
|
| 159 |
+
.status-label.down { color: var(--danger); }
|
| 160 |
+
.latency-chip {
|
| 161 |
+
font-size: 0.68rem; font-family: monospace; font-weight: 600;
|
| 162 |
+
color: var(--primary); background: rgba(14,165,233,0.08);
|
| 163 |
+
padding: 3px 8px; border-radius: 5px; text-align: center;
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
/* ===== MODAL ===== */
|
| 167 |
+
.modal-overlay {
|
| 168 |
+
display: none; position: fixed; inset: 0;
|
| 169 |
+
background: rgba(0,0,0,0.6); backdrop-filter: blur(6px);
|
| 170 |
+
z-index: 999; justify-content: center; align-items: center; padding: 1rem;
|
| 171 |
+
}
|
| 172 |
+
.modal-overlay.open { display: flex; }
|
| 173 |
+
.modal-box {
|
| 174 |
+
width: 700px; max-width: 95vw; max-height: 88vh; overflow-y: auto;
|
| 175 |
+
background: var(--bg-card); border: 1px solid var(--border);
|
| 176 |
+
border-radius: 16px; box-shadow: 0 25px 50px rgba(0,0,0,0.5);
|
| 177 |
+
}
|
| 178 |
+
.modal-header {
|
| 179 |
+
display: flex; justify-content: space-between; align-items: center;
|
| 180 |
+
padding: 1.2rem 1.5rem; border-bottom: 1px solid var(--border);
|
| 181 |
+
}
|
| 182 |
+
.modal-header h2 { font-size: 1.1rem; font-weight: 800; }
|
| 183 |
+
.modal-header p { font-size: 0.75rem; color: var(--text-secondary); margin-top: 2px; }
|
| 184 |
+
.modal-close {
|
| 185 |
+
background: none; border: none; color: var(--text-secondary);
|
| 186 |
+
font-size: 1.3rem; cursor: pointer; padding: 4px 8px; border-radius: 6px;
|
| 187 |
+
transition: 0.15s;
|
| 188 |
+
}
|
| 189 |
+
.modal-close:hover { background: rgba(255,255,255,0.05); color: #fff; }
|
| 190 |
+
.modal-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 0.8rem; padding: 1.2rem 1.5rem; }
|
| 191 |
+
.modal-card {
|
| 192 |
+
background: rgba(0,0,0,0.2); border: 1px solid var(--border);
|
| 193 |
+
border-radius: 10px; padding: 0.8rem;
|
| 194 |
+
}
|
| 195 |
+
.modal-card h4 {
|
| 196 |
+
display: flex; align-items: center; gap: 6px;
|
| 197 |
+
font-size: 0.72rem; font-weight: 700; margin-bottom: 0.6rem;
|
| 198 |
+
}
|
| 199 |
+
.modal-card canvas { width: 100% !important; height: 130px !important; }
|
| 200 |
+
|
| 201 |
+
/* ===== TOOLTIPS ===== */
|
| 202 |
+
.tip {
|
| 203 |
+
display: inline-flex; align-items: center; justify-content: center;
|
| 204 |
+
width: 14px; height: 14px; border-radius: 50%;
|
| 205 |
+
background: rgba(14,165,233,0.08); color: var(--text-dim);
|
| 206 |
+
font-size: 0.5rem; font-weight: 900; cursor: help;
|
| 207 |
+
position: relative; margin-left: 4px; border: 1px solid rgba(14,165,233,0.15);
|
| 208 |
+
}
|
| 209 |
+
.tip:hover::after {
|
| 210 |
+
content: attr(data-tip);
|
| 211 |
+
position: absolute; bottom: calc(100% + 6px); left: 50%; transform: translateX(-50%);
|
| 212 |
+
background: #1a2332; color: var(--text-primary);
|
| 213 |
+
padding: 6px 10px; border-radius: 6px; font-size: 0.62rem; font-weight: 500;
|
| 214 |
+
white-space: nowrap; z-index: 100; border: 1px solid var(--border);
|
| 215 |
+
box-shadow: 0 4px 12px rgba(0,0,0,0.4); pointer-events: none;
|
| 216 |
+
max-width: 250px; white-space: normal; line-height: 1.4;
|
| 217 |
+
}
|
| 218 |
+
|
| 219 |
+
/* ===== OPS TAB ===== */
|
| 220 |
+
.ops-grid { display: grid; grid-template-columns: 1.2fr 0.8fr; gap: 1rem; }
|
| 221 |
+
.input-group { margin-bottom: 0.6rem; }
|
| 222 |
+
.input-group label { display: block; font-size: 0.6rem; font-weight: 800; color: var(--text-dim); letter-spacing: 0.5px; margin-bottom: 4px; text-transform: uppercase; }
|
| 223 |
+
.pro-input {
|
| 224 |
+
width: 100%; padding: 8px 12px; background: rgba(0,0,0,0.25);
|
| 225 |
+
border: 1px solid var(--border); border-radius: 8px;
|
| 226 |
+
color: #fff; font-size: 0.78rem; outline: none; transition: 0.2s;
|
| 227 |
+
}
|
| 228 |
+
.pro-input:focus { border-color: rgba(14,165,233,0.4); }
|
| 229 |
+
.btn-primary {
|
| 230 |
+
display: inline-flex; align-items: center; justify-content: center; gap: 6px;
|
| 231 |
+
width: 100%; padding: 10px; background: linear-gradient(135deg, var(--primary), #0284c7);
|
| 232 |
+
color: #fff; border: none; border-radius: 8px;
|
| 233 |
+
font-size: 0.75rem; font-weight: 800; cursor: pointer;
|
| 234 |
+
text-transform: uppercase; letter-spacing: 0.5px; transition: 0.2s;
|
| 235 |
+
}
|
| 236 |
+
.btn-primary:hover { filter: brightness(1.1); }
|
| 237 |
+
.btn-danger-outline {
|
| 238 |
+
width: 100%; padding: 8px; background: transparent;
|
| 239 |
+
border: 1px solid rgba(244,63,94,0.2); border-radius: 8px;
|
| 240 |
+
color: var(--danger); font-size: 0.7rem; font-weight: 700; cursor: pointer;
|
| 241 |
+
}
|
| 242 |
+
.manage-item {
|
| 243 |
+
display: flex; justify-content: space-between; align-items: center;
|
| 244 |
+
padding: 6px 10px; background: rgba(0,0,0,0.15);
|
| 245 |
+
border: 1px solid var(--border); border-radius: 6px; margin-bottom: 4px;
|
| 246 |
+
}
|
| 247 |
+
.manage-item b { font-size: 0.75rem; }
|
| 248 |
+
.manage-item span { font-size: 0.6rem; color: var(--text-dim); }
|
| 249 |
+
.btn-remove {
|
| 250 |
+
padding: 3px 8px; background: rgba(244,63,94,0.08);
|
| 251 |
+
border: 1px solid rgba(244,63,94,0.15); border-radius: 4px;
|
| 252 |
+
color: var(--danger); font-size: 0.6rem; font-weight: 800;
|
| 253 |
+
cursor: pointer; transition: 0.15s; text-transform: uppercase;
|
| 254 |
+
}
|
| 255 |
+
.btn-remove:hover { background: var(--danger); color: #fff; }
|
| 256 |
+
|
| 257 |
+
/* ===== LICENSE OVERLAYS ===== */
|
| 258 |
+
.auth-overlay {
|
| 259 |
+
position: fixed; inset: 0; background: var(--bg-main);
|
| 260 |
+
display: flex; justify-content: center; align-items: center; z-index: 99999;
|
| 261 |
+
}
|
| 262 |
+
.auth-box { max-width: 380px; width: 90%; text-align: center; }
|
| 263 |
+
|
| 264 |
+
/* ===== ANIMATIONS ===== */
|
| 265 |
+
@keyframes slideUp { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
|
| 266 |
+
.fade-in { animation: slideUp 0.35s ease forwards; }
|
| 267 |
+
|
| 268 |
+
/* ===== SCROLLBAR ===== */
|
| 269 |
+
::-webkit-scrollbar { width: 5px; }
|
| 270 |
+
::-webkit-scrollbar-track { background: transparent; }
|
| 271 |
+
::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.06); border-radius: 3px; }
|
static/style_v6.css
ADDED
|
@@ -0,0 +1,463 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
| 2 |
+
:root{
|
| 3 |
+
--bg0:#02060E;--bg1:#060D1A;--bg2:#091422;--bg3:#0D1C30;
|
| 4 |
+
--surface:rgba(9,20,34,0.92);--glass:rgba(30,100,200,0.05);
|
| 5 |
+
--b1:rgba(30,120,255,0.12);--b2:rgba(30,120,255,0.25);--b3:rgba(30,120,255,0.5);
|
| 6 |
+
--blue:#1E78FF;--blue-dim:rgba(30,120,255,0.7);--blue-glow:rgba(30,120,255,0.2);
|
| 7 |
+
--cyan:#00D4FF;--cyan-glow:rgba(0,212,255,0.15);
|
| 8 |
+
--green:#00E887;--green-dim:rgba(0,232,135,0.7);--green-glow:rgba(0,232,135,0.12);
|
| 9 |
+
--red:#FF3A5C;--red-dim:rgba(255,58,92,0.7);--red-glow:rgba(255,58,92,0.12);
|
| 10 |
+
--amber:#FFAA00;--amber-glow:rgba(255,170,0,0.12);
|
| 11 |
+
--violet:#9B6DFF;
|
| 12 |
+
--t1:#EDF2FF;--t2:rgba(180,210,255,0.7);--t3:rgba(120,160,210,0.45);--t4:rgba(80,120,180,0.3);
|
| 13 |
+
--mono:'IBM Plex Mono',monospace;--display:'Barlow Condensed',sans-serif;--body:'Barlow',sans-serif;
|
| 14 |
+
--r-sm:8px;--r-md:12px;--r-lg:16px;--r-xl:20px;
|
| 15 |
+
--sidebar-w:62px;
|
| 16 |
+
--ease:cubic-bezier(.4,0,.2,1);
|
| 17 |
+
--header-bg:rgba(2,6,14,0.85);
|
| 18 |
+
--sidebar-bg:linear-gradient(180deg,rgba(6,13,26,.98),rgba(3,8,18,.99));
|
| 19 |
+
--card-shadow:0 8px 32px rgba(0,0,0,.3);
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
:root[data-theme="light"]{
|
| 23 |
+
--bg0:#F4F6FC;--bg1:#FFFFFF;--bg2:#EBF0F9;--bg3:#DFE6F5;
|
| 24 |
+
--surface:#FFFFFF;--glass:rgba(30,100,200,0.04);
|
| 25 |
+
--b1:rgba(30,90,200,0.15);--b2:rgba(30,90,200,0.3);--b3:rgba(30,90,200,0.55);
|
| 26 |
+
--blue:#1E66FF;--blue-dim:rgba(30,102,255,0.8);--blue-glow:rgba(30,102,255,0.15);
|
| 27 |
+
--cyan:#009BBF;--cyan-glow:rgba(0,155,191,0.12);
|
| 28 |
+
--green:#04A760;--green-dim:rgba(4,167,96,0.8);--green-glow:rgba(4,167,96,0.12);
|
| 29 |
+
--red:#E11D48;--red-dim:rgba(225,29,72,0.8);--red-glow:rgba(225,29,72,0.12);
|
| 30 |
+
--amber:#D97706;--amber-glow:rgba(217,119,6,0.12);
|
| 31 |
+
--violet:#8B5CF6;
|
| 32 |
+
--t1:#0F172A;--t2:#334155;--t3:#64748B;--t4:#94A3B8;
|
| 33 |
+
--header-bg:rgba(255,255,255,0.92);
|
| 34 |
+
--sidebar-bg:linear-gradient(180deg,#FFFFFF,#F1F5F9);
|
| 35 |
+
--card-shadow:0 8px 24px rgba(15,23,42,.06);
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
:root[data-theme="cyber"]{
|
| 39 |
+
--bg0:#050014;--bg1:#0D0028;--bg2:#16003B;--bg3:#210052;
|
| 40 |
+
--surface:rgba(13,0,40,0.94);--glass:rgba(255,0,212,0.06);
|
| 41 |
+
--b1:rgba(255,0,212,0.2);--b2:rgba(255,0,212,0.4);--b3:rgba(255,0,212,0.7);
|
| 42 |
+
--blue:#00F0FF;--blue-dim:rgba(0,240,255,0.8);--blue-glow:rgba(0,240,255,0.25);
|
| 43 |
+
--cyan:#FF00D4;--cyan-glow:rgba(255,0,212,0.2);
|
| 44 |
+
--green:#00FF88;--green-dim:rgba(0,255,136,0.8);--green-glow:rgba(0,255,136,0.2);
|
| 45 |
+
--red:#FF0055;--red-dim:rgba(255,0,85,0.8);--red-glow:rgba(255,0,85,0.2);
|
| 46 |
+
--amber:#FFB800;--amber-glow:rgba(255,184,0,0.2);
|
| 47 |
+
--violet:#BD00FF;
|
| 48 |
+
--t1:#FFFFFF;--t2:rgba(230,200,255,0.8);--t3:rgba(180,140,220,0.6);--t4:rgba(140,100,180,0.4);
|
| 49 |
+
--header-bg:rgba(5,0,20,0.9);
|
| 50 |
+
--sidebar-bg:linear-gradient(180deg,#0D0028,#050014);
|
| 51 |
+
--card-shadow:0 8px 32px rgba(255,0,212,.15);
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
html,body{height:100%;background:var(--bg0);color:var(--t1);font-family:var(--body);overflow-x:hidden;font-size:14px;transition:background-color .3s var(--ease),color .3s var(--ease);}
|
| 55 |
+
body, .sidebar, .header, .panel, .kpi, .pro-input, .lat-row, .svc-tbl, .modal, .modal-back{transition:background-color .3s var(--ease),border-color .3s var(--ease),color .3s var(--ease),box-shadow .3s var(--ease);}
|
| 56 |
+
|
| 57 |
+
/* ─── BG LAYERS ─── */
|
| 58 |
+
#bg-grid{position:fixed;inset:0;pointer-events:none;z-index:0;
|
| 59 |
+
background-image:linear-gradient(var(--b1) 1px,transparent 1px),
|
| 60 |
+
linear-gradient(90deg,var(--b1) 1px,transparent 1px);
|
| 61 |
+
background-size:48px 48px;opacity:0.6;}
|
| 62 |
+
#bg-glow{position:fixed;inset:0;pointer-events:none;z-index:0;
|
| 63 |
+
background:
|
| 64 |
+
radial-gradient(ellipse 55% 40% at 75% 15%,var(--blue-glow) 0%,transparent 65%),
|
| 65 |
+
radial-gradient(ellipse 35% 50% at 5% 85%,var(--cyan-glow) 0%,transparent 55%),
|
| 66 |
+
radial-gradient(ellipse 25% 30% at 50% 50%,var(--glass) 0%,transparent 70%);}
|
| 67 |
+
|
| 68 |
+
/* ─── SIDEBAR ─── */
|
| 69 |
+
.sidebar{position:fixed;left:0;top:0;bottom:0;width:var(--sidebar-w);
|
| 70 |
+
background:var(--sidebar-bg);
|
| 71 |
+
border-right:1px solid var(--b1);z-index:200;
|
| 72 |
+
display:flex;flex-direction:column;align-items:center;padding:16px 0 20px;gap:4px;}
|
| 73 |
+
.s-logo{width:36px;height:36px;border-radius:10px;margin-bottom:20px;
|
| 74 |
+
background:linear-gradient(135deg,var(--blue),#0040CC);
|
| 75 |
+
display:flex;align-items:center;justify-content:center;
|
| 76 |
+
font-family:var(--display);font-size:14px;font-weight:800;color:#fff;letter-spacing:.5px;
|
| 77 |
+
box-shadow:0 0 24px rgba(30,120,255,0.45),0 0 60px rgba(30,120,255,0.15);}
|
| 78 |
+
.s-btn{width:40px;height:40px;border-radius:var(--r-sm);border:none;background:transparent;
|
| 79 |
+
color:var(--t3);cursor:pointer;display:flex;align-items:center;justify-content:center;
|
| 80 |
+
font-size:16px;transition:all .2s var(--ease);position:relative;}
|
| 81 |
+
.s-btn:hover{background:var(--glass);color:var(--blue);}
|
| 82 |
+
.s-btn.act{background:rgba(30,120,255,0.12);color:var(--blue);
|
| 83 |
+
box-shadow:inset 0 0 0 1px var(--b2);}
|
| 84 |
+
.s-btn.act::before{content:'';position:absolute;left:-1px;top:50%;transform:translateY(-50%);
|
| 85 |
+
width:3px;height:18px;background:var(--blue);border-radius:0 3px 3px 0;
|
| 86 |
+
box-shadow:0 0 10px var(--blue);}
|
| 87 |
+
.s-btn svg{width:16px;height:16px;}
|
| 88 |
+
.s-spacer{flex:1;}
|
| 89 |
+
.s-avatar{width:32px;height:32px;border-radius:50%;background:linear-gradient(135deg,#1a3a7a,#0d2050);
|
| 90 |
+
border:1px solid var(--b2);display:flex;align-items:center;justify-content:center;
|
| 91 |
+
font-size:11px;font-weight:700;color:var(--blue);font-family:var(--mono);}
|
| 92 |
+
|
| 93 |
+
/* ─── APP ─── */
|
| 94 |
+
.app{margin-left:var(--sidebar-w);min-height:100vh;position:relative;z-index:1;}
|
| 95 |
+
|
| 96 |
+
/* ─── ALERT BANNER ─── */
|
| 97 |
+
.alert-banner{
|
| 98 |
+
background:linear-gradient(90deg,rgba(255,58,92,0.15),rgba(255,58,92,0.08),transparent);
|
| 99 |
+
border-bottom:1px solid rgba(255,58,92,0.25);
|
| 100 |
+
padding:8px 28px;display:flex;align-items:center;gap:12px;
|
| 101 |
+
font-family:var(--mono);font-size:11px;color:var(--red);
|
| 102 |
+
display:none; /* hidden by default */
|
| 103 |
+
}
|
| 104 |
+
.alert-banner-dot{width:6px;height:6px;border-radius:50%;background:var(--red);
|
| 105 |
+
animation:pulse-red 1.2s infinite;}
|
| 106 |
+
@keyframes pulse-red{0%,100%{box-shadow:0 0 0 0 rgba(255,58,92,.6);}50%{box-shadow:0 0 0 6px rgba(255,58,92,0);}}
|
| 107 |
+
.alert-banner-sep{width:1px;height:12px;background:rgba(255,58,92,0.3);}
|
| 108 |
+
.alert-banner-text{color:var(--t2);}
|
| 109 |
+
|
| 110 |
+
/* ─── HEADER ─── */
|
| 111 |
+
.header{display:flex;align-items:center;justify-content:space-between;
|
| 112 |
+
padding:16px 28px;border-bottom:1px solid var(--b1);
|
| 113 |
+
background:var(--header-bg);backdrop-filter:blur(24px);
|
| 114 |
+
position:sticky;top:0;z-index:100;}
|
| 115 |
+
.h-left{display:flex;align-items:center;gap:16px;}
|
| 116 |
+
.h-title{font-family:var(--display);font-size:22px;font-weight:700;letter-spacing:-.2px;}
|
| 117 |
+
.h-sub{font-size:11px;color:var(--t3);font-family:var(--mono);margin-top:1px;}
|
| 118 |
+
.h-right{display:flex;align-items:center;gap:8px;}
|
| 119 |
+
.live-chip{display:flex;align-items:center;gap:6px;padding:5px 12px;border-radius:20px;
|
| 120 |
+
background:rgba(0,232,135,0.08);border:1px solid rgba(0,232,135,0.2);
|
| 121 |
+
font-family:var(--mono);font-size:9px;font-weight:600;letter-spacing:2px;color:var(--green);}
|
| 122 |
+
.live-chip-dot{width:5px;height:5px;border-radius:50%;background:var(--green);animation:pulse-g 1.6s infinite;}
|
| 123 |
+
@keyframes pulse-g{0%,100%{box-shadow:0 0 0 0 rgba(0,232,135,.6);}50%{box-shadow:0 0 0 5px rgba(0,232,135,0);}}
|
| 124 |
+
.theme-btn {
|
| 125 |
+
display: flex; align-items: center; gap: 6px; padding: 6px 12px;
|
| 126 |
+
border-radius: 20px; border: 1px solid var(--b2);
|
| 127 |
+
background: var(--glass); color: var(--t1);
|
| 128 |
+
font-family: var(--mono); font-size: 11px; font-weight: 600;
|
| 129 |
+
cursor: pointer; transition: all 0.25s var(--ease);
|
| 130 |
+
}
|
| 131 |
+
.theme-btn:hover {
|
| 132 |
+
background: var(--blue-glow); border-color: var(--blue);
|
| 133 |
+
transform: translateY(-1px); box-shadow: 0 4px 14px var(--blue-glow);
|
| 134 |
+
}
|
| 135 |
+
.hbtn{padding:7px 14px;border-radius:var(--r-sm);font-size:11px;font-weight:600;
|
| 136 |
+
cursor:pointer;transition:all .2s;font-family:var(--body);letter-spacing:.3px;
|
| 137 |
+
display:flex;align-items:center;gap:6px;}
|
| 138 |
+
.hbtn-ghost{border:1px solid var(--b1);background:transparent;color:var(--t2);}
|
| 139 |
+
.hbtn-ghost:hover{border-color:var(--b2);color:var(--blue);}
|
| 140 |
+
.hbtn-prim{border:1px solid var(--b2);background:rgba(30,120,255,0.12);color:var(--blue);}
|
| 141 |
+
.hbtn-prim:hover{background:rgba(30,120,255,0.2);}
|
| 142 |
+
.clock-box{font-family:var(--mono);font-size:11px;color:var(--t3);
|
| 143 |
+
padding:5px 12px;border:1px solid var(--b1);border-radius:var(--r-sm);letter-spacing:1px;}
|
| 144 |
+
.region-sel{background:var(--bg2);border:1px solid var(--b1);border-radius:var(--r-sm);
|
| 145 |
+
color:var(--t2);font-family:var(--body);font-size:11px;padding:6px 10px;cursor:pointer;outline:none;}
|
| 146 |
+
.region-sel:focus{border-color:var(--b2);}
|
| 147 |
+
|
| 148 |
+
/* ─── TABS ─── */
|
| 149 |
+
.main-tab { display: none; padding:20px 28px; flex-direction:column; gap:20px; }
|
| 150 |
+
.main-tab.act { display: flex; animation: fadein 0.3s var(--ease); }
|
| 151 |
+
@keyframes fadein { from { opacity:0; transform:translateY(10px); } to { opacity:1; transform:none; } }
|
| 152 |
+
|
| 153 |
+
/* ─── KPI ROW ─── */
|
| 154 |
+
.kpi-row{display:grid;grid-template-columns:repeat(4,1fr);gap:14px;}
|
| 155 |
+
.kpi{background:var(--surface);border:1px solid var(--b1);border-radius:var(--r-lg);
|
| 156 |
+
padding:18px 20px;position:relative;
|
| 157 |
+
transition:border-color .3s,transform .2s,box-shadow .3s;cursor:default;
|
| 158 |
+
backdrop-filter:blur(12px);}
|
| 159 |
+
.kpi:hover{border-color:var(--b2);transform:translateY(-1px);box-shadow:0 8px 32px rgba(0,0,0,.3);}
|
| 160 |
+
.kpi-accent-line{position:absolute;top:0;left:0;right:0;height:2px;
|
| 161 |
+
background:linear-gradient(90deg,transparent 0%,var(--ac,var(--blue)) 40%,var(--ac,var(--blue)) 60%,transparent 100%);}
|
| 162 |
+
.kpi-icon{position:absolute;right:16px;top:16px;font-size:22px;opacity:0.12;}
|
| 163 |
+
.kpi-label{font-size:9px;font-weight:700;letter-spacing:2.5px;text-transform:uppercase;
|
| 164 |
+
color:var(--t3);margin-bottom:8px;display:flex;align-items:center;gap:6px;font-family:var(--mono);}
|
| 165 |
+
.kpi-val{font-family:var(--display);font-size:38px;font-weight:700;line-height:1;letter-spacing:-.5px;
|
| 166 |
+
color:var(--vc,var(--blue));}
|
| 167 |
+
.kpi-delta{display:inline-flex;align-items:center;gap:4px;margin-top:6px;
|
| 168 |
+
padding:2px 7px;border-radius:4px;font-size:10px;font-family:var(--mono);font-weight:500;}
|
| 169 |
+
.kpi-delta.up{background:rgba(0,232,135,0.1);color:var(--green);}
|
| 170 |
+
.kpi-delta.down{background:rgba(255,58,92,0.1);color:var(--red);}
|
| 171 |
+
.kpi-delta.neutral{background:rgba(30,120,255,0.1);color:var(--blue);}
|
| 172 |
+
.kpi-bar{position:absolute;bottom:0;left:0;right:0;height:3px;background:rgba(30,120,255,0.06);}
|
| 173 |
+
.kpi-bar-fill{height:100%;background:var(--ac,var(--blue));opacity:.4;transition:width 1.5s var(--ease);}
|
| 174 |
+
|
| 175 |
+
/* ─── GRID 3-COL ─── */
|
| 176 |
+
.row2{display:grid;grid-template-columns:1fr 1fr 340px;gap:16px;}
|
| 177 |
+
.row3{display:grid;grid-template-columns:1fr 320px;gap:16px;}
|
| 178 |
+
|
| 179 |
+
/* ─── PANEL ─── */
|
| 180 |
+
.panel{background:var(--surface);border:1px solid var(--b1);border-radius:var(--r-lg);
|
| 181 |
+
backdrop-filter:blur(12px);}
|
| 182 |
+
.ph{padding:14px 20px;border-bottom:1px solid var(--b1);
|
| 183 |
+
display:flex;align-items:center;justify-content:space-between;}
|
| 184 |
+
.ph-title{font-family:var(--mono);font-size:9px;font-weight:600;letter-spacing:2.5px;
|
| 185 |
+
text-transform:uppercase;color:var(--t2);display:flex;align-items:center;gap:8px;}
|
| 186 |
+
.ph-icon{color:var(--blue);}
|
| 187 |
+
.ph-badge{font-family:var(--mono);font-size:9px;padding:2px 8px;border-radius:4px;
|
| 188 |
+
font-weight:700;letter-spacing:1.5px;}
|
| 189 |
+
.ph-badge.live{background:rgba(0,232,135,0.12);color:var(--green);border:1px solid rgba(0,232,135,.2);}
|
| 190 |
+
.ph-badge.warn{background:rgba(255,58,92,0.12);color:var(--red);}
|
| 191 |
+
.pb{padding:16px 20px;}
|
| 192 |
+
|
| 193 |
+
/* ─── LATENCY MATRIX ─── */
|
| 194 |
+
.lat-row{display:grid;grid-template-columns:155px 1fr 72px;
|
| 195 |
+
align-items:center;gap:12px;padding:7px 4px;
|
| 196 |
+
border-radius:var(--r-sm);transition:background .2s;cursor:default;}
|
| 197 |
+
.lat-row:hover{background:rgba(30,120,255,0.04);}
|
| 198 |
+
.lat-name{font-size:11px;color:var(--t2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
|
| 199 |
+
.lat-track{height:3px;background:rgba(30,120,255,0.08);border-radius:2px;overflow:hidden;position:relative;}
|
| 200 |
+
.lat-fill{height:100%;border-radius:2px;background:linear-gradient(90deg,rgba(30,120,255,0.4),var(--cyan));
|
| 201 |
+
transition:width 1.2s var(--ease);}
|
| 202 |
+
.lat-fill.hot{background:linear-gradient(90deg,rgba(255,170,0,0.5),var(--amber));}
|
| 203 |
+
.lat-ms{font-family:var(--mono);font-size:10px;color:var(--cyan);text-align:right;}
|
| 204 |
+
|
| 205 |
+
/* ─── UPTIME HEATMAP ─── */
|
| 206 |
+
.heatmap{display:flex;flex-direction:column;gap:8px;}
|
| 207 |
+
.hm-label{font-family:var(--mono);font-size:8px;color:var(--t4);letter-spacing:1px;}
|
| 208 |
+
.hm-grid{display:flex;gap:2px;}
|
| 209 |
+
.hm-cell{width:10px;height:10px;border-radius:2px;cursor:pointer;transition:transform .15s;}
|
| 210 |
+
.hm-cell:hover{transform:scale(1.3);}
|
| 211 |
+
.hm-legend{display:flex;align-items:center;gap:6px;font-family:var(--mono);font-size:9px;color:var(--t4);}
|
| 212 |
+
.hm-leg-cell{width:8px;height:8px;border-radius:1px;}
|
| 213 |
+
|
| 214 |
+
/* ─── INCIDENT FEED ─── */
|
| 215 |
+
.inc-list{display:flex;flex-direction:column;gap:8px;max-height:340px;overflow-y:auto;}
|
| 216 |
+
.inc-item{padding:12px 14px;border-radius:var(--r-sm);border:1px solid;position:relative;overflow:hidden;
|
| 217 |
+
transition:transform .2s,border-color .2s;}
|
| 218 |
+
.inc-item:hover{transform:translateX(2px);}
|
| 219 |
+
.inc-item.crit{background:rgba(255,58,92,0.05);border-color:rgba(255,58,92,0.2);}
|
| 220 |
+
.inc-item.warn{background:rgba(255,170,0,0.05);border-color:rgba(255,170,0,0.2);}
|
| 221 |
+
.inc-item::before{content:'';position:absolute;left:0;top:0;bottom:0;width:3px;}
|
| 222 |
+
.inc-item.crit::before{background:var(--red);}
|
| 223 |
+
.inc-item.warn::before{background:var(--amber);}
|
| 224 |
+
.inc-hd{display:flex;align-items:center;justify-content:space-between;margin-bottom:5px;}
|
| 225 |
+
.inc-name{font-size:12px;font-weight:700;}
|
| 226 |
+
.inc-tag{font-size:8px;font-weight:800;letter-spacing:1.5px;padding:2px 7px;
|
| 227 |
+
border-radius:4px;font-family:var(--mono);text-transform:uppercase;}
|
| 228 |
+
.inc-tag.down{background:rgba(255,58,92,0.2);color:var(--red);}
|
| 229 |
+
.inc-tag.warn{background:rgba(255,170,0,0.2);color:var(--amber);}
|
| 230 |
+
.inc-msg{font-size:11px;color:var(--t3);line-height:1.5;}
|
| 231 |
+
.inc-time{display:flex;align-items:center;gap:6px;margin-top:6px;font-size:9px;color:var(--t4);font-family:var(--mono);}
|
| 232 |
+
.inc-elapsed{padding:1px 6px;background:rgba(255,255,255,0.06);border-radius:3px;}
|
| 233 |
+
|
| 234 |
+
/* ─── SERVICE TABLE ─── */
|
| 235 |
+
.svc-toolbar{display:flex;align-items:center;gap:10px;margin-bottom:14px;}
|
| 236 |
+
.svc-search{flex:1;position:relative;}
|
| 237 |
+
.svc-search input{width:100%;background:rgba(30,120,255,0.04);border:1px solid var(--b1);
|
| 238 |
+
border-radius:var(--r-sm);padding:9px 14px 9px 36px;color:var(--t1);
|
| 239 |
+
font-size:12px;font-family:var(--body);outline:none;transition:border .2s;}
|
| 240 |
+
.svc-search input:focus{border-color:var(--b2);}
|
| 241 |
+
.svc-search input::placeholder{color:var(--t4);}
|
| 242 |
+
.svc-search-ico{position:absolute;left:12px;top:50%;transform:translateY(-50%);
|
| 243 |
+
color:var(--t4);font-size:12px;pointer-events:none;}
|
| 244 |
+
.filter-chip{padding:5px 12px;border-radius:20px;font-size:10px;font-weight:600;cursor:pointer;
|
| 245 |
+
border:1px solid var(--b1);background:transparent;color:var(--t3);
|
| 246 |
+
transition:all .2s;font-family:var(--mono);}
|
| 247 |
+
.filter-chip:hover,.filter-chip.sel{border-color:var(--b2);background:rgba(30,120,255,0.1);color:var(--blue);}
|
| 248 |
+
.filter-chip.red.sel{border-color:rgba(255,58,92,.35);background:rgba(255,58,92,0.1);color:var(--red);}
|
| 249 |
+
|
| 250 |
+
.svc-tbl{width:100%;border-collapse:collapse;}
|
| 251 |
+
.svc-tbl thead tr{border-bottom:1px solid var(--b1);}
|
| 252 |
+
.svc-tbl th{font-family:var(--mono);font-size:8px;font-weight:600;letter-spacing:2px;
|
| 253 |
+
text-transform:uppercase;color:var(--t4);padding:7px 12px;text-align:left;cursor:pointer;
|
| 254 |
+
user-select:none;transition:color .2s;white-space:nowrap;}
|
| 255 |
+
.svc-tbl th:hover{color:var(--blue);}
|
| 256 |
+
.svc-tbl th.sort-asc::after{content:' ↑';}
|
| 257 |
+
.svc-tbl th.sort-desc::after{content:' ↓';}
|
| 258 |
+
.svc-tbl tbody tr{border-bottom:1px solid rgba(30,120,255,0.04);
|
| 259 |
+
transition:background .15s;cursor:pointer;}
|
| 260 |
+
.svc-tbl tbody tr:hover{background:rgba(30,120,255,0.04);}
|
| 261 |
+
.svc-tbl td{padding:11px 12px;vertical-align:middle;}
|
| 262 |
+
.td-name{font-size:12px;font-weight:600;margin-bottom:2px;}
|
| 263 |
+
.td-url{font-size:9px;color:var(--t4);font-family:var(--mono);}
|
| 264 |
+
.scope-tag{display:inline-block;padding:2px 8px;border-radius:4px;
|
| 265 |
+
font-size:8px;font-weight:700;letter-spacing:1px;text-transform:uppercase;
|
| 266 |
+
border:1px solid var(--b1);color:var(--t4);font-family:var(--mono);}
|
| 267 |
+
.sla-bar-wrap{width:80px;height:4px;background:rgba(255,255,255,0.06);border-radius:2px;overflow:hidden;}
|
| 268 |
+
.sla-bar{height:100%;border-radius:2px;transition:width 1s var(--ease);}
|
| 269 |
+
.sla-pct{font-family:var(--mono);font-size:9px;color:var(--t3);margin-top:2px;}
|
| 270 |
+
|
| 271 |
+
.spark30{display:flex;gap:1.5px;align-items:flex-end;height:20px;}
|
| 272 |
+
.sp-bar{width:3px;border-radius:1px;transition:height .4s;}
|
| 273 |
+
.sp-up{background:var(--green);opacity:.8;}
|
| 274 |
+
.sp-down{background:var(--red);opacity:.9;}
|
| 275 |
+
|
| 276 |
+
.pill{display:inline-flex;align-items:center;gap:5px;padding:3px 9px;
|
| 277 |
+
border-radius:5px;font-size:9px;font-weight:700;letter-spacing:1px;
|
| 278 |
+
text-transform:uppercase;font-family:var(--mono);}
|
| 279 |
+
.pill.online{background:rgba(0,232,135,0.09);border:1px solid rgba(0,232,135,.2);color:var(--green);}
|
| 280 |
+
.pill.down{background:rgba(255,58,92,0.09);border:1px solid rgba(255,58,92,.2);color:var(--red);}
|
| 281 |
+
.pill.degraded{background:rgba(255,170,0,0.09);border:1px solid rgba(255,170,0,.2);color:var(--amber);}
|
| 282 |
+
.pill-dot{width:4px;height:4px;border-radius:50%;background:currentColor;}
|
| 283 |
+
.pill.down .pill-dot{animation:blink .9s step-start infinite;}
|
| 284 |
+
@keyframes blink{0%,100%{opacity:1;}50%{opacity:.2;}}
|
| 285 |
+
|
| 286 |
+
.lat-td{font-family:var(--mono);font-size:11px;text-align:right;}
|
| 287 |
+
|
| 288 |
+
/* ─── CHART CANVAS ─── */
|
| 289 |
+
.chart-wrap{padding:0 16px 16px;position:relative;}
|
| 290 |
+
canvas{display:block;}
|
| 291 |
+
.chart-yaxis{position:absolute;left:20px;top:0;display:flex;flex-direction:column;
|
| 292 |
+
justify-content:space-between;height:calc(100% - 20px);
|
| 293 |
+
font-family:var(--mono);font-size:8px;color:var(--t4);pointer-events:none;}
|
| 294 |
+
.chart-xaxis{display:flex;justify-content:space-between;padding:4px 4px 0;
|
| 295 |
+
font-family:var(--mono);font-size:8px;color:var(--t4);}
|
| 296 |
+
|
| 297 |
+
/* ─── WORLD MAP (SVG) ─── */
|
| 298 |
+
.world-wrap{padding:12px 16px 14px;position:relative;}
|
| 299 |
+
.map-node{cursor:pointer;transition:r .2s;}
|
| 300 |
+
.map-node circle{transition:r .2s,opacity .2s;}
|
| 301 |
+
.map-node:hover circle{r:6;}
|
| 302 |
+
.node-ring{animation:ping 2s ease-out infinite;}
|
| 303 |
+
@keyframes ping{0%{r:6;opacity:.6;}100%{r:16;opacity:0;}}
|
| 304 |
+
|
| 305 |
+
/* ─── MODAL ─── */
|
| 306 |
+
.modal-back{position:fixed;inset:0;z-index:500;
|
| 307 |
+
background:rgba(1,4,10,.88);backdrop-filter:blur(12px);
|
| 308 |
+
display:none;align-items:center;justify-content:center;}
|
| 309 |
+
.modal-back.open{display:flex;}
|
| 310 |
+
.modal{background:linear-gradient(135deg,rgba(6,14,28,.99),rgba(4,10,22,.99));
|
| 311 |
+
border:1px solid var(--b2);border-radius:var(--r-xl);width:660px;max-width:95vw;
|
| 312 |
+
max-height:90vh;overflow-y:auto;
|
| 313 |
+
box-shadow:0 0 80px rgba(30,100,200,0.25),0 0 200px rgba(30,100,200,0.08);
|
| 314 |
+
animation:mdin .22s var(--ease);}
|
| 315 |
+
@keyframes mdin{from{opacity:0;transform:scale(.96) translateY(14px);}to{opacity:1;transform:none;}}
|
| 316 |
+
.modal-hd{padding:22px 24px 18px;border-bottom:1px solid var(--b1);
|
| 317 |
+
display:flex;align-items:flex-start;justify-content:space-between;position:sticky;top:0;
|
| 318 |
+
background:rgba(6,14,28,.98);z-index:10;}
|
| 319 |
+
.modal-hd-info{}
|
| 320 |
+
.modal-title{font-family:var(--display);font-size:22px;font-weight:700;}
|
| 321 |
+
.modal-meta{display:flex;align-items:center;gap:10px;margin-top:5px;}
|
| 322 |
+
.modal-url{font-family:var(--mono);font-size:10px;color:var(--t4);}
|
| 323 |
+
.modal-status-chip{display:inline-flex;align-items:center;gap:5px;
|
| 324 |
+
padding:3px 10px;border-radius:5px;font-family:var(--mono);font-size:9px;font-weight:700;letter-spacing:1px;}
|
| 325 |
+
.mclose{width:30px;height:30px;border-radius:var(--r-sm);border:1px solid var(--b1);
|
| 326 |
+
background:transparent;color:var(--t3);cursor:pointer;font-size:14px;
|
| 327 |
+
display:flex;align-items:center;justify-content:center;transition:all .2s;flex-shrink:0;}
|
| 328 |
+
.mclose:hover{border-color:var(--red);color:var(--red);}
|
| 329 |
+
.modal-tabs{display:flex;gap:0;border-bottom:1px solid var(--b1);}
|
| 330 |
+
.mtab{padding:11px 20px;font-size:11px;font-weight:600;cursor:pointer;border:none;
|
| 331 |
+
background:transparent;color:var(--t3);border-bottom:2px solid transparent;
|
| 332 |
+
transition:all .2s;font-family:var(--body);}
|
| 333 |
+
.mtab:hover{color:var(--t1);}
|
| 334 |
+
.mtab.act{color:var(--blue);border-bottom-color:var(--blue);}
|
| 335 |
+
.mtab-panel{padding:20px 24px;display:none;}
|
| 336 |
+
.mtab-panel.act{display:block;}
|
| 337 |
+
.modal-grid{display:grid;grid-template-columns:1fr 1fr;gap:14px;}
|
| 338 |
+
.m-card{background:rgba(30,120,255,0.03);border:1px solid var(--b1);
|
| 339 |
+
border-radius:var(--r-md);padding:14px 16px;}
|
| 340 |
+
.m-card-title{font-family:var(--mono);font-size:8px;font-weight:600;letter-spacing:2px;
|
| 341 |
+
text-transform:uppercase;color:var(--t3);margin-bottom:12px;
|
| 342 |
+
display:flex;align-items:center;gap:6px;}
|
| 343 |
+
.mc-icon{font-size:11px;}
|
| 344 |
+
.m-stat{font-family:var(--display);font-size:28px;font-weight:700;color:var(--t1);}
|
| 345 |
+
.m-stat-unit{font-size:14px;font-weight:400;color:var(--t3);}
|
| 346 |
+
.m-stat-sub{font-size:10px;color:var(--t4);font-family:var(--mono);margin-top:4px;}
|
| 347 |
+
.nominal{display:flex;flex-direction:column;align-items:center;justify-content:center;
|
| 348 |
+
height:70px;gap:6px;}
|
| 349 |
+
.nominal-ico{font-size:22px;}
|
| 350 |
+
.nominal-txt{font-family:var(--mono);font-size:10px;font-weight:700;letter-spacing:2px;color:var(--green);}
|
| 351 |
+
.incident-txt{font-family:var(--mono);font-size:10px;font-weight:700;letter-spacing:1px;color:var(--red);}
|
| 352 |
+
|
| 353 |
+
/* history table */
|
| 354 |
+
.h-tbl{width:100%;border-collapse:collapse;}
|
| 355 |
+
.h-tbl th{font-family:var(--mono);font-size:8px;letter-spacing:2px;text-transform:uppercase;
|
| 356 |
+
color:var(--t4);padding:6px 10px;text-align:left;border-bottom:1px solid var(--b1);}
|
| 357 |
+
.h-tbl td{padding:8px 10px;font-size:11px;border-bottom:1px solid rgba(30,120,255,0.04);}
|
| 358 |
+
.h-tbl td:last-child{font-family:var(--mono);font-size:10px;}
|
| 359 |
+
|
| 360 |
+
/* ─── TOAST ─── */
|
| 361 |
+
#toasts{position:fixed;bottom:20px;right:20px;z-index:999999;
|
| 362 |
+
display:flex;flex-direction:column;gap:8px;align-items:flex-end;}
|
| 363 |
+
.toast{padding:12px 16px;border-radius:var(--r-md);
|
| 364 |
+
background:#0F172A;border:1px solid #334155;
|
| 365 |
+
box-shadow:0 12px 36px rgba(0,0,0,.6);
|
| 366 |
+
display:flex;align-items:center;gap:10px;
|
| 367 |
+
font-size:12px;min-width:280px;max-width:440px;
|
| 368 |
+
animation:toast-in .3s var(--ease);cursor:pointer;color:#FFFFFF;}
|
| 369 |
+
.toast.exit{animation:toast-out .3s var(--ease) forwards;}
|
| 370 |
+
@keyframes toast-in{from{opacity:0;transform:translateX(20px);}to{opacity:1;transform:none;}}
|
| 371 |
+
@keyframes toast-out{from{opacity:1;transform:none;}to{opacity:0;transform:translateX(20px);}}
|
| 372 |
+
.toast-ico{font-size:16px;flex-shrink:0;}
|
| 373 |
+
.toast-body{flex:1;}
|
| 374 |
+
.toast-title{font-weight:700;font-size:12px;color:#FFFFFF;}
|
| 375 |
+
.toast-msg{font-size:11px;color:#94A3B8;margin-top:2px;line-height:1.45;word-break:break-word;}
|
| 376 |
+
.toast.err{border-color:rgba(255,58,92,.6);background:#1E1018;}
|
| 377 |
+
.toast.ok{border-color:rgba(0,232,135,.6);background:#091A14;}
|
| 378 |
+
.toast.info{border-color:#334155;background:#0F172A;}
|
| 379 |
+
|
| 380 |
+
/* ─── LOADING OVERLAY ─── */
|
| 381 |
+
#loader{position:fixed;inset:0;z-index:9999;background:var(--bg0);
|
| 382 |
+
display:flex;flex-direction:column;align-items:center;justify-content:center;gap:16px;
|
| 383 |
+
transition:opacity .5s;}
|
| 384 |
+
#loader.hide{opacity:0;pointer-events:none;}
|
| 385 |
+
.loader-logo{font-family:var(--display);font-size:32px;font-weight:800;letter-spacing:4px;color:var(--t1);}
|
| 386 |
+
.loader-bar{width:200px;height:2px;background:rgba(30,120,255,0.15);border-radius:1px;overflow:hidden;}
|
| 387 |
+
.loader-fill{height:100%;background:linear-gradient(90deg,var(--blue),var(--cyan));
|
| 388 |
+
border-radius:1px;animation:load-bar 1.4s var(--ease) forwards;}
|
| 389 |
+
@keyframes load-bar{from{width:0;}to{width:100%;}}
|
| 390 |
+
.loader-txt{font-family:var(--mono);font-size:10px;color:var(--t4);letter-spacing:2px;}
|
| 391 |
+
|
| 392 |
+
/* ─── ANIM ─── */
|
| 393 |
+
.fade-up{animation:fu .5s var(--ease) both;}
|
| 394 |
+
@keyframes fu{from{opacity:0;transform:translateY(10px);}to{opacity:1;transform:none;}}
|
| 395 |
+
.fade-up:nth-child(1){animation-delay:.06s;}
|
| 396 |
+
.fade-up:nth-child(2){animation-delay:.12s;}
|
| 397 |
+
.fade-up:nth-child(3){animation-delay:.18s;}
|
| 398 |
+
.fade-up:nth-child(4){animation-delay:.24s;}
|
| 399 |
+
|
| 400 |
+
/* ─── ADDITIONS FOR OPERATIONS AND TEST LAB ─── */
|
| 401 |
+
.ops-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
|
| 402 |
+
.ops-grid-3 { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 12px; }
|
| 403 |
+
.input-group { margin-bottom: 12px; }
|
| 404 |
+
.input-group label { display: block; font-family: var(--mono); font-size: 9px; font-weight: 600; letter-spacing: 1px; text-transform: uppercase; color: var(--t3); margin-bottom: 6px; }
|
| 405 |
+
.pro-input { width: 100%; background: rgba(30,120,255,0.04); border: 1px solid var(--b1); border-radius: var(--r-sm); padding: 10px 14px; color: var(--t1); font-size: 13px; font-family: var(--body); outline: none; transition: border .2s; }
|
| 406 |
+
.pro-input:focus { border-color: var(--b2); }
|
| 407 |
+
.pro-input::placeholder { color: var(--t4); }
|
| 408 |
+
.btn-primary { width: 100%; padding: 10px; background: rgba(30,120,255,0.12); border: 1px solid var(--b2); border-radius: var(--r-sm); color: var(--blue); font-size: 12px; font-weight: 600; cursor: pointer; display: flex; align-items: center; justify-content: center; gap: 8px; transition: 0.2s; }
|
| 409 |
+
.btn-primary:hover { background: rgba(30,120,255,0.2); }
|
| 410 |
+
.btn-danger-outline { width: 100%; padding: 8px; background: rgba(255,58,92,0.05); border: 1px solid rgba(255,58,92,0.2); border-radius: var(--r-sm); color: var(--red); font-size: 11px; font-weight: 600; cursor: pointer; transition: 0.2s; margin-top:10px; }
|
| 411 |
+
.btn-danger-outline:hover { background: rgba(255,58,92,0.1); }
|
| 412 |
+
.manage-item { display: flex; justify-content: space-between; align-items: center; padding: 10px 14px; background: rgba(30,120,255,0.02); border: 1px solid var(--b1); border-radius: var(--r-sm); margin-bottom: 6px; }
|
| 413 |
+
.manage-item-info b { font-size: 12px; color: var(--t1); }
|
| 414 |
+
.manage-item-info span { font-size: 10px; color: var(--t4); font-family: var(--mono); display: block; margin-top:2px; }
|
| 415 |
+
.btn-remove { padding: 4px 10px; background: rgba(255,58,92,0.08); border: 1px solid rgba(255,58,92,0.15); border-radius: 4px; color: var(--red); font-size: 10px; font-weight: 600; cursor: pointer; transition: 0.2s; font-family:var(--mono); text-transform:uppercase; letter-spacing:1px; }
|
| 416 |
+
.btn-remove:hover { background: var(--red); color: #fff; }
|
| 417 |
+
|
| 418 |
+
/* ─── AUTH OVERLAY ─── */
|
| 419 |
+
.auth-overlay { position: fixed; inset: 0; background: rgba(2,6,14,0.95); display: flex; justify-content: center; align-items: center; z-index: 99999; backdrop-filter: blur(10px); }
|
| 420 |
+
.auth-box { width: 400px; max-width: 90%; background: var(--surface); border: 1px solid var(--b2); border-radius: var(--r-xl); padding: 32px; text-align: center; box-shadow: 0 20px 60px rgba(30,120,255,0.1); }
|
| 421 |
+
.auth-box .s-logo { margin: 0 auto 20px; }
|
| 422 |
+
.auth-box h3 { font-family: var(--display); font-size: 22px; font-weight: 700; margin-bottom: 8px; }
|
| 423 |
+
.auth-box p { font-size: 12px; color: var(--t3); margin-bottom: 24px; }
|
| 424 |
+
|
| 425 |
+
/* ─── TOOLTIPS (HIGH CONTRAST & LEGIBILITY) ─── */
|
| 426 |
+
.tip {
|
| 427 |
+
display: inline-flex; align-items: center; justify-content: center;
|
| 428 |
+
width: 15px; height: 15px; border-radius: 50%;
|
| 429 |
+
background: var(--cyan-glow); color: var(--cyan);
|
| 430 |
+
font-size: 10px; font-weight: 800; cursor: help;
|
| 431 |
+
position: relative; margin-left: 6px; border: 1px solid var(--b2);
|
| 432 |
+
font-family: var(--mono); vertical-align: middle;
|
| 433 |
+
}
|
| 434 |
+
.tip:hover::after {
|
| 435 |
+
content: attr(data-tip);
|
| 436 |
+
position: absolute; bottom: calc(100% + 8px); left: 50%; transform: translateX(-50%);
|
| 437 |
+
background: #0F172A; color: #FFFFFF;
|
| 438 |
+
padding: 8px 12px; border-radius: var(--r-sm); font-size: 11px; font-weight: 500;
|
| 439 |
+
font-family: var(--body);
|
| 440 |
+
white-space: normal; z-index: 10000; border: 1px solid #334155;
|
| 441 |
+
box-shadow: 0 12px 32px rgba(0,0,0,0.6); pointer-events: none;
|
| 442 |
+
width: max-content; max-width: 280px; line-height: 1.45; text-transform: none; letter-spacing: normal; text-align: left;
|
| 443 |
+
}
|
| 444 |
+
|
| 445 |
+
/* ─── TEST LAB DIAGNOSTIC SUITE ─── */
|
| 446 |
+
.testlab-grid { display: grid; grid-template-columns: 380px 1fr; gap: 16px; width: 100%; }
|
| 447 |
+
.console-box {
|
| 448 |
+
background: #030712; border: 1px solid var(--b2); border-radius: var(--r-md);
|
| 449 |
+
padding: 14px 16px; font-family: var(--mono); font-size: 11px;
|
| 450 |
+
min-height: 240px; max-height: 320px; overflow-y: auto;
|
| 451 |
+
box-shadow: inset 0 2px 10px rgba(0,0,0,0.8);
|
| 452 |
+
}
|
| 453 |
+
.console-line { line-height: 1.6; margin-bottom: 4px; word-break: break-all; }
|
| 454 |
+
.console-line.text-dim { color: #64748B; }
|
| 455 |
+
.console-line.text-ok { color: #10B981; font-weight: 600; }
|
| 456 |
+
.console-line.text-warn { color: #F59E0B; }
|
| 457 |
+
.console-line.text-err { color: #EF4444; font-weight: 700; }
|
| 458 |
+
.console-line.text-cyan { color: #00D4FF; }
|
| 459 |
+
.testlab-kpis { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; }
|
| 460 |
+
.t-kpi { background: var(--surface); border: 1px solid var(--b1); border-radius: var(--r-md); padding: 12px 14px; backdrop-filter: blur(12px); }
|
| 461 |
+
.t-kpi-label { font-family: var(--mono); font-size: 8px; font-weight: 700; letter-spacing: 1.5px; color: var(--t3); margin-bottom: 4px; }
|
| 462 |
+
.t-kpi-val { font-family: var(--display); font-size: 20px; font-weight: 700; color: var(--t1); }
|
| 463 |
+
|
templates/dashboard_v5.html
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>StatusPilot — Operations Center</title>
|
| 7 |
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 8 |
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
|
| 9 |
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
| 10 |
+
<link rel="stylesheet" href="/static/style_v5.css">
|
| 11 |
+
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
| 12 |
+
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
| 13 |
+
</head>
|
| 14 |
+
<body>
|
| 15 |
+
|
| 16 |
+
<!-- ===== AUTH OVERLAY ===== -->
|
| 17 |
+
<div id="master-auth-overlay" class="auth-overlay">
|
| 18 |
+
<div class="auth-box card" style="padding:2rem;">
|
| 19 |
+
<div class="sidebar-logo" style="justify-content:center; margin-bottom:1.2rem; padding:0;">
|
| 20 |
+
<div class="icon"><i class="fas fa-shield-halved"></i><span class="dot"></span></div>
|
| 21 |
+
<span>STATUSPILOT</span>
|
| 22 |
+
</div>
|
| 23 |
+
<h3 style="font-weight:800; font-size:1rem; margin-bottom:6px;">Restricted Access</h3>
|
| 24 |
+
<p style="color:var(--text-secondary); font-size:0.78rem; margin-bottom:1.2rem;">Authenticate with your Master Key</p>
|
| 25 |
+
<input type="password" id="master-key-input" class="pro-input" placeholder="••••••••••••" style="text-align:center; letter-spacing:3px; margin-bottom:10px;">
|
| 26 |
+
<button id="master-login-btn" class="btn-primary"><i class="fas fa-lock-open"></i> Unlock</button>
|
| 27 |
+
</div>
|
| 28 |
+
</div>
|
| 29 |
+
|
| 30 |
+
<!-- ===== LICENSE OVERLAY ===== -->
|
| 31 |
+
<div id="license-overlay" class="auth-overlay" style="display:none;">
|
| 32 |
+
<div class="auth-box card" style="padding:2rem;">
|
| 33 |
+
<div class="sidebar-logo" style="justify-content:center; margin-bottom:1.2rem; padding:0;">
|
| 34 |
+
<div class="icon"><i class="fas fa-gem" style="color:#a78bfa;"></i></div>
|
| 35 |
+
<span style="color:#a78bfa;">STATUSPILOT PRO</span>
|
| 36 |
+
</div>
|
| 37 |
+
<h3 style="font-weight:800; font-size:1rem; margin-bottom:6px;">Enterprise Activation</h3>
|
| 38 |
+
<p style="color:var(--text-secondary); font-size:0.78rem; margin-bottom:1.2rem;">Enter your license credentials</p>
|
| 39 |
+
<input type="email" id="lic-email" class="pro-input" placeholder="Purchase Email" style="margin-bottom:8px;">
|
| 40 |
+
<input type="text" id="lic-key" class="pro-input" placeholder="License Key (SG-XXXX-XXXX)" style="text-transform:uppercase; margin-bottom:10px;">
|
| 41 |
+
<button id="activate-btn" class="btn-primary" style="background:linear-gradient(135deg,#a78bfa,#8b5cf6);">Activate</button>
|
| 42 |
+
</div>
|
| 43 |
+
</div>
|
| 44 |
+
|
| 45 |
+
<!-- ===== ANALYTICS MODAL ===== -->
|
| 46 |
+
<div id="matrix-modal" class="modal-overlay">
|
| 47 |
+
<div class="modal-box">
|
| 48 |
+
<div class="modal-header">
|
| 49 |
+
<div>
|
| 50 |
+
<h2 id="modal-title">Service Analytics</h2>
|
| 51 |
+
<p id="modal-subtitle">Historical performance data</p>
|
| 52 |
+
</div>
|
| 53 |
+
<button class="modal-close" id="modal-close-btn">×</button>
|
| 54 |
+
</div>
|
| 55 |
+
<div class="modal-grid">
|
| 56 |
+
<div class="modal-card">
|
| 57 |
+
<h4 style="color:var(--accent);"><i class="fas fa-check-circle"></i> Availability <span class="tip" data-tip="Shows UP/DOWN status over time. 100 = UP, 0 = DOWN.">?</span></h4>
|
| 58 |
+
<canvas id="uptimeChart"></canvas>
|
| 59 |
+
</div>
|
| 60 |
+
<div class="modal-card">
|
| 61 |
+
<h4 style="color:var(--primary);"><i class="fas fa-bolt"></i> Latency (ms) <span class="tip" data-tip="Response time in milliseconds. Lower = faster. Above 500ms = slow.">?</span></h4>
|
| 62 |
+
<canvas id="latencyChart"></canvas>
|
| 63 |
+
</div>
|
| 64 |
+
<div class="modal-card">
|
| 65 |
+
<h4 style="color:var(--warning);"><i class="fas fa-chart-pie"></i> Error Profile <span class="tip" data-tip="Ratio of successful vs failed checks. Green = OK, Red = Issues.">?</span></h4>
|
| 66 |
+
<div style="height:130px; display:flex; justify-content:center;"><canvas id="errorChart"></canvas></div>
|
| 67 |
+
</div>
|
| 68 |
+
<div class="modal-card">
|
| 69 |
+
<h4 style="color:var(--danger);"><i class="fas fa-exclamation-triangle"></i> Incidents <span class="tip" data-tip="Log of outages with timestamps and error messages.">?</span></h4>
|
| 70 |
+
<div id="incident-log" style="max-height:130px; overflow-y:auto;"></div>
|
| 71 |
+
</div>
|
| 72 |
+
</div>
|
| 73 |
+
</div>
|
| 74 |
+
</div>
|
| 75 |
+
|
| 76 |
+
<!-- ===== MAIN APP ===== -->
|
| 77 |
+
<div class="app" id="main-app" style="filter:blur(15px); opacity:0; pointer-events:none;">
|
| 78 |
+
|
| 79 |
+
<!-- Sidebar -->
|
| 80 |
+
<aside class="sidebar">
|
| 81 |
+
<div class="sidebar-logo">
|
| 82 |
+
<div class="icon"><i class="fas fa-shield-halved"></i><span class="dot"></span></div>
|
| 83 |
+
<span>STATUSPILOT</span>
|
| 84 |
+
</div>
|
| 85 |
+
<nav class="sidebar-nav">
|
| 86 |
+
<a href="#" class="active" id="nav-dashboard"><i class="fas fa-gauge-high"></i> Dashboard</a>
|
| 87 |
+
<a href="#" id="nav-test"><i class="fas fa-vial"></i> Test Lab</a>
|
| 88 |
+
<a href="#" id="nav-config"><i class="fas fa-sliders"></i> Operations</a>
|
| 89 |
+
</nav>
|
| 90 |
+
<div class="sidebar-footer">
|
| 91 |
+
<div class="sidebar-status">
|
| 92 |
+
<div class="label">System Status</div>
|
| 93 |
+
<div class="value"><span class="pulse"></span> All Systems Online</div>
|
| 94 |
+
</div>
|
| 95 |
+
</div>
|
| 96 |
+
</aside>
|
| 97 |
+
|
| 98 |
+
<!-- Main -->
|
| 99 |
+
<div class="main">
|
| 100 |
+
<!-- Top Bar -->
|
| 101 |
+
<div class="topbar">
|
| 102 |
+
<div class="topbar-left">
|
| 103 |
+
<h1>Operations Center</h1>
|
| 104 |
+
<div class="live-dot"></div>
|
| 105 |
+
<span class="clock" id="live-clock">--:--:--</span>
|
| 106 |
+
</div>
|
| 107 |
+
<div class="topbar-right">
|
| 108 |
+
<div class="btn-outline" style="padding:5px 10px;">
|
| 109 |
+
<i class="fas fa-globe" style="color:var(--primary); font-size:0.7rem;"></i>
|
| 110 |
+
<select id="location-filter" style="background:transparent; border:none; color:var(--text-secondary); font-size:0.72rem; font-weight:600; outline:none; cursor:pointer;"></select>
|
| 111 |
+
</div>
|
| 112 |
+
<button id="download-excel-btn" class="btn-outline"><i class="fas fa-file-export"></i> Export</button>
|
| 113 |
+
</div>
|
| 114 |
+
</div>
|
| 115 |
+
|
| 116 |
+
<div class="content">
|
| 117 |
+
|
| 118 |
+
<!-- ===== DASHBOARD TAB ===== -->
|
| 119 |
+
<div id="section-dashboard" class="content-section fade-in">
|
| 120 |
+
|
| 121 |
+
<!-- Latency Matrix -->
|
| 122 |
+
<div class="card latency-section" style="margin-bottom:1rem;">
|
| 123 |
+
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:0.8rem;">
|
| 124 |
+
<div class="card-title" style="margin-bottom:0;">
|
| 125 |
+
<i class="fas fa-bolt" style="color:var(--primary);"></i>
|
| 126 |
+
Global Latency Matrix
|
| 127 |
+
<span class="tip" data-tip="Real-time response time comparison across all services. Bars show latency in milliseconds — shorter is faster.">?</span>
|
| 128 |
+
</div>
|
| 129 |
+
<div class="live-badge"><span class="pulse"></span> LIVE</div>
|
| 130 |
+
</div>
|
| 131 |
+
<div id="latency-matrix-bars"></div>
|
| 132 |
+
</div>
|
| 133 |
+
|
| 134 |
+
<!-- KPI Stats -->
|
| 135 |
+
<div class="stats-grid">
|
| 136 |
+
<div class="card">
|
| 137 |
+
<div class="card-title"><div class="dot" style="background:var(--primary);"></div> Active Monitors <span class="tip" data-tip="Total services, websites, and SSL certs being tracked.">?</span></div>
|
| 138 |
+
<div class="stat-value" id="stat-monitors">0</div>
|
| 139 |
+
</div>
|
| 140 |
+
<div class="card">
|
| 141 |
+
<div class="card-title"><div class="dot" style="background:var(--accent);"></div> Global Uptime <span class="tip" data-tip="Average availability across all monitors. 100% = zero downtime.">?</span></div>
|
| 142 |
+
<div class="stat-value" style="color:var(--accent);" id="stat-uptime">0%</div>
|
| 143 |
+
</div>
|
| 144 |
+
<div class="card">
|
| 145 |
+
<div class="card-title"><div class="dot" style="background:var(--danger);"></div> Active Incidents <span class="tip" data-tip="Services currently DOWN. 0 = all systems operational.">?</span></div>
|
| 146 |
+
<div class="stat-value" style="color:var(--danger);" id="stat-incidents">0</div>
|
| 147 |
+
</div>
|
| 148 |
+
<div class="card">
|
| 149 |
+
<div class="card-title"><div class="dot" style="background:var(--primary);"></div> Avg. Latency <span class="tip" data-tip="Average response time in ms. Below 200ms = excellent.">?</span></div>
|
| 150 |
+
<div class="stat-value" id="stat-latency">0ms</div>
|
| 151 |
+
</div>
|
| 152 |
+
</div>
|
| 153 |
+
|
| 154 |
+
<!-- Search -->
|
| 155 |
+
<div class="search-wrap">
|
| 156 |
+
<i class="fas fa-search"></i>
|
| 157 |
+
<input type="text" id="search-services" class="search-bar" placeholder="Search services..." style="padding-left:2rem;">
|
| 158 |
+
</div>
|
| 159 |
+
|
| 160 |
+
<!-- Service List -->
|
| 161 |
+
<div class="svc-list" id="status-grid">
|
| 162 |
+
<div style="text-align:center; padding:2rem; color:var(--text-dim);">
|
| 163 |
+
<i class="fas fa-circle-notch fa-spin"></i>
|
| 164 |
+
<p style="margin-top:0.6rem; font-size:0.8rem; font-weight:600;">Connecting to Infrastructure...</p>
|
| 165 |
+
</div>
|
| 166 |
+
</div>
|
| 167 |
+
</div>
|
| 168 |
+
|
| 169 |
+
<!-- ===== TEST LAB TAB ===== -->
|
| 170 |
+
<div id="section-test" class="content-section fade-in" style="display:none;">
|
| 171 |
+
<div class="card" style="max-width:600px;">
|
| 172 |
+
<div class="card-title"><i class="fas fa-vial" style="color:var(--primary);"></i> Manual Diagnostics</div>
|
| 173 |
+
<div style="display:grid; grid-template-columns:1fr 1fr; gap:10px; margin-bottom:10px;">
|
| 174 |
+
<div class="input-group">
|
| 175 |
+
<label>Target <span class="tip" data-tip="Select which service to test.">?</span></label>
|
| 176 |
+
<select id="test-tool-select" class="pro-input"></select>
|
| 177 |
+
</div>
|
| 178 |
+
<div class="input-group">
|
| 179 |
+
<label>Node <span class="tip" data-tip="Test server location.">?</span></label>
|
| 180 |
+
<select id="test-location-select" class="pro-input">
|
| 181 |
+
<option>Global Anycast</option><option>US East</option><option>EU Central</option>
|
| 182 |
+
</select>
|
| 183 |
+
</div>
|
| 184 |
+
</div>
|
| 185 |
+
<button id="run-test-btn" class="btn-primary"><i class="fas fa-satellite-dish"></i> Execute Trace</button>
|
| 186 |
+
<div id="test-results" style="margin-top:1rem;"></div>
|
| 187 |
+
</div>
|
| 188 |
+
</div>
|
| 189 |
+
|
| 190 |
+
<!-- ===== OPERATIONS TAB ===== -->
|
| 191 |
+
<div id="section-config" class="content-section fade-in" style="display:none;">
|
| 192 |
+
|
| 193 |
+
<!-- M365 Monitor -->
|
| 194 |
+
<div class="card" style="margin-bottom:1rem; border-color:rgba(14,165,233,0.2);">
|
| 195 |
+
<div class="card-title"><i class="fab fa-microsoft" style="color:var(--primary);"></i> M365 Email Monitoring <span class="tip" data-tip="Monitors Microsoft 365 mail flow by sending a test email and verifying delivery. Uses App Password (not your main password). Credentials stored only on your server.">?</span></div>
|
| 196 |
+
<div style="display:grid; grid-template-columns:1fr 1fr 1fr; gap:8px; margin-bottom:10px;">
|
| 197 |
+
<div class="input-group"><label>Sender <span class="tip" data-tip="M365 email that sends test pulses.">?</span></label><input type="email" id="m365-sender" class="pro-input" placeholder="alerts@company.com"></div>
|
| 198 |
+
<div class="input-group"><label>App Password <span class="tip" data-tip="Generate from Microsoft account security settings. NOT your main password. Can be revoked anytime.">?</span></label><input type="password" id="m365-pass" class="pro-input" placeholder="••••••••"></div>
|
| 199 |
+
<div class="input-group"><label>Recipient <span class="tip" data-tip="Email that receives test pulses. Can be same as sender.">?</span></label><input type="email" id="m365-recipient" class="pro-input" placeholder="verify@company.com"></div>
|
| 200 |
+
</div>
|
| 201 |
+
<button id="activate-m365-btn" class="btn-primary"><i class="fas fa-paper-plane"></i> Activate Mail Flow Monitor</button>
|
| 202 |
+
</div>
|
| 203 |
+
|
| 204 |
+
<div class="ops-grid">
|
| 205 |
+
<!-- Infrastructure List -->
|
| 206 |
+
<div class="card">
|
| 207 |
+
<div class="card-title"><i class="fas fa-server" style="color:var(--accent);"></i> Active Infrastructure <span class="tip" data-tip="All monitored services. Click REMOVE to decommission.">?</span></div>
|
| 208 |
+
<div id="manage-svc-list" style="max-height:300px; overflow-y:auto;"></div>
|
| 209 |
+
<button id="purge-stale-btn" class="btn-danger-outline" style="margin-top:8px;"><i class="fas fa-broom"></i> Purge Stale Data</button>
|
| 210 |
+
</div>
|
| 211 |
+
<!-- Add New -->
|
| 212 |
+
<div class="card">
|
| 213 |
+
<div class="card-title"><i class="fas fa-plus-circle" style="color:var(--warning);"></i> New Node <span class="tip" data-tip="Add a website, API, or SSL cert to monitor.">?</span></div>
|
| 214 |
+
<div class="input-group"><label>Name</label><input type="text" id="new-svc-name" class="pro-input" placeholder="Service Name"></div>
|
| 215 |
+
<div class="input-group"><label>Location</label><input type="text" id="new-svc-loc" class="pro-input" placeholder="Global"></div>
|
| 216 |
+
<div class="input-group"><label>URL</label><input type="url" id="new-svc-url" class="pro-input" placeholder="https://example.com"></div>
|
| 217 |
+
<div class="input-group"><label>Type</label><select id="new-svc-type" class="pro-input"><option value="http_code">🌐 Website</option><option value="ssl">🔒 SSL</option><option value="json_status">⚙️ API</option></select></div>
|
| 218 |
+
<button id="add-svc-btn" class="btn-primary" style="background:linear-gradient(135deg,var(--warning),#d97706);"><i class="fas fa-bolt"></i> Activate</button>
|
| 219 |
+
</div>
|
| 220 |
+
</div>
|
| 221 |
+
|
| 222 |
+
<!-- Alert Config -->
|
| 223 |
+
<div class="card" style="margin-top:1rem;">
|
| 224 |
+
<div class="card-title"><i class="fas fa-bell"></i> Alert Configuration <span class="tip" data-tip="Configure how you get notified when services go DOWN or come back UP. SMTP = cloud email, Outlook = local desktop.">?</span></div>
|
| 225 |
+
<div style="display:grid; grid-template-columns:1fr 1fr 1fr; gap:8px;">
|
| 226 |
+
<div class="input-group"><label>Mode <span class="tip" data-tip="SMTP = Cloud relay. Outlook = Local bot.">?</span></label><select id="alert-mode" class="pro-input"><option value="SMTP">SMTP (Cloud)</option><option value="Outlook">Local Outlook</option></select></div>
|
| 227 |
+
<div class="input-group"><label>Interval <span class="tip" data-tip="Check frequency in seconds. 60 = once per minute.">?</span></label><input type="number" id="check-interval" class="pro-input" placeholder="60"></div>
|
| 228 |
+
<div class="input-group"><label>SMTP User <span class="tip" data-tip="Email address used to send alerts.">?</span></label><input type="email" id="smtp-user" class="pro-input" placeholder="alerts@co.com"></div>
|
| 229 |
+
</div>
|
| 230 |
+
<div class="input-group" style="margin-top:6px;"><label>SMTP Password <span class="tip" data-tip="App password for the SMTP sender.">?</span></label><input type="password" id="smtp-pass" class="pro-input" placeholder="••••••••"></div>
|
| 231 |
+
<button id="save-settings-btn" class="btn-primary" style="margin-top:8px; background:linear-gradient(135deg,#6366f1,#8b5cf6);"><i class="fas fa-save"></i> Save Configuration</button>
|
| 232 |
+
</div>
|
| 233 |
+
</div>
|
| 234 |
+
</div>
|
| 235 |
+
|
| 236 |
+
<!-- Footer -->
|
| 237 |
+
<div style="text-align:center; padding:1rem; font-size:0.6rem; color:var(--text-dim);">
|
| 238 |
+
STATUSPILOT Operations Center • <span id="footer-year"></span> • Real-time monitoring dashboard
|
| 239 |
+
</div>
|
| 240 |
+
</div>
|
| 241 |
+
</div>
|
| 242 |
+
|
| 243 |
+
<script>document.getElementById('footer-year').textContent = new Date().getFullYear();</script>
|
| 244 |
+
<script src="/static/script_v5.js"></script>
|
| 245 |
+
</body>
|
| 246 |
+
</html>
|
templates/dashboard_v6.html
ADDED
|
@@ -0,0 +1,452 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>StatusPilot — Operations Center</title>
|
| 7 |
+
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@300;400;500;600&family=Barlow+Condensed:wght@300;400;500;600;700;800&family=Barlow:wght@300;400;500;600&display=swap" rel="stylesheet">
|
| 8 |
+
<link rel="stylesheet" href="/static/style_v6.css?v=5.1.8">
|
| 9 |
+
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
| 10 |
+
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
| 11 |
+
</head>
|
| 12 |
+
<body>
|
| 13 |
+
|
| 14 |
+
<!-- AUTH OVERLAYS -->
|
| 15 |
+
<div id="master-auth-overlay" class="auth-overlay" style="display:none;">
|
| 16 |
+
<div class="auth-box">
|
| 17 |
+
<div class="s-logo">SP</div>
|
| 18 |
+
<h3>Restricted Access</h3>
|
| 19 |
+
<p>Authenticate with your Master Key</p>
|
| 20 |
+
<input type="password" id="master-key-input" class="pro-input" placeholder="••••••••••••" style="text-align:center; letter-spacing:3px; margin-bottom:16px;">
|
| 21 |
+
<button id="master-login-btn" class="btn-primary">Unlock</button>
|
| 22 |
+
</div>
|
| 23 |
+
</div>
|
| 24 |
+
|
| 25 |
+
<div id="license-overlay" class="auth-overlay" style="display:none;">
|
| 26 |
+
<div class="auth-box">
|
| 27 |
+
<div class="s-logo" style="background:linear-gradient(135deg,#9B6DFF,#5a3bd8);">PRO</div>
|
| 28 |
+
<h3>Enterprise Activation</h3>
|
| 29 |
+
<p>Enter your license credentials</p>
|
| 30 |
+
<input type="email" id="lic-email" class="pro-input" placeholder="Purchase Email" style="margin-bottom:12px;">
|
| 31 |
+
<input type="text" id="lic-key" class="pro-input" placeholder="License Key (SG-XXXX-XXXX)" style="text-transform:uppercase; margin-bottom:16px;">
|
| 32 |
+
<button id="activate-btn" class="btn-primary" style="background:rgba(155,109,255,0.15); border-color:var(--violet); color:var(--violet);">Activate</button>
|
| 33 |
+
</div>
|
| 34 |
+
</div>
|
| 35 |
+
|
| 36 |
+
<!-- LOADER -->
|
| 37 |
+
<div id="loader">
|
| 38 |
+
<div class="loader-logo">STATUSPILOT</div>
|
| 39 |
+
<div class="loader-bar"><div class="loader-fill"></div></div>
|
| 40 |
+
<div class="loader-txt">CONNECTING TO INFRASTRUCTURE...</div>
|
| 41 |
+
</div>
|
| 42 |
+
|
| 43 |
+
<!-- BG -->
|
| 44 |
+
<div id="bg-grid"></div>
|
| 45 |
+
<div id="bg-glow"></div>
|
| 46 |
+
|
| 47 |
+
<!-- SIDEBAR -->
|
| 48 |
+
<nav class="sidebar">
|
| 49 |
+
<div class="s-logo">SP</div>
|
| 50 |
+
<button class="s-btn act" title="Dashboard" onclick="switchMainTab('dashboard', this)">
|
| 51 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg>
|
| 52 |
+
</button>
|
| 53 |
+
<button class="s-btn" title="Test Lab" onclick="switchMainTab('testlab', this)">
|
| 54 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 3v11l-4 5h14l-4-5V3"/><path d="M6 3h12"/></svg>
|
| 55 |
+
</button>
|
| 56 |
+
<button class="s-btn" title="Operations" onclick="switchMainTab('config', this)">
|
| 57 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M12 1v4M12 19v4M4.22 4.22l2.83 2.83M16.95 16.95l2.83 2.83M1 12h4M19 12h4M4.22 19.78l2.83-2.83M16.95 7.05l2.83-2.83"/></svg>
|
| 58 |
+
</button>
|
| 59 |
+
<div class="s-spacer"></div>
|
| 60 |
+
<div class="s-avatar" title="System Status: Online" style="background:var(--green-glow); border-color:var(--green); color:var(--green);">OK</div>
|
| 61 |
+
</nav>
|
| 62 |
+
|
| 63 |
+
<!-- APP -->
|
| 64 |
+
<div class="app" id="main-app" style="filter:blur(15px); opacity:0; pointer-events:none;">
|
| 65 |
+
<!-- ALERT BANNER -->
|
| 66 |
+
<div class="alert-banner" id="alert-banner">
|
| 67 |
+
<div class="alert-banner-dot"></div>
|
| 68 |
+
<strong>ACTIVE INCIDENTS</strong>
|
| 69 |
+
<div class="alert-banner-sep"></div>
|
| 70 |
+
<span class="alert-banner-text" id="alert-banner-text">Loading incidents...</span>
|
| 71 |
+
</div>
|
| 72 |
+
|
| 73 |
+
<!-- HEADER -->
|
| 74 |
+
<header class="header">
|
| 75 |
+
<div class="h-left">
|
| 76 |
+
<div>
|
| 77 |
+
<div class="h-title" style="display:flex; align-items:center; gap:12px;">
|
| 78 |
+
<img src="data:image/png;base64,{{ logo_b64 }}" height="38" alt="PARLE TECH" style="height:38px; width:auto; vertical-align:middle;" />
|
| 79 |
+
<span style="opacity:0.3; font-weight:300;">|</span>
|
| 80 |
+
<span>Operations Center</span>
|
| 81 |
+
</div>
|
| 82 |
+
<div class="h-sub">STATUSPILOT PRO · ENTERPRISE NOC</div>
|
| 83 |
+
</div>
|
| 84 |
+
<div class="live-chip"><span class="live-chip-dot"></span>LIVE</div>
|
| 85 |
+
<button class="theme-btn" id="theme-btn" onclick="cycleTheme()" title="Switch Dashboard Theme (Dark Pro / Light Mode / Cyber Pulse)">
|
| 86 |
+
<span id="theme-icon">🌙</span> <span id="theme-label">Dark Pro</span>
|
| 87 |
+
</button>
|
| 88 |
+
</div>
|
| 89 |
+
<div class="h-right">
|
| 90 |
+
<div style="display:flex; align-items:center; gap:6px;">
|
| 91 |
+
<div class="clock-box" id="clock">--:--:-- UTC</div>
|
| 92 |
+
<select id="tzSel" onchange="" title="Select Timezone" style="background:rgba(255,255,255,0.07); border:1px solid rgba(255,255,255,0.12); color:var(--t2); font-size:10px; font-family:var(--mono); padding:4px 6px; border-radius:6px; cursor:pointer; outline:none; letter-spacing:0.5px;">
|
| 93 |
+
<option value="UTC" data-label="UTC" selected>🌐 UTC</option>
|
| 94 |
+
<option value="America/New_York" data-label="EST">🇺🇸 EST (New York)</option>
|
| 95 |
+
<option value="America/Chicago" data-label="CST">🇺🇸 CST (Chicago)</option>
|
| 96 |
+
<option value="America/Denver" data-label="MST">🇺🇸 MST (Denver)</option>
|
| 97 |
+
<option value="America/Los_Angeles" data-label="PST">🇺🇸 PST (Los Angeles)</option>
|
| 98 |
+
<option value="Europe/London" data-label="GMT">🇬🇧 GMT (London)</option>
|
| 99 |
+
<option value="Europe/Paris" data-label="CET">🇪🇺 CET (Paris)</option>
|
| 100 |
+
<option value="Asia/Dubai" data-label="GST">🇦🇪 GST (Dubai)</option>
|
| 101 |
+
<option value="Asia/Kolkata" data-label="IST">🇮🇳 IST (India)</option>
|
| 102 |
+
<option value="Asia/Singapore" data-label="SGT">🇸🇬 SGT (Singapore)</option>
|
| 103 |
+
<option value="Asia/Tokyo" data-label="JST">🇯🇵 JST (Tokyo)</option>
|
| 104 |
+
<option value="Australia/Sydney" data-label="AEST">🇦🇺 AEST (Sydney)</option>
|
| 105 |
+
</select>
|
| 106 |
+
</div>
|
| 107 |
+
<select class="region-sel" id="regionSel" onchange="filterRegion()">
|
| 108 |
+
<option value="all">🌐 All Regions</option>
|
| 109 |
+
</select>
|
| 110 |
+
<button class="hbtn hbtn-ghost" onclick="exportCSV()">
|
| 111 |
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="7,10 12,15 17,10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
|
| 112 |
+
Export CSV
|
| 113 |
+
</button>
|
| 114 |
+
<button class="hbtn hbtn-prim" onclick="switchMainTab('testlab', document.querySelectorAll('.s-btn')[1])">
|
| 115 |
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="22,12 18,12 15,21 9,3 6,12 2,12"/></svg>
|
| 116 |
+
Diagnostics
|
| 117 |
+
</button>
|
| 118 |
+
</div>
|
| 119 |
+
</header>
|
| 120 |
+
|
| 121 |
+
<!-- DASHBOARD TAB -->
|
| 122 |
+
<main class="main-tab act" id="tab-dashboard">
|
| 123 |
+
<!-- KPIs -->
|
| 124 |
+
<div class="kpi-row">
|
| 125 |
+
<div class="kpi fade-up" style="--ac:var(--blue)">
|
| 126 |
+
<div class="kpi-accent-line"></div>
|
| 127 |
+
<div class="kpi-icon">📡</div>
|
| 128 |
+
<div class="kpi-label">Active Monitors <span class="tip" data-tip="Total number of nodes, sites, and APIs currently being tracked by StatusPilot.">?</span></div>
|
| 129 |
+
<div class="kpi-val" style="--vc:var(--blue)" id="kpi-mon">0</div>
|
| 130 |
+
<div class="kpi-delta neutral">↔ Stable Tracking</div>
|
| 131 |
+
<div class="kpi-bar"><div class="kpi-bar-fill" style="width:100%"></div></div>
|
| 132 |
+
</div>
|
| 133 |
+
<div class="kpi fade-up" style="--ac:var(--green)">
|
| 134 |
+
<div class="kpi-accent-line"></div>
|
| 135 |
+
<div class="kpi-icon">📶</div>
|
| 136 |
+
<div class="kpi-label">Global Uptime <span class="tip" data-tip="Average availability across all active monitors. 100% means zero downtime.">?</span></div>
|
| 137 |
+
<div class="kpi-val" style="--vc:var(--green)" id="kpi-up">100%</div>
|
| 138 |
+
<div class="kpi-delta up" id="kpi-up-delta">↑ 100%</div>
|
| 139 |
+
<div class="kpi-bar"><div class="kpi-bar-fill" id="kpi-up-bar" style="width:100%;background:var(--green);opacity:.5"></div></div>
|
| 140 |
+
</div>
|
| 141 |
+
<div class="kpi fade-up" id="kpi-inc-card" style="--ac:var(--green)">
|
| 142 |
+
<div class="kpi-accent-line"></div>
|
| 143 |
+
<div class="kpi-icon">🚨</div>
|
| 144 |
+
<div class="kpi-label">Active Incidents <span class="tip" data-tip="Number of services currently offline or failing checks. 0 means all systems operational.">?</span></div>
|
| 145 |
+
<div class="kpi-val" style="--vc:var(--green)" id="kpi-inc">0</div>
|
| 146 |
+
<div class="kpi-delta up" id="kpi-inc-desc">All systems operational</div>
|
| 147 |
+
<div class="kpi-bar"><div class="kpi-bar-fill" id="kpi-inc-bar" style="width:100%;background:var(--green);opacity:.5"></div></div>
|
| 148 |
+
</div>
|
| 149 |
+
<div class="kpi fade-up" style="--ac:var(--amber)">
|
| 150 |
+
<div class="kpi-accent-line"></div>
|
| 151 |
+
<div class="kpi-icon">⚡</div>
|
| 152 |
+
<div class="kpi-label">Avg. Latency <span class="tip" data-tip="Real-time average response time in milliseconds across all infrastructure.">?</span></div>
|
| 153 |
+
<div class="kpi-val" style="--vc:var(--amber)" id="kpi-lat">0ms</div>
|
| 154 |
+
<div class="kpi-delta neutral" id="kpi-lat-desc">Live Average</div>
|
| 155 |
+
<div class="kpi-bar"><div class="kpi-bar-fill" id="kpi-lat-bar" style="width:50%;background:var(--amber);opacity:.5"></div></div>
|
| 156 |
+
</div>
|
| 157 |
+
</div>
|
| 158 |
+
|
| 159 |
+
<!-- ROW 2 -->
|
| 160 |
+
<div class="row2">
|
| 161 |
+
<div class="panel">
|
| 162 |
+
<div class="ph">
|
| 163 |
+
<div class="ph-title"><span class="ph-icon">⚡</span>Latency Trend (Live) <span class="tip" data-tip="Continuous 60-minute moving average of global response times compared against SLA threshold.">?</span></div>
|
| 164 |
+
<div class="ph-badge live">● LIVE</div>
|
| 165 |
+
</div>
|
| 166 |
+
<div class="chart-wrap" style="padding:16px 16px 8px">
|
| 167 |
+
<canvas id="trendChart" height="110"></canvas>
|
| 168 |
+
<div class="chart-xaxis" id="trendXAxis"></div>
|
| 169 |
+
</div>
|
| 170 |
+
</div>
|
| 171 |
+
|
| 172 |
+
<div class="panel">
|
| 173 |
+
<div class="ph">
|
| 174 |
+
<div class="ph-title"><span class="ph-icon">📅</span>Uptime Heatmap <span class="tip" data-tip="Historical 90-day availability matrix. Dark green is 100% uptime, red indicates outages.">?</span></div>
|
| 175 |
+
<div style="font-family:var(--mono);font-size:9px;color:var(--t4)">Per service · history</div>
|
| 176 |
+
</div>
|
| 177 |
+
<div class="pb">
|
| 178 |
+
<div class="heatmap" id="heatmap"></div>
|
| 179 |
+
<div class="hm-legend" style="margin-top:10px">
|
| 180 |
+
<span>Less</span>
|
| 181 |
+
<div class="hm-leg-cell" style="background:rgba(0,232,135,0.15)"></div>
|
| 182 |
+
<div class="hm-leg-cell" style="background:rgba(0,232,135,0.4)"></div>
|
| 183 |
+
<div class="hm-leg-cell" style="background:rgba(0,232,135,0.75)"></div>
|
| 184 |
+
<div class="hm-leg-cell" style="background:var(--green)"></div>
|
| 185 |
+
<span>More</span>
|
| 186 |
+
<div style="flex:1"></div>
|
| 187 |
+
<div class="hm-leg-cell" style="background:rgba(255,58,92,0.7)"></div>
|
| 188 |
+
<span style="color:var(--red)">Down</span>
|
| 189 |
+
</div>
|
| 190 |
+
</div>
|
| 191 |
+
</div>
|
| 192 |
+
|
| 193 |
+
<div class="panel">
|
| 194 |
+
<div class="ph">
|
| 195 |
+
<div class="ph-title"><span style="color:var(--red)">▲</span> Incidents <span class="tip" data-tip="Live feed of critical infrastructure failures and warnings.">?</span></div>
|
| 196 |
+
<div class="ph-badge warn" id="inc-badge">0 OPEN</div>
|
| 197 |
+
</div>
|
| 198 |
+
<div class="pb" style="padding-top:12px">
|
| 199 |
+
<div class="inc-list" id="incList"></div>
|
| 200 |
+
</div>
|
| 201 |
+
</div>
|
| 202 |
+
</div>
|
| 203 |
+
|
| 204 |
+
<!-- ROW 3 -->
|
| 205 |
+
<div class="row3">
|
| 206 |
+
<div class="panel">
|
| 207 |
+
<div class="ph">
|
| 208 |
+
<div class="ph-title">⊞ Infrastructure Services <span class="tip" data-tip="Detailed registry of all monitored nodes including 30-day sparklines, current status, and live latency. Click any row for deep diagnostics.">?</span></div>
|
| 209 |
+
<div style="font-family:var(--mono);font-size:9px;color:var(--t4)" id="svcCount">0 MONITORED</div>
|
| 210 |
+
</div>
|
| 211 |
+
<div class="pb">
|
| 212 |
+
<div class="svc-toolbar">
|
| 213 |
+
<div class="svc-search">
|
| 214 |
+
<span class="svc-search-ico">⌕</span>
|
| 215 |
+
<input type="text" id="searchBox" placeholder="Search services, URLs, tags..." oninput="filterTable()">
|
| 216 |
+
</div>
|
| 217 |
+
<button class="filter-chip sel" id="fc-all" onclick="setFilter('all',this)">All</button>
|
| 218 |
+
<button class="filter-chip" id="fc-online" onclick="setFilter('online',this)">Online</button>
|
| 219 |
+
<button class="filter-chip red" id="fc-down" onclick="setFilter('down',this)">Issues</button>
|
| 220 |
+
</div>
|
| 221 |
+
<table class="svc-tbl">
|
| 222 |
+
<thead>
|
| 223 |
+
<tr>
|
| 224 |
+
<th onclick="sortTable('name')">Service</th>
|
| 225 |
+
<th onclick="sortTable('scope')">Scope</th>
|
| 226 |
+
<th>Timeline</th>
|
| 227 |
+
<th>Status</th>
|
| 228 |
+
<th onclick="sortTable('latency')" style="text-align:right">Latency ↓</th>
|
| 229 |
+
</tr>
|
| 230 |
+
</thead>
|
| 231 |
+
<tbody id="svcBody"></tbody>
|
| 232 |
+
</table>
|
| 233 |
+
</div>
|
| 234 |
+
</div>
|
| 235 |
+
|
| 236 |
+
<div style="display:flex;flex-direction:column;gap:16px;">
|
| 237 |
+
<div class="panel">
|
| 238 |
+
<div class="ph"><div class="ph-title"><span class="ph-icon">⚡</span>Global Latency <span class="tip" data-tip="Top 8 slowest endpoints by current response time (ms).">?</span></div></div>
|
| 239 |
+
<div class="pb" id="latMatrix"></div>
|
| 240 |
+
</div>
|
| 241 |
+
<div class="panel">
|
| 242 |
+
<div class="ph"><div class="ph-title">🌐 Node Map <span class="tip" data-tip="Geospatial visualization of infrastructure health across regions.">?</span></div></div>
|
| 243 |
+
<div class="world-wrap" id="worldMap"></div>
|
| 244 |
+
</div>
|
| 245 |
+
</div>
|
| 246 |
+
</div>
|
| 247 |
+
</main>
|
| 248 |
+
|
| 249 |
+
<!-- TEST LAB TAB -->
|
| 250 |
+
<main class="main-tab" id="tab-testlab">
|
| 251 |
+
<div class="testlab-grid">
|
| 252 |
+
<!-- Left: Diagnostic Probe Controls -->
|
| 253 |
+
<div class="panel">
|
| 254 |
+
<div class="ph">
|
| 255 |
+
<div class="ph-title"><span class="ph-icon" style="color:var(--cyan)">⚡</span> Manual Diagnostics Probe <span class="tip" data-tip="Execute forced on-demand traces to specific endpoints to verify routing and response.">?</span></div>
|
| 256 |
+
<div class="ph-badge live">● PROBE READY</div>
|
| 257 |
+
</div>
|
| 258 |
+
<div class="pb">
|
| 259 |
+
<div class="input-group">
|
| 260 |
+
<label>Target Infrastructure Endpoint <span class="tip" data-tip="Select which managed node to run the manual trace against.">?</span></label>
|
| 261 |
+
<select id="test-tool-select" class="pro-input"></select>
|
| 262 |
+
</div>
|
| 263 |
+
<div class="input-group">
|
| 264 |
+
<label>Origin Probe Location <span class="tip" data-tip="Choose the geographic origin of the diagnostic trace.">?</span></label>
|
| 265 |
+
<select id="test-location-select" class="pro-input">
|
| 266 |
+
<option value="Global Anycast">🌐 Global Anycast Network</option>
|
| 267 |
+
<option value="US East">🇺🇸 US East (N. Virginia)</option>
|
| 268 |
+
<option value="US West">🇺🇸 US West (California)</option>
|
| 269 |
+
<option value="EU Central">🇪🇺 EU Central (Frankfurt)</option>
|
| 270 |
+
<option value="UK London">🇬🇧 UK (London)</option>
|
| 271 |
+
<option value="Asia Pacific">🌏 Asia Pacific (Tokyo)</option>
|
| 272 |
+
</select>
|
| 273 |
+
</div>
|
| 274 |
+
<div class="input-group">
|
| 275 |
+
<label>Diagnostic Protocol <span class="tip" data-tip="Select analysis protocol layer.">?</span></label>
|
| 276 |
+
<select id="test-proto-select" class="pro-input">
|
| 277 |
+
<option value="HTTP_TLS">🌐 HTTP/2 + TLS 1.3 Full Stack Trace</option>
|
| 278 |
+
<option value="SSL_CERT">🔒 SSL/TLS Certificate Audit</option>
|
| 279 |
+
<option value="JSON_PAYLOAD">⚙️ REST API Payload Health Check</option>
|
| 280 |
+
</select>
|
| 281 |
+
</div>
|
| 282 |
+
<button id="run-test-btn" class="btn-primary" style="margin-top:16px; padding:12px; font-size:13px; background:rgba(0,212,255,0.12); border-color:var(--cyan); color:var(--cyan);">
|
| 283 |
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="22,12 18,12 15,21 9,3 6,12 2,12"/></svg>
|
| 284 |
+
Execute Diagnostic Trace
|
| 285 |
+
</button>
|
| 286 |
+
</div>
|
| 287 |
+
</div>
|
| 288 |
+
|
| 289 |
+
<!-- Right: Real-time Telemetry Terminal Console -->
|
| 290 |
+
<div style="display:flex; flex-direction:column; gap:16px;">
|
| 291 |
+
<div class="panel">
|
| 292 |
+
<div class="ph">
|
| 293 |
+
<div class="ph-title"><span class="ph-icon" style="color:var(--cyan)">🖥️</span> Live Diagnostic Telemetry Console <span class="tip" data-tip="Real-time protocol traceroute and latency metrics stream.">?</span></div>
|
| 294 |
+
<div class="ph-badge live" id="console-badge">STANDBY</div>
|
| 295 |
+
</div>
|
| 296 |
+
<div class="pb" style="padding:14px;">
|
| 297 |
+
<div class="console-box" id="console-output">
|
| 298 |
+
<div class="console-line text-dim">[SYSTEM] StatusPilot Diagnostic Engine v5.1 Ready.</div>
|
| 299 |
+
<div class="console-line text-dim">[SYSTEM] Select an infrastructure target and click "Execute Diagnostic Trace" to initiate probe...</div>
|
| 300 |
+
</div>
|
| 301 |
+
</div>
|
| 302 |
+
</div>
|
| 303 |
+
|
| 304 |
+
<div class="testlab-kpis">
|
| 305 |
+
<div class="t-kpi">
|
| 306 |
+
<div class="t-kpi-label">DNS RESOLVE</div>
|
| 307 |
+
<div class="t-kpi-val" id="tk-dns">-- ms</div>
|
| 308 |
+
</div>
|
| 309 |
+
<div class="t-kpi">
|
| 310 |
+
<div class="t-kpi-label">TCP HANDSHAKE</div>
|
| 311 |
+
<div class="t-kpi-val" id="tk-tcp">-- ms</div>
|
| 312 |
+
</div>
|
| 313 |
+
<div class="t-kpi">
|
| 314 |
+
<div class="t-kpi-label">TTFB (LATENCY)</div>
|
| 315 |
+
<div class="t-kpi-val" id="tk-ttfb">-- ms</div>
|
| 316 |
+
</div>
|
| 317 |
+
<div class="t-kpi">
|
| 318 |
+
<div class="t-kpi-label">PROBE RESULT</div>
|
| 319 |
+
<div class="t-kpi-val" style="color:var(--green)" id="tk-status">READY</div>
|
| 320 |
+
</div>
|
| 321 |
+
</div>
|
| 322 |
+
</div>
|
| 323 |
+
</div>
|
| 324 |
+
</main>
|
| 325 |
+
|
| 326 |
+
<!-- OPERATIONS TAB -->
|
| 327 |
+
<main class="main-tab" id="tab-config">
|
| 328 |
+
<!-- M365 Panel -->
|
| 329 |
+
<div class="panel" style="margin-bottom:20px; border-color:var(--b2);">
|
| 330 |
+
<div class="ph"><div class="ph-title"><span class="ph-icon" style="color:var(--cyan);">✉️</span> M365 Email Monitoring <span class="tip" data-tip="Monitors Microsoft 365 mail flow by sending a test email and verifying delivery. Uses App Password (not your main password).">?</span></div></div>
|
| 331 |
+
<div class="pb">
|
| 332 |
+
<div class="ops-grid-3">
|
| 333 |
+
<div class="input-group"><label>Sender Email</label><input type="email" id="m365-sender" class="pro-input" placeholder="alerts@company.com"></div>
|
| 334 |
+
<div class="input-group"><label>App Password</label><input type="password" id="m365-pass" class="pro-input" placeholder="••••••••"></div>
|
| 335 |
+
<div class="input-group"><label>Recipient Email</label><input type="email" id="m365-recipient" class="pro-input" placeholder="verify@company.com"></div>
|
| 336 |
+
</div>
|
| 337 |
+
<button id="activate-m365-btn" class="btn-primary" style="background:rgba(0,212,255,0.1); border-color:var(--cyan); color:var(--cyan);">Activate Mail Flow Monitor</button>
|
| 338 |
+
</div>
|
| 339 |
+
</div>
|
| 340 |
+
|
| 341 |
+
<div class="ops-grid">
|
| 342 |
+
<!-- Active Infrastructure -->
|
| 343 |
+
<div class="panel">
|
| 344 |
+
<div class="ph"><div class="ph-title"><span class="ph-icon">📋</span> Active Infrastructure <span class="tip" data-tip="All monitored services. Click REMOVE to decommission a node permanently.">?</span></div></div>
|
| 345 |
+
<div class="pb">
|
| 346 |
+
<div id="manage-svc-list" style="max-height: 250px; overflow-y: auto; margin-bottom:10px;"></div>
|
| 347 |
+
<button id="purge-stale-btn" class="btn-danger-outline">Purge Stale Data</button>
|
| 348 |
+
</div>
|
| 349 |
+
</div>
|
| 350 |
+
|
| 351 |
+
<!-- Add Node -->
|
| 352 |
+
<div class="panel">
|
| 353 |
+
<div class="ph"><div class="ph-title"><span class="ph-icon" style="color:var(--green)">➕</span> Add New Node <span class="tip" data-tip="Provision a new website, API, or SSL certificate into the monitoring engine.">?</span></div></div>
|
| 354 |
+
<div class="pb">
|
| 355 |
+
<div class="input-group"><label>Service Name</label><input type="text" id="new-svc-name" class="pro-input" placeholder="e.g. Acme API"></div>
|
| 356 |
+
<div class="input-group"><label>Location / Scope</label><input type="text" id="new-svc-loc" class="pro-input" placeholder="e.g. US East"></div>
|
| 357 |
+
<div class="input-group"><label>Endpoint URL</label><input type="url" id="new-svc-url" class="pro-input" placeholder="https://..."></div>
|
| 358 |
+
<div class="input-group">
|
| 359 |
+
<label>Check Type</label>
|
| 360 |
+
<select id="new-svc-type" class="pro-input">
|
| 361 |
+
<option value="http_code">🌐 Website (HTTP)</option>
|
| 362 |
+
<option value="ssl">🔒 SSL Certificate</option>
|
| 363 |
+
<option value="json_status">⚙️ API (JSON)</option>
|
| 364 |
+
</select>
|
| 365 |
+
</div>
|
| 366 |
+
<button id="add-svc-btn" class="btn-primary" style="background:rgba(0,232,135,0.1); border-color:var(--green); color:var(--green);">Provision Node</button>
|
| 367 |
+
</div>
|
| 368 |
+
</div>
|
| 369 |
+
</div>
|
| 370 |
+
|
| 371 |
+
<!-- Alert Config -->
|
| 372 |
+
<div class="panel" style="margin-top:20px;">
|
| 373 |
+
<div class="ph"><div class="ph-title"><span class="ph-icon" style="color:var(--amber)">🔔</span> Alert Configuration <span class="tip" data-tip="Configure how you receive notifications when incidents occur or resolve.">?</span></div></div>
|
| 374 |
+
<div class="pb">
|
| 375 |
+
<div class="ops-grid-3">
|
| 376 |
+
<div class="input-group"><label>Alert Mode</label><select id="alert-mode" class="pro-input"><option value="SMTP">SMTP (Cloud)</option><option value="Outlook">Local Outlook</option></select></div>
|
| 377 |
+
<div class="input-group"><label>Interval (sec)</label><input type="number" id="check-interval" class="pro-input" placeholder="60"></div>
|
| 378 |
+
<div class="input-group"><label>SMTP User</label><input type="email" id="smtp-user" class="pro-input" placeholder="alerts@co.com"></div>
|
| 379 |
+
</div>
|
| 380 |
+
<div class="input-group" style="margin-top:6px;"><label>SMTP Password</label><input type="password" id="smtp-pass" class="pro-input" placeholder="••••••••"></div>
|
| 381 |
+
<div class="input-group" style="margin-top:6px;"><label>HTTPS Webhook Gateway URL (Google Apps Script / Custom Bypass)</label><input type="text" id="email-webhook-url" class="pro-input" placeholder="https://script.google.com/macros/s/.../exec"></div>
|
| 382 |
+
<div class="input-group" style="margin-top:6px;"><label>Alert Recipient Emails (comma separated)</label><input type="text" id="alert-recipients" class="pro-input" placeholder="admin@co.com, team@co.com"></div>
|
| 383 |
+
<div style="display:flex; gap:10px; margin-top:10px;">
|
| 384 |
+
<button id="save-settings-btn" class="btn-primary" style="flex:1; background:rgba(255,170,0,0.1); border-color:var(--amber); color:var(--amber);">Save Configuration</button>
|
| 385 |
+
<button id="test-alert-btn" class="btn-primary" style="flex:1; background:rgba(0,212,255,0.1); border-color:var(--cyan); color:var(--cyan);">Send Test Alert Email</button>
|
| 386 |
+
</div>
|
| 387 |
+
</div>
|
| 388 |
+
</div>
|
| 389 |
+
</main>
|
| 390 |
+
</div><!-- /app -->
|
| 391 |
+
|
| 392 |
+
<!-- MODAL -->
|
| 393 |
+
<div class="modal-back" id="modal" onclick="bgClose(event)">
|
| 394 |
+
<div class="modal" id="modalBox">
|
| 395 |
+
<div class="modal-hd">
|
| 396 |
+
<div class="modal-hd-info">
|
| 397 |
+
<div class="modal-title" id="mTitle">Service</div>
|
| 398 |
+
<div class="modal-meta">
|
| 399 |
+
<div class="modal-url" id="mUrl">https://...</div>
|
| 400 |
+
<div class="modal-status-chip online pill" id="mStatusChip">
|
| 401 |
+
<span class="pill-dot"></span><span id="mStatusTxt">ONLINE</span>
|
| 402 |
+
</div>
|
| 403 |
+
</div>
|
| 404 |
+
</div>
|
| 405 |
+
<button class="mclose" onclick="closeModal()">✕</button>
|
| 406 |
+
</div>
|
| 407 |
+
<div class="modal-tabs">
|
| 408 |
+
<button class="mtab act" onclick="switchModalTab('overview',this)">Overview</button>
|
| 409 |
+
<button class="mtab" onclick="switchModalTab('history',this)">History</button>
|
| 410 |
+
</div>
|
| 411 |
+
<div class="mtab-panel act" id="mtab-overview">
|
| 412 |
+
<div class="modal-grid">
|
| 413 |
+
<div class="m-card">
|
| 414 |
+
<div class="m-card-title"><span class="mc-icon" style="color:var(--green)">◎</span>Availability</div>
|
| 415 |
+
<canvas id="mAvailChart" height="72" width="280"></canvas>
|
| 416 |
+
</div>
|
| 417 |
+
<div class="m-card">
|
| 418 |
+
<div class="m-card-title"><span class="mc-icon" style="color:var(--cyan)">⚡</span>Latency (ms)</div>
|
| 419 |
+
<canvas id="mLatChart" height="72" width="280"></canvas>
|
| 420 |
+
</div>
|
| 421 |
+
<div class="m-card">
|
| 422 |
+
<div class="m-card-title"><span class="mc-icon" style="color:var(--amber)">◔</span>Error Profile</div>
|
| 423 |
+
<div style="display:flex;align-items:center;gap:16px;padding-top:4px;">
|
| 424 |
+
<canvas id="mDonut" width="72" height="72"></canvas>
|
| 425 |
+
<div style="display:flex;flex-direction:column;gap:8px;">
|
| 426 |
+
<div style="display:flex;align-items:center;gap:8px;font-size:11px;"><div style="width:10px;height:10px;border-radius:2px;background:var(--green)"></div><span id="mOkPct">—</span> Operational</div>
|
| 427 |
+
<div style="display:flex;align-items:center;gap:8px;font-size:11px;"><div style="width:10px;height:10px;border-radius:2px;background:var(--red)"></div><span id="mErrPct">—</span> Errors</div>
|
| 428 |
+
</div>
|
| 429 |
+
</div>
|
| 430 |
+
</div>
|
| 431 |
+
<div class="m-card">
|
| 432 |
+
<div class="m-card-title"><span class="mc-icon" style="color:var(--red)">▲</span>Status Indicator</div>
|
| 433 |
+
<div id="mIncidentBox"></div>
|
| 434 |
+
</div>
|
| 435 |
+
</div>
|
| 436 |
+
</div>
|
| 437 |
+
<div class="mtab-panel" id="mtab-history">
|
| 438 |
+
<div style="margin-bottom:14px;font-family:var(--mono);font-size:9px;color:var(--t4);letter-spacing:1px;">LAST 10 CHECK RESULTS</div>
|
| 439 |
+
<table class="h-tbl" id="histTable">
|
| 440 |
+
<thead><tr><th>Timestamp</th><th>Status</th><th>Latency</th><th>Message</th></tr></thead>
|
| 441 |
+
<tbody id="histBody"></tbody>
|
| 442 |
+
</table>
|
| 443 |
+
</div>
|
| 444 |
+
</div>
|
| 445 |
+
</div>
|
| 446 |
+
|
| 447 |
+
<!-- TOASTS -->
|
| 448 |
+
<div id="toasts"></div>
|
| 449 |
+
|
| 450 |
+
<script src="/static/script_v6.js?v=5.1.8"></script>
|
| 451 |
+
</body>
|
| 452 |
+
</html>
|
update_logo_b64.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import base64, io
|
| 2 |
+
from PIL import Image
|
| 3 |
+
|
| 4 |
+
src = r'C:\Users\Suresh\OneDrive - Parle Tech\Desktop\Services Alert\Cloud_Version\static\parle_tech_logo.png'
|
| 5 |
+
img = Image.open(src).convert('RGBA')
|
| 6 |
+
print('SIZE:', img.size)
|
| 7 |
+
buf = io.BytesIO()
|
| 8 |
+
img.save(buf, format='PNG')
|
| 9 |
+
b64 = base64.b64encode(buf.getvalue()).decode('utf-8')
|
| 10 |
+
print('B64_LEN:', len(b64))
|
| 11 |
+
|
| 12 |
+
target = r'c:\Users\Suresh\OneDrive - Parle Tech\Desktop\Services Alert\Cloud_Version\app.py'
|
| 13 |
+
lines = open(target, 'r', encoding='utf-8').readlines()
|
| 14 |
+
new_lines = []
|
| 15 |
+
updated = False
|
| 16 |
+
for l in lines:
|
| 17 |
+
if l.startswith('PARLE_TECH_LOGO_B64 ='):
|
| 18 |
+
new_lines.append('PARLE_TECH_LOGO_B64 = "' + b64 + '"\n')
|
| 19 |
+
updated = True
|
| 20 |
+
else:
|
| 21 |
+
new_lines.append(l)
|
| 22 |
+
open(target, 'w', encoding='utf-8').writelines(new_lines)
|
| 23 |
+
print('Updated B64:', updated)
|