new version
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user