new version

This commit is contained in:
2026-04-20 09:00:42 +02:00
parent a2358ed0c3
commit c7ff9f162c
7 changed files with 291 additions and 28 deletions

View File

@@ -15,8 +15,15 @@
.badge-zabbix_sync { background: #6f42c1; }
.badge-snmp_transfer { background: #20c997; }
.badge-error { background: #dc3545; }
.scroll-table { max-height: 420px; overflow-y: auto; }
.scroll-table { max-height: 420px; overflow: auto; }
thead.sticky-top th { position: sticky; top: 0; z-index: 1; }
.clickable-card { cursor: pointer; transition: box-shadow .15s; }
.clickable-card:hover { box-shadow: 0 0 0 2px #dc354580; }
.clickable-card.active { box-shadow: 0 0 0 2px #dc3545; background: #fff5f5; }
#player-table th { cursor: pointer; user-select: none; white-space: nowrap; }
#player-table th:hover { background: #343a40cc; }
#player-table th.sort-asc::after { content: ' ▲'; font-size: .7em; }
#player-table th.sort-desc::after { content: ' ▼'; font-size: .7em; }
</style>
</head>
<body>
@@ -43,9 +50,9 @@
</div>
</div>
<div class="col-6 col-md-3">
<div class="card stat-card text-center py-3 h-100">
<div class="card stat-card text-center py-3 h-100 clickable-card" id="offline-card" role="button" title="Click to filter offline players">
<div class="display-6 fw-bold text-danger">{{ total - online }}</div>
<div class="text-muted small">Offline</div>
<div class="text-muted small">Offline <span class="text-muted" style="font-size:.7rem">(click to filter)</span></div>
</div>
</div>
<div class="col-6 col-md-3">
@@ -62,29 +69,40 @@
<!-- Player table -->
<div class="card shadow-sm mb-4">
<div class="card-header d-flex justify-content-between align-items-center">
<span class="fw-semibold">Players</span>
<span class="badge bg-secondary">{{ total }}</span>
<span class="fw-semibold">Players <span id="filter-badge" class="badge bg-danger ms-1 d-none">Offline only ✕</span></span>
<div class="d-flex align-items-center gap-2">
<span class="badge bg-secondary" id="player-count">{{ total }}</span>
<button class="btn btn-sm btn-outline-primary" id="sync-btn" onclick="triggerSync()">↻ Sync now</button>
</div>
</div>
<div class="card-body p-0">
<div class="scroll-table">
<table class="table table-sm table-hover mb-0">
<table class="table table-sm table-hover mb-0" id="player-table">
<thead class="table-dark sticky-top">
<tr>
<th>Yodeck ID</th>
<th>Name</th>
<th>Hostname</th>
<th>Workspace</th>
<th>Type</th>
<th>Status</th>
<th>Last Seen (UTC)</th>
<th>Last Pushed</th>
<th>Status Updated</th>
<th>Resolution</th>
<th>Hardware</th>
<th>Last IP</th>
<th>eth0 IP</th>
<th>Updating</th>
<th>Registered</th>
</tr>
</thead>
<tbody>
<tbody id="player-tbody">
{% for p in players %}
<tr>
<tr data-online="{{ '1' if p.online else '0' }}">
<td class="text-muted font-monospace">{{ p.id }}</td>
<td>{{ p.name }}</td>
<td class="font-monospace small">{{ p.hostname or '—' }}</td>
<td>{{ p.workspace_name or '—' }}</td>
<td>{{ p.player_type or '—' }}</td>
<td>
@@ -95,6 +113,12 @@
{% endif %}
</td>
<td class="font-monospace small">{{ (p.last_seen or '—')[:19] }}</td>
<td class="font-monospace small">{{ (p.last_pushed or '—')[:19] }}</td>
<td class="font-monospace small">{{ (p.status_last_updated or '—')[:19] }}</td>
<td class="small">{{ p.screen_resolution or '—' }}</td>
<td class="small">{{ p.hardware_version or '—' }}</td>
<td class="font-monospace small">{{ p.last_ip_address or '—' }}</td>
<td class="font-monospace small">{{ p.eth0_ip or '—' }}</td>
<td>{% if p.updating %}<span class="text-warning fw-semibold">Yes</span>{% else %}No{% endif %}</td>
<td>{% if p.registered %}<span class="text-success"></span>{% else %}<span class="text-danger"></span>{% endif %}</td>
</tr>
@@ -146,5 +170,76 @@
<a href="/api/logs" class="text-muted">logs</a>
</div>
<script>
let offlineFilter = false;
function applyFilter() {
const rows = document.querySelectorAll('#player-tbody tr');
let visible = 0;
rows.forEach(r => {
const show = !offlineFilter || r.dataset.online === '0';
r.style.display = show ? '' : 'none';
if (show) visible++;
});
document.getElementById('player-count').textContent = visible;
document.getElementById('filter-badge').classList.toggle('d-none', !offlineFilter);
document.getElementById('offline-card').classList.toggle('active', offlineFilter);
}
document.getElementById('offline-card').addEventListener('click', () => {
offlineFilter = !offlineFilter;
applyFilter();
});
document.getElementById('filter-badge').addEventListener('click', e => {
e.stopPropagation();
offlineFilter = false;
applyFilter();
});
// Column sorting
let sortCol = -1, sortAsc = true;
document.querySelectorAll('#player-table thead th').forEach((th, idx) => {
th.addEventListener('click', () => {
if (sortCol === idx) {
sortAsc = !sortAsc;
} else {
sortCol = idx;
sortAsc = true;
}
document.querySelectorAll('#player-table thead th').forEach(h => h.classList.remove('sort-asc', 'sort-desc'));
th.classList.add(sortAsc ? 'sort-asc' : 'sort-desc');
const tbody = document.getElementById('player-tbody');
const rows = Array.from(tbody.querySelectorAll('tr'));
rows.sort((a, b) => {
const av = a.cells[idx]?.textContent.trim() ?? '';
const bv = b.cells[idx]?.textContent.trim() ?? '';
const an = parseFloat(av), bn = parseFloat(bv);
const cmp = (!isNaN(an) && !isNaN(bn)) ? an - bn : av.localeCompare(bv);
return sortAsc ? cmp : -cmp;
});
rows.forEach(r => tbody.appendChild(r));
applyFilter();
});
});
function triggerSync() {
const btn = document.getElementById('sync-btn');
btn.disabled = true;
btn.textContent = '↻ Syncing…';
fetch('/api/sync', { method: 'POST' })
.then(r => r.json())
.then(d => {
btn.textContent = d.ok ? '✓ Done' : '✗ Failed';
setTimeout(() => location.reload(), 1500);
})
.catch(() => {
btn.textContent = '✗ Error';
btn.disabled = false;
});
}
</script>
</body>
</html>