347 lines
16 KiB
Twig
Executable File
347 lines
16 KiB
Twig
Executable File
{% 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 %}
|
||
{% set groupSlug = groupName|lower|replace({' ': '-'}) %}
|
||
<div class="card mb-3 border-0 shadow-sm">
|
||
<div class="card-header text-white py-2 accordion-header"
|
||
data-group="{{ groupSlug }}"
|
||
data-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 accordion-icon"></i>
|
||
</div>
|
||
</div>
|
||
<div id="group-{{ loop.index }}" class="collapse" data-group="{{ groupSlug }}">
|
||
<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 автообновление + Accordion состояние -->
|
||
<script>
|
||
(function() {
|
||
const COOKIE_NAME = 'dashboard_accordion';
|
||
let accordionState = {};
|
||
|
||
function getCookie(name) {
|
||
const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));
|
||
return match ? JSON.parse(decodeURIComponent(match[2])) : {};
|
||
}
|
||
|
||
function saveCookie(state) {
|
||
document.cookie = COOKIE_NAME + '=' + encodeURIComponent(JSON.stringify(state)) +
|
||
'; path=/; max-age=' + (30 * 24 * 60 * 60);
|
||
}
|
||
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
accordionState = getCookie(COOKIE_NAME);
|
||
|
||
// Инициализация: устанавливаем начальное состояние из cookies
|
||
document.querySelectorAll('.accordion-header').forEach(function(header) {
|
||
const groupId = header.dataset.group;
|
||
const targetId = header.dataset.target;
|
||
const target = document.querySelector(targetId);
|
||
|
||
if (target) {
|
||
if (accordionState[groupId] === 'collapsed') {
|
||
target.classList.remove('show');
|
||
header.querySelector('.accordion-icon').className = 'fas fa-chevron-down';
|
||
} else {
|
||
// По умолчанию открыто
|
||
target.classList.add('show');
|
||
header.querySelector('.accordion-icon').className = 'fas fa-chevron-up';
|
||
}
|
||
}
|
||
});
|
||
|
||
// Клик на заголовке accordion
|
||
document.querySelectorAll('.accordion-header').forEach(function(header) {
|
||
header.addEventListener('click', function(e) {
|
||
const groupId = this.dataset.group;
|
||
const targetId = this.dataset.target;
|
||
const target = document.querySelector(targetId);
|
||
const icon = this.querySelector('.accordion-icon');
|
||
|
||
if (target.classList.contains('show')) {
|
||
// Сворачиваем
|
||
target.classList.remove('show');
|
||
icon.className = 'fas fa-chevron-down';
|
||
accordionState[groupId] = 'collapsed';
|
||
} else {
|
||
// Разворачиваем
|
||
target.classList.add('show');
|
||
icon.className = 'fas fa-chevron-up';
|
||
accordionState[groupId] = 'expanded';
|
||
}
|
||
saveCookie(accordionState);
|
||
});
|
||
});
|
||
|
||
// AJAX обновление
|
||
function updateDashboard() {
|
||
fetch('/api/dashboard/stats')
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
data.forEach(server => {
|
||
const cpuVal = document.getElementById('cpu-val-' + server.id);
|
||
if (cpuVal && server.metrics.cpu_load) {
|
||
cpuVal.textContent = server.metrics.cpu_load.value + '%';
|
||
}
|
||
const ramVal = document.getElementById('ram-val-' + server.id);
|
||
if (ramVal && server.metrics.ram_used) {
|
||
ramVal.textContent = server.metrics.ram_used.value + '%';
|
||
}
|
||
const diskVal = document.getElementById('disk-val-' + server.id);
|
||
if (diskVal && server.metrics.disk) {
|
||
diskVal.textContent = server.metrics.disk.value + '%';
|
||
}
|
||
const updatedAt = document.getElementById('updated-at-' + server.id);
|
||
if (updatedAt && server.updated_at) {
|
||
updatedAt.textContent = server.updated_at.split(' ')[1].substring(0, 5);
|
||
}
|
||
});
|
||
})
|
||
.catch(err => console.log('Dashboard update error:', err));
|
||
}
|
||
|
||
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 %}
|