209 lines
9.0 KiB
HTML
209 lines
9.0 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}Дашборд — InfoUser Monitor{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<h2>Компьютеры</h2>
|
|
<div class="d-flex gap-2">
|
|
<button type="button" class="btn btn-outline-success btn-sm" id="export-csv-btn">
|
|
<i class="bi bi-download"></i> Экспорт CSV
|
|
</button>
|
|
<a href="/notifications" class="btn btn-outline-info btn-sm">
|
|
<i class="bi bi-bell"></i> Уведомления {% if unread_count %}<span class="badge bg-danger">{{ unread_count }}</span>{% endif %}
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<form id="filter-form" class="row g-2 mb-4 p-3 bg-white rounded shadow-sm" method="get" action="/">
|
|
<div class="col-md-2">
|
|
<select name="status" class="form-select form-select-sm">
|
|
<option value="">Все статусы</option>
|
|
<option value="online" {% if filters.status == 'online' %}selected{% endif %}>Online</option>
|
|
<option value="offline" {% if filters.status == 'offline' %}selected{% endif %}>Offline</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<input type="text" name="user" class="form-control form-control-sm" placeholder="Пользователь" value="{{ filters.user }}">
|
|
</div>
|
|
<div class="col-md-3">
|
|
<input type="text" name="search" class="form-control form-control-sm" placeholder="Поиск по хосту/IP/OS" value="{{ filters.search }}">
|
|
</div>
|
|
<div class="col-md-2">
|
|
<select name="sort" class="form-select form-select-sm">
|
|
<option value="hostname" {% if filters.sort == 'hostname' %}selected{% endif %}>Сортировка: хост</option>
|
|
<option value="last_seen" {% if filters.sort == 'last_seen' %}selected{% endif %}>Последний чек</option>
|
|
<option value="current_user" {% if filters.sort == 'current_user' %}selected{% endif %}>Пользователь</option>
|
|
<option value="status" {% if filters.sort == 'status' %}selected{% endif %}>Статус</option>
|
|
<option value="current_cpu_percent" {% if filters.sort == 'current_cpu_percent' %}selected{% endif %}>CPU</option>
|
|
<option value="current_ram_percent" {% if filters.sort == 'current_ram_percent' %}selected{% endif %}>RAM</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-1">
|
|
<select name="order" class="form-select form-select-sm">
|
|
<option value="asc" {% if filters.order == 'asc' %}selected{% endif %}>Asc</option>
|
|
<option value="desc" {% if filters.order == 'desc' %}selected{% endif %}>Desc</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<button type="submit" class="btn btn-primary btn-sm w-100">Применить</button>
|
|
</div>
|
|
</form>
|
|
|
|
<table class="table table-hover align-middle" id="computers-table">
|
|
<thead class="table-dark">
|
|
<tr>
|
|
<th>Хост</th>
|
|
<th>Пользователь</th>
|
|
<th>IP</th>
|
|
<th>Статус</th>
|
|
<th>CPU</th>
|
|
<th>RAM</th>
|
|
<th>OS</th>
|
|
<th>Последний чек</th>
|
|
<th></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for c in computers %}
|
|
<tr data-id="{{ c.id }}">
|
|
<td><strong>{{ c.hostname }}</strong></td>
|
|
<td>
|
|
{% if c.current_user and ',' in c.current_user %}
|
|
<span class="badge bg-info text-dark" title="{{ c.current_user }}">за ПК работают {{ c.current_user.split(',') | length }} пользователя</span>
|
|
{% else %}
|
|
{{ c.current_user or '-' }}
|
|
{% endif %}
|
|
</td>
|
|
<td>{{ c.current_ip or '-' }}</td>
|
|
<td>
|
|
{% if c.status == 'online' %}
|
|
<span class="badge bg-success">Online</span>
|
|
{% else %}
|
|
<span class="badge bg-secondary">Offline</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>{{ c.current_cpu_percent | round(1) if c.current_cpu_percent is not none else '-' }}%</td>
|
|
<td>{{ c.current_ram_percent | round(1) if c.current_ram_percent is not none else '-' }}%</td>
|
|
<td>{{ c.os_info or '-' }}</td>
|
|
<td class="last-seen">{{ c.last_seen | localtime }}</td>
|
|
<td>
|
|
<a href="/computers/{{ c.id }}" class="btn btn-sm btn-outline-primary">Детали</a>
|
|
{% if user.role == 'admin' %}
|
|
<button class="btn btn-sm btn-outline-danger delete-pc" data-id="{{ c.id }}" data-hostname="{{ c.hostname }}">Удалить</button>
|
|
{% endif %}
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
|
|
{% if not computers %}
|
|
<div class="alert alert-info">Пока нет данных. Установите и запустите агент на клиентских ПК.</div>
|
|
{% endif %}
|
|
{% endblock %}
|
|
|
|
{% block scripts %}
|
|
<script>
|
|
const isAdmin = {{ 'true' if user.role == 'admin' else 'false' }};
|
|
|
|
function formatCurrentUser(value) {
|
|
if (!value) return '-';
|
|
if (value.includes(',')) {
|
|
const count = value.split(',').length;
|
|
return `<span class="badge bg-info text-dark" title="${escapeHtml(value)}">за ПК работают ${count} пользователя</span>`;
|
|
}
|
|
return escapeHtml(value);
|
|
}
|
|
|
|
function formatServerTime(isoString) {
|
|
if (!isoString) return '-';
|
|
const d = new Date(isoString);
|
|
const parts = new Intl.DateTimeFormat('sv-SE', {
|
|
timeZone: window.SERVER_TIMEZONE,
|
|
year: 'numeric', month: '2-digit', day: '2-digit',
|
|
hour: '2-digit', minute: '2-digit',
|
|
hour12: false,
|
|
}).formatToParts(d);
|
|
const p = {};
|
|
for (const part of parts) {
|
|
if (part.type !== 'literal') p[part.type] = part.value;
|
|
}
|
|
return `${p.year}-${p.month}-${p.day} ${p.hour}:${p.minute}`;
|
|
}
|
|
|
|
function currentQuery() {
|
|
return new URLSearchParams(window.location.search).toString();
|
|
}
|
|
|
|
async function deleteComputer(id, hostname) {
|
|
if (!confirm(`Удалить ПК ${hostname}?`)) return;
|
|
const res = await fetch(`/api/computers/${id}`, {method: 'DELETE', credentials: 'include'});
|
|
if (res.ok) location.reload();
|
|
else if (res.status === 401) location.href = '/login';
|
|
else alert('Ошибка удаления');
|
|
}
|
|
|
|
async function exportCsv() {
|
|
const res = await fetch('/api/computers/export/csv?' + currentQuery(), {credentials: 'include'});
|
|
if (!res.ok) {
|
|
if (res.status === 401) location.href = '/login';
|
|
else alert('Ошибка экспорта');
|
|
return;
|
|
}
|
|
const blob = await res.blob();
|
|
const url = window.URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
const disposition = res.headers.get('content-disposition');
|
|
let filename = 'computers.csv';
|
|
if (disposition) {
|
|
const match = disposition.match(/filename="?([^";]+)"?/);
|
|
if (match) filename = match[1];
|
|
}
|
|
a.download = filename;
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
a.remove();
|
|
window.URL.revokeObjectURL(url);
|
|
}
|
|
|
|
document.getElementById('export-csv-btn').addEventListener('click', exportCsv);
|
|
|
|
document.querySelectorAll('.delete-pc').forEach(btn => {
|
|
btn.addEventListener('click', () => deleteComputer(btn.dataset.id, btn.dataset.hostname));
|
|
});
|
|
|
|
async function loadDashboard() {
|
|
try {
|
|
const res = await fetch('/api/computers?' + currentQuery(), {credentials: 'include'});
|
|
if (!res.ok) return location.reload();
|
|
const data = await res.json();
|
|
const tbody = document.querySelector('#computers-table tbody');
|
|
tbody.innerHTML = data.map(c => `
|
|
<tr data-id="${c.id}">
|
|
<td><strong>${escapeHtml(c.hostname)}</strong></td>
|
|
<td>${formatCurrentUser(c.current_user)}</td>
|
|
<td>${escapeHtml(c.current_ip || '-')}</td>
|
|
<td>${c.status === 'online' ? '<span class="badge bg-success">Online</span>' : '<span class="badge bg-secondary">Offline</span>'}</td>
|
|
<td>${c.current_cpu_percent != null ? c.current_cpu_percent.toFixed(1) : '-'}%</td>
|
|
<td>${c.current_ram_percent != null ? c.current_ram_percent.toFixed(1) : '-'}%</td>
|
|
<td>${escapeHtml(c.os_info || '-')}</td>
|
|
<td class="last-seen">${formatServerTime(c.last_seen)}</td>
|
|
<td>
|
|
<a href="/computers/${c.id}" class="btn btn-sm btn-outline-primary">Детали</a>
|
|
${isAdmin ? `<button class="btn btn-sm btn-outline-danger delete-pc" data-id="${c.id}" data-hostname="${escapeHtml(c.hostname)}">Удалить</button>` : ''}
|
|
</td>
|
|
</tr>
|
|
`).join('');
|
|
document.querySelectorAll('.delete-pc').forEach(btn => {
|
|
btn.addEventListener('click', () => deleteComputer(btn.dataset.id, btn.dataset.hostname));
|
|
});
|
|
} catch (e) {
|
|
console.error('Dashboard refresh failed', e);
|
|
}
|
|
}
|
|
setInterval(loadDashboard, 60000);
|
|
</script>
|
|
{% endblock %}
|