PHP Classes

File: templates/binkp.twig

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

Contents

Class file image Download
?{% extends "base.twig" %} {% block title %}{{ t('ui.binkp.page_title') }}{% endblock %} {% block content %} <div class="container-fluid"> <div class="row"> <div class="col-md-12"> <h1>{{ t('ui.binkp.heading') }}</h1> <p class="text-muted">{{ t('ui.binkp.subheading') }}</p> </div> </div> <!-- Status Overview --> <div class="row mb-4"> <div class="col-md-3"> <div class="card"> <div class="card-body text-center"> <h5 class="card-title">{{ t('ui.binkp.system_status') }}</h5> <p class="card-text" id="system-status">{{ t('ui.common.loading') }}</p> </div> </div> </div> <div class="col-md-3"> <div class="card"> <div class="card-body text-center"> <h5 class="card-title">{{ t('ui.binkp.inbound_queue') }}</h5> <p class="card-text" id="inbound-count">{{ t('ui.common.loading') }}</p> </div> </div> </div> <div class="col-md-3"> <div class="card"> <div class="card-body text-center"> <h5 class="card-title">{{ t('ui.binkp.outbound_queue') }}</h5> <p class="card-text" id="outbound-count">{{ t('ui.common.loading') }}</p> </div> </div> </div> <div class="col-md-3"> <div class="card"> <div class="card-body text-center"> <h5 class="card-title">{{ t('ui.binkp.uplinks') }}</h5> <p class="card-text" id="uplink-count">{{ t('ui.common.loading') }}</p> </div> </div> </div> </div> <!-- Navigation Tabs --> <ul class="nav nav-tabs" id="binkpTabs" role="tablist"> <li class="nav-item" role="presentation"> <button class="nav-link active" id="status-tab" data-bs-toggle="tab" data-bs-target="#status" type="button" role="tab">{{ t('ui.binkp.status_tab') }}</button> </li> <li class="nav-item" role="presentation"> <button class="nav-link" id="uplinks-tab" data-bs-toggle="tab" data-bs-target="#uplinks" type="button" role="tab">{{ t('ui.binkp.uplinks_tab') }}</button> </li> <li class="nav-item" role="presentation"> <button class="nav-link" id="queues-tab" data-bs-toggle="tab" data-bs-target="#queues" type="button" role="tab">{{ t('ui.binkp.queues_tab') }}</button> </li> <li class="nav-item" role="presentation"> <button class="nav-link" id="kept-packets-tab" data-bs-toggle="tab" data-bs-target="#kept-packets" type="button" role="tab"{% if license_valid %} onclick="onKeptPacketsTabOpen()"{% endif %}> {{ t('ui.binkp.kept_packets_tab') }}{% if not license_valid %} <i class="fas fa-lock ms-1 text-warning small"></i>{% endif %} </button> </li> <li class="nav-item" role="presentation"> <a class="nav-link" id="sessions-tab" href="/admin/binkp-sessions">{{ t('ui.admin.binkp_sessions.heading', {}, locale, ['common']) }}</a> </li> <li class="nav-item" role="presentation"> <button class="nav-link" id="logs-tab" data-bs-toggle="tab" data-bs-target="#logs" type="button" role="tab">{{ t('ui.binkp.logs_tab') }}</button> </li> </ul> <div class="tab-content mt-3" id="binkpTabContent"> <!-- Status Tab --> <div class="tab-pane fade show active" id="status" role="tabpanel"> <div class="row"> <div class="col-md-6"> <div class="card"> <div class="card-header d-flex justify-content-between align-items-center"> <h5>{{ t('ui.binkp.system_information') }}</h5> <button class="btn btn-sm btn-outline-primary" onclick="refreshBinkpOverview()"> <i class="fas fa-refresh"></i> {{ t('ui.common.refresh') }} </button> </div> <div class="card-body" id="system-info"> {{ t('ui.binkp.loading_system_information') }} </div> </div> </div> <div class="col-md-6"> <div class="card"> <div class="card-header d-flex justify-content-between align-items-center"> <h5>{{ t('ui.binkp.uplink_status') }}</h5> <button class="btn btn-sm btn-success" onclick="pollAllUplinks()"> <i class="fas fa-sync"></i> {{ t('ui.binkp.poll_all') }} </button> </div> <div class="card-body" id="uplink-status"> {{ t('ui.binkp.loading_uplink_status') }} </div> </div> </div> </div> </div> <!-- Uplinks Tab --> <div class="tab-pane fade" id="uplinks" role="tabpanel"> <div class="d-flex justify-content-between align-items-center mb-3"> <h5>{{ t('ui.binkp.configured_uplinks') }}</h5> {# <button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addUplinkModal"> <i class="fas fa-plus"></i> {{ t('ui.binkp.add_uplink') }} </button>#} </div> <div class="table-responsive"> <table class="table table-striped" id="uplinksTable"> <thead> <tr> <th>{{ t('ui.binkp.uplink') }}</th> <th>{{ t('ui.common.hostname') }}</th> <th>{{ t('ui.common.port') }}</th> <th>{{ t('ui.binkp.status') }}</th> <th>{{ t('ui.binkp.schedule') }}</th> <th>{{ t('ui.binkp.actions') }}</th> </tr> </thead> <tbody> <!-- Populated by JavaScript --> </tbody> </table> </div> </div> <!-- Queues Tab --> <div class="tab-pane fade" id="queues" role="tabpanel"> <div class="row"> <div class="col-md-6"> <div class="card"> <div class="card-header d-flex justify-content-between align-items-center"> <h5>{{ t('ui.binkp.inbound_queue') }}</h5> <button class="btn btn-sm btn-success" onclick="processInbound()"> <i class="fas fa-play"></i> {{ t('ui.binkp.process') }} </button> </div> <div class="card-body"> <div id="inbound-files">{{ t('ui.common.loading') }}</div> </div> </div> </div> <div class="col-md-6"> <div class="card"> <div class="card-header d-flex justify-content-between align-items-center"> <h5>{{ t('ui.binkp.outbound_queue') }}</h5> <button class="btn btn-sm btn-success" onclick="processOutbound()"> <i class="fas fa-paper-plane"></i> {{ t('ui.binkp.send') }} </button> </div> <div class="card-body"> <div id="outbound-files">{{ t('ui.common.loading') }}</div> </div> </div> </div> </div> </div> <!-- Kept Packets Tab --> <div class="tab-pane fade" id="kept-packets" role="tabpanel"> {% if license_valid %} <div class="d-flex justify-content-between align-items-center mb-3"> <h5>{{ t('ui.binkp.kept_packets_tab') }}</h5> <div class="btn-group" role="group"> <button type="button" class="btn btn-sm btn-outline-primary active" id="keptInboundBtn" onclick="switchKeptType('inbound')"> <i class="fas fa-inbox"></i> {{ t('ui.binkp.kept_inbound') }} </button> <button type="button" class="btn btn-sm btn-outline-primary" id="keptOutboundBtn" onclick="switchKeptType('outbound')"> <i class="fas fa-paper-plane"></i> {{ t('ui.binkp.kept_outbound') }} </button> </div> </div> <div id="kept-packets-content"> <div class="text-center text-muted py-4"><span class="spinner-border spinner-border-sm me-2"></span>{{ t('ui.common.loading') }}</div> </div> {% else %} <div style="position:relative;"> <!-- Blurred placeholder content --> <div style="filter:blur(4px); pointer-events:none; user-select:none; opacity:0.6;"> <div class="d-flex justify-content-between align-items-center mb-3"> <h5>{{ t('ui.binkp.kept_packets_tab') }}</h5> <div class="btn-group" role="group"> <button type="button" class="btn btn-sm btn-outline-primary active" disabled> <i class="fas fa-inbox"></i> {{ t('ui.binkp.kept_inbound') }} </button> <button type="button" class="btn btn-sm btn-outline-primary" disabled> <i class="fas fa-paper-plane"></i> {{ t('ui.binkp.kept_outbound') }} </button> </div> </div> <div class="accordion"> <div class="accordion-item"> <h2 class="accordion-header"> <button class="accordion-button" type="button" disabled> <strong>Mar-18-2026</strong><span class="badge bg-secondary ms-2">12</span> </button> </h2> <div class="accordion-collapse collapse show"> <div class="accordion-body p-0"> <table class="table table-sm mb-0"> <thead><tr><th>Filename</th><th>From</th><th>To</th><th>Messages</th><th>Size</th><th>Date</th></tr></thead> <tbody> <tr><td><code>69ba718d.pkt</code></td><td>1:1/100</td><td>1:2/200</td><td>3</td><td>4.2 KB</td><td>2026-03-18 09:15:00</td></tr> <tr><td><code>69ba93c4.pkt</code></td><td>1:1/100</td><td>1:3/300</td><td>7</td><td>8.1 KB</td><td>2026-03-18 09:22:00</td></tr> <tr><td><code>69bab14d.pkt</code></td><td>1:2/200</td><td>1:1/100</td><td>1</td><td>1.9 KB</td><td>2026-03-18 09:30:00</td></tr> </tbody> </table> </div> </div> </div> </div> </div> <!-- Lock overlay --> <div style="position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:0.75rem;"> <i class="fas fa-lock fa-3x text-warning"></i> <h5 class="mb-0">{{ t('ui.binkp.kept_packets_tab') }}</h5> <p class="text-muted mb-0">{{ t('ui.binkp.kept_packets_locked') }}</p> <a href="/admin/licensing" class="btn btn-warning btn-sm mt-1"> <i class="fas fa-key me-1"></i>{{ t('ui.binkp.kept_packets_register') }} </a> </div> </div> {% endif %} </div> <!-- Logs Tab --> <div class="tab-pane fade" id="logs" role="tabpanel"> <div class="d-flex justify-content-between align-items-center mb-3"> <h5>{{ t('ui.binkp.logs_heading') }}</h5> <div class="d-flex align-items-center gap-2 flex-wrap"> <div class="input-group input-group-sm" style="width: 220px;"> <span class="input-group-text"><i class="fas fa-search"></i></span> <input type="text" class="form-control" id="logSearch" placeholder="{{ t('ui.common.search') }}" oninput="filterLogs()"> <button class="btn btn-outline-secondary" type="button" id="logSearchClear" onclick="clearLogSearch()" style="display:none;" title="{{ t('ui.common.clear_search') }}"><i class="fas fa-times"></i></button> </div> <small class="text-muted" id="logMatchCount" style="display:none;"></small> <select class="form-select form-select-sm w-auto" id="logLines"> <option value="50">{{ t('ui.binkp.lines_option', {'count': 50}) }}</option> <option value="100" selected>{{ t('ui.binkp.lines_option', {'count': 100}) }}</option> <option value="200">{{ t('ui.binkp.lines_option', {'count': 200}) }}</option> <option value="500">{{ t('ui.binkp.lines_option', {'count': 500}) }}</option> </select> <button class="btn btn-sm btn-outline-primary" onclick="refreshLogs()"> <i class="fas fa-refresh"></i> {{ t('ui.common.refresh') }} </button> </div> </div> <div class="card"> <div class="card-body p-0"> <pre id="log-content" style="height: 400px; overflow-y: auto; margin: 0; padding: 10px; font-size: 0.8em;"></pre> </div> </div> <!-- Advanced Log Search --> <div class="mt-3"> <button class="btn btn-sm btn-outline-secondary" type="button" onclick="toggleAdvancedLogSearch()"> <i class="fas fa-search-plus"></i> {{ t('ui.binkp.advanced_log_search') }} </button> </div> <div id="logAdvancedSearchPanel" style="display:none;" class="mt-2"> <div class="card border-secondary"> <div class="card-header py-2 d-flex align-items-center gap-2"> <i class="fas fa-search"></i> <strong>{{ t('ui.binkp.advanced_log_search') }}</strong> <small class="text-muted ms-auto" id="logSearchSummary"></small> </div> <div class="card-body"> <p class="text-muted small mb-2">{{ t('ui.binkp.advanced_log_search_help') }}</p> <div class="input-group mb-3"> <input type="text" class="form-control" id="logAdvancedQuery" placeholder="{{ t('ui.binkp.advanced_log_search_placeholder') }}" onkeydown="if(event.key==='Enter') runAdvancedLogSearch()"> <button class="btn btn-primary" type="button" id="logAdvancedSearchBtn" onclick="runAdvancedLogSearch()"> <i class="fas fa-search"></i> {{ t('ui.common.search') }} </button> </div> <div id="logAdvancedResults" style="display:none;"> <small class="d-block mb-1"> <span>{{ t('ui.binkp.log_search_legend_match') }}</span> &nbsp;·&nbsp; <span style="opacity:0.45;">{{ t('ui.binkp.log_search_legend_context') }}</span> </small> <pre id="log-search-content" style="height: 500px; overflow-y: auto; margin: 0; padding: 10px; font-size: 0.8em;"></pre> </div> </div> </div> </div> </div> </div> </div> <!-- Packet Inspector Modal --> <div class="modal fade" id="packetInspectorModal" tabindex="-1"> <div class="modal-dialog modal-lg modal-dialog-scrollable"> <div class="modal-content"> <div class="modal-header"> <h5 class="modal-title"><i class="fas fa-file-code me-2"></i><span id="pktModalTitle"></span></h5> <button type="button" class="btn-close" data-bs-dismiss="modal"></button> </div> <div class="modal-body" id="pktModalBody"> <div class="text-center py-4"><span class="spinner-border"></span></div> </div> <div class="modal-footer"> <a id="pktDownloadBtn" class="btn btn-outline-primary" href="#"> <i class="fas fa-download me-1"></i>{{ t('ui.files.download') }} </a> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ t('ui.common.close') }}</button> </div> </div> </div> </div> <!-- Add Uplink Modal --> <div class="modal fade" id="addUplinkModal" tabindex="-1"> <div class="modal-dialog"> <div class="modal-content"> <div class="modal-header"> <h5 class="modal-title">{{ t('ui.binkp.add_new_uplink') }}</h5> <button type="button" class="btn-close" data-bs-dismiss="modal"></button> </div> <div class="modal-body"> <form id="addUplinkForm"> <div class="mb-3"> <label class="form-label">{{ t('ui.binkp.ftn_address_required') }}</label> <input type="text" class="form-control" name="address" required placeholder="{{ t('ui.binkp.ftn_address_placeholder') }}"> </div> <div class="mb-3"> <label class="form-label">{{ t('ui.binkp.hostname_required') }}</label> <input type="text" class="form-control" name="hostname" required placeholder="{{ t('ui.binkp.hostname_placeholder') }}"> </div> <div class="mb-3"> <label class="form-label">{{ t('ui.common.port') }}</label> <input type="number" class="form-control" name="port" value="24554" min="1" max="65535"> </div> <div class="mb-3"> <label class="form-label">{{ t('ui.common.password') }}</label> <input type="password" class="form-control" name="password"> </div> <div class="mb-3"> <label class="form-label">{{ t('ui.binkp.poll_schedule_cron') }}</label> <input type="text" class="form-control" id="pollScheduleInput" name="poll_schedule" value="0 */4 * * *" placeholder="{{ t('ui.binkp.poll_schedule_placeholder') }}"> <small class="form-text text-muted">{{ t('ui.binkp.default_every_4_hours') }}</small> </div> <div class="form-check"> <input type="checkbox" class="form-check-input" name="enabled" checked> <label class="form-check-label">{{ t('ui.binkp.enabled') }}</label> </div> </form> </div> <div class="modal-footer"> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ t('ui.common.cancel') }}</button> <button type="button" class="btn btn-primary" onclick="addUplink()">{{ t('ui.binkp.add_uplink') }}</button> </div> </div> </div> </div> <script> let binkpData = {}; let binkpUplinks = []; function uiT(key, fallback, params = {}) { if (window.t) { return window.t(key, params, fallback); } return fallback; } function apiError(payload, fallback) { if (window.getApiErrorMessage) { return window.getApiErrorMessage(payload, fallback); } return payload && payload.error ? payload.error : fallback; } function apiMessage(payload, fallback) { if (window.getApiMessage) { return window.getApiMessage(payload, fallback); } return payload && payload.message ? payload.message : fallback; } function formatLocalizedDateTime(value, fallback = null) { if (!value || value === 'Never' || value === 'Unknown') { return value || fallback || '?'; } const parser = typeof window.parseAppDate === 'function' ? window.parseAppDate : function(dateString) { if (!dateString) { return null; } let normalized = String(dateString).trim().replace(' ', 'T'); if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$/.test(normalized)) { normalized += 'Z'; } else if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[+-]\d{2}$/.test(normalized)) { normalized += ':00'; } else if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[+-]\d{4}$/.test(normalized)) { normalized = normalized.replace(/([+-]\d{2})(\d{2})$/, '$1:$2'); } const parsed = new Date(normalized); return Number.isNaN(parsed.getTime()) ? null : parsed; }; const date = parser(value); if (!date) { return fallback !== null ? fallback : String(value); } const userTimezone = window.userSettings?.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC'; 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' }); } // Initialize page document.addEventListener('DOMContentLoaded', function() { loadI18nNamespaces(['common', 'errors']).then(function() { refreshBinkpOverview(); refreshQueues(); refreshLogs(); CronBuilder.init('pollScheduleInput'); setInterval(function() { refreshQueues(); refreshBinkpOverview(); }, 15000); }); document.getElementById('addUplinkModal').addEventListener('shown.bs.modal', function () { CronBuilder.refresh('pollScheduleInput'); }); }); function refreshBinkpOverview() { refreshStatus(); refreshUplinks(); } function refreshStatus() { console.log('Calling /api/binkp/status...'); fetch('/api/binkp/status') .then(response => { console.log('Status response:', response.status, response.statusText); if (!response.ok) { throw new Error(uiT('ui.binkp.http_status_error', 'HTTP {code}: {text}', { code: response.status, text: response.statusText })); } return response.json(); }) .then(data => { console.log('Status data received:', data); binkpData = data; updateStatusDisplay(data); }) .catch(error => { console.error('Error fetching status:', error); document.getElementById('system-status').textContent = `${uiT('ui.common.error', 'Error')}: ${error.message}`; document.getElementById('system-info').innerHTML = `<div class="alert alert-danger">${uiT('ui.binkp.error_loading_status_prefix', 'Error loading status:')} ${escapeHtml(error.message)}</div>`; }); } function updateStatusDisplay(data) { // Update cards document.getElementById('system-status').textContent = data.system.address; document.getElementById('inbound-count').textContent = uiT('ui.binkp.file_count', '{count} files', { count: data.queues.inbound.pending_files }); document.getElementById('outbound-count').textContent = uiT('ui.binkp.file_count', '{count} files', { count: data.queues.outbound.pending_files }); document.getElementById('uplink-count').textContent = uiT('ui.binkp.configured_count', '{count} configured', { count: binkpUplinks.length }); // Update system info const systemInfo = document.getElementById('system-info'); systemInfo.innerHTML = ` <dl class="row"> <dt class="col-sm-4">${uiT('ui.binkp.address', 'Address')}:</dt> <dd class="col-sm-8">${data.system.address}</dd> <dt class="col-sm-4">${uiT('ui.binkp.sysop', 'Sysop')}:</dt> <dd class="col-sm-8">${data.system.sysop}</dd> <dt class="col-sm-4">${uiT('ui.binkp.location', 'Location')}:</dt> <dd class="col-sm-8">${data.system.location}</dd> <dt class="col-sm-4">${uiT('ui.common.port', 'Port')}:</dt> <dd class="col-sm-8">${data.system.port}</dd> <dt class="col-sm-4">${uiT('ui.common.hostname', 'Hostname')}:</dt> <dd class="col-sm-8">${data.system.hostname}</dd> </dl> `; } function refreshUplinks() { console.log('Calling /api/binkp/uplinks...'); fetch('/api/binkp/uplinks') .then(response => { console.log('Uplinks response:', response.status, response.statusText); if (!response.ok) { throw new Error(uiT('ui.binkp.http_status_error', 'HTTP {code}: {text}', { code: response.status, text: response.statusText })); } return response.json(); }) .then(data => { console.log('Uplinks data received:', data); binkpUplinks = Array.isArray(data.uplinks) ? data.uplinks : []; document.getElementById('uplink-count').textContent = uiT('ui.binkp.configured_count', '{count} configured', { count: binkpUplinks.length }); renderUplinkStatusList(binkpUplinks); updateUplinksTable(data.uplinks); }) .catch(error => { console.error('Error fetching uplinks:', error); document.getElementById('uplink-status').innerHTML = `<div class="alert alert-danger">${uiT('ui.binkp.error_loading_uplinks_prefix', 'Error loading uplinks:')} ${escapeHtml(error.message)}</div>`; const tbody = document.querySelector('#uplinksTable tbody'); if (tbody) { tbody.innerHTML = `<tr><td colspan="6" class="text-danger">${uiT('ui.binkp.error_loading_uplinks_prefix', 'Error loading uplinks:')} ${escapeHtml(error.message)}</td></tr>`; } }); } function renderUplinkStatusList(uplinks) { const uplinkStatus = document.getElementById('uplink-status'); if (!Array.isArray(uplinks) || uplinks.length === 0) { uplinkStatus.innerHTML = `<p class="text-muted">${uiT('ui.binkp.no_uplinks_configured', 'No uplinks configured')}</p>`; return; } let html = ''; uplinks.forEach(uplink => { const address = String(uplink.address || ''); const escapedAddress = escapeHtml(address); const enabledBadge = uplink.enabled ? `<span class="badge bg-success ms-2">${uiT('ui.binkp.enabled', 'Enabled')}</span>` : `<span class="badge bg-secondary ms-2">${uiT('ui.binkp.disabled', 'Disabled')}</span>`; const initialStatus = getInitialUplinkConnectivityResult(uplink); const statusHtml = initialStatus !== null ? renderUplinkConnectivityHtml(initialStatus) : renderCheckStatusIcon(address); html += ` <div class="d-flex justify-content-between align-items-center mb-2" data-uplink-list-row="${escapeHtmlAttr(address)}"> <div> <span>${escapedAddress}</span>${enabledBadge} </div> <div data-uplink-list-status="${escapeHtmlAttr(address)}"> ${statusHtml} </div> </div> `; }); uplinkStatus.innerHTML = html; } function updateUplinksTable(uplinks) { const tbody = document.querySelector('#uplinksTable tbody'); tbody.innerHTML = ''; uplinks.forEach(uplink => { const enabledBadge = uplink.enabled ? `<span class="badge bg-success">${uiT('ui.binkp.enabled', 'Enabled')}</span>` : `<span class="badge bg-secondary">${uiT('ui.binkp.disabled', 'Disabled')}</span>`; const address = String(uplink.address || ''); const initialStatus = getInitialUplinkConnectivityResult(uplink); const canPoll = Boolean(address) && Boolean(uplink.enabled); const tableStatusHtml = initialStatus !== null ? renderUplinkConnectivityHtml(initialStatus) : renderCheckStatusIcon(address); const row = tbody.insertRow(); row.innerHTML = ` <td> <div>${escapeHtml(address)}</div> <div class="small mt-1">${enabledBadge}</div> </td> <td>${escapeHtml(String(uplink.hostname || ''))}</td> <td>${uplink.port}</td> <td data-uplink-table-status="${escapeHtmlAttr(address)}">${tableStatusHtml}</td> <td><code>${escapeHtml(String(uplink.poll_schedule || ''))}</code></td> <td> <button class="btn btn-sm btn-primary" onclick="pollUplink('${escapeJsString(address)}')"${canPoll ? '' : ' disabled'}>${uiT('ui.binkp.poll', 'Poll')}</button> <!-- <button class="btn btn-sm btn-danger" onclick="removeUplink('${uplink.address}')">Remove</button> --> </td> `; }); } function checkUplinkConnectivity(address) { updateUplinkConnectivityDisplay(address, {checking: true}); fetch(`/api/binkp/uplink-status?address=${encodeURIComponent(address)}`) .then(response => { if (!response.ok) { throw new Error(uiT('ui.binkp.http_status_error', 'HTTP {code}: {text}', { code: response.status, text: response.statusText })); } return response.json(); }) .then(result => { updateUplinkConnectivityDisplay(address, result); }) .catch(error => { updateUplinkConnectivityDisplay(address, { success: false, error: error.message }); }); } function renderCheckStatusIcon(address) { const title = uiT('ui.binkp.check_status_title', 'Check connectivity status'); const label = uiT('ui.binkp.check_status', 'Check'); return `<button class="btn btn-link btn-sm p-0 text-muted" onclick="checkUplinkConnectivity('${escapeJsString(address)}')" title="${escapeHtmlAttr(title)}"><i class="fas fa-circle-question me-1"></i><span class="small">${escapeHtml(label)}</span></button>`; } function getInitialUplinkConnectivityResult(uplink) { const address = String(uplink && uplink.address ? uplink.address : ''); if (!address) { return { success: false, skipped: true, not_configured: true }; } if (!uplink.enabled) { return { success: false, skipped: true, disabled: true }; } return null; } function updateUplinkConnectivityDisplay(address, result) { const html = renderUplinkConnectivityHtml(result); const listNode = document.querySelector(`[data-uplink-list-status="${cssEscape(address)}"]`); if (listNode) { listNode.innerHTML = html; } const tableNode = document.querySelector(`[data-uplink-table-status="${cssEscape(address)}"]`); if (tableNode) { tableNode.innerHTML = html; } } function renderPendingConnectivityBadge() { return `<span class="text-muted"><span class="spinner-border spinner-border-sm me-2" aria-hidden="true"></span>${uiT('ui.binkp.checking', 'Checking...')}</span>`; } function renderUplinkConnectivityHtml(result) { if (!result) { return `<span class="text-muted">?</span>`; } if (result.checking) { return renderPendingConnectivityBadge(); } if (result && result.disabled) { return `<span class="badge bg-secondary">${uiT('ui.binkp.disabled', 'Disabled')}</span>`; } if (result && result.not_configured) { return `<span class="badge bg-secondary">${uiT('ui.common.not_configured', 'Not configured')}</span>`; } if (result && result.success) { return `<span class="badge bg-success">${uiT('ui.binkp.online', 'Online')}</span>`; } const errorText = result && result.error ? escapeHtml(String(result.error)) : uiT('ui.binkp.offline', 'Offline'); return `<div><span class="badge bg-danger">${uiT('ui.binkp.offline', 'Offline')}</span><div class="small text-muted mt-1">${errorText}</div></div>`; } function refreshQueues() { console.log('Calling /api/binkp/files/inbound and /api/binkp/files/outbound...'); Promise.all([ fetch('/api/binkp/files/inbound').then(r => { console.log('Inbound response:', r.status, r.statusText); if (!r.ok) throw new Error(uiT('ui.binkp.inbound_api_http_error', 'Inbound API: HTTP {code}', { code: r.status })); return r.json(); }), fetch('/api/binkp/files/outbound').then(r => { console.log('Outbound response:', r.status, r.statusText); if (!r.ok) throw new Error(uiT('ui.binkp.outbound_api_http_error', 'Outbound API: HTTP {code}', { code: r.status })); return r.json(); }) ]) .then(([inboundData, outboundData]) => { console.log('Queue data received:', {inboundData, outboundData}); updateInboundFiles(inboundData); updateOutboundFiles(outboundData); }) .catch(error => { console.error('Error fetching queues:', error); document.getElementById('inbound-files').innerHTML = `<div class="text-danger">${uiT('ui.common.error', 'Error')}: ${escapeHtml(error.message)}</div>`; document.getElementById('outbound-files').innerHTML = `<div class="text-danger">${uiT('ui.common.error', 'Error')}: ${escapeHtml(error.message)}</div>`; }); } function updateInboundFiles(data) { const container = document.getElementById('inbound-files'); let html = `<p><strong>${uiT('ui.binkp.pending', 'Pending')}:</strong> ${uiT('ui.binkp.file_count', '{count} files', { count: data.pending.length })}</p>`; if (data.pending.length > 0) { html += '<ul class="list-group list-group-flush">'; data.pending.forEach(file => { const enc = encodeURIComponent(file.filename); const isPkt = file.filename.toLowerCase().endsWith('.pkt'); const nameHtml = isPkt ? `<a href="#" class="font-monospace text-decoration-none" onclick="openQueuePacketInspector('inbound', '${enc}'); return false;">${escapeHtml(file.filename)}</a>` : `<span class="font-monospace">${escapeHtml(file.filename)}</span>`; html += `<li class="list-group-item d-flex justify-content-between align-items-center"> ${nameHtml} <small class="text-muted">${formatBytes(file.size)}</small> </li>`; }); html += '</ul>'; } if (data.errors.length > 0) { html += `<p class="mt-3"><strong>${uiT('ui.binkp.errors', 'Errors')}:</strong> ${uiT('ui.binkp.file_count', '{count} files', { count: data.errors.length })}</p>`; } container.innerHTML = html; } function updateOutboundFiles(data) { const container = document.getElementById('outbound-files'); let html = `<p><strong>${uiT('ui.binkp.pending', 'Pending')}:</strong> ${uiT('ui.binkp.file_count', '{count} files', { count: data.files.length })}</p>`; if (data.files.length > 0) { html += '<ul class="list-group list-group-flush">'; data.files.forEach(file => { const enc = encodeURIComponent(file.filename); const isPkt = file.filename.toLowerCase().endsWith('.pkt'); const nameHtml = isPkt ? `<a href="#" class="font-monospace text-decoration-none" onclick="openQueuePacketInspector('outbound', '${enc}'); return false;">${escapeHtml(file.filename)}</a>` : `<span class="font-monospace">${escapeHtml(file.filename)}</span>`; html += `<li class="list-group-item d-flex justify-content-between align-items-center"> ${nameHtml} <small class="text-muted">${formatBytes(file.size)} ? ${uiT('ui.binkp.message_count', '{count} msgs', { count: file.message_count })}</small> </li>`; }); html += '</ul>'; } container.innerHTML = html; } function openQueuePacketInspector(type, filename) { const decoded = decodeURIComponent(filename); document.getElementById('pktModalTitle').textContent = decoded; document.getElementById('pktModalBody').innerHTML = '<div class="text-center py-4"><span class="spinner-border"></span></div>'; document.getElementById('pktDownloadBtn').href = `/api/binkp/queue/download?type=${type}&filename=${filename}`; const modal = bootstrap.Modal.getOrCreateInstance(document.getElementById('packetInspectorModal')); modal.show(); fetch(`/api/binkp/queue/inspect?type=${type}&filename=${filename}`) .then(r => r.json()) .then(data => { if (!data.success) { document.getElementById('pktModalBody').innerHTML = `<div class="alert alert-danger">${data.error || 'Failed to load packet.'}</div>`; return; } renderPacketInspector(data); }) .catch(err => { document.getElementById('pktModalBody').innerHTML = `<div class="alert alert-danger">${err.message}</div>`; }); } let _logLines = []; function refreshLogs() { const lines = document.getElementById('logLines').value; fetch(`/api/binkp/logs?lines=${lines}`) .then(response => response.json()) .then(data => { _logLines = data.logs || []; filterLogs(); }) .catch(error => console.error('Error fetching logs:', error)); } function filterLogs() { const query = document.getElementById('logSearch').value.trim(); const logContent = document.getElementById('log-content'); const matchCount = document.getElementById('logMatchCount'); const clearBtn = document.getElementById('logSearchClear'); if (!query) { logContent.textContent = _logLines.join('\n'); logContent.scrollTop = logContent.scrollHeight; matchCount.style.display = 'none'; clearBtn.style.display = 'none'; return; } clearBtn.style.display = ''; // Case-insensitive filter + highlight const lowerQuery = query.toLowerCase(); const matched = _logLines.filter(line => line.toLowerCase().includes(lowerQuery)); // Build highlighted HTML const escaped = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const re = new RegExp(`(${escaped})`, 'gi'); logContent.innerHTML = matched .map(line => line .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;') .replace(re, '<mark>$1</mark>') ) .join('\n'); matchCount.textContent = window.t('ui.binkp.log_matches', {count: matched.length, total: _logLines.length}, `${matched.length} / ${_logLines.length} lines`); matchCount.style.display = ''; logContent.scrollTop = 0; } function clearLogSearch() { document.getElementById('logSearch').value = ''; filterLogs(); } // ?? Kept Packets ????????????????????????????????????????????????????????????? let _keptType = 'inbound'; let _keptLoaded = { inbound: false, outbound: false }; function onKeptPacketsTabOpen() { if (!_keptLoaded[_keptType]) { loadKeptPackets(_keptType); } } function switchKeptType(type) { _keptType = type; document.getElementById('keptInboundBtn').classList.toggle('active', type === 'inbound'); document.getElementById('keptOutboundBtn').classList.toggle('active', type === 'outbound'); if (!_keptLoaded[type]) { loadKeptPackets(type); } else { // Already loaded ? just re-render from cache renderKeptPackets(type, window._keptData[type]); } } window._keptData = { inbound: null, outbound: null }; function loadKeptPackets(type) { const container = document.getElementById('kept-packets-content'); container.innerHTML = '<div class="text-center text-muted py-4"><span class="spinner-border spinner-border-sm me-2"></span>' + window.t('ui.common.loading', {}, 'Loading...') + '</div>'; fetch(`/api/binkp/kept-packets?type=${type}`) .then(r => r.json()) .then(data => { _keptLoaded[type] = true; window._keptData[type] = data; renderKeptPackets(type, data); }) .catch(err => { container.innerHTML = `<div class="alert alert-danger">${err.message}</div>`; }); } function renderKeptPackets(type, data) { const container = document.getElementById('kept-packets-content'); if (!data || !data.success) { container.innerHTML = `<div class="alert alert-warning">${data?.error || 'Failed to load.'}</div>`; return; } if (!data.groups || !data.groups.length) { container.innerHTML = `<p class="text-muted">${window.t('ui.binkp.kept_packets_empty', {}, 'No kept packets found.')}</p>`; return; } const totalLabel = window.t('ui.binkp.kept_packets_total', { count: data.total }, `${data.total} packet(s)`); let html = `<p class="text-muted small mb-3">${totalLabel}</p><div class="accordion" id="keptAccordion">`; data.groups.forEach((group, gi) => { const groupId = `keptGroup${gi}`; const collapseId = `keptCollapse${gi}`; const dateLabel = group.date || '?'; const count = group.packets.length; html += ` <div class="accordion-item"> <h2 class="accordion-header" id="${groupId}"> <button class="accordion-button${gi > 0 ? ' collapsed' : ''}" type="button" data-bs-toggle="collapse" data-bs-target="#${collapseId}" aria-expanded="${gi === 0 ? 'true' : 'false'}" aria-controls="${collapseId}"> <strong>${dateLabel}</strong> <span class="badge bg-secondary ms-2">${count}</span> </button> </h2> <div id="${collapseId}" class="accordion-collapse collapse${gi === 0 ? ' show' : ''}" data-bs-parent="#keptAccordion"> <div class="accordion-body p-0"> <table class="table table-sm table-hover mb-0"> <thead><tr> <th>{{ t('ui.common.filename') }}</th> <th>{{ t('ui.binkp.from_address') }}</th> <th>{{ t('ui.binkp.to_address') }}</th> <th>{{ t('ui.binkp.message_count') }}</th> <th>{{ t('ui.common.size') }}</th> <th>{{ t('ui.common.date') }}</th> </tr></thead> <tbody>`; group.packets.forEach(pkt => { const size = pkt.size < 1024 ? pkt.size + ' B' : pkt.size < 1048576 ? (pkt.size / 1024).toFixed(1) + ' KB' : (pkt.size / 1048576).toFixed(2) + ' MB'; const escDate = encodeURIComponent(group.date); const escFile = encodeURIComponent(pkt.filename); if (pkt.file_type === 'bundle') { html += `<tr> <td><a href="#" class="font-monospace text-decoration-none" onclick="openBundleInspector('${type}','${escDate}','${escFile}');return false;"><i class="fas fa-file-archive me-1 text-warning"></i>${pkt.filename}</a></td> <td colspan="2"><span class="text-muted small">{{ t('ui.binkp.bundle_arcmail', {}, 'Arcmail bundle') }}</span></td> <td>?</td> <td>${size}</td> <td><small class="text-muted">${escapeHtml(formatLocalizedDateTime(pkt.modified))}</small></td> </tr>`; } else { const msgs = pkt.message_count > 0 ? pkt.message_count : '?'; html += `<tr> <td><a href="#" class="font-monospace text-decoration-none" onclick="openPacketInspector('${type}','${escDate}','${escFile}');return false;">${pkt.filename}</a></td> <td>${pkt.orig_address}</td> <td>${pkt.dest_address}</td> <td>${msgs}</td> <td>${size}</td> <td><small class="text-muted">${escapeHtml(formatLocalizedDateTime(pkt.modified))}</small></td> </tr>`; } }); html += `</tbody></table></div></div></div>`; }); html += '</div>'; container.innerHTML = html; } function openPacketInspector(type, date, filename) { document.getElementById('pktModalTitle').textContent = decodeURIComponent(filename); document.getElementById('pktModalBody').innerHTML = '<div class="text-center py-4"><span class="spinner-border"></span></div>'; document.getElementById('pktDownloadBtn').href = `/api/binkp/kept-packets/download?type=${type}&date=${date}&filename=${filename}`; const modal = bootstrap.Modal.getOrCreateInstance(document.getElementById('packetInspectorModal')); modal.show(); fetch(`/api/binkp/kept-packets/inspect?type=${type}&date=${date}&filename=${filename}`) .then(r => r.json()) .then(data => { if (!data.success) { document.getElementById('pktModalBody').innerHTML = `<div class="alert alert-danger">${data.error || 'Failed to load packet.'}</div>`; return; } renderPacketInspector(data); }) .catch(err => { document.getElementById('pktModalBody').innerHTML = `<div class="alert alert-danger">${err.message}</div>`; }); } function openBundleInspector(type, date, filename) { const decoded = decodeURIComponent(filename); document.getElementById('pktModalTitle').innerHTML = `<i class="fas fa-file-archive me-2 text-warning"></i>${decoded}`; document.getElementById('pktModalBody').innerHTML = '<div class="text-center py-4"><span class="spinner-border"></span></div>'; document.getElementById('pktDownloadBtn').href = `/api/binkp/kept-packets/bundle/download?type=${type}&date=${date}&filename=${filename}`; document.getElementById('pktDownloadBtn').style.display = ''; const modal = bootstrap.Modal.getOrCreateInstance(document.getElementById('packetInspectorModal')); modal.show(); fetch(`/api/binkp/kept-packets/bundle/list?type=${type}&date=${date}&filename=${filename}`) .then(r => r.json()) .then(data => { if (!data.success) { document.getElementById('pktModalBody').innerHTML = `<div class="alert alert-danger">${data.error || window.t('ui.binkp.bundle_open_failed', {}, 'Failed to open bundle.')}</div>`; return; } renderBundleContents(type, date, filename, data); }) .catch(err => { document.getElementById('pktModalBody').innerHTML = `<div class="alert alert-danger">${err.message}</div>`; }); } function renderBundleContents(type, date, bundleFilename, data) { const bundleSizeKb = data.bundle_size < 1024 ? data.bundle_size + ' B' : data.bundle_size < 1048576 ? (data.bundle_size / 1024).toFixed(1) + ' KB' : (data.bundle_size / 1048576).toFixed(2) + ' MB'; let html = `<p class="text-muted small mb-3">`; if (!data.packets || !data.packets.length) { html += window.t('ui.binkp.bundle_empty', {}, 'No .pkt files found in this bundle.') + `</p>`; document.getElementById('pktModalBody').innerHTML = html; return; } const countLabel = window.t('ui.binkp.bundle_packet_count', { count: data.packets.length }, `${data.packets.length} packet(s) in bundle`); html += `${countLabel} &mdash; ${bundleSizeKb}</p>`; html += `<div class="table-responsive"><table class="table table-sm table-hover mb-0"> <thead><tr> <th>{{ t('ui.common.filename') }}</th> <th>{{ t('ui.common.size') }}</th> <th></th> </tr></thead><tbody>`; data.packets.forEach(pkt => { const pktSize = pkt.size < 1024 ? pkt.size + ' B' : pkt.size < 1048576 ? (pkt.size / 1024).toFixed(1) + ' KB' : (pkt.size / 1048576).toFixed(2) + ' MB'; const escBundle = encodeURIComponent(bundleFilename); const escPkt = encodeURIComponent(pkt.filename); html += `<tr> <td><code>${pkt.filename}</code></td> <td><small class="text-muted">${pktSize}</small></td> <td><a href="#" class="btn btn-sm btn-outline-primary py-0" onclick="openBundlePacketInspector('${type}','${encodeURIComponent(date)}','${escBundle}','${escPkt}');return false;"> <i class="fas fa-search me-1"></i>{{ t('ui.binkp.bundle_view_packet') }}</a></td> </tr>`; }); html += `</tbody></table></div>`; document.getElementById('pktModalBody').innerHTML = html; } function openBundlePacketInspector(type, date, bundleFilename, pktFilename) { const decodedBundle = decodeURIComponent(bundleFilename); const decodedPkt = decodeURIComponent(pktFilename); document.getElementById('pktModalTitle').innerHTML = `<a href="#" class="text-decoration-none me-1" onclick="openBundleInspector('${type}','${date}','${bundleFilename}');return false;" title="{{ t('ui.binkp.bundle_back') }}"><i class="fas fa-arrow-left"></i></a><i class="fas fa-file-archive me-1 text-warning"></i>${decodedBundle} / <span class="font-monospace">${decodedPkt}</span>`; document.getElementById('pktModalBody').innerHTML = '<div class="text-center py-4"><span class="spinner-border"></span></div>'; document.getElementById('pktDownloadBtn').style.display = 'none'; fetch(`/api/binkp/kept-packets/bundle/inspect?type=${type}&date=${date}&bundle=${bundleFilename}&pkt=${pktFilename}`) .then(r => r.json()) .then(data => { document.getElementById('pktDownloadBtn').style.display = ''; if (!data.success) { document.getElementById('pktModalBody').innerHTML = `<div class="alert alert-danger">${data.error || window.t('ui.binkp.pkt_load_failed', {}, 'Failed to load packet.')}</div>`; return; } renderPacketInspector(data); }) .catch(err => { document.getElementById('pktDownloadBtn').style.display = ''; document.getElementById('pktModalBody').innerHTML = `<div class="alert alert-danger">${err.message}</div>`; }); } function renderPacketInspector(data) { const p = data.packet; const sizeKb = p.file_size < 1024 ? p.file_size + ' B' : p.file_size < 1048576 ? (p.file_size / 1024).toFixed(1) + ' KB' : (p.file_size / 1048576).toFixed(2) + ' MB'; let html = ` <h6 class="text-muted mb-3">{{ t('ui.binkp.pkt_header') }}</h6> <table class="table table-sm table-bordered mb-4"> <tbody> <tr><th style="width:35%">{{ t('ui.binkp.from_address') }}</th><td><code>${p.orig_address}</code></td></tr> <tr><th>{{ t('ui.binkp.to_address') }}</th><td><code>${p.dest_address}</code></td></tr> <tr><th>{{ t('ui.common.date') }}</th><td>${escapeHtml(String(p.created || '?'))}</td></tr> <tr><th>{{ t('ui.common.size') }}</th><td>${sizeKb}</td></tr> <tr><th>{{ t('ui.binkp.pkt_version') }}</th><td>${p.packet_version}</td></tr> <tr><th>{{ t('ui.binkp.pkt_product') }}</th><td>${p.product_code}</td></tr> <tr><th>{{ t('ui.binkp.pkt_password') }}</th><td>${p.has_password ? '<span class="badge bg-warning text-dark">{{ t("ui.binkp.pkt_password_set") }}</span>' : '<span class="text-muted">{{ t("ui.binkp.pkt_no_password") }}</span>'}</td></tr> </tbody> </table>`; if (!data.messages || !data.messages.length) { html += `<p class="text-muted">{{ t('ui.binkp.pkt_no_messages') }}</p>`; } else { html += `<h6 class="text-muted mb-2">{{ t('ui.binkp.pkt_messages') }} <span class="badge bg-secondary">${data.messages.length}</span></h6> <div class="table-responsive"> <table class="table table-sm table-hover mb-0"> <thead><tr> <th>#</th> <th>{{ t('ui.binkp.pkt_from') }}</th> <th>{{ t('ui.binkp.pkt_to') }}</th> <th>{{ t('ui.binkp.pkt_subject') }}</th> <th>{{ t('ui.common.date') }}</th> <th>{{ t('ui.binkp.pkt_flags') }}</th> </tr></thead> <tbody>`; data.messages.forEach((msg, i) => { const flags = msg.flags.length ? msg.flags.map(f => `<span class="badge bg-secondary me-1">${f}</span>`).join('') : '?'; const addrInfo = (msg.orig_addr !== '0:0' ? `<br><small class="text-muted">${msg.orig_addr} ? ${msg.dest_addr}</small>` : ''); html += `<tr> <td class="text-muted">${i + 1}</td> <td>${msg.from}${addrInfo}</td> <td>${msg.to}</td> <td>${msg.subject || '<span class="text-muted">?</span>'}</td> <td><small class="text-muted">${escapeHtml(String(msg.date || '?'))}</small></td> <td>${flags}</td> </tr>`; }); html += `</tbody></table></div>`; } document.getElementById('pktModalBody').innerHTML = html; } // ????????????????????????????????????????????????????????????????????????????? function toggleAdvancedLogSearch() { const panel = document.getElementById('logAdvancedSearchPanel'); const isHidden = panel.style.display === 'none'; panel.style.display = isHidden ? '' : 'none'; if (isHidden) { document.getElementById('logAdvancedQuery').focus(); } } function runAdvancedLogSearch() { const q = document.getElementById('logAdvancedQuery').value.trim(); if (q.length < 2) return; const btn = document.getElementById('logAdvancedSearchBtn'); const resultsDiv = document.getElementById('logAdvancedResults'); const content = document.getElementById('log-search-content'); const summary = document.getElementById('logSearchSummary'); btn.disabled = true; btn.innerHTML = '<span class="spinner-border spinner-border-sm"></span>'; resultsDiv.style.display = ''; content.textContent = ''; summary.textContent = ''; fetch(`/api/binkp/logs/search?q=${encodeURIComponent(q)}`) .then(r => r.json()) .then(data => { btn.disabled = false; btn.innerHTML = '<i class="fas fa-search"></i> ' + window.t('ui.common.search', {}, 'Search'); if (!data.success) { content.textContent = data.error || 'Search failed.'; return; } if (!data.lines || !data.lines.length) { content.textContent = window.t('ui.binkp.log_search_no_results', {}, 'No results found.'); summary.textContent = ''; return; } summary.textContent = window.t('ui.binkp.log_search_summary', {matches: data.match_count, sessions: data.pid_count}, `${data.match_count} matches across ${data.pid_count} session(s)`); // Build highlighted HTML ? match lines normal weight, context lines muted const escaped = q.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const re = new RegExp(`(${escaped})`, 'gi'); content.innerHTML = data.lines.map(item => { const safe = item.line .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;') .replace(re, '<mark>$1</mark>'); if (item.is_match) { return `<span style="display:block;">${safe}</span>`; } return `<span style="display:block;opacity:0.55;">${safe}</span>`; }).join(''); content.scrollTop = 0; }) .catch(err => { btn.disabled = false; btn.innerHTML = '<i class="fas fa-search"></i> ' + window.t('ui.common.search', {}, 'Search'); content.textContent = 'Error: ' + err.message; }); } function pollUplink(address) { console.log('Polling uplink:', address); fetch('/api/binkp/poll', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ address: address }) }) .then(response => { console.log('Poll response status:', response.status, response.statusText); if (!response.ok) { throw new Error(uiT('ui.binkp.http_status_error', 'HTTP {code}: {text}', { code: response.status, text: response.statusText })); } return response.json(); }) .then(data => { console.log('Poll response data:', data); if (data.success) { const result = data.result || {}; const exitCode = result.exit_code ?? null; const details = formatPollDetails(result); const message = exitCode !== null ? uiT('ui.binkp.poll_completed_exit', 'Poll completed (exit {code})', { code: exitCode }) : apiMessage(data, uiT('ui.api.binkp.poll_triggered', 'BinkP poll triggered')); showAlert(message + details, 'success', { persistent: true }); if (exitCode === 0 || exitCode === '0') { triggerPacketProcessing(); } } else { showAlert(uiT('ui.binkp.poll_failed_prefix', 'Poll failed: ') + apiError(data, uiT('ui.binkp.poll_failed', 'Poll failed')), 'danger', { persistent: true }); } refreshBinkpOverview(); }) .catch(error => { console.error('Poll error:', error); showAlert(uiT('ui.binkp.error_polling_uplink_prefix', 'Error polling uplink: ') + error.message, 'danger', { persistent: true }); }); } function pollAllUplinks() { const warning = uiT('ui.binkp.poll_all_confirm_warning', 'Warning: Excessive polling may lead to rate limiting or IP bans by remote systems.'); const msg = uiT('ui.binkp.poll_all_confirm', 'Poll all uplinks now?') + '\n\n' + warning; if (!confirm(msg)) return; fetch('/api/binkp/poll-all', { method: 'POST' }) .then(response => response.json()) .then(data => { if (data.success) { const result = data.result || {}; const exitCode = result.exit_code ?? null; const details = formatPollDetails(result); const message = exitCode !== null ? uiT('ui.binkp.all_uplinks_polled_exit', 'All uplinks polled (exit {code})', { code: exitCode }) : apiMessage(data, uiT('ui.api.binkp.poll_all_triggered', 'BinkP poll-all triggered')); showAlert(message + details, 'success', { persistent: true }); if (exitCode === 0 || exitCode === '0') { triggerPacketProcessing(); } } else { showAlert(uiT('ui.binkp.polling_failed_prefix', 'Polling failed: ') + apiError(data, uiT('ui.binkp.polling_failed', 'Polling failed')), 'danger', { persistent: true }); } refreshBinkpOverview(); }) .catch(error => { showAlert(uiT('ui.binkp.error_polling_uplinks_prefix', 'Error polling uplinks: ') + error.message, 'danger', { persistent: true }); }); } function processInbound() { fetch('/api/binkp/process/inbound', { method: 'POST' }) .then(response => response.json()) .then(data => { if (data.success) { showAlert(apiMessage(data, uiT('ui.binkp.inbound_processing_completed', 'Inbound processing completed')), 'success'); refreshQueues(); refreshBinkpOverview(); } else { showAlert(uiT('ui.binkp.processing_failed_prefix', 'Processing failed: ') + apiError(data, uiT('ui.binkp.processing_failed', 'Processing failed')), 'danger'); } }) .catch(error => { showAlert(uiT('ui.binkp.error_processing_inbound_prefix', 'Error processing inbound: ') + error.message, 'danger'); }); } function processOutbound() { fetch('/api/binkp/process/outbound', { method: 'POST' }) .then(response => response.json()) .then(data => { if (data.success) { showAlert(apiMessage(data, uiT('ui.binkp.outbound_processing_completed', 'Outbound processing completed')), 'success'); refreshQueues(); refreshBinkpOverview(); } else { showAlert(uiT('ui.binkp.processing_failed_prefix', 'Processing failed: ') + apiError(data, uiT('ui.binkp.processing_failed', 'Processing failed')), 'danger'); } }) .catch(error => { showAlert(uiT('ui.binkp.error_processing_outbound_prefix', 'Error processing outbound: ') + error.message, 'danger'); }); } function addUplink() { const form = document.getElementById('addUplinkForm'); const formData = new FormData(form); const data = Object.fromEntries(formData.entries()); data.enabled = formData.has('enabled'); fetch('/api/binkp/uplinks', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }) .then(response => response.json()) .then(result => { if (result.success) { showAlert(apiMessage(result, uiT('ui.binkp.uplink_added_success', 'Uplink added successfully')), 'success'); document.getElementById('addUplinkModal').querySelector('.btn-close').click(); form.reset(); refreshBinkpOverview(); } else { showAlert(uiT('ui.binkp.add_uplink_failed_prefix', 'Failed to add uplink: ') + apiError(result, uiT('ui.binkp.add_uplink_failed', 'Failed to add uplink')), 'danger'); } }) .catch(error => { showAlert(uiT('ui.binkp.error_adding_uplink_prefix', 'Error adding uplink: ') + error.message, 'danger'); }); } function removeUplink(address) { if (!confirm(uiT('ui.binkp.remove_uplink_confirm', 'Remove uplink {address}?', { address }))) return; fetch(`/api/binkp/uplinks/${address}`, { method: 'DELETE' }) .then(response => response.json()) .then(data => { if (data.success) { showAlert(apiMessage(data, uiT('ui.binkp.uplink_removed_success', 'Uplink removed successfully')), 'success'); refreshBinkpOverview(); } else { showAlert(uiT('ui.binkp.remove_uplink_failed_prefix', 'Failed to remove uplink: ') + apiError(data, uiT('ui.binkp.remove_uplink_failed', 'Failed to remove uplink')), 'danger'); } }) .catch(error => { showAlert(uiT('ui.binkp.error_removing_uplink_prefix', 'Error removing uplink: ') + error.message, 'danger'); }); } function showAlert(message, type, options = {}) { const alertDiv = document.createElement('div'); alertDiv.className = `alert alert-${type} alert-dismissible fade show`; alertDiv.innerHTML = ` ${message} <button type="button" class="btn-close" data-bs-dismiss="alert"></button> `; const container = document.querySelector('.container-fluid'); container.insertBefore(alertDiv, container.firstChild); if (!options.persistent) { setTimeout(() => { alertDiv.remove(); }, 5000); } } function formatBytes(bytes) { if (bytes === 0) return uiT('ui.binkp.zero_bytes', '0 Bytes'); const k = 1024; const sizes = [ uiT('ui.binkp.byte_units.bytes', 'Bytes'), uiT('ui.binkp.byte_units.kb', 'KB'), uiT('ui.binkp.byte_units.mb', 'MB'), uiT('ui.binkp.byte_units.gb', 'GB') ]; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; } function formatPollDetails(result) { if (!result || (!result.stdout && !result.stderr)) { return ''; } const stdout = (result.stdout || '').trim(); const stderr = (result.stderr || '').trim(); let details = ''; if (stdout) { details += `<br><div class="mt-1"><small class="text-muted">${uiT('ui.binkp.stdout', 'STDOUT')}:</small><pre class="mt-1 mb-0">${escapeHtml(stdout)}</pre></div>`; } if (stderr) { details += `<br><div class="mt-1"><small class="text-warning">${uiT('ui.binkp.stderr', 'STDERR')}:</small><pre class="mt-1 mb-0">${escapeHtml(stderr)}</pre></div>`; } return details; } function triggerPacketProcessing() { fetch('/api/binkp/process-packets', { method: 'POST' }) .then(response => response.json()) .then(data => { if (!data.success) { showAlert(uiT('ui.binkp.packet_processing_failed_prefix', 'Packet processing failed: ') + apiError(data, uiT('ui.binkp.unknown_error', 'Unknown error')), 'danger', { persistent: true }); return; } const result = data.result || {}; const exitCode = result.exit_code ?? null; const details = formatPollDetails(result); const message = exitCode !== null ? uiT('ui.binkp.packet_processing_completed_exit', 'Packet processing completed (exit {code})', { code: exitCode }) : apiMessage(data, uiT('ui.api.binkp.process_packets_started', 'Packet processing started')); showAlert(message + details, 'success', { persistent: true }); }) .catch(error => { showAlert(uiT('ui.binkp.error_processing_packets_prefix', 'Error processing packets: ') + error.message, 'danger', { persistent: true }); }); } function escapeHtml(value) { return String(value) .replaceAll('&', '&amp;') .replaceAll('<', '&lt;') .replaceAll('>', '&gt;') .replaceAll('"', '&quot;') .replaceAll("'", '&#039;'); } function escapeHtmlAttr(value) { return escapeHtml(value); } function escapeJsString(value) { return String(value) .replaceAll('\\', '\\\\') .replaceAll("'", "\\'"); } function cssEscape(value) { if (window.CSS && typeof window.CSS.escape === 'function') { return window.CSS.escape(String(value)); } return String(value).replace(/[^a-zA-Z0-9_-]/g, function(char) { return '\\' + char; }); } </script> <script src="/js/cron-builder.js"></script> {% endblock %}