PHP Classes

File: templates/admin/dashboard.twig

Recommend this page to a friend!
  Packages of Matthew Asham   Binkterm PHP   templates/admin/dashboard.twig   Download  
File: templates/admin/dashboard.twig
Role: Auxiliary data
Content type: text/plain
Description: Auxiliary data
Class: Binkterm PHP
Bulletin board system based on the Web
Author: By
Last change:
Date: 5 days ago
Size: 32,970 bytes
 

Contents

Class file image Download
{% extends "base.twig" %} {% block title %}{{ t('ui.admin.dashboard.page_title', {}, locale, ['common']) }}{% endblock %} {% block content %} <div class="container-fluid"> <div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom"> <h1 class="h2">{{ t('ui.admin.dashboard.heading', {}, locale, ['common']) }}</h1> </div> <!-- System Statistics --> <div class="row mb-4"> <div class="col-xl-3 col-md-6 mb-4"> <div class="card border-left-primary shadow h-100 py-2"> <div class="card-body"> <div class="row no-gutters align-items-center"> <div class="col mr-2"> <div class="text-xs font-weight-bold text-primary text-uppercase mb-1">{{ t('ui.admin.dashboard.total_users', {}, locale, ['common']) }}</div> <div class="h5 mb-0 font-weight-bold text-gray-800">{{ stats.total_users }}</div> </div> <div class="col-auto"> <i class="fas fa-users fa-2x text-gray-300"></i> </div> </div> </div> </div> </div> <div class="col-xl-3 col-md-6 mb-4"> <div class="card border-left-success shadow h-100 py-2"> <div class="card-body"> <div class="row no-gutters align-items-center"> <div class="col mr-2"> <div class="text-xs font-weight-bold text-success text-uppercase mb-1">{{ t('ui.admin.dashboard.active_users', {}, locale, ['common']) }}</div> <div class="h5 mb-0 font-weight-bold text-gray-800">{{ stats.active_users }}</div> </div> <div class="col-auto"> <i class="fas fa-user-check fa-2x text-gray-300"></i> </div> </div> </div> </div> </div> <div class="col-xl-3 col-md-6 mb-4"> <div class="card border-left-info shadow h-100 py-2"> <div class="card-body"> <div class="row no-gutters align-items-center"> <div class="col mr-2"> <div class="text-xs font-weight-bold text-info text-uppercase mb-1">{{ t('ui.admin.dashboard.total_netmail', {}, locale, ['common']) }}</div> <div class="h5 mb-0 font-weight-bold text-gray-800">{{ stats.total_netmail }}</div> </div> <div class="col-auto"> <i class="fas fa-envelope fa-2x text-gray-300"></i> </div> </div> </div> </div> </div> <div class="col-xl-3 col-md-6 mb-4"> <div class="card border-left-warning shadow h-100 py-2"> <div class="card-body"> <div class="row no-gutters align-items-center"> <div class="col mr-2"> <div class="text-xs font-weight-bold text-warning text-uppercase mb-1">{{ t('ui.admin.dashboard.total_echomail', {}, locale, ['common']) }}</div> <div class="h5 mb-0 font-weight-bold text-gray-800">{{ stats.total_echomail }}</div> </div> <div class="col-auto"> <i class="fas fa-comments fa-2x text-gray-300"></i> </div> </div> </div> </div> </div> </div> <!-- Quick Actions --> <div class="row mb-4"> <div class="col-lg-6 mb-4"> <div class="card shadow"> <div class="card-header py-3"> <h6 class="m-0 font-weight-bold text-primary">{{ t('ui.admin.dashboard.quick_actions', {}, locale, ['common']) }}</h6> </div> <div class="card-body"> <div class="d-grid gap-2"> <a href="/admin/users" class="btn btn-primary"> <i class="fas fa-users me-2"></i>{{ t('ui.base.admin.manage_users', {}, locale, ['common']) }} </a> <a href="/binkp" class="btn btn-secondary"> <i class="fas fa-network-wired me-2"></i>{{ t('ui.base.admin.binkp_status', {}, locale, ['common']) }} </a> <a href="/echoareas" class="btn btn-info"> <i class="fas fa-comments me-2"></i>{{ t('ui.base.admin.echo_areas', {}, locale, ['common']) }} </a> <a href="/admin/crashmail" class="btn btn-warning"> <i class="fas fa-bolt me-2"></i>{{ t('ui.admin.crashmail_queue.heading', {}, locale, ['common']) }} </a> <a href="/admin/insecure-nodes" class="btn btn-outline-secondary"> <i class="fas fa-shield-alt me-2"></i>{{ t('ui.admin.insecure_nodes.heading', {}, locale, ['common']) }} </a> <a href="/admin/binkp-sessions" class="btn btn-outline-info"> <i class="fas fa-history me-2"></i>{{ t('ui.admin.binkp_sessions.heading', {}, locale, ['common']) }} </a> <a href="/admin/activity-stats" class="btn btn-outline-primary"> <i class="fas fa-chart-bar me-2"></i>{{ t('ui.base.admin.activity_stats', {}, locale, ['common']) }} </a> <a href="/admin/ai-usage" class="btn btn-outline-dark text-white"> <i class="fas fa-robot me-2"></i>{{ t('ui.base.admin.ai_usage', {}, locale, ['common']) }} </a> {% if license_valid %} <a href="/admin/economy" class="btn btn-outline-success"> <i class="fas fa-coins me-2"></i>{{ t('ui.base.admin.economy_viewer', {}, locale, ['common']) }} </a> <a href="/admin/referrals" class="btn btn-outline-success"> <i class="fas fa-user-plus me-2"></i>{{ t('ui.base.admin.referrals', {}, locale, ['common']) }} </a> {% else %} <a href="#" class="btn btn-outline-secondary disabled" style="pointer-events:auto;cursor:not-allowed;" title="{{ t('ui.base.admin.registered_feature', {}, locale, ['common']) }}" onclick="return false;"> <i class="fas fa-lock me-2"></i>{{ t('ui.base.admin.economy_viewer', {}, locale, ['common']) }} </a> <a href="#" class="btn btn-outline-secondary disabled" style="pointer-events:auto;cursor:not-allowed;" title="{{ t('ui.base.admin.registered_feature', {}, locale, ['common']) }}" onclick="return false;"> <i class="fas fa-lock me-2"></i>{{ t('ui.base.admin.referrals', {}, locale, ['common']) }} </a> {% endif %} </div> </div> </div> <div class="card shadow mt-4"> <div class="card-header py-3"> <h6 class="m-0 font-weight-bold text-primary">{{ t('ui.admin.dashboard.service_status', {}, locale, ['common']) }}</h6> </div> <div class="card-body"> {% set coreServices = { 'admin_daemon': t('ui.admin.dashboard.service.admin_daemon', {}, locale, ['common']), 'binkp_scheduler': t('ui.admin.dashboard.service.binkp_scheduler', {}, locale, ['common']), 'binkp_server': t('ui.admin.dashboard.service.binkp_server', {}, locale, ['common']), 'realtime_server': t('ui.admin.dashboard.service.realtime_server', {}, locale, ['common']) } %} {% set optionalServices = { 'ftp_daemon': t('ui.admin.dashboard.service.ftp_daemon', {}, locale, ['common']), 'telnetd': t('ui.admin.dashboard.service.telnetd', {}, locale, ['common']), 'ssh_daemon': t('ui.admin.dashboard.service.ssh_daemon', {}, locale, ['common']), 'gemini_daemon': t('ui.admin.dashboard.service.gemini_daemon', {}, locale, ['common']), 'mrc_daemon': t('ui.admin.dashboard.service.mrc_daemon', {}, locale, ['common']), 'multiplexing_server': t('ui.admin.dashboard.service.multiplexing_server', {}, locale, ['common']), 'mcp_server': t('ui.admin.dashboard.service.mcp_server', {}, locale, ['common']) } %} {% for key, label in coreServices %} {% set info = daemon_status[key] ?? null %} <div class="row align-items-center"> <div class="col-sm-6"> <strong>{{ label }}:</strong> </div> <div class="col-sm-6"> {% if info and info.running %} <span class="badge bg-success">{{ t('ui.admin.dashboard.running', {}, locale, ['common']) }}</span> {% else %} <span class="badge bg-danger">{{ t('ui.admin.dashboard.stopped', {}, locale, ['common']) }}</span> {% endif %} {% if info and info.pid %} <small class="text-muted ms-2">{{ t('ui.admin.dashboard.pid', {}, locale, ['common']) }} {{ info.pid }}</small> {% endif %} {% if key == 'realtime_server' %} <small class="text-muted ms-2">(<span id="binkstream-transport-mode">â?¦</span>)</small> {% endif %} </div> </div> <hr> {% endfor %} {% for key, label in optionalServices %} {% set info = daemon_status[key] ?? null %} <div class="row align-items-center"> <div class="col-sm-6"> <strong>{{ label }}:</strong> <small class="text-muted ms-1">({{ t('ui.common.optional', {}, locale, ['common']) }})</small> </div> <div class="col-sm-6"> {% if info and info.running %} <span class="badge bg-success">{{ t('ui.admin.dashboard.running', {}, locale, ['common']) }}</span> {% else %} <span class="badge bg-danger">{{ t('ui.admin.dashboard.stopped', {}, locale, ['common']) }}</span> {% endif %} {% if info and info.pid %} <small class="text-muted ms-2">{{ t('ui.admin.dashboard.pid', {}, locale, ['common']) }} {{ info.pid }}</small> {% endif %} </div> </div> {% if not loop.last %} <hr> {% endif %} {% endfor %} </div> </div> </div> <div class="col-lg-6 mb-4"> <div class="card shadow mb-4"> <div class="card-header py-3"> <h6 class="m-0 font-weight-bold text-primary">{{ t('ui.admin.dashboard.system_information', {}, locale, ['common']) }}</h6> </div> <div class="card-body"> <div class="row"> <div class="col-sm-6"> <strong>{{ t('ui.admin.dashboard.admin_users', {}, locale, ['common']) }}</strong> </div> <div class="col-sm-6"> {{ stats.admin_users }} </div> </div> <hr> <div class="row"> <div class="col-sm-6"> <strong>{{ t('ui.admin.dashboard.active_sessions', {}, locale, ['common']) }}</strong> </div> <div class="col-sm-6"> {{ stats.active_sessions }} / {{ stats.all_sessions }} </div> </div> {% if stats.system_metrics_supported %} <hr> <div class="row"> <div class="col-sm-6"> <strong>{{ t('ui.admin.dashboard.load_average', {}, locale, ['common']) }}</strong> </div> <div class="col-sm-6"> <span class="text-muted">{{ stats.load_average ?? t('ui.admin.dashboard.metric_unavailable', {}, locale, ['common']) }}</span> </div> </div> <hr> <div class="row"> <div class="col-sm-6"> <strong>{{ t('ui.admin.dashboard.ram_usage', {}, locale, ['common']) }}</strong> </div> <div class="col-sm-6"> {% if stats.ram_usage %} <button type="button" class="btn btn-link p-0 align-baseline ram-usage-link" data-bs-toggle="modal" data-bs-target="#ramUsageModal"> {{ stats.ram_usage }} </button> {% else %} <span class="text-muted">{{ t('ui.admin.dashboard.metric_unavailable', {}, locale, ['common']) }}</span> {% endif %} </div> </div> {% endif %} <hr> <div class="row"> <div class="col-sm-6"> <strong>{{ t('ui.admin.dashboard.system_address', {}, locale, ['common']) }}</strong> </div> <div class="col-sm-6"> {% if system_addresses is defined and system_addresses|length > 0 %} {{ system_addresses|join(', ') }} {% else %} {{ fidonetOrigin }} {% endif %} </div> </div> <hr> <div class="row"> <div class="col-sm-6"> <strong>{{ t('ui.admin.dashboard.version', {}, locale, ['common']) }}</strong> </div> <div class="col-sm-6"> {{ app_version }} </div> </div> <hr> <div class="row"> <div class="col-sm-6"> <strong>{{ t('ui.admin.dashboard.registration_status', {}, locale, ['common']) }}</strong> </div> <div class="col-sm-6"> {% if license_valid %} <span class="badge bg-success">{{ t('ui.admin.dashboard.registration_registered', {}, locale, ['common']) }}</span><BR/> <span class="text-muted ms-1">{{ t('ui.admin.dashboard.registration_to', {'system': license_system_name, 'licensee': license_licensee}, locale, ['common']) }}</span> {% else %} <span class="badge bg-secondary">{{ t('ui.admin.dashboard.registration_unregistered', {}, locale, ['common']) }}</span> <a href="/admin/licensing" class="ms-2 small">{{ t('ui.admin.dashboard.registration_link', {}, locale, ['common']) }}</a> {% endif %} </div> </div> <hr> <div class="row"> <div class="col-sm-6"> <strong>{{ t('ui.admin.dashboard.git_branch_commit', {}, locale, ['common']) }}</strong> </div> <div class="col-sm-6"> {% if git_branch %} <span class="badge bg-info">{{ git_branch }}</span> {% endif %} {% if git_commit %} <span class="text-muted">{{ git_commit|slice(0, 8) }}</span> {% else %} <span class="text-muted">{{ t('ui.common.not_applicable', {}, locale, ['common']) }}</span> {% endif %} </div> </div> <hr> <div class="row"> <div class="col-sm-6"> <strong>{{ t('ui.admin.dashboard.database_version', {}, locale, ['common']) }}</strong> </div> <div class="col-sm-6"> {% if db_version %} <span class="text-muted">{{ db_version }}</span> {% else %} <span class="text-muted">{{ t('ui.common.not_applicable', {}, locale, ['common']) }}</span> {% endif %} </div> </div> <hr> <div class="row align-items-center"> <div class="col-sm-6"> <strong>{{ t('ui.admin.dashboard.db_size', {}, locale, ['common']) }}</strong> </div> <div class="col-sm-6"> <span class="text-muted">{{ db_summary.db_size ?? 'N/A' }}</span> <a href="/admin/database-stats" class="ms-2 small">{{ t('ui.admin.dashboard.db_stats_link', {}, locale, ['common']) }} ?</a> </div> </div> </div> </div> <div class="card shadow d-none"> <div class="card-header py-3"> <h6 class="m-0 font-weight-bold text-primary">{{ t('ui.admin.dashboard.service_status', {}, locale, ['common']) }}</h6> </div> <div class="card-body"> {% set coreServices = { 'admin_daemon': t('ui.admin.dashboard.service.admin_daemon', {}, locale, ['common']), 'binkp_scheduler': t('ui.admin.dashboard.service.binkp_scheduler', {}, locale, ['common']), 'binkp_server': t('ui.admin.dashboard.service.binkp_server', {}, locale, ['common']), 'realtime_server': t('ui.admin.dashboard.service.realtime_server', {}, locale, ['common']) } %} {% set optionalServices = { 'ftp_daemon': t('ui.admin.dashboard.service.ftp_daemon', {}, locale, ['common']), 'telnetd': t('ui.admin.dashboard.service.telnetd', {}, locale, ['common']), 'ssh_daemon': t('ui.admin.dashboard.service.ssh_daemon', {}, locale, ['common']), 'gemini_daemon': t('ui.admin.dashboard.service.gemini_daemon', {}, locale, ['common']), 'mrc_daemon': t('ui.admin.dashboard.service.mrc_daemon', {}, locale, ['common']), 'multiplexing_server': t('ui.admin.dashboard.service.multiplexing_server', {}, locale, ['common']), 'mcp_server': t('ui.admin.dashboard.service.mcp_server', {}, locale, ['common']) } %} {% for key, label in coreServices %} {% set info = daemon_status[key] ?? null %} <div class="row align-items-center"> <div class="col-sm-6"> <strong>{{ label }}:</strong> </div> <div class="col-sm-6"> {% if info and info.running %} <span class="badge bg-success">{{ t('ui.admin.dashboard.running', {}, locale, ['common']) }}</span> {% else %} <span class="badge bg-danger">{{ t('ui.admin.dashboard.stopped', {}, locale, ['common']) }}</span> {% endif %} {% if info and info.pid %} <small class="text-muted ms-2">{{ t('ui.admin.dashboard.pid', {}, locale, ['common']) }} {{ info.pid }}</small> {% endif %} {% if key == 'realtime_server' %} <small class="text-muted ms-2">(<span id="binkstream-transport-mode">?</span>)</small> {% endif %} </div> </div> <hr> {% endfor %} {% for key, label in optionalServices %} {% set info = daemon_status[key] ?? null %} <div class="row align-items-center"> <div class="col-sm-6"> <strong>{{ label }}:</strong> <small class="text-muted ms-1">({{ t('ui.common.optional', {}, locale, ['common']) }})</small> </div> <div class="col-sm-6"> {% if info and info.running %} <span class="badge bg-success">{{ t('ui.admin.dashboard.running', {}, locale, ['common']) }}</span> {% else %} <span class="badge bg-danger">{{ t('ui.admin.dashboard.stopped', {}, locale, ['common']) }}</span> {% endif %} {% if info and info.pid %} <small class="text-muted ms-2">{{ t('ui.admin.dashboard.pid', {}, locale, ['common']) }} {{ info.pid }}</small> {% endif %} </div> </div> {% if not loop.last %} <hr> {% endif %} {% endfor %} </div> </div> <div class="card shadow mt-4"> <div class="card-header py-3 d-flex justify-content-between align-items-center"> <h6 class="m-0 font-weight-bold text-primary">Active BinkP Sessions</h6> <a href="/admin/binkp-sessions" class="small">View all</a> </div> <div class="card-body"> <div id="adminBinkpActiveSessions" class="small text-muted">Loading...</div> </div> </div> </div> </div> </div> <div class="modal fade" id="ramUsageModal" tabindex="-1" aria-hidden="true"> <div class="modal-dialog modal-xl modal-dialog-scrollable"> <div class="modal-content"> <div class="modal-header"> <h5 class="modal-title">binktop</h5> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{{ t('ui.common.close', {}, locale, ['common']) }}"></button> </div> <div class="modal-body"> <div id="ramUsageOutput" class="ansi-art-container small"><pre class="ansi-art mb-0">{{ t('ui.common.loading', {}, locale, ['common']) }}</pre></div> </div> <div class="modal-footer"> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ t('ui.common.close', {}, locale, ['common']) }}</button> </div> </div> </div> </div> <style> .border-left-primary { border-left: 0.25rem solid #4e73df !important; } .border-left-success { border-left: 0.25rem solid #1cc88a !important; } .border-left-info { border-left: 0.25rem solid #36b9cc !important; } .border-left-warning { border-left: 0.25rem solid #f6c23e !important; } /* Recent Updates Styling for Admin Dashboard */ /* Background colors are handled by theme stylesheets */ .update-item { border-left: 3px solid #4e73df; padding-left: 0.75rem; border-radius: 0.25rem; padding: 0.75rem; margin-bottom: 0.75rem !important; } .update-item:hover { transition: background-color 0.2s ease; } .update-item .badge { font-size: 0.7em; } .update-item h6 { line-height: 1.2; } .update-item p { font-size: 0.9em; line-height: 1.3; } .ram-usage-link { text-decoration: none; } .ram-usage-link:hover, .ram-usage-link:focus { text-decoration: underline; } #ramUsageOutput { min-height: 24rem; overflow-x: auto; } #ramUsageOutput .ansi-art { min-width: 100%; white-space: pre; display: inline-block; } </style> {% endblock %} {% block scripts %} <script src="/js/ansisys.js"></script> <script> (function () { var modeEl = document.getElementById('binkstream-transport-mode'); if (!modeEl) { return; } var modeLabels = { ws: 'WebSocket', sse: 'SSE', poll: 'poll' }; function updateMode(mode) { modeEl.textContent = modeLabels[mode] || mode; } function init() { if (window.BinkStream) { updateMode(window.BinkStream.getMode()); window.BinkStream.on('transport', function (data) { if (data && data.mode) { updateMode(data.mode); } }); } else { modeEl.textContent = '?'; } } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })(); </script> <script> (function () { var modalEl = document.getElementById('ramUsageModal'); var outputEl = document.getElementById('ramUsageOutput'); var refreshTimer = null; var requestInFlight = false; var hasLoadedOutput = false; if (!modalEl || !outputEl) { return; } function uiT(key, fallback) { if (typeof window.t === 'function') { return window.t(key, {}, fallback); } return fallback; } function renderBinktopOutput(text) { if (typeof window.renderAnsiBuffer === 'function') { outputEl.innerHTML = '<pre class="ansi-art mb-0">' + renderAnsiBuffer(text || '', 100, 120) + '</pre>'; return; } outputEl.innerHTML = '<pre class="ansi-art mb-0"></pre>'; var pre = outputEl.querySelector('pre'); if (pre) { pre.textContent = text || ''; } } function loadBinktop() { if (requestInFlight) { return; } requestInFlight = true; if (!hasLoadedOutput) { renderBinktopOutput(uiT('ui.common.loading', 'Loading...')); } fetch('/admin/api/ram-usage') .then(function (response) { return response.json().then(function (body) { if (!response.ok || body.success === false) { throw body; } return body; }); }) .then(function (body) { hasLoadedOutput = true; renderBinktopOutput(body.output || uiT('ui.admin.dashboard.metric_unavailable', 'Unavailable')); }) .catch(function (error) { hasLoadedOutput = true; renderBinktopOutput((error && error.error) || uiT('ui.admin.dashboard.ram_usage_modal_failed', 'Failed to load RAM usage details.')); }) .finally(function () { requestInFlight = false; }); } modalEl.addEventListener('show.bs.modal', function () { hasLoadedOutput = false; loadBinktop(); clearInterval(refreshTimer); refreshTimer = setInterval(loadBinktop, 2000); }); modalEl.addEventListener('hidden.bs.modal', function () { clearInterval(refreshTimer); refreshTimer = null; }); })(); </script> <script> (function () { var activeSessionsEl = document.getElementById('adminBinkpActiveSessions'); var activeSessionsTimer = null; if (!activeSessionsEl) { return; } function formatDuration(seconds) { if (seconds < 1) { return '&lt;1s'; } if (seconds < 60) { return Math.round(seconds) + 's'; } if (seconds < 3600) { return Math.round(seconds / 60) + 'm'; } return Math.round(seconds / 3600) + 'h'; } function escapeHtml(text) { var div = document.createElement('div'); div.textContent = text || ''; return div.innerHTML; } function loadActiveBinkpSessions() { fetch('/admin/api/binkp-sessions?status=active&limit=6') .then(function (response) { return response.json(); }) .then(function (data) { var sessions = Array.isArray(data.sessions) ? data.sessions : []; if (sessions.length === 0) { activeSessionsEl.innerHTML = '<span class="text-muted">No active BinkP sessions.</span>'; return; } activeSessionsEl.innerHTML = sessions.map(function (session) { var direction = session.is_inbound ? 'In' : 'Out'; var remoteAddress = escapeHtml(session.remote_address || 'Unknown'); var sessionType = escapeHtml(session.session_type || 'unknown'); var summary = (session.messages_received || 0) + ' in / ' + (session.messages_sent || 0) + ' out'; var duration = session.duration_seconds ? formatDuration(session.duration_seconds) : '&lt;1s'; return '' + '<div class="update-item mb-2">' + '<div class="d-flex justify-content-between align-items-start gap-2">' + '<div>' + '<div><code>' + remoteAddress + '</code></div>' + '<div class="text-muted">' + direction + ' | ' + sessionType + ' | ' + summary + '</div>' + '</div>' + '<span class="badge bg-primary">active</span>' + '</div>' + '<div class="text-muted mt-1">Running ' + duration + '</div>' + '</div>'; }).join(''); }) .catch(function () { activeSessionsEl.innerHTML = '<span class="text-danger">Failed to load BinkP sessions.</span>'; }); } function scheduleReload() { clearTimeout(activeSessionsTimer); activeSessionsTimer = setTimeout(loadActiveBinkpSessions, 300); } function init() { loadActiveBinkpSessions(); if (window.BinkStream && typeof window.BinkStream.on === 'function') { window.BinkStream.on('binkp_session', scheduleReload); window.addEventListener('beforeunload', function cleanupRealtime() { window.removeEventListener('beforeunload', cleanupRealtime); if (window.BinkStream && typeof window.BinkStream.off === 'function') { window.BinkStream.off('binkp_session', scheduleReload); } }); } } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })(); </script> {% endblock %}