PHP Classes

File: templates/admin/binkp_sessions.twig

Recommend this page to a friend!
  Packages of Matthew Asham   Binkterm PHP   templates/admin/binkp_sessions.twig   Download  
File: templates/admin/binkp_sessions.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: 41,646 bytes
 

Contents

Class file image Download
{% extends "base.twig" %} {% block title %}{{ t('ui.admin.binkp_sessions.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"><i class="fas fa-network-wired me-2"></i>{{ t('ui.admin.binkp_sessions.heading', {}, locale, ['common']) }}</h1> <div class="btn-toolbar"> <button class="btn btn-outline-primary me-2" id="refreshSessions"> <i class="fas fa-sync-alt me-1"></i>{{ t('ui.common.refresh', {}, locale, ['common']) }} </button> <button type="button" class="btn btn-outline-secondary" onclick="goBackFromBinkpSessions()"> <i class="fas fa-arrow-left me-1"></i>{{ t('ui.common.back', {}, locale, ['common']) }} </button> </div> </div> <!-- Session Statistics --> <div class="row mb-4"> <div class="col-xl-2 col-md-4 mb-4"> <div class="card border-left-primary shadow h-100 py-2"> <div class="card-body"> <div class="text-xs font-weight-bold text-primary text-uppercase mb-1" id="statTotalLabel">{{ t('ui.admin.binkp_sessions.total_24h', {}, locale, ['common']) }}</div> <div class="h5 mb-0 font-weight-bold" id="statTotal">-</div> </div> </div> </div> <div class="col-xl-2 col-md-4 mb-4"> <div class="card border-left-success shadow h-100 py-2"> <div class="card-body"> <div class="text-xs font-weight-bold text-success text-uppercase mb-1">{{ t('ui.admin.binkp_sessions.secure', {}, locale, ['common']) }}</div> <div class="h5 mb-0 font-weight-bold" id="statSecure">-</div> </div> </div> </div> <div class="col-xl-2 col-md-4 mb-4"> <div class="card border-left-warning shadow h-100 py-2"> <div class="card-body"> <div class="text-xs font-weight-bold text-warning text-uppercase mb-1">{{ t('ui.admin.binkp_sessions.insecure', {}, locale, ['common']) }}</div> <div class="h5 mb-0 font-weight-bold" id="statInsecure">-</div> </div> </div> </div> <div class="col-xl-2 col-md-4 mb-4"> <div class="card border-left-info shadow h-100 py-2"> <div class="card-body"> <div class="text-xs font-weight-bold text-info text-uppercase mb-1">{{ t('ui.admin.binkp_sessions.crash_out', {}, locale, ['common']) }}</div> <div class="h5 mb-0 font-weight-bold" id="statCrash">-</div> </div> </div> </div> <div class="col-xl-2 col-md-4 mb-4"> <div class="card border-left-success shadow h-100 py-2"> <div class="card-body"> <div class="text-xs font-weight-bold text-success text-uppercase mb-1">{{ t('ui.admin.binkp_sessions.successful', {}, locale, ['common']) }}</div> <div class="h5 mb-0 font-weight-bold" id="statSuccessful">-</div> </div> </div> </div> <div class="col-xl-2 col-md-4 mb-4"> <div class="card border-left-danger shadow h-100 py-2"> <div class="card-body"> <div class="text-xs font-weight-bold text-danger text-uppercase mb-1">{{ t('ui.admin.binkp_sessions.failed', {}, locale, ['common']) }}</div> <div class="h5 mb-0 font-weight-bold" id="statFailed">-</div> </div> </div> </div> </div> <div class="card shadow mb-4"> <div class="card-header py-3 d-flex flex-wrap justify-content-between align-items-center gap-2"> <h6 class="m-0 font-weight-bold text-primary" id="binkpChartTitle">{{ t('ui.admin.binkp_sessions.traffic_chart', {}, locale, ['common']) }}</h6> <div class="d-flex flex-wrap align-items-center gap-2"> <label for="binkpChartType" class="form-label mb-0 small text-muted">{{ t('ui.admin.binkp_sessions.chart_type_label', {}, locale, ['common']) }}</label> <select class="form-select form-select-sm" id="binkpChartType" style="width: auto;"> <option value="traffic">{{ t('ui.admin.binkp_sessions.traffic_chart', {}, locale, ['common']) }}</option> <option value="uplinks">{{ t('ui.admin.binkp_sessions.uplink_connections_chart', {}, locale, ['common']) }}</option> </select> <div class="btn-group btn-group-sm" role="group" aria-label="{{ t('ui.admin.binkp_sessions.chart_period_label', {}, locale, ['common']) }}"> <button type="button" class="btn btn-outline-secondary active" data-period="day">{{ t('ui.admin.binkp_sessions.period_24h', {}, locale, ['common']) }}</button> <button type="button" class="btn btn-outline-secondary" data-period="week">{{ t('ui.admin.binkp_sessions.period_7d', {}, locale, ['common']) }}</button> <button type="button" class="btn btn-outline-secondary" data-period="month">{{ t('ui.admin.binkp_sessions.period_1m', {}, locale, ['common']) }}</button> </div> </div> <div class="small text-muted d-flex flex-wrap gap-3" id="binkpChartLegend"> <span><span class="binkp-chart-legend binkp-chart-legend-in"></span>{{ t('ui.admin.binkp_sessions.files_in', {}, locale, ['common']) }}</span> <span><span class="binkp-chart-legend binkp-chart-legend-out"></span>{{ t('ui.admin.binkp_sessions.files_out', {}, locale, ['common']) }}</span> </div> </div> <div class="card-body"> <div id="binkpPrimaryChart" class="binkp-traffic-chart text-muted">{{ t('ui.common.loading', {}, locale, ['common']) }}</div> </div> </div> <!-- Filter --> <div class="card shadow mb-4"> <div class="card-header py-3"> <h6 class="m-0 font-weight-bold text-primary">{{ t('ui.admin.binkp_sessions.filter_sessions', {}, locale, ['common']) }}</h6> </div> <div class="card-body"> <div class="row g-3"> <div class="col-md-3"> <label class="form-label">{{ t('ui.admin.binkp_sessions.session_type', {}, locale, ['common']) }}</label> <select class="form-select" id="filterType"> <option value="">{{ t('ui.common.all', {}, locale, ['common']) }}</option> <option value="secure">{{ t('ui.admin.binkp_sessions.secure', {}, locale, ['common']) }}</option> <option value="insecure">{{ t('ui.admin.binkp_sessions.insecure', {}, locale, ['common']) }}</option> <option value="crash_outbound">{{ t('ui.admin.binkp_sessions.crash_outbound', {}, locale, ['common']) }}</option> </select> </div> <div class="col-md-3"> <label class="form-label">{{ t('ui.common.status', {}, locale, ['common']) }}</label> <select class="form-select" id="filterStatus"> <option value="">{{ t('ui.common.all', {}, locale, ['common']) }}</option> <option value="success">{{ t('ui.admin.binkp_sessions.success', {}, locale, ['common']) }}</option> <option value="failed">{{ t('ui.admin.binkp_sessions.failed', {}, locale, ['common']) }}</option> <option value="rejected">{{ t('ui.admin.binkp_sessions.rejected', {}, locale, ['common']) }}</option> </select> </div> <div class="col-md-3"> <label class="form-label">{{ t('ui.admin.binkp_sessions.remote_address', {}, locale, ['common']) }}</label> <input type="text" class="form-control" id="filterAddress" placeholder="{{ t('ui.admin.binkp_sessions.remote_address_placeholder', {}, locale, ['common']) }}"> </div> <div class="col-md-3 d-flex align-items-end"> <button class="btn btn-primary" id="applyFilter"> <i class="fas fa-filter me-1"></i>{{ t('ui.admin.binkp_sessions.apply_filter', {}, locale, ['common']) }} </button> </div> </div> </div> </div> <!-- Sessions Table --> <div class="card shadow mb-4"> <div class="card-header py-3"> <div class="d-flex flex-wrap justify-content-between align-items-center gap-2"> <h6 class="m-0 font-weight-bold text-primary">{{ t('ui.admin.binkp_sessions.recent_sessions', {}, locale, ['common']) }}</h6> <div class="d-flex flex-wrap align-items-center gap-2"> <label for="sessionsPerPage" class="form-label mb-0 small text-muted">{{ t('ui.admin.binkp_sessions.rows_per_page', {}, locale, ['common']) }}</label> <select class="form-select form-select-sm" id="sessionsPerPage" style="width: auto;"> <option value="25">25</option> <option value="50">50</option> <option value="100">100</option> <option value="250">250</option> </select> </div> </div> </div> <div class="card-body"> <div class="table-responsive"> <table class="table table-striped table-hover table-sm" id="sessionsTable"> <thead> <tr> <th>{{ t('ui.admin.binkp_sessions.time', {}, locale, ['common']) }}</th> <th>{{ t('ui.admin.binkp_sessions.remote_address', {}, locale, ['common']) }}</th> <th>{{ t('ui.admin.binkp_sessions.ip', {}, locale, ['common']) }}</th> <th>{{ t('ui.admin.binkp_sessions.pid', {}, locale, ['common']) }}</th> <th>{{ t('ui.admin.binkp_sessions.type', {}, locale, ['common']) }}</th> <th>{{ t('ui.admin.binkp_sessions.direction', {}, locale, ['common']) }}</th> <th>{{ t('ui.admin.binkp_sessions.files_in', {}, locale, ['common']) }}</th> <th>{{ t('ui.admin.binkp_sessions.files_out', {}, locale, ['common']) }}</th> <th>{{ t('ui.common.status', {}, locale, ['common']) }}</th> <th>{{ t('ui.admin.binkp_sessions.duration', {}, locale, ['common']) }}</th> </tr> </thead> <tbody id="sessionsTableBody"> <tr> <td colspan="10" class="text-center">{{ t('ui.common.loading', {}, locale, ['common']) }}</td> </tr> </tbody> </table> </div> <div class="d-flex flex-wrap justify-content-between align-items-center gap-2 mt-3"> <div class="small text-muted" id="sessionsPaginationStatus">{{ t('ui.common.loading', {}, locale, ['common']) }}</div> <nav aria-label="{{ t('ui.admin.binkp_sessions.pagination_aria', {}, locale, ['common']) }}"> <ul class="pagination pagination-sm mb-0"> <li class="page-item"> <button type="button" class="page-link" id="sessionsPrevPage">{{ t('ui.common.previous', {}, locale, ['common']) }}</button> </li> <li class="page-item disabled"> <span class="page-link" id="sessionsPageIndicator">1 / 1</span> </li> <li class="page-item"> <button type="button" class="page-link" id="sessionsNextPage">{{ t('ui.common.next', {}, locale, ['common']) }}</button> </li> </ul> </nav> </div> </div> </div> </div> <div class="modal fade" id="binkpSessionLogModal" 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" id="binkpSessionLogModalTitle">{{ t('ui.admin.binkp_sessions.log_modal_title', {}, locale, ['common']) }}</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="binkpSessionLogMeta" class="small text-muted mb-3"></div> <pre id="binkpSessionLogBody" class="small mb-0 bg-dark text-light p-3 rounded" style="min-height: 20rem;">{{ t('ui.admin.binkp_sessions.loading_log', {}, locale, ['common']) }}</pre> </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; } .border-left-danger { border-left: 0.25rem solid #e74a3b !important; } #sessionsTableBody tr[data-session-id] { cursor: pointer; } .binkp-traffic-chart { min-height: 260px; } .binkp-traffic-chart svg { width: 100%; height: 260px; display: block; } .binkp-chart-grid { stroke: rgba(108, 117, 125, 0.2); stroke-width: 1; } .binkp-chart-axis { stroke: rgba(108, 117, 125, 0.35); stroke-width: 1; } .binkp-chart-label { fill: currentColor; font-size: 12px; } .binkp-chart-line-in { stroke: #36b9cc; stroke-width: 3; fill: none; } .binkp-chart-line-out { stroke: #1cc88a; stroke-width: 3; fill: none; } .binkp-chart-point-in { fill: #36b9cc; } .binkp-chart-point-out { fill: #1cc88a; } .binkp-chart-bar { fill: #4e73df; } .binkp-chart-legend { display: inline-block; width: 12px; height: 12px; border-radius: 999px; margin-right: 0.35rem; vertical-align: -1px; } .binkp-chart-legend-in { background: #36b9cc; } .binkp-chart-legend-out { background: #1cc88a; } </style> <script> function uiT(key, fallback, params = {}) { if (window.t) { return window.t(key, params, fallback); } return fallback; } document.addEventListener('DOMContentLoaded', function() { loadI18nNamespaces(['common', 'errors']).then(function() { initSessionPagination(); loadStats(); loadSessions(); initRealtime(); initSessionLogModal(); }); document.getElementById('refreshSessions').addEventListener('click', function() { loadStats(); loadSessions(); }); document.getElementById('applyFilter').addEventListener('click', function() { sessionsCurrentPage = 1; loadSessions(); }); document.getElementById('sessionsPrevPage').addEventListener('click', function() { if (sessionsCurrentPage > 1) { sessionsCurrentPage--; renderSessionsTable(); } }); document.getElementById('sessionsNextPage').addEventListener('click', function() { const totalPages = getSessionsTotalPages(); if (sessionsCurrentPage < totalPages) { sessionsCurrentPage++; renderSessionsTable(); } }); document.getElementById('sessionsPerPage').addEventListener('change', function(event) { const value = Number(event.target.value) || DEFAULT_SESSIONS_PER_PAGE; sessionsPerPage = normalizeSessionsPerPage(value); sessionsCurrentPage = 1; persistSessionsPerPage(); renderSessionsTable(); }); document.getElementById('binkpChartType').addEventListener('change', function(event) { selectedChartType = String(event.target.value || 'traffic'); renderSelectedChart(); }); document.querySelectorAll('[data-period]').forEach(function(button) { button.addEventListener('click', function() { const period = String(button.getAttribute('data-period') || 'day'); if (period === selectedStatsPeriod) { return; } selectedStatsPeriod = period; updatePeriodButtons(); loadStats(); }); }); }); const SESSIONS_PER_PAGE_KEY = 'admin.binkpSessions.perPage'; const DEFAULT_SESSIONS_PER_PAGE = 25; const MAX_SESSION_FETCH = 250; const ALLOWED_SESSION_PAGE_SIZES = [25, 50, 100, 250]; const PERIOD_LABELS = { day: uiT('ui.admin.binkp_sessions.period_24h', '24 Hours'), week: uiT('ui.admin.binkp_sessions.period_7d', '7 Days'), month: uiT('ui.admin.binkp_sessions.period_1m', '1 Month') }; let allSessions = []; let sessionsCurrentPage = 1; let sessionsPerPage = DEFAULT_SESSIONS_PER_PAGE; let selectedStatsPeriod = 'day'; let selectedChartType = 'traffic'; let latestStatsPayload = null; function goBackFromBinkpSessions() { if (window.history.length > 1) { window.history.back(); return; } window.location.href = '/binkp'; } function initSessionPagination() { sessionsPerPage = loadStoredSessionsPerPage(); const select = document.getElementById('sessionsPerPage'); if (select) { select.value = String(sessionsPerPage); } } function normalizeSessionsPerPage(value) { return ALLOWED_SESSION_PAGE_SIZES.includes(value) ? value : DEFAULT_SESSIONS_PER_PAGE; } function loadStoredSessionsPerPage() { try { const storedValue = window.UserStorage ? window.UserStorage.getItem(SESSIONS_PER_PAGE_KEY) : null; return normalizeSessionsPerPage(Number(storedValue)); } catch (_) { return DEFAULT_SESSIONS_PER_PAGE; } } function persistSessionsPerPage() { try { if (window.UserStorage) { window.UserStorage.setItem(SESSIONS_PER_PAGE_KEY, String(sessionsPerPage)); } } catch (_) {} } function getSessionsTotalPages() { if (!allSessions.length) { return 1; } return Math.max(1, Math.ceil(allSessions.length / sessionsPerPage)); } function loadStats() { updateStatsSummaryLabel(); fetch(`/admin/api/binkp-sessions/stats?period=${encodeURIComponent(selectedStatsPeriod)}`) .then(response => response.json()) .then(data => { latestStatsPayload = data || {}; document.getElementById('statTotal').textContent = data.total_sessions || 0; document.getElementById('statSecure').textContent = data.secure_sessions || 0; document.getElementById('statInsecure').textContent = data.insecure_sessions || 0; document.getElementById('statCrash').textContent = data.crash_sessions || 0; document.getElementById('statSuccessful').textContent = data.successful || 0; document.getElementById('statFailed').textContent = data.failed || 0; renderSelectedChart(); }) .catch(error => { console.error(uiT('ui.admin.binkp_sessions.load_stats_failed', 'Error loading stats') + ':', error); latestStatsPayload = null; renderSelectedChart(); }); } function renderTrafficChart(points) { renderLineChart({ containerId: 'binkpPrimaryChart', points, emptyText: uiT('ui.admin.binkp_sessions.traffic_chart_empty', 'No file traffic recorded in the selected period.'), ariaLabel: uiT('ui.admin.binkp_sessions.traffic_chart_aria', 'BinkP file traffic for the selected period'), series: [ { key: 'messages_received', lineClass: 'binkp-chart-line-in', pointClass: 'binkp-chart-point-in' }, { key: 'messages_sent', lineClass: 'binkp-chart-line-out', pointClass: 'binkp-chart-point-out' } ] }); } function renderUplinkConnectionsChart(points) { renderBarChart({ containerId: 'binkpPrimaryChart', points, valueKey: 'count', emptyText: uiT('ui.admin.binkp_sessions.uplink_connections_chart_empty', 'No uplink connections recorded in the selected period.'), ariaLabel: uiT('ui.admin.binkp_sessions.uplink_connections_chart_aria', 'BinkP uplink connections for the selected period') }); } function renderSelectedChart() { updateChartHeader(); if (selectedChartType === 'uplinks') { renderUplinkConnectionsChart(Array.isArray(latestStatsPayload?.uplink_connections) ? latestStatsPayload.uplink_connections : []); return; } renderTrafficChart(Array.isArray(latestStatsPayload?.timeline) ? latestStatsPayload.timeline : []); } function renderLineChart(config) { const container = document.getElementById(config.containerId); if (!container) { return; } const points = Array.isArray(config.points) ? config.points : []; if (points.length === 0) { container.innerHTML = `<div class="text-muted">${escapeHtml(config.emptyText)}</div>`; return; } const chart = createChartFrame(points, point => config.series.map(series => Number(point[series.key] || 0))); const xLabels = buildXAxisLabels(points, chart, formatTrafficChartLabel); const seriesMarkup = config.series.map(series => { const path = buildLinePath(points, chart, series.key); const dots = points.map((point, index) => `<circle class="${series.pointClass}" cx="${chart.xFor(index)}" cy="${chart.yFor(point[series.key])}" r="3"></circle>`).join(''); return `<path class="${series.lineClass}" d="${path}"></path>${dots}`; }).join(''); container.innerHTML = renderChartSvg(chart, config.ariaLabel, `${seriesMarkup}${xLabels}`); } function renderBarChart(config) { const container = document.getElementById(config.containerId); if (!container) { return; } const points = Array.isArray(config.points) ? config.points : []; if (points.length === 0) { container.innerHTML = `<div class="text-muted">${escapeHtml(config.emptyText)}</div>`; return; } const chart = createChartFrame(points, point => [Number(point[config.valueKey] || 0)], { bottom: 68 }); const barWidth = Math.max(16, Math.min(64, chart.chartWidth / Math.max(points.length * 1.8, 1))); const bars = points.map((point, index) => { const value = Number(point[config.valueKey] || 0); const x = chart.xFor(index) - (barWidth / 2); const y = chart.yFor(value); const height = Math.max(0, chart.height - chart.padding.bottom - y); const labelY = chart.height - 18 + ((index % 2) * 12); return ` <rect class="binkp-chart-bar" x="${x}" y="${y}" width="${barWidth}" height="${height}"></rect> <text class="binkp-chart-label" x="${chart.xFor(index)}" y="${labelY}" text-anchor="middle">${escapeHtml(point.label || '')}</text> `; }).join(''); container.innerHTML = renderChartSvg(chart, config.ariaLabel, bars); } function createChartFrame(points, valueExtractor, paddingOverrides = {}) { const width = 960; const height = 260; const padding = Object.assign({ top: 18, right: 18, bottom: 34, left: 42 }, paddingOverrides); const chartWidth = width - padding.left - padding.right; const chartHeight = height - padding.top - padding.bottom; const maxValue = Math.max(1, ...points.flatMap(point => valueExtractor(point))); const xFor = function(index) { if (points.length === 1) { return padding.left + chartWidth / 2; } return padding.left + (chartWidth * index / (points.length - 1)); }; const yFor = function(value) { return padding.top + chartHeight - ((Number(value || 0) / maxValue) * chartHeight); }; const yTicks = [0, 0.25, 0.5, 0.75, 1].map(fraction => Math.round(maxValue * fraction)); const uniqueTicks = [...new Set(yTicks)].sort((a, b) => a - b); const gridLines = uniqueTicks.map(value => { const y = yFor(value); return `<g><line class="binkp-chart-grid" x1="${padding.left}" y1="${y}" x2="${width - padding.right}" y2="${y}"></line><text class="binkp-chart-label" x="${padding.left - 8}" y="${y + 4}" text-anchor="end">${value}</text></g>`; }).join(''); return { width, height, padding, chartWidth, chartHeight, xFor, yFor, gridLines }; } function renderChartSvg(chart, ariaLabel, innerMarkup) { return ` <svg viewBox="0 0 ${chart.width} ${chart.height}" preserveAspectRatio="none" role="img" aria-label="${escapeHtmlAttr(ariaLabel)}"> ${chart.gridLines} <line class="binkp-chart-axis" x1="${chart.padding.left}" y1="${chart.padding.top + chart.chartHeight}" x2="${chart.width - chart.padding.right}" y2="${chart.padding.top + chart.chartHeight}"></line> <line class="binkp-chart-axis" x1="${chart.padding.left}" y1="${chart.padding.top}" x2="${chart.padding.left}" y2="${chart.padding.top + chart.chartHeight}"></line> ${innerMarkup} </svg> `; } function buildLinePath(points, chart, key) { return points.map((point, index) => `${index === 0 ? 'M' : 'L'} ${chart.xFor(index).toFixed(2)} ${chart.yFor(point[key]).toFixed(2)}`).join(' '); } function buildXAxisLabels(points, chart, formatter) { return points.map((point, index) => { if (index !== 0 && index !== points.length - 1 && (index % Math.max(1, Math.ceil(points.length / 6))) !== 0) { return ''; } return `<text class="binkp-chart-label" x="${chart.xFor(index)}" y="${chart.height - 10}" text-anchor="middle">${escapeHtml(formatter(point))}</text>`; }).join(''); } function updatePeriodButtons() { document.querySelectorAll('[data-period]').forEach(function(button) { const isActive = button.getAttribute('data-period') === selectedStatsPeriod; button.classList.toggle('active', isActive); button.classList.toggle('btn-outline-secondary', !isActive); button.classList.toggle('btn-primary', isActive); }); } function updateStatsSummaryLabel() { const label = document.getElementById('statTotalLabel'); if (!label) { return; } label.textContent = uiT('ui.admin.binkp_sessions.total_period', 'Total ({period})', { period: PERIOD_LABELS[selectedStatsPeriod] || PERIOD_LABELS.day }); } function updateChartHeader() { const titleEl = document.getElementById('binkpChartTitle'); const legendEl = document.getElementById('binkpChartLegend'); if (titleEl) { titleEl.textContent = selectedChartType === 'uplinks' ? uiT('ui.admin.binkp_sessions.uplink_connections_chart', 'Uplink Connections') : uiT('ui.admin.binkp_sessions.traffic_chart', 'File Traffic'); } if (!legendEl) { return; } if (selectedChartType === 'uplinks') { legendEl.innerHTML = `<span><span class="binkp-chart-legend binkp-chart-legend-in"></span>${escapeHtml(uiT('ui.admin.binkp_sessions.connections', 'Connections'))}</span>`; return; } legendEl.innerHTML = ` <span><span class="binkp-chart-legend binkp-chart-legend-in"></span>${escapeHtml(uiT('ui.admin.binkp_sessions.files_in', 'Files In'))}</span> <span><span class="binkp-chart-legend binkp-chart-legend-out"></span>${escapeHtml(uiT('ui.admin.binkp_sessions.files_out', 'Files Out'))}</span> `; } function renderRemoteAddressCell(session) { const remoteAddress = session && session.remote_address; const value = String(remoteAddress || '').trim(); if (!value) { return `<code>${escapeHtml(uiT('ui.common.unknown', 'Unknown'))}</code>`; } const resolvedDomain = String((session && session.remote_domain) || '').trim(); const atIndex = value.lastIndexOf('@'); if (atIndex > 0 && atIndex < value.length - 1) { const nodeAddress = value.slice(0, atIndex); const domain = value.slice(atIndex + 1); return ` <div><code>${escapeHtml(nodeAddress)}</code></div> <small class="text-muted">${escapeHtml(domain)}</small> `; } if (resolvedDomain) { return ` <div><code>${escapeHtml(value)}</code></div> <small class="text-muted">${escapeHtml(resolvedDomain)}</small> `; } return `<code>${escapeHtml(value)}</code>`; } function loadSessions() { const sessionType = document.getElementById('filterType').value; const status = document.getElementById('filterStatus').value; const address = document.getElementById('filterAddress').value; let url = `/admin/api/binkp-sessions?limit=${MAX_SESSION_FETCH}`; if (sessionType) url += `&session_type=${sessionType}`; if (status) url += `&status=${status}`; if (address) url += `&remote_address=${encodeURIComponent(address)}`; fetch(url) .then(response => response.json()) .then(data => { const previousFirstSessionId = allSessions.length > 0 ? allSessions[0].id : null; allSessions = Array.isArray(data.sessions) ? data.sessions : []; if (previousFirstSessionId !== null && allSessions.length > 0 && allSessions[0].id !== previousFirstSessionId) { sessionsCurrentPage = 1; } const totalPages = getSessionsTotalPages(); if (sessionsCurrentPage > totalPages) { sessionsCurrentPage = totalPages; } renderSessionsTable(); }) .catch(error => { console.error(uiT('ui.admin.binkp_sessions.load_sessions_failed', 'Error loading sessions') + ':', error); allSessions = []; renderSessionsTable(true); }); } function renderSessionsTable(hasError = false) { const tbody = document.getElementById('sessionsTableBody'); const prevButton = document.getElementById('sessionsPrevPage'); const nextButton = document.getElementById('sessionsNextPage'); const pageIndicator = document.getElementById('sessionsPageIndicator'); const paginationStatus = document.getElementById('sessionsPaginationStatus'); if (!tbody || !prevButton || !nextButton || !pageIndicator || !paginationStatus) { return; } if (hasError) { tbody.innerHTML = '<tr><td colspan="10" class="text-center text-danger">' + uiT('ui.admin.binkp_sessions.load_sessions_failed', 'Error loading sessions') + '</td></tr>'; paginationStatus.textContent = uiT('ui.admin.binkp_sessions.load_sessions_failed', 'Error loading sessions'); pageIndicator.textContent = '1 / 1'; prevButton.disabled = true; nextButton.disabled = true; setPaginationDisabledState(prevButton, true); setPaginationDisabledState(nextButton, true); return; } if (!allSessions.length) { tbody.innerHTML = '<tr><td colspan="10" class="text-center text-muted">' + uiT('ui.admin.binkp_sessions.no_sessions_found', 'No sessions found') + '</td></tr>'; paginationStatus.textContent = uiT('ui.admin.binkp_sessions.page_status_empty', '0 sessions'); pageIndicator.textContent = '1 / 1'; prevButton.disabled = true; nextButton.disabled = true; setPaginationDisabledState(prevButton, true); setPaginationDisabledState(nextButton, true); return; } const totalPages = getSessionsTotalPages(); const currentPage = Math.min(Math.max(1, sessionsCurrentPage), totalPages); sessionsCurrentPage = currentPage; const startIndex = (currentPage - 1) * sessionsPerPage; const endIndex = Math.min(startIndex + sessionsPerPage, allSessions.length); const pageSessions = allSessions.slice(startIndex, endIndex); tbody.innerHTML = pageSessions.map(session => ` <tr data-session-id="${session.id}"> <td><small>${formatLocalizedDateTime(session.started_at)}</small></td> <td>${renderRemoteAddressCell(session)}</td> <td><small>${escapeHtml(session.remote_ip || '-')}</small></td> <td> <div><code>${escapeHtml(String(session.process_id || '-'))}</code></div> <small class="text-muted">${escapeHtml(session.log_file || '-')}</small> </td> <td>${getTypeBadge(session.session_type)}</td> <td>${session.is_inbound ? '<span class="badge bg-info"><i class="fas fa-arrow-down"></i> ' + uiT('ui.admin.binkp_sessions.in', 'In') + '</span>' : '<span class="badge bg-secondary"><i class="fas fa-arrow-up"></i> ' + uiT('ui.admin.binkp_sessions.out', 'Out') + '</span>'}</td> <td>${session.messages_received || 0}</td> <td>${session.messages_sent || 0}</td> <td>${getStatusBadge(session.status)} ${session.error_message ? `<br><small class="text-danger">${escapeHtml(session.error_message.substring(0, 50))}</small>` : ''}</td> <td>${session.duration_seconds ? formatDuration(session.duration_seconds) : '-'}</td> </tr> `).join(''); paginationStatus.textContent = uiT( 'ui.admin.binkp_sessions.page_status', 'Showing {start}-{end} of {total}', { start: startIndex + 1, end: endIndex, total: allSessions.length } ); pageIndicator.textContent = uiT( 'ui.admin.binkp_sessions.page_indicator', 'Page {page} of {pages}', { page: currentPage, pages: totalPages } ); prevButton.disabled = currentPage <= 1; nextButton.disabled = currentPage >= totalPages; setPaginationDisabledState(prevButton, currentPage <= 1); setPaginationDisabledState(nextButton, currentPage >= totalPages); } function setPaginationDisabledState(button, disabled) { const item = button ? button.closest('.page-item') : null; if (!item) { return; } item.classList.toggle('disabled', disabled); } let binkpSessionRefreshTimer = null; let binkpSessionRealtimeHandler = null; function scheduleRealtimeRefresh() { clearTimeout(binkpSessionRefreshTimer); binkpSessionRefreshTimer = setTimeout(function() { loadStats(); loadSessions(); }, 300); } function initRealtime() { if (!window.BinkStream || typeof window.BinkStream.on !== 'function') { return; } binkpSessionRealtimeHandler = function () { scheduleRealtimeRefresh(); }; window.BinkStream.on('binkp_session', binkpSessionRealtimeHandler); window.addEventListener('beforeunload', function cleanupRealtime() { window.removeEventListener('beforeunload', cleanupRealtime); if (window.BinkStream && typeof window.BinkStream.off === 'function' && binkpSessionRealtimeHandler) { window.BinkStream.off('binkp_session', binkpSessionRealtimeHandler); } }); } let binkpSessionLogModal = null; function initSessionLogModal() { const modalEl = document.getElementById('binkpSessionLogModal'); const tbody = document.getElementById('sessionsTableBody'); if (!modalEl || !tbody || typeof bootstrap === 'undefined') { return; } binkpSessionLogModal = new bootstrap.Modal(modalEl); tbody.addEventListener('click', function (event) { const row = event.target.closest('tr[data-session-id]'); if (!row) { return; } loadSessionLog(row.getAttribute('data-session-id')); }); } function loadSessionLog(sessionId) { const titleEl = document.getElementById('binkpSessionLogModalTitle'); const metaEl = document.getElementById('binkpSessionLogMeta'); const bodyEl = document.getElementById('binkpSessionLogBody'); if (!titleEl || !metaEl || !bodyEl || !binkpSessionLogModal) { return; } titleEl.textContent = uiT('ui.admin.binkp_sessions.log_modal_title', 'Session Log'); metaEl.textContent = ''; bodyEl.textContent = uiT('ui.admin.binkp_sessions.loading_log', 'Loading log...'); binkpSessionLogModal.show(); fetch(`/admin/api/binkp-sessions/${encodeURIComponent(sessionId)}/logs`) .then(response => response.json()) .then(data => { if (!data.success || !data.session) { throw new Error(uiT('ui.admin.binkp_sessions.log_load_failed', 'Failed to load session log')); } const session = data.session; titleEl.textContent = `${session.remote_address || uiT('ui.common.unknown', 'Unknown')} | PID ${session.process_id || '-'}`; metaEl.textContent = `${uiT('ui.admin.binkp_sessions.log_file', 'Log File')}: ${session.log_file || '-'} | ${uiT('ui.admin.binkp_sessions.pid', 'PID')}: ${session.process_id || '-'}`; const lines = data.logs && Array.isArray(data.logs.lines) ? data.logs.lines : []; if (!session.process_id) { bodyEl.textContent = uiT('ui.admin.binkp_sessions.log_unavailable', 'No PID was recorded for this session.'); return; } if (lines.length === 0) { bodyEl.textContent = uiT('ui.admin.binkp_sessions.no_log_lines', 'No log lines found for this session.'); return; } bodyEl.textContent = lines.map(line => line.line || '').join('\n'); }) .catch(error => { console.error(uiT('ui.admin.binkp_sessions.log_load_failed', 'Failed to load session log') + ':', error); bodyEl.textContent = uiT('ui.admin.binkp_sessions.log_load_failed', 'Failed to load session log'); }); } function getTypeBadge(type) { const badges = { 'secure': '<span class="badge bg-success">' + uiT('ui.admin.binkp_sessions.secure', 'Secure') + '</span>', 'insecure': '<span class="badge bg-warning">' + uiT('ui.admin.binkp_sessions.insecure', 'Insecure') + '</span>', 'crash_outbound': '<span class="badge bg-info">' + uiT('ui.admin.binkp_sessions.crash', 'Crash') + '</span>' }; return badges[type] || `<span class="badge bg-secondary">${type}</span>`; } function getStatusBadge(status) { const badges = { 'active': '<span class="badge bg-primary">' + uiT('ui.admin.binkp_sessions.active', 'Active') + '</span>', 'success': '<span class="badge bg-success">' + uiT('ui.admin.binkp_sessions.success', 'Success') + '</span>', 'failed': '<span class="badge bg-danger">' + uiT('ui.admin.binkp_sessions.failed', 'Failed') + '</span>', 'rejected': '<span class="badge bg-warning">' + uiT('ui.admin.binkp_sessions.rejected', 'Rejected') + '</span>' }; return badges[status] || `<span class="badge bg-secondary">${status}</span>`; } function formatDate(dateStr) { return formatLocalizedDateTime(dateStr); } function normalizeServerDate(dateStr) { if (!dateStr || typeof dateStr !== 'string') { return null; } let normalized = dateStr.trim().replace(' ', 'T'); normalized = normalized.replace(/([+-]\d{2})$/, '$1:00'); normalized = normalized.replace(/([+-]\d{2})(\d{2})$/, '$1:$2'); return normalized; } function formatLocalizedDateTime(dateStr) { const parser = typeof window.parseAppDate === 'function' ? window.parseAppDate : function(value) { const normalized = normalizeServerDate(value); if (!normalized) { return null; } const parsed = new Date(normalized); return Number.isNaN(parsed.getTime()) ? null : parsed; }; const date = parser(dateStr); if (!date) { return '-'; } const userTimezone = getUserTimezone(); const userDateFormat = window.userSettings?.date_format || undefined; return date.toLocaleString(userDateFormat, { timeZone: userTimezone, year: 'numeric', month: 'short', day: '2-digit', hour: 'numeric', minute: '2-digit', second: '2-digit' }); } function getUserTimezone() { return window.userSettings?.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC'; } function formatTrafficChartLabel(point) { const epochSeconds = Number(point?.bucket_epoch || 0); const userDateFormat = window.userSettings?.date_format || undefined; if (epochSeconds > 0) { const date = new Date(epochSeconds * 1000); if (!Number.isNaN(date.getTime())) { if (selectedStatsPeriod === 'day') { return date.toLocaleTimeString(userDateFormat, { hour: 'numeric', minute: '2-digit' }); } return date.toLocaleDateString(userDateFormat, { month: 'short', day: 'numeric' }); } } return point?.label || ''; } function formatDuration(seconds) { if (seconds < 1) return '<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) { if (!text) return ''; const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } function escapeHtmlAttr(text) { return escapeHtml(text); } </script> {% endblock %}