add monitor service
This commit is contained in:
2
asf-cloud-server/monitor/.env
Normal file
2
asf-cloud-server/monitor/.env
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
PC_PASS=ASF
|
||||||
|
PI_PASS=ASF_TB
|
||||||
16
asf-cloud-server/monitor/Dockerfile
Normal file
16
asf-cloud-server/monitor/Dockerfile
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
FROM python:3.11-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
iputils-ping \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
EXPOSE 8000
|
||||||
|
|
||||||
|
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||||
19
asf-cloud-server/monitor/docker-compose.yml
Normal file
19
asf-cloud-server/monitor/docker-compose.yml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
monitor-app:
|
||||||
|
build: .
|
||||||
|
container_name: monitor-app
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
- PC_PASS=ASF
|
||||||
|
- PI_PASS=ASF_TB
|
||||||
|
networks:
|
||||||
|
- app-network
|
||||||
|
- caddy_network
|
||||||
|
|
||||||
|
networks:
|
||||||
|
app-network:
|
||||||
|
driver: bridge
|
||||||
|
caddy_network:
|
||||||
|
external: true
|
||||||
66
asf-cloud-server/monitor/main.py
Normal file
66
asf-cloud-server/monitor/main.py
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
from fastapi import FastAPI
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
import os
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from monitor_logic import get_ssh_data, check_web_service
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=["*"],
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Configuration from environment variables
|
||||||
|
PC_HOST = "asf-server.duckdns.org"
|
||||||
|
PC_PORT = 49152
|
||||||
|
PC_USER = "asf"
|
||||||
|
PC_PASS = os.getenv("PC_PASS", "ASF")
|
||||||
|
|
||||||
|
PI_HOST = "rpi-asf-tb.duckdns.org"
|
||||||
|
PI_PORT = 2222
|
||||||
|
PI_USER = "asf_tb"
|
||||||
|
PI_PASS = os.getenv("PI_PASS", "ASF_TB")
|
||||||
|
|
||||||
|
WEB_SERVICES = [
|
||||||
|
{"name": "Gitea", "url": "https://gitea.nabd-co.com/"},
|
||||||
|
{"name": "OpenProject", "url": "https://openproject.nabd-co.com/"},
|
||||||
|
{"name": "Draw.io", "url": "https://drawio.nabd-co.com/"},
|
||||||
|
{"name": "TestArena", "url": "https://testarena.nabd-co.com/"},
|
||||||
|
{"name": "TBM", "url": "https://tbm.asf.nabd-co.com/"},
|
||||||
|
{"name": "Board", "url": "https://board.nabd-co.com/"},
|
||||||
|
]
|
||||||
|
|
||||||
|
@app.get("/api/status")
|
||||||
|
async def get_status():
|
||||||
|
# Run SSH checks in parallel
|
||||||
|
pc_task = asyncio.to_thread(get_ssh_data, PC_HOST, PC_PORT, PC_USER, PC_PASS)
|
||||||
|
pi_task = asyncio.to_thread(get_ssh_data, PI_HOST, PI_PORT, PI_USER, PI_PASS)
|
||||||
|
|
||||||
|
# Run web checks in parallel
|
||||||
|
web_tasks = [asyncio.to_thread(check_web_service, s["url"]) for s in WEB_SERVICES]
|
||||||
|
|
||||||
|
pc_res, pi_res, *web_res = await asyncio.gather(pc_task, pi_task, *web_tasks)
|
||||||
|
|
||||||
|
services = []
|
||||||
|
for i, res in enumerate(web_res):
|
||||||
|
services.append({
|
||||||
|
"name": WEB_SERVICES[i]["name"],
|
||||||
|
"url": WEB_SERVICES[i]["url"],
|
||||||
|
**res
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
"pc": pc_res,
|
||||||
|
"pi": pi_res,
|
||||||
|
"services": services
|
||||||
|
}
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import uvicorn
|
||||||
|
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||||
516
asf-cloud-server/monitor/monitor.html
Normal file
516
asf-cloud-server/monitor/monitor.html
Normal file
@@ -0,0 +1,516 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>ASF Infrastructure Monitor</title>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600;700&display=swap" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--bg-color: #020617;
|
||||||
|
--card-bg: rgba(15, 23, 42, 0.8);
|
||||||
|
--accent-blue: #38bdf8;
|
||||||
|
--accent-purple: #a855f7;
|
||||||
|
--status-green: #10b981;
|
||||||
|
--status-red: #ef4444;
|
||||||
|
--status-yellow: #f59e0b;
|
||||||
|
--text-main: #f8fafc;
|
||||||
|
--text-dim: #94a3b8;
|
||||||
|
--glass-border: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Outfit', sans-serif;
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
background-image:
|
||||||
|
radial-gradient(circle at 20% 20%, rgba(56, 189, 248, 0.05) 0%, transparent 40%),
|
||||||
|
radial-gradient(circle at 80% 80%, rgba(168, 85, 247, 0.05) 0%, transparent 40%);
|
||||||
|
color: var(--text-main);
|
||||||
|
margin: 0;
|
||||||
|
padding: 20px;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 50px;
|
||||||
|
animation: fadeInDown 0.8s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
background: linear-gradient(to right, var(--accent-blue), var(--accent-purple));
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
header p {
|
||||||
|
color: var(--text-dim);
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(450px, 1fr));
|
||||||
|
gap: 30px;
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: var(--card-bg);
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
border: 1px solid var(--glass-border);
|
||||||
|
border-radius: 24px;
|
||||||
|
padding: 25px;
|
||||||
|
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.3);
|
||||||
|
transition: transform 0.3s ease, border-color 0.3s ease;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
border-color: rgba(56, 189, 248, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
border-bottom: 1px solid var(--glass-border);
|
||||||
|
padding-bottom: 20px;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-info h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-info small {
|
||||||
|
color: var(--text-dim);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge {
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-online {
|
||||||
|
background: rgba(16, 185, 129, 0.1);
|
||||||
|
color: var(--status-green);
|
||||||
|
border: 1px solid rgba(16, 185, 129, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-offline {
|
||||||
|
background: rgba(239, 68, 68, 0.1);
|
||||||
|
color: var(--status-red);
|
||||||
|
border: 1px solid rgba(239, 68, 68, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gauge-wrapper {
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 16px;
|
||||||
|
text-align: center;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gauge-container {
|
||||||
|
height: 140px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gauge-label {
|
||||||
|
position: absolute;
|
||||||
|
top: 60%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gauge-value {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gauge-name {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: var(--text-dim);
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metrics-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 15px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-card {
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 16px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-label {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--text-dim);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-value {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-main);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Web Services */
|
||||||
|
.service-list {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 15px 20px;
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
border-radius: 16px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-item:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border-color: var(--glass-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-name {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-status {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeInDown {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: var(--bg-color);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 1000;
|
||||||
|
transition: opacity 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border: 5px solid #FFF;
|
||||||
|
border-bottom-color: var(--accent-blue);
|
||||||
|
border-radius: 50%;
|
||||||
|
display: inline-block;
|
||||||
|
box-sizing: border-box;
|
||||||
|
animation: rotation 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes rotation {
|
||||||
|
0% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="loading" class="loading-overlay">
|
||||||
|
<span class="loader"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<header>
|
||||||
|
<h1><i class="fas fa-shield-halved"></i> ASF Infrastructure Monitor</h1>
|
||||||
|
<p>Real-time system health and service availability</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="dashboard-grid">
|
||||||
|
<!-- Ubuntu Server PC -->
|
||||||
|
<div class="card" id="card-pc">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="device-info">
|
||||||
|
<i class="fas fa-server fa-2x" style="color: #fb923c;"></i>
|
||||||
|
<div>
|
||||||
|
<h2>Ubuntu Server PC</h2>
|
||||||
|
<small>asf-server.duckdns.org</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="pc-status" class="status-badge status-offline">
|
||||||
|
<i class="fas fa-circle"></i> <span>Offline</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="stats-grid">
|
||||||
|
<div class="gauge-wrapper">
|
||||||
|
<div class="gauge-container"><canvas id="pcCpu"></canvas></div>
|
||||||
|
<div class="gauge-label">
|
||||||
|
<span class="gauge-value" id="pcCpuVal">0%</span>
|
||||||
|
<span class="gauge-name">CPU</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="gauge-wrapper">
|
||||||
|
<div class="gauge-container"><canvas id="pcRam"></canvas></div>
|
||||||
|
<div class="gauge-label">
|
||||||
|
<span class="gauge-value" id="pcRamVal">0%</span>
|
||||||
|
<span class="gauge-name">RAM</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="metrics-row">
|
||||||
|
<div class="metric-card">
|
||||||
|
<span class="metric-label">Temperature</span>
|
||||||
|
<span class="metric-value" id="pcTemp">--°C</span>
|
||||||
|
</div>
|
||||||
|
<div class="metric-card">
|
||||||
|
<span class="metric-label">Disk Usage</span>
|
||||||
|
<span class="metric-value" id="pcDisk">--%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Raspberry Pi -->
|
||||||
|
<div class="card" id="card-pi">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="device-info">
|
||||||
|
<i class="fab fa-raspberry-pi fa-2x" style="color: #e11d48;"></i>
|
||||||
|
<div>
|
||||||
|
<h2>Raspberry Pi</h2>
|
||||||
|
<small>rpi-asf-tb.duckdns.org</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="pi-status" class="status-badge status-offline">
|
||||||
|
<i class="fas fa-circle"></i> <span>Offline</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="stats-grid">
|
||||||
|
<div class="gauge-wrapper">
|
||||||
|
<div class="gauge-container"><canvas id="piCpu"></canvas></div>
|
||||||
|
<div class="gauge-label">
|
||||||
|
<span class="gauge-value" id="piCpuVal">0%</span>
|
||||||
|
<span class="gauge-name">CPU</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="gauge-wrapper">
|
||||||
|
<div class="gauge-container"><canvas id="piRam"></canvas></div>
|
||||||
|
<div class="gauge-label">
|
||||||
|
<span class="gauge-value" id="piRamVal">0%</span>
|
||||||
|
<span class="gauge-name">RAM</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="metrics-row">
|
||||||
|
<div class="metric-card">
|
||||||
|
<span class="metric-label">Temperature</span>
|
||||||
|
<span class="metric-value" id="piTemp">--°C</span>
|
||||||
|
</div>
|
||||||
|
<div class="metric-card">
|
||||||
|
<span class="metric-label">Disk Usage</span>
|
||||||
|
<span class="metric-value" id="piDisk">--%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Web Services -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="device-info">
|
||||||
|
<i class="fas fa-globe fa-2x" style="color: var(--accent-blue);"></i>
|
||||||
|
<h2>Web Services</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ul class="service-list" id="service-list">
|
||||||
|
<!-- Services will be injected here -->
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const charts = {};
|
||||||
|
|
||||||
|
function createGauge(id, color) {
|
||||||
|
const ctx = document.getElementById(id).getContext('2d');
|
||||||
|
charts[id] = new Chart(ctx, {
|
||||||
|
type: 'doughnut',
|
||||||
|
data: {
|
||||||
|
datasets: [{
|
||||||
|
data: [0, 100],
|
||||||
|
backgroundColor: [color, 'rgba(255, 255, 255, 0.05)'],
|
||||||
|
borderWidth: 0,
|
||||||
|
borderRadius: 10,
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
cutout: '85%',
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: {
|
||||||
|
legend: { display: false },
|
||||||
|
tooltip: { enabled: false }
|
||||||
|
},
|
||||||
|
animation: { duration: 1000 }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateGauge(id, value) {
|
||||||
|
if (charts[id]) {
|
||||||
|
const val = parseFloat(value) || 0;
|
||||||
|
charts[id].data.datasets[0].data = [val, 100 - val];
|
||||||
|
charts[id].update();
|
||||||
|
document.getElementById(id + 'Val').textContent = val.toFixed(1) + '%';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateStatus(id, status) {
|
||||||
|
const el = document.getElementById(id + '-status');
|
||||||
|
const span = el.querySelector('span');
|
||||||
|
el.className = 'status-badge ' + (status === 'online' ? 'status-online' : 'status-offline');
|
||||||
|
span.textContent = status.charAt(0).toUpperCase() + status.slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchData() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/status');
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
// Update PC
|
||||||
|
updateStatus('pc', data.pc.status);
|
||||||
|
if (data.pc.status === 'online') {
|
||||||
|
updateGauge('pcCpu', data.pc.cpu);
|
||||||
|
updateGauge('pcRam', data.pc.ram);
|
||||||
|
document.getElementById('pcTemp').textContent = data.pc.temp + '°C';
|
||||||
|
document.getElementById('pcDisk').textContent = data.pc.disk + '%';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update PI
|
||||||
|
updateStatus('pi', data.pi.status);
|
||||||
|
if (data.pi.status === 'online') {
|
||||||
|
updateGauge('piCpu', data.pi.cpu);
|
||||||
|
updateGauge('piRam', data.pi.ram);
|
||||||
|
document.getElementById('piTemp').textContent = data.pi.temp + '°C';
|
||||||
|
document.getElementById('piDisk').textContent = data.pi.disk + '%';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update Services
|
||||||
|
const serviceList = document.getElementById('service-list');
|
||||||
|
serviceList.innerHTML = data.services.map(s => `
|
||||||
|
<li class="service-item">
|
||||||
|
<div class="service-name">
|
||||||
|
<i class="fas ${getServiceIcon(s.name)}"></i>
|
||||||
|
${s.name}
|
||||||
|
</div>
|
||||||
|
<div class="service-status" style="color: ${s.status === 'online' ? 'var(--status-green)' : 'var(--status-red)'}">
|
||||||
|
<i class="fas ${s.status === 'online' ? 'fa-check-circle' : 'fa-times-circle'}"></i>
|
||||||
|
${s.status === 'online' ? s.latency : 'Offline'}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
`).join('');
|
||||||
|
|
||||||
|
document.getElementById('loading').style.opacity = '0';
|
||||||
|
setTimeout(() => document.getElementById('loading').style.display = 'none', 500);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching data:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getServiceIcon(name) {
|
||||||
|
const icons = {
|
||||||
|
'Gitea': 'fa-code-branch',
|
||||||
|
'OpenProject': 'fa-tasks',
|
||||||
|
'Draw.io': 'fa-project-diagram',
|
||||||
|
'TestArena': 'fa-vial',
|
||||||
|
'TBM': 'fa-microchip',
|
||||||
|
'Board': 'fa-chalkboard'
|
||||||
|
};
|
||||||
|
return icons[name] || 'fa-globe';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize
|
||||||
|
createGauge('pcCpu', '#38bdf8');
|
||||||
|
createGauge('pcRam', '#a855f7');
|
||||||
|
createGauge('piCpu', '#38bdf8');
|
||||||
|
createGauge('piRam', '#a855f7');
|
||||||
|
|
||||||
|
fetchData();
|
||||||
|
setInterval(fetchData, 60000); // Update every 1 minute
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
59
asf-cloud-server/monitor/monitor_logic.py
Normal file
59
asf-cloud-server/monitor/monitor_logic.py
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import paramiko
|
||||||
|
import requests
|
||||||
|
import time
|
||||||
|
import re
|
||||||
|
|
||||||
|
def get_ssh_data(host, port, user, password):
|
||||||
|
try:
|
||||||
|
ssh = paramiko.SSHClient()
|
||||||
|
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
ssh.connect(host, port=port, username=user, password=password, timeout=10)
|
||||||
|
|
||||||
|
# CPU Usage
|
||||||
|
stdin, stdout, stderr = ssh.exec_command("top -bn1 | grep 'Cpu(s)' | sed 's/.*, *\\([0-9.]*\\)%* id.*/\\1/' | awk '{print 100 - $1}'")
|
||||||
|
cpu_usage = stdout.read().decode().strip()
|
||||||
|
|
||||||
|
# CPU Temp
|
||||||
|
# For Ubuntu Server
|
||||||
|
stdin, stdout, stderr = ssh.exec_command("cat /sys/class/thermal/thermal_zone0/temp 2>/dev/null || vcgencmd measure_temp 2>/dev/null")
|
||||||
|
temp_raw = stdout.read().decode().strip()
|
||||||
|
if "temp=" in temp_raw:
|
||||||
|
cpu_temp = temp_raw.replace("temp=", "").replace("'C", "")
|
||||||
|
elif temp_raw:
|
||||||
|
cpu_temp = str(float(temp_raw) / 1000)
|
||||||
|
else:
|
||||||
|
cpu_temp = "N/A"
|
||||||
|
|
||||||
|
# RAM Status
|
||||||
|
stdin, stdout, stderr = ssh.exec_command("free -m | awk 'NR==2{printf \"%.2f\", $3*100/$2 }'")
|
||||||
|
ram_usage = stdout.read().decode().strip()
|
||||||
|
|
||||||
|
# Disk Space
|
||||||
|
stdin, stdout, stderr = ssh.exec_command("df -h / | awk 'NR==2{print $5}' | sed 's/%//'")
|
||||||
|
disk_usage = stdout.read().decode().strip()
|
||||||
|
|
||||||
|
ssh.close()
|
||||||
|
return {
|
||||||
|
"status": "online",
|
||||||
|
"cpu": cpu_usage,
|
||||||
|
"temp": cpu_temp,
|
||||||
|
"ram": ram_usage,
|
||||||
|
"disk": disk_usage
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
return {
|
||||||
|
"status": "offline",
|
||||||
|
"error": str(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
def check_web_service(url):
|
||||||
|
try:
|
||||||
|
start_time = time.time()
|
||||||
|
response = requests.get(url, timeout=5)
|
||||||
|
latency = int((time.time() - start_time) * 1000)
|
||||||
|
if response.status_code == 200:
|
||||||
|
return {"status": "online", "latency": f"{latency}ms"}
|
||||||
|
else:
|
||||||
|
return {"status": "error", "code": response.status_code}
|
||||||
|
except Exception:
|
||||||
|
return {"status": "offline"}
|
||||||
5
asf-cloud-server/monitor/requirements.txt
Normal file
5
asf-cloud-server/monitor/requirements.txt
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
fastapi
|
||||||
|
uvicorn
|
||||||
|
requests
|
||||||
|
paramiko
|
||||||
|
python-dotenv
|
||||||
BIN
asf-cloud-server/monitor/tbm.ico
Normal file
BIN
asf-cloud-server/monitor/tbm.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 184 KiB |
BIN
asf-cloud-server/monitor/testarena.png
Normal file
BIN
asf-cloud-server/monitor/testarena.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 MiB |
Reference in New Issue
Block a user