mirvmon/templates/dashboard.twig

296 lines
14 KiB
Twig
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% extends "layout.twig" %}
{% block content %}
<div class="row mb-3">
<div class="col-12 d-flex justify-content-between align-items-center">
<h2 class="mb-0"><i class="fas fa-tachometer-alt"></i> Дашборд</h2>
<a href="/servers/create" class="btn btn-primary btn-sm">
<i class="fas fa-plus"></i> Добавить
</a>
</div>
</div>
<!-- Компактная статистика -->
<div class="row mb-3">
<div class="col-6 col-md-3 mb-2">
<div class="card border-0 shadow-sm bg-primary text-white">
<div class="card-body py-2 px-3">
<div class="d-flex justify-content-between align-items-center">
<div>
<small>Серверов</small>
<h4 class="mb-0">{{ stats.total_servers }}</h4>
</div>
<i class="fas fa-server fa-2x opacity-50"></i>
</div>
</div>
</div>
</div>
<div class="col-6 col-md-3 mb-2">
<div class="card border-0 shadow-sm bg-success text-white">
<div class="card-body py-2 px-3">
<div class="d-flex justify-content-between align-items-center">
<div>
<small>Онлайн</small>
<h4 class="mb-0">{{ stats.servers_with_metrics }}</h4>
</div>
<i class="fas fa-check-circle fa-2x opacity-50"></i>
</div>
</div>
</div>
</div>
<div class="col-6 col-md-3 mb-2">
<div class="card border-0 shadow-sm bg-warning text-dark">
<div class="card-body py-2 px-3">
<div class="d-flex justify-content-between align-items-center">
<div>
<small>Предупреждения</small>
<h4 class="mb-0">{{ stats.warnings }}</h4>
</div>
<i class="fas fa-exclamation-triangle fa-2x opacity-50"></i>
</div>
</div>
</div>
</div>
<div class="col-6 col-md-3 mb-2">
<div class="card border-0 shadow-sm bg-danger text-white">
<div class="card-body py-2 px-3">
<div class="d-flex justify-content-between align-items-center">
<div>
<small>Критические</small>
<h4 class="mb-0">{{ stats.criticals }}</h4>
</div>
<i class="fas fa-radiation fa-2x opacity-50"></i>
</div>
</div>
</div>
</div>
</div>
<!-- Серверы по группам -->
{% if groups|length == 0 %}
<div class="card">
<div class="card-body text-center py-5">
<i class="fas fa-server fa-4x text-muted mb-3"></i>
<h4>Серверы пока не добавлены</h4>
<a href="/servers/create" class="btn btn-primary">
<i class="fas fa-plus"></i> Добавить сервер
</a>
</div>
</div>
{% else %}
{% for groupName, group in groups %}
<div class="card mb-3 border-0 shadow-sm">
<div class="card-header text-white py-2" data-bs-toggle="collapse" data-bs-target="#group-{{ loop.index }}" style="cursor: pointer; background-color: {{ group.color|default('#6c757d') }};">
<div class="d-flex justify-content-between align-items-center">
<h5 class="mb-0">
<i class="{{ group.icon|default('fa-server') }}"></i>
{{ groupName }}
<span class="badge bg-light text-dark ms-2">{{ group.servers|length }}</span>
</h5>
<i class="fas fa-chevron-down"></i>
</div>
</div>
<div id="group-{{ loop.index }}" class="collapse show">
<div class="card-body p-2">
<div class="row g-2">
{% for server in group.servers %}
<div class="col-6 col-lg-4 col-xl-3">
<a href="/servers/{{ server.id }}" class="text-decoration-none">
<div class="server-card compact p-2 rounded
{% if server.status == 'offline' %}border-danger{% elseif server.status == 'warning' %}border-warning{% else %}border-success{% endif %}">
<div class="d-flex justify-content-between align-items-start mb-1">
<div class="d-flex align-items-center">
{% if server.status == 'online' %}
<span class="status-dot bg-success me-2"></span>
{% elseif server.status == 'warning' %}
<span class="status-dot bg-warning me-2"></span>
{% else %}
<span class="status-dot bg-danger me-2"></span>
{% endif %}
<strong class="server-name">{{ server.name }}</strong>
</div>
{% if server.active_alerts > 0 %}
<span class="badge bg-danger badge-sm">{{ server.active_alerts }}</span>
{% endif %}
</div>
<!-- Метрики с иконками -->
<div class="compact-metrics">
{% if server.latest_metrics['cpu_load'] is defined %}
{% set cpu_val = server.latest_metrics['cpu_load'].value %}
{% set cpu_t = server.thresholds['cpu_load']|default(null) %}
{% if cpu_t %}
{% set cpu_color_icon = cpu_val >= cpu_t.critical ? 'text-danger' : (cpu_val >= cpu_t.warning ? 'text-warning' : 'text-success') %}
{% else %}
{% set cpu_color_icon = cpu_val > 80 ? 'text-danger' : (cpu_val > 60 ? 'text-warning' : 'text-success') %}
{% endif %}
<span id="cpu-icon-{{ server.id }}" class="metric-icon {{ cpu_color_icon }}" title="CPU">
<i class="fas fa-microchip"></i>
</span>
<span id="cpu-val-{{ server.id }}" class="metric-val {{ cpu_color_icon }}">{{ cpu_val }}%</span>
{% endif %}
{% if server.latest_metrics['ram_used'] is defined %}
{% set ram_val = server.latest_metrics['ram_used'].value %}
{% set ram_t = server.thresholds['ram_used']|default(null) %}
{% if ram_t %}
{% set ram_color_icon = ram_val >= ram_t.critical ? 'text-danger' : (ram_val >= ram_t.warning ? 'text-warning' : 'text-success') %}
{% else %}
{% set ram_color_icon = ram_val > 80 ? 'text-danger' : (ram_val > 60 ? 'text-warning' : 'text-success') %}
{% endif %}
<span id="ram-icon-{{ server.id }}" class="metric-icon {{ ram_color_icon }}" title="RAM">
<i class="fas fa-memory"></i>
</span>
<span id="ram-val-{{ server.id }}" class="metric-val {{ ram_color_icon }}">{{ ram_val }}%</span>
{% endif %}
{% set disk_metric = server.latest_metrics['disk_used_root'] ?? server.latest_metrics['disk_used'] ?? null %}
{% if disk_metric is not null %}
{% set disk_val = disk_metric.value %}
{% set disk_t = server.thresholds['disk_used_root']|default(null) %}
{% if disk_t %}
{% set disk_color_icon = disk_val >= disk_t.critical ? 'text-danger' : (disk_val >= disk_t.warning ? 'text-warning' : 'text-success') %}
{% else %}
{% set disk_color_icon = disk_val > 90 ? 'text-danger' : (disk_val > 75 ? 'text-warning' : 'text-success') %}
{% endif %}
<span id="disk-icon-{{ server.id }}" class="metric-icon {{ disk_color_icon }}" title="Disk">
<i class="fas fa-hdd"></i>
</span>
<span id="disk-val-{{ server.id }}" class="metric-val {{ disk_color_icon }}">{{ disk_val }}%</span>
{% endif %}
</div>
{% if server.latest_metrics['uptime'] is defined %}
<div class="uptime-mini text-muted small mt-1">
<i class="fas fa-clock"></i>
{% set uptime_sec = server.latest_metrics['uptime'].value %}
{% if uptime_sec >= 86400 %}
{{ (uptime_sec / 86400)|round(0, 'floor') }}д {{ ((uptime_sec % 86400) / 3600)|round(0, 'floor') }}ч
{% elseif uptime_sec >= 3600 %}
{{ (uptime_sec / 3600)|round(0, 'floor') }}ч {{ ((uptime_sec % 3600) / 60)|round(0, 'floor') }}м
{% else %}
{{ (uptime_sec / 60)|round(0, 'floor') }}м
{% endif %}
</div>
{% endif %}
<!-- Время обновления (для AJAX) -->
<div class="text-muted small updated-at" id="updated-at-{{ server.id }}">
{% if server.last_metrics_at %}
{{ server.last_metrics_at|date('H:i') }}
{% else %}
{% endif %}
</div>
</div>
</a>
</div>
{% endfor %}
</div>
</div>
</div>
</div>
{% endfor %}
{% endif %}
<!-- AJAX автообновление -->
<script>
(function() {
let updateInterval = null;
function updateDashboard() {
fetch('/api/dashboard/stats')
.then(response => response.json())
.then(data => {
data.forEach(server => {
// CPU
const cpuVal = document.getElementById('cpu-val-' + server.id);
if (cpuVal && server.metrics.cpu_load) {
cpuVal.textContent = server.metrics.cpu_load.value + '%';
}
// RAM
const ramVal = document.getElementById('ram-val-' + server.id);
if (ramVal && server.metrics.ram_used) {
ramVal.textContent = server.metrics.ram_used.value + '%';
}
// Disk
const diskVal = document.getElementById('disk-val-' + server.id);
if (diskVal && server.metrics.disk) {
diskVal.textContent = server.metrics.disk.value + '%';
}
// Updated time
const updatedAt = document.getElementById('updated-at-' + server.id);
if (updatedAt) {
updatedAt.textContent = server.updated_at.split(' ')[1].substring(0, 5);
}
});
})
.catch(err => console.log('Dashboard update error:', err));
}
// Запускаем обновление каждые 30 секунд
document.addEventListener('DOMContentLoaded', function() {
updateInterval = setInterval(updateDashboard, 30000);
});
})();
</script>
<style>
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
display: inline-block;
}
.server-card.compact {
background: #f8f9fa;
border-left: 4px solid #28a745;
transition: all 0.2s;
}
.server-card.compact:hover {
background: #e9ecef;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.server-card.border-warning { border-left-color: #ffc107 !important; }
.server-card.border-danger { border-left-color: #dc3545 !important; }
.server-name {
font-size: 0.9rem;
max-width: 120px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.compact-metrics {
display: flex;
gap: 6px;
font-size: 0.8rem;
align-items: center;
}
.metric-icon {
font-size: 0.9rem;
}
.metric-val {
font-weight: 600;
min-width: 35px;
}
.metric-icon.text-danger, .metric-val.text-danger { color: #dc3545 !important; }
.metric-icon.text-warning, .metric-val.text-warning { color: #ffc107 !important; }
.metric-icon.text-success, .metric-val.text-success { color: #28a745 !important; }
.badge-sm {
font-size: 0.65rem;
padding: 0.15em 0.4em;
}
.uptime-mini i {
font-size: 0.7rem;
}
.updated-at {
font-size: 0.65rem;
margin-top: 2px;
}
</style>
{% endblock %}