Files
info-user/server/templates/dashboard.html
T
Hidosi 7b2e5172ea Add active user session tracking from agent user :0 2026-07-16 15:46
user     pts/0        2026-07-16 15:50 (:0)
user     pts/1        2026-07-16 16:46 (:0)
user     pts/2        2026-07-17 09:05 (:0) to server UI
2026-07-17 09:44:39 +07:00

193 lines
8.6 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">{% if c.last_seen %}{{ c.last_seen.strftime('%Y-%m-%d %H:%M') }}{% else %}-{% endif %}</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 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">${c.last_seen ? c.last_seen.replace('T', ' ').slice(0, 16) : '-'}</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 %}