mirvmon/templates/dashboard.twig

339 lines
15 KiB
Twig
Executable File
Raw Permalink 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="fas {{ group.icon|default('fa-server')|replace({'fas ': ''}) }}"></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';
function getState() {
try {
const match = document.cookie.match(new RegExp('(^| )' + COOKIE_NAME + '=([^;]+)'));
return match ? JSON.parse(decodeURIComponent(match[2])) : {};
} catch(e) { return {}; }
}
function setState(state) {
document.cookie = COOKIE_NAME + '=' + encodeURIComponent(JSON.stringify(state)) + '; path=/; max-age=' + (30*24*60*60);
}
function toggleAccordion(header, target, icon) {
const groupId = header.dataset.group;
const isOpen = target.classList.contains('show');
if (isOpen) {
target.classList.remove('show');
icon.className = 'fas fa-chevron-down';
var state = getState();
state[groupId] = 'collapsed';
setState(state);
} else {
target.classList.add('show');
icon.className = 'fas fa-chevron-up';
var state = getState();
state[groupId] = 'expanded';
setState(state);
}
}
document.addEventListener('DOMContentLoaded', function() {
var savedState = getState();
document.querySelectorAll('.accordion-header').forEach(function(header) {
var icon = header.querySelector('.accordion-icon');
var targetId = header.dataset.target;
var target = document.querySelector(targetId);
var groupId = header.dataset.group;
if (target && icon) {
var isCollapsed = savedState[groupId] === 'collapsed';
if (isCollapsed) {
target.classList.remove('show');
icon.className = 'fas fa-chevron-down';
} else {
target.classList.add('show');
icon.className = 'fas fa-chevron-up';
}
header.addEventListener('click', function() {
toggleAccordion(header, target, icon);
});
}
});
// AJAX
function updateDashboard() {
fetch('/api/dashboard/stats')
.then(function(r) { return r.json(); })
.then(function(data) {
data.forEach(function(server) {
var cpuVal = document.getElementById('cpu-val-' + server.id);
if (cpuVal && server.metrics.cpu_load) cpuVal.textContent = server.metrics.cpu_load.value + '%';
var ramVal = document.getElementById('ram-val-' + server.id);
if (ramVal && server.metrics.ram_used) ramVal.textContent = server.metrics.ram_used.value + '%';
var diskVal = document.getElementById('disk-val-' + server.id);
if (diskVal && server.metrics.disk) diskVal.textContent = server.metrics.disk.value + '%';
var updatedAt = document.getElementById('updated-at-' + server.id);
if (updatedAt && server.updated_at) updatedAt.textContent = server.updated_at.split(' ')[1].substring(0, 5);
});
});
}
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 %}