{% extends "base.twig" %}
{% block title %}{{ t('ui.whos_online.page_title', {}, locale, ['common']) }} - {{ system_name }}{% endblock %}
{% block content %}
<div class="container mt-4">
<div class="d-flex justify-content-between align-items-center mb-3">
<h1><i class="fas fa-user-friends"></i> {{ t('ui.whos_online.heading', {}, locale, ['common']) }}</h1>
<small class="text-muted">{{ t('ui.whos_online.active_last_minutes', {'minutes': online_minutes}, locale, ['common']) }}</small>
</div>
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h6 class="mb-0">{{ t('ui.whos_online.online_users', {}, locale, ['common']) }}</h6>
<span class="badge bg-secondary" id="onlineCount">{{ online_user_count|default(online_users|length) }}</span>
</div>
<div class="card-body p-0">
<div id="onlineTableWrap" class="{{ online_users|length > 0 ? '' : 'd-none' }}">
<div class="table-responsive">
<table class="table table-striped align-middle mb-0">
<thead>
<tr>
<th>{{ t('ui.common.username', {}, locale, ['common']) }}</th>
<th>{{ t('ui.common.location', {}, locale, ['common']) }}</th>
{% if current_user and current_user.is_admin %}
<th>{{ t('ui.whos_online.service', {}, locale, ['common']) }}</th>
<th>{{ t('ui.whos_online.activity', {}, locale, ['common']) }}</th>
<th>{{ t('ui.whos_online.idle', {}, locale, ['common']) }}</th>
{% endif %}
</tr>
</thead>
<tbody id="onlineUsersBody">
{% for user in online_users %}
<tr>
<td>
<a href="/profile/{{ user.username|url_encode }}" class="text-decoration-none">{{ user.username }}</a>
</td>
<td>{{ user.location ?: t('ui.common.unknown', {}, locale, ['common']) }}</td>
{% if current_user and current_user.is_admin %}
<td><span class="badge bg-secondary">{{ user.service ?: 'web' }}</span></td>
<td>{{ user.activity ?: '-' }}</td>
<td class="idle-time" data-last-activity-ts="{{ user.last_activity|date('U') }}">-</td>
{% endif %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div class="p-3 {{ online_users|length > 0 ? 'd-none' : '' }}" id="onlineEmptyState">
<div class="alert alert-secondary mb-0">
<i class="fas fa-user-slash me-2"></i>
{{ t('ui.whos_online.no_users_online', {}, locale, ['common']) }}
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
function uiT(key, fallback, params = {}) {
if (window.t) {
return window.t(key, params, fallback);
}
return fallback;
}
const onlineMinutesLabel = document.querySelector('.text-muted');
const onlineCount = document.getElementById('onlineCount');
const onlineTableWrap = document.getElementById('onlineTableWrap');
const onlineUsersBody = document.getElementById('onlineUsersBody');
const onlineEmptyState = document.getElementById('onlineEmptyState');
const isAdmin = !!(window.currentUserIsAdmin && window.currentUserIsAdmin === true);
function formatIdle(ts) {
if (!ts) return '-';
const diffSec = Math.floor(Date.now() / 1000 - ts);
if (diffSec < 0) return '-';
if (diffSec < 60) return diffSec + 's';
const diffMin = Math.floor(diffSec / 60);
if (diffMin < 60) return diffMin + 'm';
const diffHr = Math.floor(diffMin / 60);
const remMin = diffMin % 60;
return remMin ? `${diffHr}h ${remMin}m` : `${diffHr}h`;
}
function refreshIdleTimes() {
document.querySelectorAll('.idle-time[data-last-activity-ts]').forEach(el => {
el.textContent = formatIdle(parseInt(el.dataset.lastActivityTs, 10));
});
}
function updateOnlineView(payload) {
if (!payload || !Array.isArray(payload.users)) {
return;
}
const users = payload.users;
const onlineUserCountValue = payload.online_user_count ?? users.length;
if (onlineCount) {
onlineCount.textContent = onlineUserCountValue;
}
if (onlineMinutesLabel && payload.online_minutes) {
onlineMinutesLabel.textContent = uiT(
'ui.whos_online.active_last_minutes',
`Active in last ${payload.online_minutes} minutes`,
{ minutes: payload.online_minutes }
);
}
if (users.length === 0) {
if (onlineTableWrap) onlineTableWrap.classList.add('d-none');
if (onlineEmptyState) onlineEmptyState.classList.remove('d-none');
return;
}
if (onlineTableWrap) onlineTableWrap.classList.remove('d-none');
if (onlineEmptyState) onlineEmptyState.classList.add('d-none');
if (onlineUsersBody) {
onlineUsersBody.innerHTML = users.map(user => {
const location = user.location && user.location !== '' ? user.location : uiT('ui.common.unknown', 'Unknown');
const username = escapeHtml(user.username || '');
const usernameCell = `<a href="/profile/${encodeURIComponent(user.username)}" class="text-decoration-none">${username}</a>`;
const ts = user.last_activity_ts || null;
const serviceCell = isAdmin ? `<td><span class="badge bg-secondary">${escapeHtml(user.service || 'web')}</span></td>` : '';
const activityCell = isAdmin ? `<td>${user.activity && user.activity !== '' ? escapeHtml(user.activity) : '-'}</td>` : '';
const idleCell = isAdmin ? `<td class="idle-time" data-last-activity-ts="${ts}">${formatIdle(ts)}</td>` : '';
return `<tr>
<td>${usernameCell}</td>
<td>${escapeHtml(location)}</td>
${serviceCell}
${activityCell}
${idleCell}
</tr>`;
}).join('');
}
}
function refreshOnline() {
fetch('/api/whosonline')
.then(res => res.json())
.then(updateOnlineView)
.catch(() => {});
}
refreshIdleTimes();
setInterval(refreshIdleTimes, 10000);
setInterval(refreshOnline, 60000);
</script>
{% endblock %}
|