mirvmon/templates/dashboard.twig

347 lines
16 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 %}
{% 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 %}