PHP Classes

File: templates/nodelist/view.twig

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

Contents

Class file image Download
{% extends "base.twig" %} {% block title %}{{ t('ui.nodelist.node_details', {}, locale, ['common']) }} - {{ system_name }}{% endblock %} {% block head %} <link rel="stylesheet" href="/assets/leaflet/leaflet.css"> <style> #nodeMap { height: 300px; width: 100%; border-radius: 0.375rem; background: #111827; } #nodeMap .leaflet-tile-pane { filter: grayscale(0.45) saturate(0.45) brightness(0.38) contrast(1.28); } #nodeMap .leaflet-control-container .leaflet-bar a, #nodeMap .leaflet-control-zoom a { background: rgba(15, 23, 42, 0.92); color: #e2e8f0; border-bottom-color: rgba(148, 163, 184, 0.18); } #nodeMap .leaflet-control-container .leaflet-bar a:hover, #nodeMap .leaflet-control-zoom a:hover { background: rgba(30, 41, 59, 0.98); color: #fff; } #nodeMap .leaflet-popup-content-wrapper, #nodeMap .leaflet-popup-tip { background: #111827; color: #e2e8f0; } </style> {% endblock %} {% block content %} <div class="container mt-4"> <div class="row"> <div class="col-12"> <div class="d-flex justify-content-between align-items-center mb-4"> <h2><i class="fas fa-server"></i> {{ t('ui.nodelist.node_details') }}</h2> <a href="/nodelist" class="btn btn-secondary"> <i class="fas fa-arrow-left"></i> {{ t('ui.nodelist.back_to_list') }} </a> </div> <div class="row"> <div class="col-lg-8"> <div class="card"> <div class="card-header"> <h4 class="mb-0"> {{ node.full_address }} {% if node.keyword_type %} <span class="badge bg-primary ms-2">{{ node.keyword_type }}</span> {% endif %} </h4> </div> <div class="card-body"> <div class="row"> <div class="col-md-6"> <h6>{{ t('ui.nodelist.system_information') }}</h6> <table class="table table-sm"> <tr> <th width="120">{{ t('ui.nodelist.system_name') }}:</th> <td>{{ node.system_name|default(t('ui.nodelist.not_specified')) }}</td> </tr> <tr> <th>{{ t('ui.nodelist.sysop') }}:</th> <td>{{ node.sysop_name|default(t('ui.nodelist.not_specified')) }}</td> </tr> <tr> <th>{{ t('ui.nodelist.location') }}:</th> <td>{{ node.location|default(t('ui.nodelist.not_specified')) }}</td> </tr> <tr> <th>{{ t('ui.nodelist.phone') }}:</th> <td> {% if node.phone and node.phone != '-Unpublished-' %} {{ node.phone }} {% else %} <span class="text-muted">{{ t('ui.nodelist.unpublished') }}</span> {% endif %} {% if node.flags['INA'] is defined %} <br><a href="telnet://{{ node.flags['INA'] }}" class="host-link" data-hostname="{{ node.flags['INA'] }}">{{ node.flags['INA'] }}</a> {% endif %} </td> </tr> <tr> <th>{{ t('ui.nodelist.baud_rate') }}:</th> <td> {% if node.baud_rate > 0 %} {{ node.baud_rate }} {% else %} <span class="text-muted">{{ t('ui.nodelist.not_specified') }}</span> {% endif %} </td> </tr> </table> </div> <div class="col-md-6"> <h6>{{ t('ui.nodelist.address_components') }}</h6> <table class="table table-sm"> <tr> <th width="80">{{ t('ui.nodelist.zone') }}:</th> <td>{{ node.zone }}</td> </tr> <tr> <th>{{ t('ui.nodelist.net') }}:</th> <td>{{ node.net }}</td> </tr> <tr> <th>{{ t('ui.nodelist.node') }}:</th> <td>{{ node.node }}</td> </tr> <tr> <th>{{ t('ui.nodelist.point') }}:</th> <td>{{ node.point }}</td> </tr> <tr> <th>{{ t('ui.nodelist.full') }}:</th> <td><strong>{{ node.full_address }}</strong></td> </tr> </table> </div> </div> {% if node.flags and node.flags|length > 0 %} <hr> <h6>{{ t('ui.nodelist.capabilities_flags') }}</h6> {% set flagDescriptions = { 'IBN': 'Internet BinkP Node ? accepts binkp connections', 'INA': 'Internet Address ? hostname or IP for internet connections', 'ITN': 'Internet Telnet Node ? accepts telnet connections', 'IFC': 'Internet File/Conference ? internet file sharing', 'IFT': 'Internet FTP ? accepts FTP connections', 'IVM': 'Internet VoIP/Messaging', 'IMI': 'Internet MIME email', 'ISE': 'Internet SMTP/Email', 'IEM': 'Internet Email', 'IP': 'Internet IP address (legacy)', 'BINKP': 'BinkP capable', 'CM': 'Continuous Mail ? accepts mail 24 hours a day', 'MO': 'Mail Only ? does not accept human callers', 'LO': 'Listed Only ? only accepts calls from listed nodes', 'MN': 'Mail and News', 'FREQ': 'File Request ? will serve files on request', 'PING': 'Responds to ping requests', 'TRACE': 'Netmail tracing supported', 'HOLD': 'Mail on hold ? not currently accepting connections', 'DOWN': 'System down ? temporarily unavailable', 'PVT': 'Private node ? address not published', 'HUB': 'Hub node', 'HOST': 'Net host node', 'REGION': 'Regional coordinator', 'ZONE': 'Zone coordinator', 'NEC': 'No EchoMail', 'V21': 'V.21 modem (300 baud)', 'V22': 'V.22 modem (1200 baud)', 'V29': 'V.29 modem (9600 baud)', 'V32': 'V.32 modem (9600 baud)', 'V32T': 'V.32 Terbo modem (19200 baud)', 'V32B': 'V.32bis modem (14400 baud)', 'VFC': 'Rockwell V.FC modem (28800 baud)', 'V34': 'V.34 modem (28800 baud)', 'V42': 'V.42 error correction', 'V42B': 'V.42bis compression', 'V90C': 'V.90 modem ? client mode (56k download)', 'V90S': 'V.90 modem ? server mode (56k upload)', 'H96': 'HST 9600', 'H14': 'HST 14400', 'H16': 'HST 16800', 'MAX': 'MaxLink protocol', 'PEP': 'PEP high-speed protocol', 'ZYX': 'ZyXEL proprietary protocol', 'XA': 'Xmodem/ASCII transfer', 'XB': 'Xmodem/CRC transfer', 'XC': 'Xmodem/1K transfer', 'XP': 'Xmodem/1K-G transfer', 'ZMO': 'ZModem (original)', 'ZMB': 'ZModem with crash recovery', 'XW': 'WXmodem (windowed Xmodem)', 'XX': 'Xmodem/CRC (alternate)', 'ISDN': 'ISDN connection', 'ISDNA': 'ISDN async', 'X25': 'X.25 packet network', 'POINTS24': 'Points file available 24 hours', 'ANUS': 'Allow Null User Session ? accepts anonymous/insecure sessions' } %} <div class="row"> {% for flag_name, flag_value in node.flags %} <div class="col-md-4 mb-2"> <span class="badge bg-info me-1" {% if flagDescriptions[flag_name] is defined %} data-bs-toggle="tooltip" data-bs-title="{{ flagDescriptions[flag_name] }}" {% endif %}>{{ flag_name }}</span> {% if flag_value != true and flag_value != '1' %} <small class="text-muted">{{ flag_value }}</small> {% endif %} </div> {% endfor %} </div> {% endif %} </div> </div> </div> <div class="col-lg-4"> <!-- Actions Card --> <div class="card mb-3"> <div class="card-header"> <h6 class="mb-0">{{ t('ui.nodelist.quick_actions') }}</h6> </div> <div class="card-body"> <div class="d-grid gap-2"> {% if user %} <a href="/compose/netmail?to={{ node.full_address|url_encode }}&to_name={{ node.sysop_name|url_encode }}" class="btn btn-success"> <i class="fas fa-envelope"></i> {{ t('ui.nodelist.send_netmail') }} </a> {% else %} <a href="/login" class="btn btn-outline-primary"> <i class="fas fa-sign-in-alt"></i> {{ t('ui.nodelist.login_to_send_netmail') }} </a> {% endif %} {% if freq_experimental_enabled and user and user.is_admin %} <button class="btn btn-info" onclick="showAllfilesModal()"> <i class="fas fa-file-download"></i> {{ t('ui.nodelist.request_allfiles') }} </button> {% endif %} </div> </div> </div> <!-- Connection Info Card --> {% if node.flags and node.flags|length > 0 %} <div class="card"> <div class="card-header"> <h6 class="mb-0">{{ t('ui.nodelist.connection_information') }}</h6> </div> <div class="card-body"> {% set connection_info = [] %} {% if node.flags['IBN'] is defined %} {% set connection_info = connection_info|merge([t('ui.nodelist.internet_binkp')]) %} {% endif %} {% if node.flags['INA'] is defined %} {% set connection_info = connection_info|merge([t('ui.nodelist.internet_telnet')]) %} {% endif %} {% if node.flags['ITN'] is defined %} {% set connection_info = connection_info|merge([t('ui.nodelist.internet_telnet')]) %} {% endif %} {% if node.phone and node.phone != '-Unpublished-' %} {% set connection_info = connection_info|merge([t('ui.nodelist.dialup_pots')]) %} {% endif %} {% if connection_info|length > 0 %} <ul class="list-unstyled mb-0"> {% for info in connection_info %} <li><i class="fas fa-check text-success"></i> {{ info }}</li> {% endfor %} </ul> {% else %} <p class="text-muted mb-0">{{ t('ui.nodelist.no_connection_info') }}</p> {% endif %} {% if node.flags['CM'] is defined %} <div class="alert alert-info mt-2 mb-0 py-2"> <small><i class="fas fa-clock"></i> {{ t('ui.nodelist.continuous_mail') }}</small> </div> {% endif %} {% if node.flags['HOLD'] is defined %} <div class="alert alert-warning mt-2 mb-0 py-2"> <small><i class="fas fa-pause"></i> {{ t('ui.nodelist.mail_on_hold') }}</small> </div> {% endif %} {% if node.flags['DOWN'] is defined %} <div class="alert alert-danger mt-2 mb-0 py-2"> <small><i class="fas fa-times"></i> {{ t('ui.nodelist.system_down') }}</small> </div> {% endif %} </div> </div> {% endif %} </div> </div> {% if node.latitude and node.longitude %} <div class="row mt-4"> <div class="col-12"> <div class="card" style="background: linear-gradient(180deg, #1e293b 0%, #0f172a 100%); border-color: rgba(148,163,184,0.18);"> <div class="card-header" style="border-color: rgba(148,163,184,0.18);"> <h6 class="mb-0" style="color:#e2e8f0;"><i class="fas fa-map-marker-alt me-2"></i>{{ node.location|default(node.full_address) }}</h6> </div> <div class="card-body p-2"> <div id="nodeMap"></div> </div> </div> </div> </div> {% else %} <div class="row mt-4"> <div class="col-12"> <div class="card"> <div class="card-body text-muted small"> <i class="fas fa-map-marker-alt me-1"></i> {{ t('ui.nodelist.no_coordinates') }} </div> </div> </div> </div> {% endif %} </div> </div> </div> <script src="/assets/leaflet/leaflet.js"></script> {% if node.latitude and node.longitude %} <script> (function() { const lat = {{ node.latitude|json_encode|raw }}; const lng = {{ node.longitude|json_encode|raw }}; const name = {{ node.system_name|default('')|json_encode|raw }}; const addr = {{ node.full_address|json_encode|raw }}; const loc = {{ node.location|default('')|json_encode|raw }}; const map = L.map('nodeMap', { scrollWheelZoom: false }).setView([lat, lng], 8); L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '\u00a9 <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>', maxZoom: 18 }).addTo(map); L.marker([lat, lng]) .addTo(map) .bindPopup('<strong>' + name + '</strong><br>' + addr + (loc ? '<br><small>' + loc + '</small>' : '')) .openPopup(); })(); </script> {% endif %} {% if freq_experimental_enabled and user and user.is_admin %} <!-- ALLFILES FREQ Modal --> <div class="modal fade" id="allfilesModal" tabindex="-1"> <div class="modal-dialog"> <div class="modal-content"> <div class="modal-header"> <h5 class="modal-title">{{ t('ui.nodelist.request_allfiles_title', {'node': node.full_address}) }}</h5> <button type="button" class="btn-close" data-bs-dismiss="modal"></button> </div> <div class="modal-body"> <p>{{ t('ui.nodelist.request_allfiles_body') }}</p> {% if not (node.flags and node.flags['FREQ'] is defined) %} <div class="alert alert-warning py-2 small"> <i class="fas fa-exclamation-triangle me-1"></i>{{ t('ui.nodelist.freq_not_advertised') }} </div> {% endif %} <div class="mb-3"> <label for="allfilesFilename" class="form-label">{{ t('ui.nodelist.request_allfiles_filename') }}</label> <select class="form-select" id="allfilesFilename" onchange="toggleAllfilesFilenameOther()"> <option value="ALLFILES">ALLFILES</option> <option value="FILES">FILES</option> <option value="FILELIST">FILELIST</option> <option value="NODELIST">NODELIST</option> <option value="NODEDIFF">NODEDIFF</option> <option value="">{{ t('ui.nodelist.request_allfiles_to_other') }}</option> </select> <input type="text" class="form-control mt-2 d-none" id="allfilesFilenameOther" placeholder="{{ t('ui.nodelist.request_allfiles_filename_placeholder') }}" autocomplete="off"> </div> <div class="mb-3"> <label for="allfilesToName" class="form-label">{{ t('ui.nodelist.request_allfiles_to') }}</label> <select class="form-select" id="allfilesToName" onchange="toggleAllfilesToNameOther()"> <option value="AllFix">AllFix</option> <option value="FileFix">FileFix</option> <option value="FileMgr">FileMgr</option> <option value="FileReq">FileReq</option> <option value="Files">Files</option> <option value="Sysop">Sysop</option> <option value="">{{ t('ui.nodelist.request_allfiles_to_other') }}</option> </select> <input type="text" class="form-control mt-2 d-none" id="allfilesToNameOther" placeholder="{{ t('ui.nodelist.request_allfiles_to_other_placeholder') }}" autocomplete="off"> </div> <div class="mb-3"> <label for="allfilesPassword" class="form-label">{{ t('ui.nodelist.request_allfiles_password') }}</label> <input type="text" class="form-control" id="allfilesPassword" autocomplete="off"> <div class="form-text">{{ t('ui.nodelist.request_allfiles_password_help') }}</div> </div> <div class="mb-3"> <div class="form-check"> <input class="form-check-input" type="checkbox" id="allfilesCrashmail" checked> <label class="form-check-label" for="allfilesCrashmail"> {{ t('ui.nodelist.request_allfiles_crashmail') }} </label> </div> <div class="form-text">{{ t('ui.nodelist.request_allfiles_crashmail_help') }}</div> </div> <div id="allfilesError" class="alert alert-danger d-none"></div> </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-info" id="allfilesSendBtn" onclick="sendAllfilesFreq()"> <i class="fas fa-file-download"></i> {{ t('ui.nodelist.send_request') }} </button> </div> </div> </div> </div> {% endif %} <!-- Context Menu for Host Links --> <div id="hostContextMenu" class="context-menu" style="display: none; position: absolute; z-index: 9999;"> <ul class="list-unstyled mb-0"> <li><a href="#" id="menuHttp" class="context-menu-item"><i class="fas fa-globe"></i> {{ t('ui.nodelist.http') }}</a></li> <li><a href="#" id="menuTelnet" class="context-menu-item"><i class="fas fa-terminal"></i> {{ t('ui.nodelist.telnet') }}</a></li> <li><a href="#" id="menuSsh" class="context-menu-item"><i class="fas fa-lock"></i> {{ t('ui.nodelist.ssh') }}</a></li> </ul> </div> <style> .context-menu { background-color: var(--bs-body-bg, #fff); border: 1px solid var(--bs-border-color, #dee2e6); border-radius: 0.375rem; box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15); min-width: 150px; padding: 0.5rem 0; } .context-menu-item { display: block; padding: 0.5rem 1rem; text-decoration: none; color: var(--bs-body-color, #212529); transition: background-color 0.15s ease; } .context-menu-item:hover { background-color: var(--bs-secondary-bg, #e9ecef); color: var(--bs-body-color, #212529); } .context-menu-item i { width: 20px; margin-right: 0.5rem; } </style> <script> // Context menu for host links document.addEventListener('DOMContentLoaded', function() { // Initialise Bootstrap tooltips for flag descriptions document.querySelectorAll('[data-bs-toggle="tooltip"]').forEach(function(el) { new bootstrap.Tooltip(el); }); const contextMenu = document.getElementById('hostContextMenu'); const menuHttp = document.getElementById('menuHttp'); const menuTelnet = document.getElementById('menuTelnet'); const menuSsh = document.getElementById('menuSsh'); let currentHostname = ''; // Show context menu on right-click of host links document.addEventListener('contextmenu', function(e) { const hostLink = e.target.closest('.host-link'); if (hostLink) { e.preventDefault(); currentHostname = hostLink.dataset.hostname; // Position the menu at the cursor contextMenu.style.left = e.pageX + 'px'; contextMenu.style.top = e.pageY + 'px'; contextMenu.style.display = 'block'; } }); // Hide context menu on any click document.addEventListener('click', function(e) { if (!contextMenu.contains(e.target)) { contextMenu.style.display = 'none'; } }); // Hide context menu on scroll document.addEventListener('scroll', function() { contextMenu.style.display = 'none'; }); // Helper function to strip port number from hostname function stripPort(hostname) { // Remove port number if present (e.g., "example.com:24554" -> "example.com") return hostname.split(':')[0]; } // ALLFILES FREQ functions window.showAllfilesModal = function() { document.getElementById('allfilesFilename').value = 'ALLFILES'; document.getElementById('allfilesFilenameOther').value = ''; document.getElementById('allfilesFilenameOther').classList.add('d-none'); document.getElementById('allfilesToName').value = 'FileFix'; document.getElementById('allfilesToNameOther').value = ''; document.getElementById('allfilesToNameOther').classList.add('d-none'); document.getElementById('allfilesPassword').value = ''; document.getElementById('allfilesCrashmail').checked = true; document.getElementById('allfilesError').classList.add('d-none'); document.getElementById('allfilesSendBtn').disabled = false; new bootstrap.Modal(document.getElementById('allfilesModal')).show(); }; window.toggleAllfilesFilenameOther = function() { const sel = document.getElementById('allfilesFilename'); const other = document.getElementById('allfilesFilenameOther'); if (sel.value === '') { other.classList.remove('d-none'); other.focus(); } else { other.classList.add('d-none'); } }; window.toggleAllfilesToNameOther = function() { const sel = document.getElementById('allfilesToName'); const other = document.getElementById('allfilesToNameOther'); if (sel.value === '') { other.classList.remove('d-none'); other.focus(); } else { other.classList.add('d-none'); } }; window.sendAllfilesFreq = function() { const filenameSel = document.getElementById('allfilesFilename'); const filename = (filenameSel.value === '' ? document.getElementById('allfilesFilenameOther').value.trim() : filenameSel.value) || 'ALLFILES'; const toNameSel = document.getElementById('allfilesToName'); const toName = (toNameSel.value === '' ? document.getElementById('allfilesToNameOther').value.trim() : toNameSel.value) || 'FileFix'; const password = document.getElementById('allfilesPassword').value.trim(); const crashmail = document.getElementById('allfilesCrashmail').checked; const btn = document.getElementById('allfilesSendBtn'); const errorBox = document.getElementById('allfilesError'); btn.disabled = true; errorBox.classList.add('d-none'); const subject = password ? filename + ' ' + password : filename; fetch('/api/messages/send', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ type: 'netmail', to_address: '{{ node.full_address|e('js') }}', to_name: toName, subject: subject, message_text: '', is_freq: true, crashmail: crashmail }) }) .then(r => r.json()) .then(data => { if (data.success) { bootstrap.Modal.getInstance(document.getElementById('allfilesModal')).hide(); if (window.showSuccess) { showSuccess('{{ t('ui.nodelist.request_allfiles_sent')|e('js') }}'); } } else { errorBox.textContent = (window.getApiErrorMessage ? window.getApiErrorMessage(data, '') : '') || data.error || '{{ t('ui.nodelist.request_allfiles_failed')|e('js') }}'; errorBox.classList.remove('d-none'); btn.disabled = false; } }) .catch(() => { errorBox.textContent = '{{ t('ui.nodelist.request_allfiles_failed')|e('js') }}'; errorBox.classList.remove('d-none'); btn.disabled = false; }); }; // Handle menu item clicks menuHttp.addEventListener('click', function(e) { e.preventDefault(); window.open('http://' + stripPort(currentHostname), '_blank'); contextMenu.style.display = 'none'; }); menuTelnet.addEventListener('click', function(e) { e.preventDefault(); window.location.href = 'telnet://' + stripPort(currentHostname); contextMenu.style.display = 'none'; }); menuSsh.addEventListener('click', function(e) { e.preventDefault(); window.location.href = 'ssh://' + stripPort(currentHostname); contextMenu.style.display = 'none'; }); }); </script> {% endblock %}